Compare commits

..

34 Commits

Author SHA1 Message Date
konstin
e7fb73500d Try thin lto
Let's see what impact on performance this has
2023-11-07 17:01:45 +01:00
Aarni Koskela
7dabc4598b Allow RUFF_NO_CACHE environment variable (like RUFF_CACHE_DIR) (#8538)
## Summary

Being able to set `--no-cache` without touching the command line makes
comparing formatter speed with e.g. Hyperfine a lot easier; Black allows
one to set `BLACK_CACHE_DIR=/dev/null`, but setting
`RUFF_CACHE_DIR=/dev/null` has Ruff choke:

```
error: Failed to initialize cache at /dev/null: Not a directory (os error 20)
error: Failed to initialize cache at /dev/null: Not a directory (os error 20)
warning: Failed to open cache file '/dev/null/0.1.4/18160934645386409287': Not a directory (os error 20)
```

Alternately, we could make a `/dev/null` (or `nul` on Windows) cache
directory imply `--no-cache`?

## Test Plan

None yet.
2023-11-07 08:35:28 -06:00
Andrew Gallant
6a1fa4778f Reject more syntactically invalid Python programs (#8524)
## Summary

This commit adds some additional error checking to the parser such that
assignments that are invalid syntax are rejected. This covers the
obvious cases like `5 = 3` and some not so obvious cases like `x + y =
42`.

This does add an additional recursive call to the parser for the cases
handling assignments. I had initially been concerned about doing this,
but `set_context` is already doing recursion during assignments, so I
didn't feel as though this was changing any fundamental performance
characteristics of the parser. (Also, in practice, I would expect any
such recursion here to be quite shallow since the recursion is done on
the target of an assignment. Such things are rarely nested much in
practice.)

Fixes #6895

## Test Plan

I've added unit tests covering every case that is detected as invalid on
an `Expr`.
2023-11-07 07:16:06 -05:00
Charlie Marsh
c3d6d5d006 Add singleton escape hatch to B008 documentation (#8501)
## Summary:

Closes: https://github.com/astral-sh/ruff/issues/8378.
2023-11-07 04:53:45 +00:00
qdegraaf
9a8400a287 Avoid raising TRIO115 violations for trio.sleep(...) calls with non-number values (#8532)
## Summary

Fixes bug in `TRIO115` where it would not `return` for values that were
not a `NumberLiteral` so
```python
x = "bla"
trio.sleep(x)
```
would set off a false positive

## Test Plan

Added test case to fixture
2023-11-06 16:49:12 -06:00
Juan Orduz
d71c65d0c8 Add PyMC Marketing to Users (#8529)
Add [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing) to
users. See https://github.com/pymc-labs/pymc-marketing/pull/424
2023-11-06 16:21:49 -06:00
doolio
7f92bfbc4a docs: Add missing toml config tabs (#8512) 2023-11-06 21:12:38 +00:00
Charlie Marsh
37301375c8 Make SIM118 fix as safe when the expression is a known dictionary (#8525)
## Summary

Given `key in obj.keys()`, `obj` _could_ be a dictionary, or it could be
another type that defines
a `.keys()` method. In the latter case, removing the `.keys()` attribute
could lead to a runtime error.

Previously, we marked all `SIM118` fixes as unsafe for this reason;
however, in preview, we now mark them as safe if we can
infer that the expression is a dictionary.

## Test Plan

Added a preview fixture.
2023-11-06 21:06:33 +00:00
Aarni Koskela
c07947bfac Add Pillow to Ruff users (#8523)
## Summary

See https://github.com/python-pillow/Pillow/pull/6966 :)

## Test Plan

Looked at the Markdown preview!
2023-11-06 12:59:06 -06:00
T-256
72964529a5 Skip ecosystem check when no changes detected (#8520)
<!--
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

For example, https://github.com/astral-sh/ruff/pull/8512 doesn't need
ecosystem check
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
2023-11-06 12:18:20 -06:00
Charlie Marsh
eab8ca4d7e Add dedicated method to find typed binding (#8517)
## Summary

We have this pattern in a bunch of places, where we find the _only_
binding to a name (and return `None`) if it's bound multiple times. This
PR DRYs it up into a method on `SemanticModel`.
2023-11-06 11:25:32 -05:00
Zanie Blue
5b3e922050 Upgrade pre-commit dependencies (#8518) 2023-11-06 10:08:22 -06:00
Zanie Blue
311a7751f9 Ensure ecosystem project errors are properly fenced (#8516)
Fixes bug where errors could be unfenced resulting in hidden remaining
content

e.g. https://github.com/astral-sh/ruff/pull/8508#issuecomment-1794960132
2023-11-06 09:35:07 -06:00
Charlie Marsh
5e2bb8ca07 Add a Fix constructor that takes Applicability as an argument (#8514)
## Summary

If you want to create an edit with dynamic applicability, you have to
branch and repeat the edit entirely between the two branches. If you
further need the edit itself to be dynamic (e.g., perhaps you have a
single edit in one case, vs. multiple in another), you suddenly have
four branches. This PR just adds an alternate constructor that takes
applicability as an argument, as an escape hatch.
2023-11-06 09:45:10 -05:00
konsti
3c8d9d45fb Recommend project.requires-python over target-version (#8513)
**Summary** Recommend the standardized, shared `project.requires-python`
over ruff's custom `target-version`. See
https://mastodon.social/deck/@davidism@mas.to/111347072204727710

**Test Plan** Docs only change
2023-11-06 14:35:32 +00:00
dependabot[bot]
82c3c513d2 Bump codspeed-criterion-compat from 2.3.0 to 2.3.1 (#8508) 2023-11-06 14:32:40 +00:00
dependabot[bot]
f2dc01e3aa Bump bitflags from 2.4.0 to 2.4.1 (#8511) 2023-11-06 09:20:39 -05:00
dependabot[bot]
5349143fca Bump serde_json from 1.0.107 to 1.0.108 (#8510) 2023-11-06 09:20:30 -05:00
dependabot[bot]
b6f23d57aa Bump syn from 2.0.38 to 2.0.39 (#8509) 2023-11-06 09:19:47 -05:00
dependabot[bot]
b7b6e0136e Bump serde-wasm-bindgen from 0.6.0 to 0.6.1 (#8507) 2023-11-06 09:19:30 -05:00
Ofek Lev
218f517487 Fix typo in example (#8506) 2023-11-06 12:52:14 +05:30
Dhruv Manilawala
75c669a007 Fix tab configuration docs (#8502)
Otherwise it doesn't render as expected.
2023-11-06 03:02:45 +00:00
Shantanu
2d5ce4532a Flag all comparisons against builtin types in E721 (#8491)
See #8483. Generalised fix on top of #8485

Based on the output of `print("\n".join(k for k, v in
builtins.__dict__.items() if isinstance(v, type)))`
2023-11-05 21:28:47 -05:00
qdegraaf
f3e2d12609 [TRIO] Add TRIO115: TrioZeroSleepCall (#8486)
## Summary

Adds `TRIO115` from the [flake8-trio
plugin](https://github.com/Zac-HD/flake8-trio).

## Test Plan

Added a new fixture, based on [the one from upstream
plugin](https://github.com/Zac-HD/flake8-trio/blob/main/tests/eval_files/trio115.py)

## Issue link

Relates to: https://github.com/astral-sh/ruff/issues/8451
2023-11-06 01:19:46 +00:00
Tom Kuson
de2d7e97b1 [refurb] Implement type-none-comparison (FURB169) (#8487)
## Summary

Implement
[`no-is-type-none`](https://github.com/dosisod/refurb/blob/master/refurb/checks/builtin/no_is_type_none.py)
as `type-none-comparison` (`FURB169`).

Auto-fixes comparisons that use `type` to compare the type of an object
to `type(None)` to a `None` identity check. For example,

```python
type(foo) is type(None)
```

becomes

```python
foo is None
```

Related to #1348.

## Test Plan

`cargo test`
2023-11-06 00:56:20 +00:00
Charlie Marsh
bcb737dd80 Add notes on fix safety to a few rules (#8500) 2023-11-06 00:48:57 +00:00
Charlie Marsh
8c146bbf11 Allow collapsed-ellipsis bodies in other statements (#8499)
## Summary

Black and Ruff's preview styles now collapse statements like:

```python
from contextlib import nullcontext

ctx = nullcontext()
with ctx: ...
```

Historically, we made an exception here for classes
(https://github.com/astral-sh/ruff/pull/2837). This PR extends it to
other statement kinds for consistency with the formatter.

Closes https://github.com/astral-sh/ruff/issues/8496.
2023-11-05 19:42:34 -05:00
qdegraaf
4170ef0508 [TRIO] Add TRIO105: SyncTrioCall (#8490)
## Summary

Adds `TRIO105` from the [flake8-trio
plugin](https://github.com/Zac-HD/flake8-trio). The `MethodName` logic
mirrors that of `TRIO100` to stay consistent within the plugin.

It is at 95% parity with the exception of upstream also checking for a
slightly more complex scenario where a call to `start()` on a
`trio.Nursery` context should also be immediately awaited. Upstream
plugin appears to just check for anything named `nursery` judging from
[the relevant issue](https://github.com/Zac-HD/flake8-trio/issues/56).

Unsure if we want to do so something similar or, alternatively, if there
is some capability in ruff to check for calls made on this context some
other way

## Test Plan

Added a new fixture, based on [the one from upstream
plugin](https://github.com/Zac-HD/flake8-trio/blob/main/tests/eval_files/trio105.py)

## Issue link

Refers: https://github.com/astral-sh/ruff/issues/8451
2023-11-05 19:56:10 +00:00
Chris Rose
72ebde8d38 Add instructions for configuration of Emacs (#8488)
## Summary

Add editor integration docs for `ruff format` in Emacs by way of the
Apheleia formatter library

Depends on:  https://github.com/radian-software/apheleia/issues/233
2023-11-05 17:15:59 +00:00
trag1c
1672a3d3b7 Added tabs for configuration files in the documentation (#8480)
## Summary

Closes #8384.

## Test Plan

Checked whether it renders properly on the `mkdocs serve` preview.
2023-11-05 17:10:29 +00:00
Tom Kuson
8c0d65c98e Fix F841 false negative on assignment to multiple variables (#8489)
## Summary

Closes #8441 behind preview feature flag.

## Test Plan

`cargo test`
2023-11-05 12:01:10 -05:00
Dhruv Manilawala
b3c2935fa5 Avoid D301 autofix for u prefixed strings (#8495)
This PR avoids creating the fix for `D301` if the string is prefixed
with `u` i.e., it's a unicode string. The reason being that `u` and `r`
cannot be used together as it's a syntax error.

Refer:
https://github.com/astral-sh/ruff/issues/8402#issuecomment-1788783287
2023-11-05 09:45:49 -05:00
Micha Reiser
e57bccd500 Fix multiline lambda expression statement formating (#8466)
## Summary

This PR fixes a bug in our formatter where a multiline lambda expression
statement was formatted over multiple lines without adding parentheses.

The PR "fixes" the problem by not splitting the lambda parameters if it
is not parenthesized

## Test Plan

Added test
2023-11-05 09:35:23 -05:00
qdegraaf
75c9be099f [E721] Flag comparisons to memoryview (#8485)
## Summary

Adds `memoryview` to the list of typeclasses that `fn is_type()` uses
for type comparison checks so that it raises a violation if `is`, `is
not` or `isinstance()` are not used.

## Test Plan

Added examples to existing fixture

## Issue Link

Closes: https://github.com/astral-sh/ruff/issues/8483
2023-11-04 13:41:58 +00:00
109 changed files with 5314 additions and 1139 deletions

View File

@@ -184,7 +184,11 @@ jobs:
- cargo-test-linux
- determine_changes
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
if: github.event_name == 'pull_request'
# Ecosystem check needs linter and/or formatter changes.
if: github.event_name == 'pull_request' && ${{
needs.determine_changes.outputs.linter == 'true' ||
needs.determine_changes.outputs.formatter == 'true'
}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4

View File

@@ -13,12 +13,12 @@ exclude: |
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.12.1
rev: v0.15
hooks:
- id: validate-pyproject
- repo: https://github.com/executablebooks/mdformat
rev: 0.7.16
rev: 0.7.17
hooks:
- id: mdformat
additional_dependencies:
@@ -26,16 +26,22 @@ repos:
- mdformat-admon
exclude: |
(?x)^(
docs/formatter/black.md
docs/formatter/black\.md
| docs/\w+\.md
)$
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.33.0
rev: v0.37.0
hooks:
- id: markdownlint-fix
exclude: |
(?x)^(
docs/formatter/black\.md
| docs/\w+\.md
)$
- repo: https://github.com/crate-ci/typos
rev: v1.14.12
rev: v1.16.22
hooks:
- id: typos
@@ -49,7 +55,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.3
rev: v0.1.4
hooks:
- id: ruff-format
- id: ruff
@@ -64,7 +70,7 @@ repos:
# Prettier
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0
rev: v3.0.3
hooks:
- id: prettier
types: [yaml]

View File

@@ -72,7 +72,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
charlie.r.marsh@gmail.com.
<charlie.r.marsh@gmail.com>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the

74
Cargo.lock generated
View File

@@ -210,9 +210,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bstr"
@@ -383,7 +383,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -407,9 +407,9 @@ dependencies = [
[[package]]
name = "codspeed"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d680ccd1eedd2dd7c7a3649a78c7d06e0f16b191b30d81cc58e7bc906488d344"
checksum = "918b13a0f1a32460ab3bd5debd56b5a27a7071fa5ff5dfeb3a5cf291a85b174b"
dependencies = [
"colored",
"libc",
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "codspeed-criterion-compat"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58b48b6c8e890d7d4ad0ed85e9ab4949bf7023198c006000ef6338ba84cf5b71"
checksum = "c683c7fef2b873fbbdf4062782914c652309951244bf0bd362fe608b7d6f901c"
dependencies = [
"codspeed",
"colored",
@@ -608,7 +608,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -619,7 +619,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -1129,7 +1129,7 @@ dependencies = [
"pmutil 0.6.1",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -1440,7 +1440,7 @@ version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.4.0",
"bitflags 2.4.1",
"crossbeam-channel",
"filetime",
"fsevent-sys",
@@ -1707,7 +1707,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -2067,7 +2067,7 @@ dependencies = [
"argfile",
"assert_cmd",
"bincode",
"bitflags 2.4.0",
"bitflags 2.4.1",
"cachedir",
"chrono",
"clap",
@@ -2201,7 +2201,7 @@ dependencies = [
"aho-corasick",
"annotate-snippets 0.9.1",
"anyhow",
"bitflags 2.4.0",
"bitflags 2.4.1",
"chrono",
"clap",
"colored",
@@ -2267,7 +2267,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -2293,7 +2293,7 @@ dependencies = [
name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"bitflags 2.4.0",
"bitflags 2.4.1",
"insta",
"is-macro",
"itertools 0.11.0",
@@ -2325,7 +2325,7 @@ name = "ruff_python_formatter"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.4.0",
"bitflags 2.4.1",
"clap",
"countme",
"insta",
@@ -2369,7 +2369,7 @@ dependencies = [
name = "ruff_python_literal"
version = "0.0.0"
dependencies = [
"bitflags 2.4.0",
"bitflags 2.4.1",
"hexf-parse",
"is-macro",
"itertools 0.11.0",
@@ -2383,7 +2383,7 @@ name = "ruff_python_parser"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.4.0",
"bitflags 2.4.1",
"insta",
"is-macro",
"itertools 0.11.0",
@@ -2413,7 +2413,7 @@ dependencies = [
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.4.0",
"bitflags 2.4.1",
"is-macro",
"ruff_index",
"ruff_python_ast",
@@ -2563,7 +2563,7 @@ version = "0.38.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
dependencies = [
"bitflags 2.4.0",
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys",
@@ -2682,9 +2682,9 @@ dependencies = [
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30c9933e5689bd420dc6c87b7a1835701810cbc10cd86a26e4da45b73e6b1d78"
checksum = "17ba92964781421b6cef36bf0d7da26d201e96d84e1b10e7ae6ed416e516906d"
dependencies = [
"js-sys",
"serde",
@@ -2699,7 +2699,7 @@ checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -2715,9 +2715,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.107"
version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [
"itoa",
"ryu",
@@ -2761,7 +2761,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -2856,7 +2856,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -2872,9 +2872,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.38"
version = "2.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
dependencies = [
"proc-macro2",
"quote",
@@ -2961,7 +2961,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -2973,7 +2973,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
"test-case-core",
]
@@ -2994,7 +2994,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -3131,7 +3131,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -3349,7 +3349,7 @@ checksum = "3d8c6bba9b149ee82950daefc9623b32bb1dacbfb1890e352f6b887bd582adaf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
]
[[package]]
@@ -3443,7 +3443,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
"wasm-bindgen-shared",
]
@@ -3477,7 +3477,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
"syn 2.0.39",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]

View File

@@ -13,7 +13,7 @@ license = "MIT"
[workspace.dependencies]
anyhow = { version = "1.0.69" }
bitflags = { version = "2.3.1" }
bitflags = { version = "2.4.1" }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
clap = { version = "4.4.7", features = ["derive"] }
colored = { version = "2.0.0" }
@@ -35,14 +35,14 @@ regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.15" }
serde = { version = "1.0.190", features = ["derive"] }
serde_json = { version = "1.0.107" }
serde_json = { version = "1.0.108" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.3.0", features = ["inline"] }
smallvec = { version = "1.11.1" }
static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.38" }
syn = { version = "2.0.39" }
test-case = { version = "3.2.1" }
thiserror = { version = "1.0.50" }
toml = { version = "0.7.8" }
@@ -56,7 +56,7 @@ uuid = { version = "1.5.0", features = ["v4", "fast-rng", "macro-diagnostics", "
wsl = { version = "0.1.0" }
[profile.release]
lto = "fat"
lto = "thin"
codegen-units = 1
[profile.dev.package.insta]

View File

@@ -415,6 +415,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [PDM](https://github.com/pdm-project/pdm)
- [PaddlePaddle](https://github.com/PaddlePaddle/Paddle)
- [Pandas](https://github.com/pandas-dev/pandas)
- [Pillow](https://github.com/python-pillow/Pillow)
- [Poetry](https://github.com/python-poetry/poetry)
- [Polars](https://github.com/pola-rs/polars)
- [PostHog](https://github.com/PostHog/posthog)
@@ -423,6 +424,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [PyTorch](https://github.com/pytorch/pytorch)
- [Pydantic](https://github.com/pydantic/pydantic)
- [Pylint](https://github.com/PyCQA/pylint)
- [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing)
- [Reflex](https://github.com/reflex-dev/reflex)
- [Rippling](https://rippling.com)
- [Robyn](https://github.com/sansyrox/robyn)

View File

@@ -1,5 +1,6 @@
[files]
extend-exclude = ["resources", "snapshots"]
# https://github.com/crate-ci/typos/issues/868
extend-exclude = ["**/resources/**/*", "**/snapshots/**/*"]
[default.extend-words]
hel = "hel"

View File

@@ -37,7 +37,7 @@ serde_json.workspace = true
url = "2.3.1"
ureq = "2.8.0"
criterion = { version = "0.5.1", default-features = false }
codspeed-criterion-compat = { version="2.3.0", default-features = false, optional = true}
codspeed-criterion-compat = { version="2.3.1", default-features = false, optional = true}
[dev-dependencies]
ruff_linter.path = "../ruff_linter"

View File

@@ -278,7 +278,7 @@ pub struct CheckCommand {
#[arg(long, help_heading = "Rule configuration", hide = true)]
pub dummy_variable_rgx: Option<Regex>,
/// Disable cache reads.
#[arg(short, long, help_heading = "Miscellaneous")]
#[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")]
pub no_cache: bool,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
@@ -374,7 +374,7 @@ pub struct FormatCommand {
pub config: Option<PathBuf>,
/// Disable cache reads.
#[arg(short, long, help_heading = "Miscellaneous")]
#[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")]
pub no_cache: bool,
/// Path to the cache directory.
#[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")]

View File

@@ -3,6 +3,7 @@
//! Used for <https://docs.astral.sh/ruff/settings/>.
use std::fmt::Write;
use ruff_python_trivia::textwrap;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit};
@@ -125,22 +126,57 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set:
output.push('\n');
output.push_str(&format!("**Type**: `{}`\n", field.value_type));
output.push('\n');
output.push_str(&format!(
"**Example usage**:\n\n```toml\n[tool.ruff{}]\n{}\n```\n",
if let Some(set_name) = parent_set.name() {
if set_name == "format" {
String::from(".format")
} else {
format!(".lint.{set_name}")
}
} else {
String::new()
},
field.example
output.push_str("**Example usage**:\n\n");
output.push_str(&format_tab(
"pyproject.toml",
&format_header(parent_set, ConfigurationFile::PyprojectToml),
field.example,
));
output.push_str(&format_tab(
"ruff.toml",
&format_header(parent_set, ConfigurationFile::RuffToml),
field.example,
));
output.push('\n');
}
fn format_tab(tab_name: &str, header: &str, content: &str) -> String {
format!(
"=== \"{}\"\n\n ```toml\n {}\n{}\n ```\n",
tab_name,
header,
textwrap::indent(content, " ")
)
}
fn format_header(parent_set: &Set, configuration: ConfigurationFile) -> String {
let fmt = if let Some(set_name) = parent_set.name() {
if set_name == "format" {
String::from(".format")
} else {
format!(".lint.{set_name}")
}
} else {
String::new()
};
match configuration {
ConfigurationFile::PyprojectToml => format!("[tool.ruff{fmt}]"),
ConfigurationFile::RuffToml => {
if fmt.is_empty() {
String::new()
} else {
format!("[{}]", fmt.strip_prefix('.').unwrap())
}
}
}
}
#[derive(Debug, Copy, Clone)]
enum ConfigurationFile {
PyprojectToml,
RuffToml,
}
#[derive(Default)]
struct CollectOptionsVisitor {
groups: Vec<(String, OptionSet)>,

View File

@@ -107,6 +107,30 @@ impl Fix {
}
}
/// Create a new [`Fix`] with the specified [`Applicability`] to apply an [`Edit`] element.
pub fn applicable_edit(edit: Edit, applicability: Applicability) -> Self {
Self {
edits: vec![edit],
applicability,
isolation_level: IsolationLevel::default(),
}
}
/// Create a new [`Fix`] with the specified [`Applicability`] to apply multiple [`Edit`] elements.
pub fn applicable_edits(
edit: Edit,
rest: impl IntoIterator<Item = Edit>,
applicability: Applicability,
) -> Self {
let mut edits: Vec<Edit> = std::iter::once(edit).chain(rest).collect();
edits.sort_by_key(|edit| (edit.start(), edit.end()));
Self {
edits,
applicability,
isolation_level: IsolationLevel::default(),
}
}
/// Return the [`TextSize`] of the first [`Edit`] in the [`Fix`].
pub fn min_start(&self) -> Option<TextSize> {
self.edits.first().map(Edit::start)

View File

@@ -1,3 +1,5 @@
obj = {}
key in obj.keys() # SIM118
key not in obj.keys() # SIM118

View File

@@ -0,0 +1,64 @@
import trio
async def func() -> None:
trio.run(foo) # OK, not async
# OK
await trio.aclose_forcefully(foo)
await trio.open_file(foo)
await trio.open_ssl_over_tcp_listeners(foo, foo)
await trio.open_ssl_over_tcp_stream(foo, foo)
await trio.open_tcp_listeners(foo)
await trio.open_tcp_stream(foo, foo)
await trio.open_unix_socket(foo)
await trio.run_process(foo)
await trio.sleep(5)
await trio.sleep_until(5)
await trio.lowlevel.cancel_shielded_checkpoint()
await trio.lowlevel.checkpoint()
await trio.lowlevel.checkpoint_if_cancelled()
await trio.lowlevel.open_process(foo)
await trio.lowlevel.permanently_detach_coroutine_object(foo)
await trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
await trio.lowlevel.temporarily_detach_coroutine_object(foo)
await trio.lowlevel.wait_readable(foo)
await trio.lowlevel.wait_task_rescheduled(foo)
await trio.lowlevel.wait_writable(foo)
# TRIO105
trio.aclose_forcefully(foo)
trio.open_file(foo)
trio.open_ssl_over_tcp_listeners(foo, foo)
trio.open_ssl_over_tcp_stream(foo, foo)
trio.open_tcp_listeners(foo)
trio.open_tcp_stream(foo, foo)
trio.open_unix_socket(foo)
trio.run_process(foo)
trio.serve_listeners(foo, foo)
trio.serve_ssl_over_tcp(foo, foo, foo)
trio.serve_tcp(foo, foo)
trio.sleep(foo)
trio.sleep_forever()
trio.sleep_until(foo)
trio.lowlevel.cancel_shielded_checkpoint()
trio.lowlevel.checkpoint()
trio.lowlevel.checkpoint_if_cancelled()
trio.lowlevel.open_process()
trio.lowlevel.permanently_detach_coroutine_object(foo)
trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
trio.lowlevel.temporarily_detach_coroutine_object(foo)
trio.lowlevel.wait_readable(foo)
trio.lowlevel.wait_task_rescheduled(foo)
trio.lowlevel.wait_writable(foo)
async with await trio.open_file(foo): # Ok
pass
async with trio.open_file(foo): # TRIO105
pass
def func() -> None:
# TRIO105 (without fix)
trio.open_file(foo)

View File

@@ -0,0 +1,28 @@
import trio
from trio import sleep
async def func():
await trio.sleep(0) # TRIO115
await trio.sleep(1) # OK
await trio.sleep(0, 1) # OK
await trio.sleep(...) # OK
await trio.sleep() # OK
trio.sleep(0) # TRIO115
foo = 0
trio.sleep(foo) # TRIO115
trio.sleep(1) # OK
time.sleep(0) # OK
sleep(0) # TRIO115
bar = "bar"
trio.sleep(bar)
trio.sleep(0) # TRIO115
def func():
trio.run(trio.sleep(0)) # TRIO115

View File

@@ -69,3 +69,5 @@ while 1:
#: E701:2:3
a = \
5;
#:
with x(y) as z: ...

View File

@@ -4,6 +4,9 @@ if type(res) == type(42):
#: E721
if type(res) != type(""):
pass
#: E721
if type(res) == memoryview:
pass
#: Okay
import types
@@ -47,6 +50,14 @@ if isinstance(res, str):
pass
if isinstance(res, types.MethodType):
pass
if isinstance(res, memoryview):
pass
#: Okay
if type(res) is type:
pass
#: E721
if type(res) == type:
pass
#: Okay
def func_histype(a, b, c):
pass

View File

@@ -31,3 +31,7 @@ def make_unique_pod_id(pod_id: str) -> str | None:
:param pod_id: requested pod name
:return: ``str`` valid Pod name of appropriate length
"""
def shouldnt_add_raw_here2():
u"Sum\\mary."

View File

@@ -1,5 +1,5 @@
def f(tup):
x, y = tup # this does NOT trigger F841
x, y = tup
def f():
@@ -7,17 +7,17 @@ def f():
def f():
(x, y) = coords = 1, 2 # this does NOT trigger F841
(x, y) = coords = 1, 2
if x > 1:
print(coords)
def f():
(x, y) = coords = 1, 2 # this triggers F841 on coords
(x, y) = coords = 1, 2
def f():
coords = (x, y) = 1, 2 # this triggers F841 on coords
coords = (x, y) = 1, 2
def f():

View File

@@ -0,0 +1,32 @@
"""Test fix for issue #8441.
Ref: https://github.com/astral-sh/ruff/issues/8441
"""
def foo():
...
def bar():
a = foo()
b, c = foo()
def baz():
d, _e = foo()
print(d)
def qux():
f, _ = foo()
print(f)
def quux():
g, h = foo()
print(g, h)
def quuz():
_i, _j = foo()

View File

@@ -43,6 +43,12 @@ def yes_four(x: Dict[int, str]):
del x[:]
def yes_five(x: Dict[int, str]):
# FURB131
del x[:]
x = 1
# these should not
del names["key"]

View File

@@ -0,0 +1,67 @@
foo = None
# Error.
type(foo) is type(None)
type(None) is type(foo)
type(None) is type(None)
type(foo) is not type(None)
type(None) is not type(foo)
type(None) is not type(None)
type(foo) == type(None)
type(None) == type(foo)
type(None) == type(None)
type(foo) != type(None)
type(None) != type(foo)
type(None) != type(None)
# Ok.
foo is None
foo is not None
None is foo
None is not foo
None is None
None is not None
foo is type(None)
type(foo) is None
type(None) is None
foo is not type(None)
type(foo) is not None
type(None) is not None
foo == type(None)
type(foo) == None
type(None) == None
foo != type(None)
type(foo) != None
type(None) != None
type(foo) > type(None)

View File

@@ -15,8 +15,8 @@ use crate::rules::{
flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging,
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_use_pathlib, flynt, numpy, pandas_vet,
pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff,
flake8_simplify, flake8_tidy_imports, flake8_trio, flake8_use_pathlib, flynt, numpy,
pandas_vet, pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff,
};
use crate::settings::types::PythonVersion;
@@ -926,6 +926,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::ImplicitCwd) {
refurb::rules::no_implicit_cwd(checker, call);
}
if checker.enabled(Rule::TrioSyncCall) {
flake8_trio::rules::sync_call(checker, call);
}
if checker.enabled(Rule::TrioZeroSleepCall) {
flake8_trio::rules::zero_sleep_call(checker, call);
}
}
Expr::Dict(
dict @ ast::ExprDict {
@@ -1235,6 +1241,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
comparators,
);
}
if checker.enabled(Rule::TypeNoneComparison) {
refurb::rules::type_none_comparison(checker, compare);
}
if checker.enabled(Rule::SingleItemMembershipTest) {
refurb::rules::single_item_membership_test(checker, expr, left, ops, comparators);
}

View File

@@ -292,6 +292,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-trio
(Flake8Trio, "100") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioTimeoutWithoutAwait),
(Flake8Trio, "105") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioSyncCall),
(Flake8Trio, "115") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioZeroSleepCall),
// flake8-builtins
(Flake8Builtins, "001") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinVariableShadowing),
@@ -944,6 +946,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
(Refurb, "168") => (RuleGroup::Preview, rules::refurb::rules::IsinstanceTypeNone),
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),
(Refurb, "177") => (RuleGroup::Preview, rules::refurb::rules::ImplicitCwd),

View File

@@ -163,11 +163,15 @@ mod tests {
"# ( user_content_type , _ )= TimelineEvent.objects.using(db_alias).get_or_create(",
&[]
));
assert!(comment_contains_code(
assert!(comment_contains_code("# )", &[]));
// This used to return true, but our parser has gotten a bit better
// at rejecting invalid Python syntax. And indeed, this is not valid
// Python code.
assert!(!comment_contains_code(
"# app_label=\"core\", model=\"user\"",
&[]
));
assert!(comment_contains_code("# )", &[]));
// TODO(charlie): This should be `true` under aggressive mode.
assert!(!comment_contains_code("#def foo():", &[]));

View File

@@ -27,6 +27,11 @@ use crate::checkers::ast::Checker;
/// raise AssertionError
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as changing an `assert` to a
/// `raise` will change the behavior of your program when running in
/// optimized mode (`python -O`).
///
/// ## References
/// - [Python documentation: `assert`](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement)
#[violation]

View File

@@ -50,6 +50,16 @@ use crate::checkers::ast::Checker;
/// return arg
/// ```
///
/// If the use of a singleton is intentional, assign the result call to a
/// module-level variable, and use that variable in the default argument:
/// ```python
/// ERROR = ValueError("Hosts weren't successfully added")
///
///
/// def add_host(error: Exception = ERROR) -> None:
/// ...
/// ```
///
/// ## Options
/// - `flake8-bugbear.extend-immutable-calls`
#[violation]
@@ -62,9 +72,9 @@ impl Violation for FunctionCallInDefaultArgument {
fn message(&self) -> String {
let FunctionCallInDefaultArgument { name } = self;
if let Some(name) = name {
format!("Do not perform function call `{name}` in argument defaults")
format!("Do not perform function call `{name}` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable")
} else {
format!("Do not perform function call in argument defaults")
format!("Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable")
}
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006_B008.py:102:61: B008 Do not perform function call `range` in argument defaults
B006_B008.py:102:61: B008 Do not perform function call `range` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
101 | # N.B. we're also flagging the function call in the comprehension
102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
@@ -9,21 +9,21 @@ B006_B008.py:102:61: B008 Do not perform function call `range` in argument defau
103 | pass
|
B006_B008.py:106:64: B008 Do not perform function call `range` in argument defaults
B006_B008.py:106:64: B008 Do not perform function call `range` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
| ^^^^^^^^ B008
107 | pass
|
B006_B008.py:110:60: B008 Do not perform function call `range` in argument defaults
B006_B008.py:110:60: B008 Do not perform function call `range` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
| ^^^^^^^^ B008
111 | pass
|
B006_B008.py:126:39: B008 Do not perform function call `time.time` in argument defaults
B006_B008.py:126:39: B008 Do not perform function call `time.time` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
124 | # B008
125 | # Flag function calls as default args (including if they are part of a sub-expression)
@@ -32,21 +32,21 @@ B006_B008.py:126:39: B008 Do not perform function call `time.time` in argument d
127 | ...
|
B006_B008.py:130:12: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:130:12: B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
130 | def f(when=dt.datetime.now() + dt.timedelta(days=7)):
| ^^^^^^^^^^^^^^^^^ B008
131 | pass
|
B006_B008.py:134:30: B008 Do not perform function call in argument defaults
B006_B008.py:134:30: B008 Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
134 | def can_even_catch_lambdas(a=(lambda x: x)()):
| ^^^^^^^^^^^^^^^ B008
135 | ...
|
B006_B008.py:239:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:239:31: B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
@@ -55,7 +55,7 @@ B006_B008.py:239:31: B008 Do not perform function call `dt.datetime.now` in argu
240 | pass
|
B006_B008.py:245:22: B008 Do not perform function call `map` in argument defaults
B006_B008.py:245:22: B008 Do not perform function call `map` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
243 | # Don't flag nested B006 since we can't guarantee that
244 | # it isn't made mutable by the outer operation.
@@ -64,7 +64,7 @@ B006_B008.py:245:22: B008 Do not perform function call `map` in argument default
246 | pass
|
B006_B008.py:250:19: B008 Do not perform function call `random.randint` in argument defaults
B006_B008.py:250:19: B008 Do not perform function call `random.randint` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
@@ -72,7 +72,7 @@ B006_B008.py:250:19: B008 Do not perform function call `random.randint` in argum
251 | pass
|
B006_B008.py:250:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:250:37: B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B008_extended.py:24:51: B008 Do not perform function call `Depends` in argument defaults
B008_extended.py:24:51: B008 Do not perform function call `Depends` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
|
24 | def error_due_to_missing_import(data: List[str] = Depends(None)):
| ^^^^^^^^^^^^^ B008

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
@@ -83,13 +83,14 @@ pub(crate) fn unnecessary_call_around_sorted(
expr.range(),
);
diagnostic.try_set_fix(|| {
let edit =
fixes::fix_unnecessary_call_around_sorted(expr, checker.locator(), checker.stylist())?;
if outer.id == "reversed" {
Ok(Fix::unsafe_edit(edit))
} else {
Ok(Fix::safe_edit(edit))
}
Ok(Fix::applicable_edit(
fixes::fix_unnecessary_call_around_sorted(expr, checker.locator(), checker.stylist())?,
if outer.id == "reversed" {
Applicability::Unsafe
} else {
Applicability::Safe
},
))
});
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::BindingKind;
@@ -103,17 +103,23 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
.next()
.is_some_and(char::is_alphanumeric)
{
diagnostic.set_fix(if exception_type.is_some() {
Fix::safe_edit(Edit::range_replacement(" ".to_string(), arguments.range()))
} else {
Fix::unsafe_edit(Edit::range_replacement(" ".to_string(), arguments.range()))
});
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement(" ".to_string(), arguments.range()),
if exception_type.is_some() {
Applicability::Safe
} else {
Applicability::Unsafe
},
));
} else {
diagnostic.set_fix(if exception_type.is_some() {
Fix::safe_edit(Edit::range_deletion(arguments.range()))
} else {
Fix::unsafe_edit(Edit::range_deletion(arguments.range()))
});
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_deletion(arguments.range()),
if exception_type.is_some() {
Applicability::Safe
} else {
Applicability::Unsafe
},
));
}
checker.diagnostics.push(diagnostic);

View File

@@ -55,6 +55,7 @@ mod tests {
Ok(())
}
#[test_case(Rule::InDictKeys, Path::new("SIM118.py"))]
#[test_case(Rule::IfElseBlockInsteadOfDictGet, Path::new("SIM401.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(

View File

@@ -1,9 +1,10 @@
use ruff_diagnostics::Edit;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{Applicability, Edit};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{self as ast, Arguments, CmpOp, Comprehension, Expr};
use ruff_python_semantic::analyze::typing;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange};
@@ -27,8 +28,19 @@ use crate::checkers::ast::Checker;
/// key in foo
/// ```
///
/// ## Fix safety
/// Given `key in obj.keys()`, `obj` _could_ be a dictionary, or it could be
/// another type that defines a `.keys()` method. In the latter case, removing
/// the `.keys()` attribute could lead to a runtime error.
///
/// As such, this rule's fixes are marked as unsafe. In [preview], though,
/// fixes are marked as safe when Ruff can determine that `obj` is a
/// dictionary.
///
/// ## References
/// - [Python documentation: Mapping Types](https://docs.python.org/3/library/stdtypes.html#mapping-types-dict)
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[violation]
pub struct InDictKeys {
operator: String,
@@ -113,6 +125,28 @@ fn key_in_dict(
.skip_trivia()
.find(|token| token.kind == SimpleTokenKind::Dot)
{
// The fix is only safe if we know the expression is a dictionary, since other types
// can define a `.keys()` method.
let applicability = if checker.settings.preview.is_enabled() {
let is_dict = value.as_name_expr().is_some_and(|name| {
let Some(binding) = checker
.semantic()
.only_binding(name)
.map(|id| checker.semantic().binding(id))
else {
return false;
};
typing::is_dict(binding, checker.semantic())
});
if is_dict {
Applicability::Safe
} else {
Applicability::Unsafe
}
} else {
Applicability::Unsafe
};
// If the `.keys()` is followed by (e.g.) a keyword, we need to insert a space,
// since we're removing parentheses, which could lead to invalid syntax, as in:
// ```python
@@ -126,12 +160,15 @@ fn key_in_dict(
.next()
.is_some_and(|char| char.is_ascii_alphabetic())
{
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
" ".to_string(),
range,
)));
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement(" ".to_string(), range),
applicability,
));
} else {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion(range)));
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_deletion(range),
applicability,
));
}
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,396 +1,401 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM118.py:1:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
SIM118.py:3:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
1 | key in obj.keys() # SIM118
1 | obj = {}
2 |
3 | key in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^ SIM118
2 |
3 | key not in obj.keys() # SIM118
4 |
5 | key not in obj.keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
1 |-key in obj.keys() # SIM118
1 |+key in obj # SIM118
1 1 | obj = {}
2 2 |
3 3 | key not in obj.keys() # SIM118
3 |-key in obj.keys() # SIM118
3 |+key in obj # SIM118
4 4 |
5 5 | key not in obj.keys() # SIM118
6 6 |
SIM118.py:3:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
SIM118.py:5:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
|
1 | key in obj.keys() # SIM118
2 |
3 | key not in obj.keys() # SIM118
3 | key in obj.keys() # SIM118
4 |
5 | key not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^ SIM118
4 |
5 | foo["bar"] in obj.keys() # SIM118
6 |
7 | foo["bar"] in obj.keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
1 1 | key in obj.keys() # SIM118
2 2 |
3 |-key not in obj.keys() # SIM118
3 |+key not in obj # SIM118
3 3 | key in obj.keys() # SIM118
4 4 |
5 5 | foo["bar"] in obj.keys() # SIM118
5 |-key not in obj.keys() # SIM118
5 |+key not in obj # SIM118
6 6 |
7 7 | foo["bar"] in obj.keys() # SIM118
8 8 |
SIM118.py:5:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
SIM118.py:7:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
3 | key not in obj.keys() # SIM118
4 |
5 | foo["bar"] in obj.keys() # SIM118
5 | key not in obj.keys() # SIM118
6 |
7 | foo["bar"] in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
6 |
7 | foo["bar"] not in obj.keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
2 2 |
3 3 | key not in obj.keys() # SIM118
4 4 |
5 |-foo["bar"] in obj.keys() # SIM118
5 |+foo["bar"] in obj # SIM118
6 6 |
7 7 | foo["bar"] not in obj.keys() # SIM118
8 8 |
SIM118.py:7:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
|
5 | foo["bar"] in obj.keys() # SIM118
6 |
7 | foo["bar"] not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
8 |
9 | foo['bar'] in obj.keys() # SIM118
9 | foo["bar"] not in obj.keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
4 4 |
5 5 | foo["bar"] in obj.keys() # SIM118
5 5 | key not in obj.keys() # SIM118
6 6 |
7 |-foo["bar"] not in obj.keys() # SIM118
7 |+foo["bar"] not in obj # SIM118
7 |-foo["bar"] in obj.keys() # SIM118
7 |+foo["bar"] in obj # SIM118
8 8 |
9 9 | foo['bar'] in obj.keys() # SIM118
9 9 | foo["bar"] not in obj.keys() # SIM118
10 10 |
SIM118.py:9:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
SIM118.py:9:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
|
7 | foo["bar"] not in obj.keys() # SIM118
7 | foo["bar"] in obj.keys() # SIM118
8 |
9 | foo['bar'] in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
9 | foo["bar"] not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
10 |
11 | foo['bar'] not in obj.keys() # SIM118
11 | foo['bar'] in obj.keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
6 6 |
7 7 | foo["bar"] not in obj.keys() # SIM118
7 7 | foo["bar"] in obj.keys() # SIM118
8 8 |
9 |-foo['bar'] in obj.keys() # SIM118
9 |+foo['bar'] in obj # SIM118
9 |-foo["bar"] not in obj.keys() # SIM118
9 |+foo["bar"] not in obj # SIM118
10 10 |
11 11 | foo['bar'] not in obj.keys() # SIM118
11 11 | foo['bar'] in obj.keys() # SIM118
12 12 |
SIM118.py:11:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
SIM118.py:11:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
9 | foo['bar'] in obj.keys() # SIM118
9 | foo["bar"] not in obj.keys() # SIM118
10 |
11 | foo['bar'] not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
11 | foo['bar'] in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
12 |
13 | foo() in obj.keys() # SIM118
13 | foo['bar'] not in obj.keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
8 8 |
9 9 | foo['bar'] in obj.keys() # SIM118
9 9 | foo["bar"] not in obj.keys() # SIM118
10 10 |
11 |-foo['bar'] not in obj.keys() # SIM118
11 |+foo['bar'] not in obj # SIM118
11 |-foo['bar'] in obj.keys() # SIM118
11 |+foo['bar'] in obj # SIM118
12 12 |
13 13 | foo() in obj.keys() # SIM118
13 13 | foo['bar'] not in obj.keys() # SIM118
14 14 |
SIM118.py:13:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
SIM118.py:13:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
|
11 | foo['bar'] not in obj.keys() # SIM118
11 | foo['bar'] in obj.keys() # SIM118
12 |
13 | foo() in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^ SIM118
13 | foo['bar'] not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
14 |
15 | foo() not in obj.keys() # SIM118
15 | foo() in obj.keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
10 10 |
11 11 | foo['bar'] not in obj.keys() # SIM118
11 11 | foo['bar'] in obj.keys() # SIM118
12 12 |
13 |-foo() in obj.keys() # SIM118
13 |+foo() in obj # SIM118
13 |-foo['bar'] not in obj.keys() # SIM118
13 |+foo['bar'] not in obj # SIM118
14 14 |
15 15 | foo() not in obj.keys() # SIM118
15 15 | foo() in obj.keys() # SIM118
16 16 |
SIM118.py:15:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
SIM118.py:15:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
13 | foo() in obj.keys() # SIM118
13 | foo['bar'] not in obj.keys() # SIM118
14 |
15 | foo() not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM118
15 | foo() in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^ SIM118
16 |
17 | for key in obj.keys(): # SIM118
17 | foo() not in obj.keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
12 12 |
13 13 | foo() in obj.keys() # SIM118
13 13 | foo['bar'] not in obj.keys() # SIM118
14 14 |
15 |-foo() not in obj.keys() # SIM118
15 |+foo() not in obj # SIM118
15 |-foo() in obj.keys() # SIM118
15 |+foo() in obj # SIM118
16 16 |
17 17 | for key in obj.keys(): # SIM118
18 18 | pass
17 17 | foo() not in obj.keys() # SIM118
18 18 |
SIM118.py:17:5: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
SIM118.py:17:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
|
15 | foo() not in obj.keys() # SIM118
15 | foo() in obj.keys() # SIM118
16 |
17 | for key in obj.keys(): # SIM118
| ^^^^^^^^^^^^^^^^^ SIM118
18 | pass
17 | foo() not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM118
18 |
19 | for key in obj.keys(): # SIM118
|
= help: Remove `.keys()`
Suggested fix
14 14 |
15 15 | foo() not in obj.keys() # SIM118
15 15 | foo() in obj.keys() # SIM118
16 16 |
17 |-for key in obj.keys(): # SIM118
17 |+for key in obj: # SIM118
18 18 | pass
19 19 |
20 20 | for key in list(obj.keys()):
17 |-foo() not in obj.keys() # SIM118
17 |+foo() not in obj # SIM118
18 18 |
19 19 | for key in obj.keys(): # SIM118
20 20 | pass
SIM118.py:24:8: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
SIM118.py:19:5: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
22 | del obj[key]
23 |
24 | [k for k in obj.keys()] # SIM118
| ^^^^^^^^^^^^^^^ SIM118
25 |
26 | {k for k in obj.keys()} # SIM118
17 | foo() not in obj.keys() # SIM118
18 |
19 | for key in obj.keys(): # SIM118
| ^^^^^^^^^^^^^^^^^ SIM118
20 | pass
|
= help: Remove `.keys()`
Suggested fix
21 21 | if some_property(key):
22 22 | del obj[key]
23 23 |
24 |-[k for k in obj.keys()] # SIM118
24 |+[k for k in obj] # SIM118
25 25 |
26 26 | {k for k in obj.keys()} # SIM118
27 27 |
16 16 |
17 17 | foo() not in obj.keys() # SIM118
18 18 |
19 |-for key in obj.keys(): # SIM118
19 |+for key in obj: # SIM118
20 20 | pass
21 21 |
22 22 | for key in list(obj.keys()):
SIM118.py:26:8: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
24 | [k for k in obj.keys()] # SIM118
24 | del obj[key]
25 |
26 | {k for k in obj.keys()} # SIM118
26 | [k for k in obj.keys()] # SIM118
| ^^^^^^^^^^^^^^^ SIM118
27 |
28 | {k: k for k in obj.keys()} # SIM118
28 | {k for k in obj.keys()} # SIM118
|
= help: Remove `.keys()`
Suggested fix
23 23 |
24 24 | [k for k in obj.keys()] # SIM118
23 23 | if some_property(key):
24 24 | del obj[key]
25 25 |
26 |-{k for k in obj.keys()} # SIM118
26 |+{k for k in obj} # SIM118
26 |-[k for k in obj.keys()] # SIM118
26 |+[k for k in obj] # SIM118
27 27 |
28 28 | {k: k for k in obj.keys()} # SIM118
28 28 | {k for k in obj.keys()} # SIM118
29 29 |
SIM118.py:28:11: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
SIM118.py:28:8: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
26 | {k for k in obj.keys()} # SIM118
26 | [k for k in obj.keys()] # SIM118
27 |
28 | {k: k for k in obj.keys()} # SIM118
28 | {k for k in obj.keys()} # SIM118
| ^^^^^^^^^^^^^^^ SIM118
29 |
30 | {k: k for k in obj.keys()} # SIM118
|
= help: Remove `.keys()`
Suggested fix
25 25 |
26 26 | [k for k in obj.keys()] # SIM118
27 27 |
28 |-{k for k in obj.keys()} # SIM118
28 |+{k for k in obj} # SIM118
29 29 |
30 30 | {k: k for k in obj.keys()} # SIM118
31 31 |
SIM118.py:30:11: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
28 | {k for k in obj.keys()} # SIM118
29 |
30 | {k: k for k in obj.keys()} # SIM118
| ^^^^^^^^^^^^^^^ SIM118
29 |
30 | (k for k in obj.keys()) # SIM118
31 |
32 | (k for k in obj.keys()) # SIM118
|
= help: Remove `.keys()`
Suggested fix
25 25 |
26 26 | {k for k in obj.keys()} # SIM118
27 27 |
28 |-{k: k for k in obj.keys()} # SIM118
28 |+{k: k for k in obj} # SIM118
28 28 | {k for k in obj.keys()} # SIM118
29 29 |
30 30 | (k for k in obj.keys()) # SIM118
30 |-{k: k for k in obj.keys()} # SIM118
30 |+{k: k for k in obj} # SIM118
31 31 |
32 32 | (k for k in obj.keys()) # SIM118
33 33 |
SIM118.py:30:8: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
SIM118.py:32:8: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
28 | {k: k for k in obj.keys()} # SIM118
29 |
30 | (k for k in obj.keys()) # SIM118
30 | {k: k for k in obj.keys()} # SIM118
31 |
32 | (k for k in obj.keys()) # SIM118
| ^^^^^^^^^^^^^^^ SIM118
31 |
32 | key in (obj or {}).keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
27 27 |
28 28 | {k: k for k in obj.keys()} # SIM118
29 29 |
30 |-(k for k in obj.keys()) # SIM118
30 |+(k for k in obj) # SIM118
31 31 |
32 32 | key in (obj or {}).keys() # SIM118
33 33 |
SIM118.py:32:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
30 | (k for k in obj.keys()) # SIM118
31 |
32 | key in (obj or {}).keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
33 |
34 | (key) in (obj or {}).keys() # SIM118
34 | key in (obj or {}).keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
29 29 |
30 30 | (k for k in obj.keys()) # SIM118
30 30 | {k: k for k in obj.keys()} # SIM118
31 31 |
32 |-key in (obj or {}).keys() # SIM118
32 |+key in (obj or {}) # SIM118
32 |-(k for k in obj.keys()) # SIM118
32 |+(k for k in obj) # SIM118
33 33 |
34 34 | (key) in (obj or {}).keys() # SIM118
34 34 | key in (obj or {}).keys() # SIM118
35 35 |
SIM118.py:34:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
32 | key in (obj or {}).keys() # SIM118
32 | (k for k in obj.keys()) # SIM118
33 |
34 | (key) in (obj or {}).keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
34 | key in (obj or {}).keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
35 |
36 | from typing import KeysView
36 | (key) in (obj or {}).keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
31 31 |
32 32 | key in (obj or {}).keys() # SIM118
32 32 | (k for k in obj.keys()) # SIM118
33 33 |
34 |-(key) in (obj or {}).keys() # SIM118
34 |+(key) in (obj or {}) # SIM118
34 |-key in (obj or {}).keys() # SIM118
34 |+key in (obj or {}) # SIM118
35 35 |
36 36 | from typing import KeysView
36 36 | (key) in (obj or {}).keys() # SIM118
37 37 |
SIM118.py:48:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
SIM118.py:36:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
47 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
48 | key in obj.keys()and foo
| ^^^^^^^^^^^^^^^^^ SIM118
49 | (key in obj.keys())and foo
50 | key in (obj.keys())and foo
34 | key in (obj or {}).keys() # SIM118
35 |
36 | (key) in (obj or {}).keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
37 |
38 | from typing import KeysView
|
= help: Remove `.keys()`
Suggested fix
45 45 |
46 46 |
47 47 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
48 |-key in obj.keys()and foo
48 |+key in obj and foo
49 49 | (key in obj.keys())and foo
50 50 | key in (obj.keys())and foo
51 51 |
SIM118.py:49:2: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
47 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
48 | key in obj.keys()and foo
49 | (key in obj.keys())and foo
| ^^^^^^^^^^^^^^^^^ SIM118
50 | key in (obj.keys())and foo
|
= help: Remove `.keys()`
Suggested fix
46 46 |
47 47 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
48 48 | key in obj.keys()and foo
49 |-(key in obj.keys())and foo
49 |+(key in obj)and foo
50 50 | key in (obj.keys())and foo
51 51 |
52 52 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
33 33 |
34 34 | key in (obj or {}).keys() # SIM118
35 35 |
36 |-(key) in (obj or {}).keys() # SIM118
36 |+(key) in (obj or {}) # SIM118
37 37 |
38 38 | from typing import KeysView
39 39 |
SIM118.py:50:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
48 | key in obj.keys()and foo
49 | (key in obj.keys())and foo
50 | key in (obj.keys())and foo
49 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
50 | key in obj.keys()and foo
| ^^^^^^^^^^^^^^^^^ SIM118
51 | (key in obj.keys())and foo
52 | key in (obj.keys())and foo
|
= help: Remove `.keys()`
Suggested fix
47 47 |
48 48 |
49 49 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
50 |-key in obj.keys()and foo
50 |+key in obj and foo
51 51 | (key in obj.keys())and foo
52 52 | key in (obj.keys())and foo
53 53 |
SIM118.py:51:2: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
49 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
50 | key in obj.keys()and foo
51 | (key in obj.keys())and foo
| ^^^^^^^^^^^^^^^^^ SIM118
52 | key in (obj.keys())and foo
|
= help: Remove `.keys()`
Suggested fix
48 48 |
49 49 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
50 50 | key in obj.keys()and foo
51 |-(key in obj.keys())and foo
51 |+(key in obj)and foo
52 52 | key in (obj.keys())and foo
53 53 |
54 54 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
SIM118.py:52:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
50 | key in obj.keys()and foo
51 | (key in obj.keys())and foo
52 | key in (obj.keys())and foo
| ^^^^^^^^^^^^^^^^^^^ SIM118
51 |
52 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
53 |
54 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
|
= help: Remove `.keys()`
Suggested fix
47 47 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
48 48 | key in obj.keys()and foo
49 49 | (key in obj.keys())and foo
50 |-key in (obj.keys())and foo
50 |+key in (obj)and foo
51 51 |
52 52 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
53 53 | for key in (
49 49 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
50 50 | key in obj.keys()and foo
51 51 | (key in obj.keys())and foo
52 |-key in (obj.keys())and foo
52 |+key in (obj)and foo
53 53 |
54 54 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
55 55 | for key in (
SIM118.py:53:5: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
SIM118.py:55:5: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
52 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
53 | for key in (
54 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
55 | for key in (
| _____^
54 | | self.experiment.surveys[0]
55 | | .stations[0]
56 | | .keys()
57 | | ):
56 | | self.experiment.surveys[0]
57 | | .stations[0]
58 | | .keys()
59 | | ):
| |_^ SIM118
58 | continue
60 | continue
|
= help: Remove `.keys()`
Suggested fix
53 53 | for key in (
54 54 | self.experiment.surveys[0]
55 55 | .stations[0]
56 |- .keys()
56 |+
57 57 | ):
58 58 | continue
55 55 | for key in (
56 56 | self.experiment.surveys[0]
57 57 | .stations[0]
58 |- .keys()
58 |+
59 59 | ):
60 60 | continue

View File

@@ -0,0 +1,401 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM118.py:3:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
1 | obj = {}
2 |
3 | key in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^ SIM118
4 |
5 | key not in obj.keys() # SIM118
|
= help: Remove `.keys()`
Fix
1 1 | obj = {}
2 2 |
3 |-key in obj.keys() # SIM118
3 |+key in obj # SIM118
4 4 |
5 5 | key not in obj.keys() # SIM118
6 6 |
SIM118.py:5:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
|
3 | key in obj.keys() # SIM118
4 |
5 | key not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^ SIM118
6 |
7 | foo["bar"] in obj.keys() # SIM118
|
= help: Remove `.keys()`
Fix
2 2 |
3 3 | key in obj.keys() # SIM118
4 4 |
5 |-key not in obj.keys() # SIM118
5 |+key not in obj # SIM118
6 6 |
7 7 | foo["bar"] in obj.keys() # SIM118
8 8 |
SIM118.py:7:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
5 | key not in obj.keys() # SIM118
6 |
7 | foo["bar"] in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
8 |
9 | foo["bar"] not in obj.keys() # SIM118
|
= help: Remove `.keys()`
Fix
4 4 |
5 5 | key not in obj.keys() # SIM118
6 6 |
7 |-foo["bar"] in obj.keys() # SIM118
7 |+foo["bar"] in obj # SIM118
8 8 |
9 9 | foo["bar"] not in obj.keys() # SIM118
10 10 |
SIM118.py:9:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
|
7 | foo["bar"] in obj.keys() # SIM118
8 |
9 | foo["bar"] not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
10 |
11 | foo['bar'] in obj.keys() # SIM118
|
= help: Remove `.keys()`
Fix
6 6 |
7 7 | foo["bar"] in obj.keys() # SIM118
8 8 |
9 |-foo["bar"] not in obj.keys() # SIM118
9 |+foo["bar"] not in obj # SIM118
10 10 |
11 11 | foo['bar'] in obj.keys() # SIM118
12 12 |
SIM118.py:11:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
9 | foo["bar"] not in obj.keys() # SIM118
10 |
11 | foo['bar'] in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
12 |
13 | foo['bar'] not in obj.keys() # SIM118
|
= help: Remove `.keys()`
Fix
8 8 |
9 9 | foo["bar"] not in obj.keys() # SIM118
10 10 |
11 |-foo['bar'] in obj.keys() # SIM118
11 |+foo['bar'] in obj # SIM118
12 12 |
13 13 | foo['bar'] not in obj.keys() # SIM118
14 14 |
SIM118.py:13:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
|
11 | foo['bar'] in obj.keys() # SIM118
12 |
13 | foo['bar'] not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
14 |
15 | foo() in obj.keys() # SIM118
|
= help: Remove `.keys()`
Fix
10 10 |
11 11 | foo['bar'] in obj.keys() # SIM118
12 12 |
13 |-foo['bar'] not in obj.keys() # SIM118
13 |+foo['bar'] not in obj # SIM118
14 14 |
15 15 | foo() in obj.keys() # SIM118
16 16 |
SIM118.py:15:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
13 | foo['bar'] not in obj.keys() # SIM118
14 |
15 | foo() in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^ SIM118
16 |
17 | foo() not in obj.keys() # SIM118
|
= help: Remove `.keys()`
Fix
12 12 |
13 13 | foo['bar'] not in obj.keys() # SIM118
14 14 |
15 |-foo() in obj.keys() # SIM118
15 |+foo() in obj # SIM118
16 16 |
17 17 | foo() not in obj.keys() # SIM118
18 18 |
SIM118.py:17:1: SIM118 [*] Use `key not in dict` instead of `key not in dict.keys()`
|
15 | foo() in obj.keys() # SIM118
16 |
17 | foo() not in obj.keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM118
18 |
19 | for key in obj.keys(): # SIM118
|
= help: Remove `.keys()`
Fix
14 14 |
15 15 | foo() in obj.keys() # SIM118
16 16 |
17 |-foo() not in obj.keys() # SIM118
17 |+foo() not in obj # SIM118
18 18 |
19 19 | for key in obj.keys(): # SIM118
20 20 | pass
SIM118.py:19:5: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
17 | foo() not in obj.keys() # SIM118
18 |
19 | for key in obj.keys(): # SIM118
| ^^^^^^^^^^^^^^^^^ SIM118
20 | pass
|
= help: Remove `.keys()`
Fix
16 16 |
17 17 | foo() not in obj.keys() # SIM118
18 18 |
19 |-for key in obj.keys(): # SIM118
19 |+for key in obj: # SIM118
20 20 | pass
21 21 |
22 22 | for key in list(obj.keys()):
SIM118.py:26:8: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
24 | del obj[key]
25 |
26 | [k for k in obj.keys()] # SIM118
| ^^^^^^^^^^^^^^^ SIM118
27 |
28 | {k for k in obj.keys()} # SIM118
|
= help: Remove `.keys()`
Fix
23 23 | if some_property(key):
24 24 | del obj[key]
25 25 |
26 |-[k for k in obj.keys()] # SIM118
26 |+[k for k in obj] # SIM118
27 27 |
28 28 | {k for k in obj.keys()} # SIM118
29 29 |
SIM118.py:28:8: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
26 | [k for k in obj.keys()] # SIM118
27 |
28 | {k for k in obj.keys()} # SIM118
| ^^^^^^^^^^^^^^^ SIM118
29 |
30 | {k: k for k in obj.keys()} # SIM118
|
= help: Remove `.keys()`
Fix
25 25 |
26 26 | [k for k in obj.keys()] # SIM118
27 27 |
28 |-{k for k in obj.keys()} # SIM118
28 |+{k for k in obj} # SIM118
29 29 |
30 30 | {k: k for k in obj.keys()} # SIM118
31 31 |
SIM118.py:30:11: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
28 | {k for k in obj.keys()} # SIM118
29 |
30 | {k: k for k in obj.keys()} # SIM118
| ^^^^^^^^^^^^^^^ SIM118
31 |
32 | (k for k in obj.keys()) # SIM118
|
= help: Remove `.keys()`
Fix
27 27 |
28 28 | {k for k in obj.keys()} # SIM118
29 29 |
30 |-{k: k for k in obj.keys()} # SIM118
30 |+{k: k for k in obj} # SIM118
31 31 |
32 32 | (k for k in obj.keys()) # SIM118
33 33 |
SIM118.py:32:8: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
30 | {k: k for k in obj.keys()} # SIM118
31 |
32 | (k for k in obj.keys()) # SIM118
| ^^^^^^^^^^^^^^^ SIM118
33 |
34 | key in (obj or {}).keys() # SIM118
|
= help: Remove `.keys()`
Fix
29 29 |
30 30 | {k: k for k in obj.keys()} # SIM118
31 31 |
32 |-(k for k in obj.keys()) # SIM118
32 |+(k for k in obj) # SIM118
33 33 |
34 34 | key in (obj or {}).keys() # SIM118
35 35 |
SIM118.py:34:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
32 | (k for k in obj.keys()) # SIM118
33 |
34 | key in (obj or {}).keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
35 |
36 | (key) in (obj or {}).keys() # SIM118
|
= help: Remove `.keys()`
Suggested fix
31 31 |
32 32 | (k for k in obj.keys()) # SIM118
33 33 |
34 |-key in (obj or {}).keys() # SIM118
34 |+key in (obj or {}) # SIM118
35 35 |
36 36 | (key) in (obj or {}).keys() # SIM118
37 37 |
SIM118.py:36:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
34 | key in (obj or {}).keys() # SIM118
35 |
36 | (key) in (obj or {}).keys() # SIM118
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM118
37 |
38 | from typing import KeysView
|
= help: Remove `.keys()`
Suggested fix
33 33 |
34 34 | key in (obj or {}).keys() # SIM118
35 35 |
36 |-(key) in (obj or {}).keys() # SIM118
36 |+(key) in (obj or {}) # SIM118
37 37 |
38 38 | from typing import KeysView
39 39 |
SIM118.py:50:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
49 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
50 | key in obj.keys()and foo
| ^^^^^^^^^^^^^^^^^ SIM118
51 | (key in obj.keys())and foo
52 | key in (obj.keys())and foo
|
= help: Remove `.keys()`
Fix
47 47 |
48 48 |
49 49 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
50 |-key in obj.keys()and foo
50 |+key in obj and foo
51 51 | (key in obj.keys())and foo
52 52 | key in (obj.keys())and foo
53 53 |
SIM118.py:51:2: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
49 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
50 | key in obj.keys()and foo
51 | (key in obj.keys())and foo
| ^^^^^^^^^^^^^^^^^ SIM118
52 | key in (obj.keys())and foo
|
= help: Remove `.keys()`
Fix
48 48 |
49 49 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
50 50 | key in obj.keys()and foo
51 |-(key in obj.keys())and foo
51 |+(key in obj)and foo
52 52 | key in (obj.keys())and foo
53 53 |
54 54 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
SIM118.py:52:1: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
50 | key in obj.keys()and foo
51 | (key in obj.keys())and foo
52 | key in (obj.keys())and foo
| ^^^^^^^^^^^^^^^^^^^ SIM118
53 |
54 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
|
= help: Remove `.keys()`
Fix
49 49 | # Regression test for: https://github.com/astral-sh/ruff/issues/7124
50 50 | key in obj.keys()and foo
51 51 | (key in obj.keys())and foo
52 |-key in (obj.keys())and foo
52 |+key in (obj)and foo
53 53 |
54 54 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
55 55 | for key in (
SIM118.py:55:5: SIM118 [*] Use `key in dict` instead of `key in dict.keys()`
|
54 | # Regression test for: https://github.com/astral-sh/ruff/issues/7200
55 | for key in (
| _____^
56 | | self.experiment.surveys[0]
57 | | .stations[0]
58 | | .keys()
59 | | ):
| |_^ SIM118
60 | continue
|
= help: Remove `.keys()`
Suggested fix
55 55 | for key in (
56 56 | self.experiment.surveys[0]
57 57 | .stations[0]
58 |- .keys()
58 |+
59 59 | ):
60 60 | continue

View File

@@ -0,0 +1,157 @@
use ruff_python_ast::call_path::CallPath;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum MethodName {
AcloseForcefully,
CancelScope,
CancelShieldedCheckpoint,
Checkpoint,
CheckpointIfCancelled,
FailAfter,
FailAt,
MoveOnAfter,
MoveOnAt,
OpenFile,
OpenProcess,
OpenSslOverTcpListeners,
OpenSslOverTcpStream,
OpenTcpListeners,
OpenTcpStream,
OpenUnixSocket,
PermanentlyDetachCoroutineObject,
ReattachDetachedCoroutineObject,
RunProcess,
ServeListeners,
ServeSslOverTcp,
ServeTcp,
Sleep,
SleepForever,
TemporarilyDetachCoroutineObject,
WaitReadable,
WaitTaskRescheduled,
WaitWritable,
}
impl MethodName {
/// Returns `true` if the method is async, `false` if it is sync.
pub(super) fn is_async(self) -> bool {
match self {
MethodName::AcloseForcefully
| MethodName::CancelShieldedCheckpoint
| MethodName::Checkpoint
| MethodName::CheckpointIfCancelled
| MethodName::OpenFile
| MethodName::OpenProcess
| MethodName::OpenSslOverTcpListeners
| MethodName::OpenSslOverTcpStream
| MethodName::OpenTcpListeners
| MethodName::OpenTcpStream
| MethodName::OpenUnixSocket
| MethodName::PermanentlyDetachCoroutineObject
| MethodName::ReattachDetachedCoroutineObject
| MethodName::RunProcess
| MethodName::ServeListeners
| MethodName::ServeSslOverTcp
| MethodName::ServeTcp
| MethodName::Sleep
| MethodName::SleepForever
| MethodName::TemporarilyDetachCoroutineObject
| MethodName::WaitReadable
| MethodName::WaitTaskRescheduled
| MethodName::WaitWritable => true,
MethodName::MoveOnAfter
| MethodName::MoveOnAt
| MethodName::FailAfter
| MethodName::FailAt
| MethodName::CancelScope => false,
}
}
}
impl MethodName {
pub(super) fn try_from(call_path: &CallPath<'_>) -> Option<Self> {
match call_path.as_slice() {
["trio", "CancelScope"] => Some(Self::CancelScope),
["trio", "aclose_forcefully"] => Some(Self::AcloseForcefully),
["trio", "fail_after"] => Some(Self::FailAfter),
["trio", "fail_at"] => Some(Self::FailAt),
["trio", "lowlevel", "cancel_shielded_checkpoint"] => {
Some(Self::CancelShieldedCheckpoint)
}
["trio", "lowlevel", "checkpoint"] => Some(Self::Checkpoint),
["trio", "lowlevel", "checkpoint_if_cancelled"] => Some(Self::CheckpointIfCancelled),
["trio", "lowlevel", "open_process"] => Some(Self::OpenProcess),
["trio", "lowlevel", "permanently_detach_coroutine_object"] => {
Some(Self::PermanentlyDetachCoroutineObject)
}
["trio", "lowlevel", "reattach_detached_coroutine_object"] => {
Some(Self::ReattachDetachedCoroutineObject)
}
["trio", "lowlevel", "temporarily_detach_coroutine_object"] => {
Some(Self::TemporarilyDetachCoroutineObject)
}
["trio", "lowlevel", "wait_readable"] => Some(Self::WaitReadable),
["trio", "lowlevel", "wait_task_rescheduled"] => Some(Self::WaitTaskRescheduled),
["trio", "lowlevel", "wait_writable"] => Some(Self::WaitWritable),
["trio", "move_on_after"] => Some(Self::MoveOnAfter),
["trio", "move_on_at"] => Some(Self::MoveOnAt),
["trio", "open_file"] => Some(Self::OpenFile),
["trio", "open_ssl_over_tcp_listeners"] => Some(Self::OpenSslOverTcpListeners),
["trio", "open_ssl_over_tcp_stream"] => Some(Self::OpenSslOverTcpStream),
["trio", "open_tcp_listeners"] => Some(Self::OpenTcpListeners),
["trio", "open_tcp_stream"] => Some(Self::OpenTcpStream),
["trio", "open_unix_socket"] => Some(Self::OpenUnixSocket),
["trio", "run_process"] => Some(Self::RunProcess),
["trio", "serve_listeners"] => Some(Self::ServeListeners),
["trio", "serve_ssl_over_tcp"] => Some(Self::ServeSslOverTcp),
["trio", "serve_tcp"] => Some(Self::ServeTcp),
["trio", "sleep"] => Some(Self::Sleep),
["trio", "sleep_forever"] => Some(Self::SleepForever),
_ => None,
}
}
}
impl std::fmt::Display for MethodName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MethodName::AcloseForcefully => write!(f, "trio.aclose_forcefully"),
MethodName::CancelScope => write!(f, "trio.CancelScope"),
MethodName::CancelShieldedCheckpoint => {
write!(f, "trio.lowlevel.cancel_shielded_checkpoint")
}
MethodName::Checkpoint => write!(f, "trio.lowlevel.checkpoint"),
MethodName::CheckpointIfCancelled => write!(f, "trio.lowlevel.checkpoint_if_cancelled"),
MethodName::FailAfter => write!(f, "trio.fail_after"),
MethodName::FailAt => write!(f, "trio.fail_at"),
MethodName::MoveOnAfter => write!(f, "trio.move_on_after"),
MethodName::MoveOnAt => write!(f, "trio.move_on_at"),
MethodName::OpenFile => write!(f, "trio.open_file"),
MethodName::OpenProcess => write!(f, "trio.lowlevel.open_process"),
MethodName::OpenSslOverTcpListeners => write!(f, "trio.open_ssl_over_tcp_listeners"),
MethodName::OpenSslOverTcpStream => write!(f, "trio.open_ssl_over_tcp_stream"),
MethodName::OpenTcpListeners => write!(f, "trio.open_tcp_listeners"),
MethodName::OpenTcpStream => write!(f, "trio.open_tcp_stream"),
MethodName::OpenUnixSocket => write!(f, "trio.open_unix_socket"),
MethodName::PermanentlyDetachCoroutineObject => {
write!(f, "trio.lowlevel.permanently_detach_coroutine_object")
}
MethodName::ReattachDetachedCoroutineObject => {
write!(f, "trio.lowlevel.reattach_detached_coroutine_object")
}
MethodName::RunProcess => write!(f, "trio.run_process"),
MethodName::ServeListeners => write!(f, "trio.serve_listeners"),
MethodName::ServeSslOverTcp => write!(f, "trio.serve_ssl_over_tcp"),
MethodName::ServeTcp => write!(f, "trio.serve_tcp"),
MethodName::Sleep => write!(f, "trio.sleep"),
MethodName::SleepForever => write!(f, "trio.sleep_forever"),
MethodName::TemporarilyDetachCoroutineObject => {
write!(f, "trio.lowlevel.temporarily_detach_coroutine_object")
}
MethodName::WaitReadable => write!(f, "trio.lowlevel.wait_readable"),
MethodName::WaitTaskRescheduled => write!(f, "trio.lowlevel.wait_task_rescheduled"),
MethodName::WaitWritable => write!(f, "trio.lowlevel.wait_writable"),
}
}
}

View File

@@ -1,4 +1,5 @@
//! Rules from [flake8-trio](https://pypi.org/project/flake8-trio/).
pub(super) mod method_name;
pub(crate) mod rules;
#[cfg(test)]
@@ -14,6 +15,8 @@ mod tests {
use crate::test::test_path;
#[test_case(Rule::TrioTimeoutWithoutAwait, Path::new("TRIO100.py"))]
#[test_case(Rule::TrioSyncCall, Path::new("TRIO105.py"))]
#[test_case(Rule::TrioZeroSleepCall, Path::new("TRIO115.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,3 +1,7 @@
pub(crate) use sync_call::*;
pub(crate) use timeout_without_await::*;
pub(crate) use zero_sleep_call::*;
mod sync_call;
mod timeout_without_await;
mod zero_sleep_call;

View File

@@ -0,0 +1,87 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, ExprCall};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
use crate::rules::flake8_trio::method_name::MethodName;
/// ## What it does
/// Checks for calls to trio functions that are not immediately awaited.
///
/// ## Why is this bad?
/// Many of the functions exposed by trio are asynchronous, and must be awaited
/// to take effect. Calling a trio function without an `await` can lead to
/// `RuntimeWarning` diagnostics and unexpected behaviour.
///
/// ## Example
/// ```python
/// async def double_sleep(x):
/// trio.sleep(2 * x)
/// ```
///
/// Use instead:
/// ```python
/// async def double_sleep(x):
/// await trio.sleep(2 * x)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as adding an `await` to a function
/// call changes its semantics and runtime behavior.
#[violation]
pub struct TrioSyncCall {
method_name: MethodName,
}
impl Violation for TrioSyncCall {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let Self { method_name } = self;
format!("Call to `{method_name}` is not immediately awaited")
}
fn fix_title(&self) -> Option<String> {
Some(format!("Add `await`"))
}
}
/// TRIO105
pub(crate) fn sync_call(checker: &mut Checker, call: &ExprCall) {
let Some(method_name) = ({
let Some(call_path) = checker.semantic().resolve_call_path(call.func.as_ref()) else {
return;
};
MethodName::try_from(&call_path)
}) else {
return;
};
if !method_name.is_async() {
return;
}
if checker
.semantic()
.current_expression_parent()
.is_some_and(Expr::is_await_expr)
{
return;
};
let mut diagnostic = Diagnostic::new(TrioSyncCall { method_name }, call.range);
if checker.semantic().in_async_context() {
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
pad(
"await".to_string(),
TextRange::new(call.func.start(), call.func.start()),
checker.locator(),
),
call.func.start(),
)));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,10 +1,11 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor};
use ruff_python_ast::{Expr, ExprAwait, Stmt, StmtWith, WithItem};
use ruff_python_ast::helpers::AwaitVisitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{StmtWith, WithItem};
use crate::checkers::ast::Checker;
use crate::rules::flake8_trio::method_name::MethodName;
/// ## What it does
/// Checks for trio functions that should contain await but don't.
@@ -56,6 +57,17 @@ pub(crate) fn timeout_without_await(
return;
};
if !matches!(
method_name,
MethodName::MoveOnAfter
| MethodName::MoveOnAt
| MethodName::FailAfter
| MethodName::FailAt
| MethodName::CancelScope
) {
return;
}
let mut visitor = AwaitVisitor::default();
visitor.visit_body(&with_stmt.body);
if visitor.seen_await {
@@ -67,59 +79,3 @@ pub(crate) fn timeout_without_await(
with_stmt.range,
));
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum MethodName {
MoveOnAfter,
MoveOnAt,
FailAfter,
FailAt,
CancelScope,
}
impl MethodName {
fn try_from(call_path: &CallPath<'_>) -> Option<Self> {
match call_path.as_slice() {
["trio", "move_on_after"] => Some(Self::MoveOnAfter),
["trio", "move_on_at"] => Some(Self::MoveOnAt),
["trio", "fail_after"] => Some(Self::FailAfter),
["trio", "fail_at"] => Some(Self::FailAt),
["trio", "CancelScope"] => Some(Self::CancelScope),
_ => None,
}
}
}
impl std::fmt::Display for MethodName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MethodName::MoveOnAfter => write!(f, "trio.move_on_after"),
MethodName::MoveOnAt => write!(f, "trio.move_on_at"),
MethodName::FailAfter => write!(f, "trio.fail_after"),
MethodName::FailAt => write!(f, "trio.fail_at"),
MethodName::CancelScope => write!(f, "trio.CancelScope"),
}
}
}
#[derive(Debug, Default)]
struct AwaitVisitor {
seen_await: bool,
}
impl Visitor<'_> for AwaitVisitor {
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => (),
_ => walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &Expr) {
if let Expr::Await(ExprAwait { .. }) = expr {
self.seen_await = true;
} else {
walk_expr(self, expr);
}
}
}

View File

@@ -0,0 +1,109 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Stmt;
use ruff_python_ast::{self as ast, Expr, ExprCall, Int};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
/// ## What it does
/// Checks for uses of `trio.sleep(0)`.
///
/// ## Why is this bad?
/// `trio.sleep(0)` is equivalent to calling `trio.lowlevel.checkpoint()`.
/// However, the latter better conveys the intent of the code.
///
/// ## Example
/// ```python
/// async def func():
/// await trio.sleep(0)
/// ```
///
/// Use instead:
/// ```python
/// async def func():
/// await trio.lowlevel.checkpoint()
/// ```
#[violation]
pub struct TrioZeroSleepCall;
impl AlwaysFixableViolation for TrioZeroSleepCall {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`")
}
fn fix_title(&self) -> String {
format!("Replace with `trio.lowlevel.checkpoint()`")
}
}
/// TRIO115
pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
if !checker
.semantic()
.resolve_call_path(call.func.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["trio", "sleep"]))
{
return;
}
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument("seconds", 0) else {
return;
};
match arg {
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
let Some(int) = value.as_int() else { return };
if *int != Int::ZERO {
return;
}
}
Expr::Name(ast::ExprName { id, .. }) => {
let scope = checker.semantic().current_scope();
if let Some(binding_id) = scope.get(id) {
let binding = checker.semantic().binding(binding_id);
if binding.kind.is_assignment() || binding.kind.is_named_expr_assignment() {
if let Some(parent_id) = binding.source {
let parent = checker.semantic().statement(parent_id);
if let Stmt::Assign(ast::StmtAssign { value, .. })
| Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
})
| Stmt::AugAssign(ast::StmtAugAssign { value, .. }) = parent
{
let Expr::NumberLiteral(ast::ExprNumberLiteral { value: num, .. }) =
value.as_ref()
else {
return;
};
let Some(int) = num.as_int() else { return };
if *int != Int::ZERO {
return;
}
}
}
}
}
}
_ => return,
}
let mut diagnostic = Diagnostic::new(TrioZeroSleepCall, call.range());
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("trio", "lowlevel.checkpoint"),
call.func.start(),
checker.semantic(),
)?;
let reference_edit = Edit::range_replacement(binding, call.func.range());
let arg_edit = Edit::range_deletion(call.arguments.range);
Ok(Fix::safe_edits(import_edit, [reference_edit, arg_edit]))
});
checker.diagnostics.push(diagnostic);
}

View File

@@ -0,0 +1,514 @@
---
source: crates/ruff_linter/src/rules/flake8_trio/mod.rs
---
TRIO105.py:30:5: TRIO105 [*] Call to `trio.aclose_forcefully` is not immediately awaited
|
29 | # TRIO105
30 | trio.aclose_forcefully(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
31 | trio.open_file(foo)
32 | trio.open_ssl_over_tcp_listeners(foo, foo)
|
= help: Add `await`
Suggested fix
27 27 | await trio.lowlevel.wait_writable(foo)
28 28 |
29 29 | # TRIO105
30 |- trio.aclose_forcefully(foo)
30 |+ await trio.aclose_forcefully(foo)
31 31 | trio.open_file(foo)
32 32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 33 | trio.open_ssl_over_tcp_stream(foo, foo)
TRIO105.py:31:5: TRIO105 [*] Call to `trio.open_file` is not immediately awaited
|
29 | # TRIO105
30 | trio.aclose_forcefully(foo)
31 | trio.open_file(foo)
| ^^^^^^^^^^^^^^^^^^^ TRIO105
32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 | trio.open_ssl_over_tcp_stream(foo, foo)
|
= help: Add `await`
Suggested fix
28 28 |
29 29 | # TRIO105
30 30 | trio.aclose_forcefully(foo)
31 |- trio.open_file(foo)
31 |+ await trio.open_file(foo)
32 32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 34 | trio.open_tcp_listeners(foo)
TRIO105.py:32:5: TRIO105 [*] Call to `trio.open_ssl_over_tcp_listeners` is not immediately awaited
|
30 | trio.aclose_forcefully(foo)
31 | trio.open_file(foo)
32 | trio.open_ssl_over_tcp_listeners(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 | trio.open_tcp_listeners(foo)
|
= help: Add `await`
Suggested fix
29 29 | # TRIO105
30 30 | trio.aclose_forcefully(foo)
31 31 | trio.open_file(foo)
32 |- trio.open_ssl_over_tcp_listeners(foo, foo)
32 |+ await trio.open_ssl_over_tcp_listeners(foo, foo)
33 33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 34 | trio.open_tcp_listeners(foo)
35 35 | trio.open_tcp_stream(foo, foo)
TRIO105.py:33:5: TRIO105 [*] Call to `trio.open_ssl_over_tcp_stream` is not immediately awaited
|
31 | trio.open_file(foo)
32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 | trio.open_ssl_over_tcp_stream(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
34 | trio.open_tcp_listeners(foo)
35 | trio.open_tcp_stream(foo, foo)
|
= help: Add `await`
Suggested fix
30 30 | trio.aclose_forcefully(foo)
31 31 | trio.open_file(foo)
32 32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 |- trio.open_ssl_over_tcp_stream(foo, foo)
33 |+ await trio.open_ssl_over_tcp_stream(foo, foo)
34 34 | trio.open_tcp_listeners(foo)
35 35 | trio.open_tcp_stream(foo, foo)
36 36 | trio.open_unix_socket(foo)
TRIO105.py:34:5: TRIO105 [*] Call to `trio.open_tcp_listeners` is not immediately awaited
|
32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 | trio.open_tcp_listeners(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
35 | trio.open_tcp_stream(foo, foo)
36 | trio.open_unix_socket(foo)
|
= help: Add `await`
Suggested fix
31 31 | trio.open_file(foo)
32 32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 |- trio.open_tcp_listeners(foo)
34 |+ await trio.open_tcp_listeners(foo)
35 35 | trio.open_tcp_stream(foo, foo)
36 36 | trio.open_unix_socket(foo)
37 37 | trio.run_process(foo)
TRIO105.py:35:5: TRIO105 [*] Call to `trio.open_tcp_stream` is not immediately awaited
|
33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 | trio.open_tcp_listeners(foo)
35 | trio.open_tcp_stream(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
36 | trio.open_unix_socket(foo)
37 | trio.run_process(foo)
|
= help: Add `await`
Suggested fix
32 32 | trio.open_ssl_over_tcp_listeners(foo, foo)
33 33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 34 | trio.open_tcp_listeners(foo)
35 |- trio.open_tcp_stream(foo, foo)
35 |+ await trio.open_tcp_stream(foo, foo)
36 36 | trio.open_unix_socket(foo)
37 37 | trio.run_process(foo)
38 38 | trio.serve_listeners(foo, foo)
TRIO105.py:36:5: TRIO105 [*] Call to `trio.open_unix_socket` is not immediately awaited
|
34 | trio.open_tcp_listeners(foo)
35 | trio.open_tcp_stream(foo, foo)
36 | trio.open_unix_socket(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
37 | trio.run_process(foo)
38 | trio.serve_listeners(foo, foo)
|
= help: Add `await`
Suggested fix
33 33 | trio.open_ssl_over_tcp_stream(foo, foo)
34 34 | trio.open_tcp_listeners(foo)
35 35 | trio.open_tcp_stream(foo, foo)
36 |- trio.open_unix_socket(foo)
36 |+ await trio.open_unix_socket(foo)
37 37 | trio.run_process(foo)
38 38 | trio.serve_listeners(foo, foo)
39 39 | trio.serve_ssl_over_tcp(foo, foo, foo)
TRIO105.py:37:5: TRIO105 [*] Call to `trio.run_process` is not immediately awaited
|
35 | trio.open_tcp_stream(foo, foo)
36 | trio.open_unix_socket(foo)
37 | trio.run_process(foo)
| ^^^^^^^^^^^^^^^^^^^^^ TRIO105
38 | trio.serve_listeners(foo, foo)
39 | trio.serve_ssl_over_tcp(foo, foo, foo)
|
= help: Add `await`
Suggested fix
34 34 | trio.open_tcp_listeners(foo)
35 35 | trio.open_tcp_stream(foo, foo)
36 36 | trio.open_unix_socket(foo)
37 |- trio.run_process(foo)
37 |+ await trio.run_process(foo)
38 38 | trio.serve_listeners(foo, foo)
39 39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 40 | trio.serve_tcp(foo, foo)
TRIO105.py:38:5: TRIO105 [*] Call to `trio.serve_listeners` is not immediately awaited
|
36 | trio.open_unix_socket(foo)
37 | trio.run_process(foo)
38 | trio.serve_listeners(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 | trio.serve_tcp(foo, foo)
|
= help: Add `await`
Suggested fix
35 35 | trio.open_tcp_stream(foo, foo)
36 36 | trio.open_unix_socket(foo)
37 37 | trio.run_process(foo)
38 |- trio.serve_listeners(foo, foo)
38 |+ await trio.serve_listeners(foo, foo)
39 39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 40 | trio.serve_tcp(foo, foo)
41 41 | trio.sleep(foo)
TRIO105.py:39:5: TRIO105 [*] Call to `trio.serve_ssl_over_tcp` is not immediately awaited
|
37 | trio.run_process(foo)
38 | trio.serve_listeners(foo, foo)
39 | trio.serve_ssl_over_tcp(foo, foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
40 | trio.serve_tcp(foo, foo)
41 | trio.sleep(foo)
|
= help: Add `await`
Suggested fix
36 36 | trio.open_unix_socket(foo)
37 37 | trio.run_process(foo)
38 38 | trio.serve_listeners(foo, foo)
39 |- trio.serve_ssl_over_tcp(foo, foo, foo)
39 |+ await trio.serve_ssl_over_tcp(foo, foo, foo)
40 40 | trio.serve_tcp(foo, foo)
41 41 | trio.sleep(foo)
42 42 | trio.sleep_forever()
TRIO105.py:40:5: TRIO105 [*] Call to `trio.serve_tcp` is not immediately awaited
|
38 | trio.serve_listeners(foo, foo)
39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 | trio.serve_tcp(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
41 | trio.sleep(foo)
42 | trio.sleep_forever()
|
= help: Add `await`
Suggested fix
37 37 | trio.run_process(foo)
38 38 | trio.serve_listeners(foo, foo)
39 39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 |- trio.serve_tcp(foo, foo)
40 |+ await trio.serve_tcp(foo, foo)
41 41 | trio.sleep(foo)
42 42 | trio.sleep_forever()
43 43 | trio.sleep_until(foo)
TRIO105.py:41:5: TRIO105 [*] Call to `trio.sleep` is not immediately awaited
|
39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 | trio.serve_tcp(foo, foo)
41 | trio.sleep(foo)
| ^^^^^^^^^^^^^^^ TRIO105
42 | trio.sleep_forever()
43 | trio.sleep_until(foo)
|
= help: Add `await`
Suggested fix
38 38 | trio.serve_listeners(foo, foo)
39 39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 40 | trio.serve_tcp(foo, foo)
41 |- trio.sleep(foo)
41 |+ await trio.sleep(foo)
42 42 | trio.sleep_forever()
43 43 | trio.sleep_until(foo)
44 44 | trio.lowlevel.cancel_shielded_checkpoint()
TRIO105.py:42:5: TRIO105 [*] Call to `trio.sleep_forever` is not immediately awaited
|
40 | trio.serve_tcp(foo, foo)
41 | trio.sleep(foo)
42 | trio.sleep_forever()
| ^^^^^^^^^^^^^^^^^^^^ TRIO105
43 | trio.sleep_until(foo)
44 | trio.lowlevel.cancel_shielded_checkpoint()
|
= help: Add `await`
Suggested fix
39 39 | trio.serve_ssl_over_tcp(foo, foo, foo)
40 40 | trio.serve_tcp(foo, foo)
41 41 | trio.sleep(foo)
42 |- trio.sleep_forever()
42 |+ await trio.sleep_forever()
43 43 | trio.sleep_until(foo)
44 44 | trio.lowlevel.cancel_shielded_checkpoint()
45 45 | trio.lowlevel.checkpoint()
TRIO105.py:44:5: TRIO105 [*] Call to `trio.lowlevel.cancel_shielded_checkpoint` is not immediately awaited
|
42 | trio.sleep_forever()
43 | trio.sleep_until(foo)
44 | trio.lowlevel.cancel_shielded_checkpoint()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
45 | trio.lowlevel.checkpoint()
46 | trio.lowlevel.checkpoint_if_cancelled()
|
= help: Add `await`
Suggested fix
41 41 | trio.sleep(foo)
42 42 | trio.sleep_forever()
43 43 | trio.sleep_until(foo)
44 |- trio.lowlevel.cancel_shielded_checkpoint()
44 |+ await trio.lowlevel.cancel_shielded_checkpoint()
45 45 | trio.lowlevel.checkpoint()
46 46 | trio.lowlevel.checkpoint_if_cancelled()
47 47 | trio.lowlevel.open_process()
TRIO105.py:45:5: TRIO105 [*] Call to `trio.lowlevel.checkpoint` is not immediately awaited
|
43 | trio.sleep_until(foo)
44 | trio.lowlevel.cancel_shielded_checkpoint()
45 | trio.lowlevel.checkpoint()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
46 | trio.lowlevel.checkpoint_if_cancelled()
47 | trio.lowlevel.open_process()
|
= help: Add `await`
Suggested fix
42 42 | trio.sleep_forever()
43 43 | trio.sleep_until(foo)
44 44 | trio.lowlevel.cancel_shielded_checkpoint()
45 |- trio.lowlevel.checkpoint()
45 |+ await trio.lowlevel.checkpoint()
46 46 | trio.lowlevel.checkpoint_if_cancelled()
47 47 | trio.lowlevel.open_process()
48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
TRIO105.py:46:5: TRIO105 [*] Call to `trio.lowlevel.checkpoint_if_cancelled` is not immediately awaited
|
44 | trio.lowlevel.cancel_shielded_checkpoint()
45 | trio.lowlevel.checkpoint()
46 | trio.lowlevel.checkpoint_if_cancelled()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
47 | trio.lowlevel.open_process()
48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
|
= help: Add `await`
Suggested fix
43 43 | trio.sleep_until(foo)
44 44 | trio.lowlevel.cancel_shielded_checkpoint()
45 45 | trio.lowlevel.checkpoint()
46 |- trio.lowlevel.checkpoint_if_cancelled()
46 |+ await trio.lowlevel.checkpoint_if_cancelled()
47 47 | trio.lowlevel.open_process()
48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
TRIO105.py:47:5: TRIO105 [*] Call to `trio.lowlevel.open_process` is not immediately awaited
|
45 | trio.lowlevel.checkpoint()
46 | trio.lowlevel.checkpoint_if_cancelled()
47 | trio.lowlevel.open_process()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
|
= help: Add `await`
Suggested fix
44 44 | trio.lowlevel.cancel_shielded_checkpoint()
45 45 | trio.lowlevel.checkpoint()
46 46 | trio.lowlevel.checkpoint_if_cancelled()
47 |- trio.lowlevel.open_process()
47 |+ await trio.lowlevel.open_process()
48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
TRIO105.py:48:5: TRIO105 [*] Call to `trio.lowlevel.permanently_detach_coroutine_object` is not immediately awaited
|
46 | trio.lowlevel.checkpoint_if_cancelled()
47 | trio.lowlevel.open_process()
48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
|
= help: Add `await`
Suggested fix
45 45 | trio.lowlevel.checkpoint()
46 46 | trio.lowlevel.checkpoint_if_cancelled()
47 47 | trio.lowlevel.open_process()
48 |- trio.lowlevel.permanently_detach_coroutine_object(foo)
48 |+ await trio.lowlevel.permanently_detach_coroutine_object(foo)
49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 51 | trio.lowlevel.wait_readable(foo)
TRIO105.py:49:5: TRIO105 [*] Call to `trio.lowlevel.reattach_detached_coroutine_object` is not immediately awaited
|
47 | trio.lowlevel.open_process()
48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 | trio.lowlevel.wait_readable(foo)
|
= help: Add `await`
Suggested fix
46 46 | trio.lowlevel.checkpoint_if_cancelled()
47 47 | trio.lowlevel.open_process()
48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 |- trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
49 |+ await trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 51 | trio.lowlevel.wait_readable(foo)
52 52 | trio.lowlevel.wait_task_rescheduled(foo)
TRIO105.py:50:5: TRIO105 [*] Call to `trio.lowlevel.temporarily_detach_coroutine_object` is not immediately awaited
|
48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
51 | trio.lowlevel.wait_readable(foo)
52 | trio.lowlevel.wait_task_rescheduled(foo)
|
= help: Add `await`
Suggested fix
47 47 | trio.lowlevel.open_process()
48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 |- trio.lowlevel.temporarily_detach_coroutine_object(foo)
50 |+ await trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 51 | trio.lowlevel.wait_readable(foo)
52 52 | trio.lowlevel.wait_task_rescheduled(foo)
53 53 | trio.lowlevel.wait_writable(foo)
TRIO105.py:51:5: TRIO105 [*] Call to `trio.lowlevel.wait_readable` is not immediately awaited
|
49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 | trio.lowlevel.wait_readable(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
52 | trio.lowlevel.wait_task_rescheduled(foo)
53 | trio.lowlevel.wait_writable(foo)
|
= help: Add `await`
Suggested fix
48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo)
49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 |- trio.lowlevel.wait_readable(foo)
51 |+ await trio.lowlevel.wait_readable(foo)
52 52 | trio.lowlevel.wait_task_rescheduled(foo)
53 53 | trio.lowlevel.wait_writable(foo)
54 54 |
TRIO105.py:52:5: TRIO105 [*] Call to `trio.lowlevel.wait_task_rescheduled` is not immediately awaited
|
50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 | trio.lowlevel.wait_readable(foo)
52 | trio.lowlevel.wait_task_rescheduled(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
53 | trio.lowlevel.wait_writable(foo)
|
= help: Add `await`
Suggested fix
49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo)
50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 51 | trio.lowlevel.wait_readable(foo)
52 |- trio.lowlevel.wait_task_rescheduled(foo)
52 |+ await trio.lowlevel.wait_task_rescheduled(foo)
53 53 | trio.lowlevel.wait_writable(foo)
54 54 |
55 55 | async with await trio.open_file(foo): # Ok
TRIO105.py:53:5: TRIO105 [*] Call to `trio.lowlevel.wait_writable` is not immediately awaited
|
51 | trio.lowlevel.wait_readable(foo)
52 | trio.lowlevel.wait_task_rescheduled(foo)
53 | trio.lowlevel.wait_writable(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105
54 |
55 | async with await trio.open_file(foo): # Ok
|
= help: Add `await`
Suggested fix
50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo)
51 51 | trio.lowlevel.wait_readable(foo)
52 52 | trio.lowlevel.wait_task_rescheduled(foo)
53 |- trio.lowlevel.wait_writable(foo)
53 |+ await trio.lowlevel.wait_writable(foo)
54 54 |
55 55 | async with await trio.open_file(foo): # Ok
56 56 | pass
TRIO105.py:58:16: TRIO105 [*] Call to `trio.open_file` is not immediately awaited
|
56 | pass
57 |
58 | async with trio.open_file(foo): # TRIO105
| ^^^^^^^^^^^^^^^^^^^ TRIO105
59 | pass
|
= help: Add `await`
Suggested fix
55 55 | async with await trio.open_file(foo): # Ok
56 56 | pass
57 57 |
58 |- async with trio.open_file(foo): # TRIO105
58 |+ async with await trio.open_file(foo): # TRIO105
59 59 | pass
60 60 |
61 61 |
TRIO105.py:64:5: TRIO105 Call to `trio.open_file` is not immediately awaited
|
62 | def func() -> None:
63 | # TRIO105 (without fix)
64 | trio.open_file(foo)
| ^^^^^^^^^^^^^^^^^^^ TRIO105
|
= help: Add `await`

View File

@@ -0,0 +1,119 @@
---
source: crates/ruff_linter/src/rules/flake8_trio/mod.rs
---
TRIO115.py:6:11: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
5 | async def func():
6 | await trio.sleep(0) # TRIO115
| ^^^^^^^^^^^^^ TRIO115
7 | await trio.sleep(1) # OK
8 | await trio.sleep(0, 1) # OK
|
= help: Replace with `trio.lowlevel.checkpoint()`
Fix
3 3 |
4 4 |
5 5 | async def func():
6 |- await trio.sleep(0) # TRIO115
6 |+ await trio.lowlevel.checkpoint # TRIO115
7 7 | await trio.sleep(1) # OK
8 8 | await trio.sleep(0, 1) # OK
9 9 | await trio.sleep(...) # OK
TRIO115.py:12:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
10 | await trio.sleep() # OK
11 |
12 | trio.sleep(0) # TRIO115
| ^^^^^^^^^^^^^ TRIO115
13 | foo = 0
14 | trio.sleep(foo) # TRIO115
|
= help: Replace with `trio.lowlevel.checkpoint()`
Fix
9 9 | await trio.sleep(...) # OK
10 10 | await trio.sleep() # OK
11 11 |
12 |- trio.sleep(0) # TRIO115
12 |+ trio.lowlevel.checkpoint # TRIO115
13 13 | foo = 0
14 14 | trio.sleep(foo) # TRIO115
15 15 | trio.sleep(1) # OK
TRIO115.py:14:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
12 | trio.sleep(0) # TRIO115
13 | foo = 0
14 | trio.sleep(foo) # TRIO115
| ^^^^^^^^^^^^^^^ TRIO115
15 | trio.sleep(1) # OK
16 | time.sleep(0) # OK
|
= help: Replace with `trio.lowlevel.checkpoint()`
Fix
11 11 |
12 12 | trio.sleep(0) # TRIO115
13 13 | foo = 0
14 |- trio.sleep(foo) # TRIO115
14 |+ trio.lowlevel.checkpoint # TRIO115
15 15 | trio.sleep(1) # OK
16 16 | time.sleep(0) # OK
17 17 |
TRIO115.py:18:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
16 | time.sleep(0) # OK
17 |
18 | sleep(0) # TRIO115
| ^^^^^^^^ TRIO115
19 |
20 | bar = "bar"
|
= help: Replace with `trio.lowlevel.checkpoint()`
Fix
15 15 | trio.sleep(1) # OK
16 16 | time.sleep(0) # OK
17 17 |
18 |- sleep(0) # TRIO115
18 |+ trio.lowlevel.checkpoint # TRIO115
19 19 |
20 20 | bar = "bar"
21 21 | trio.sleep(bar)
TRIO115.py:24:1: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
24 | trio.sleep(0) # TRIO115
| ^^^^^^^^^^^^^ TRIO115
|
= help: Replace with `trio.lowlevel.checkpoint()`
Fix
21 21 | trio.sleep(bar)
22 22 |
23 23 |
24 |-trio.sleep(0) # TRIO115
24 |+trio.lowlevel.checkpoint # TRIO115
25 25 |
26 26 |
27 27 | def func():
TRIO115.py:28:14: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
27 | def func():
28 | trio.run(trio.sleep(0)) # TRIO115
| ^^^^^^^^^^^^^ TRIO115
|
= help: Replace with `trio.lowlevel.checkpoint()`
Fix
25 25 |
26 26 |
27 27 | def func():
28 |- trio.run(trio.sleep(0)) # TRIO115
28 |+ trio.run(trio.lowlevel.checkpoint) # TRIO115

View File

@@ -4,7 +4,6 @@ use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::analyze::typing::is_dict;
use ruff_python_semantic::Binding;
use crate::checkers::ast::Checker;
@@ -129,22 +128,16 @@ pub(crate) fn manual_dict_comprehension(checker: &mut Checker, target: &Expr, bo
}
// Exclude non-dictionary value.
let Expr::Name(ast::ExprName {
id: subscript_name, ..
}) = subscript_value.as_ref()
let Some(name) = subscript_value.as_name_expr() else {
return;
};
let Some(binding) = checker
.semantic()
.only_binding(name)
.map(|id| checker.semantic().binding(id))
else {
return;
};
let scope = checker.semantic().current_scope();
let bindings: Vec<&Binding> = scope
.get_all(subscript_name)
.map(|binding_id| checker.semantic().binding(binding_id))
.collect();
let [binding] = bindings.as_slice() else {
return;
};
if !is_dict(binding, checker.semantic()) {
return;
}
@@ -165,8 +158,7 @@ pub(crate) fn manual_dict_comprehension(checker: &mut Checker, target: &Expr, bo
// ```
if if_test.is_some_and(|test| {
any_over_expr(test, &|expr| {
expr.as_name_expr()
.is_some_and(|expr| expr.id == *subscript_name)
ComparableExpr::from(expr) == ComparableExpr::from(name)
})
}) {
return;

View File

@@ -5,7 +5,6 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_semantic::analyze::typing::is_list;
use ruff_python_semantic::Binding;
use crate::checkers::ast::Checker;
@@ -144,20 +143,16 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, bo
}
// Avoid non-list values.
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
let Some(name) = value.as_name_expr() else {
return;
};
let bindings: Vec<&Binding> = checker
let Some(binding) = checker
.semantic()
.current_scope()
.get_all(id)
.map(|binding_id| checker.semantic().binding(binding_id))
.collect();
let [binding] = bindings.as_slice() else {
.only_binding(name)
.map(|id| checker.semantic().binding(id))
else {
return;
};
if !is_list(binding, checker.semantic()) {
return;
}
@@ -176,15 +171,12 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, bo
// ```python
// filtered = [x for x in y if x in filtered]
// ```
if let Some(value_name) = value.as_name_expr() {
if if_test.is_some_and(|test| {
any_over_expr(test, &|expr| {
expr.as_name_expr()
.is_some_and(|expr| expr.id == value_name.id)
})
}) {
return;
}
if if_test.is_some_and(|test| {
any_over_expr(test, &|expr| {
expr.as_name_expr().is_some_and(|expr| expr.id == name.id)
})
}) {
return;
}
checker

View File

@@ -3,7 +3,6 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
use ruff_python_semantic::analyze::typing::is_list;
use ruff_python_semantic::Binding;
use crate::checkers::ast::Checker;
@@ -102,20 +101,16 @@ pub(crate) fn manual_list_copy(checker: &mut Checker, target: &Expr, body: &[Stm
}
// Avoid non-list values.
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
let Some(name) = value.as_name_expr() else {
return;
};
let bindings: Vec<&Binding> = checker
let Some(binding) = checker
.semantic()
.current_scope()
.get_all(id)
.map(|binding_id| checker.semantic().binding(binding_id))
.collect();
let [binding] = bindings.as_slice() else {
.only_binding(name)
.map(|id| checker.semantic().binding(id))
else {
return;
};
if !is_list(binding, checker.semantic()) {
return;
}

View File

@@ -24,7 +24,7 @@ use ruff_source_file::Locator;
/// if foo == "blah":
/// do_blah_thing()
/// ```
///
/// [PEP 8]: https://peps.python.org/pep-0008/#other-recommendations
#[violation]
pub struct MultipleStatementsOnOneLineColon;
@@ -206,12 +206,13 @@ pub(crate) fn compound_statements(
{
colon = Some((range.start(), range.end()));
// Allow `class C: ...`-style definitions in stubs.
allow_ellipsis = class.is_some();
// Allow `class C: ...`-style definitions.
allow_ellipsis = true;
}
}
Tok::Semi => {
semi = Some((range.start(), range.end()));
allow_ellipsis = false;
}
Tok::Comment(..) | Tok::Indent | Tok::Dedent | Tok::NonLogicalNewline => {}
_ => {
@@ -223,6 +224,7 @@ pub(crate) fn compound_statements(
// Reset.
semi = None;
allow_ellipsis = false;
}
if let Some((start, end)) = colon {
@@ -245,6 +247,7 @@ pub(crate) fn compound_statements(
try_ = None;
while_ = None;
with = None;
allow_ellipsis = false;
}
}
}

View File

@@ -138,6 +138,7 @@ fn deprecated_type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
| "list"
| "dict"
| "set"
| "memoryview"
) && checker.semantic().is_builtin(id)
{
checker.diagnostics.push(Diagnostic::new(
@@ -197,7 +198,98 @@ fn is_type(expr: &Expr, semantic: &SemanticModel) -> bool {
// Ex) `type(obj) == int`
matches!(
id.as_str(),
"int" | "str" | "float" | "bool" | "complex" | "bytes" | "list" | "dict" | "set"
"bool"
| "bytearray"
| "bytes"
| "classmethod"
| "complex"
| "dict"
| "enumerate"
| "filter"
| "float"
| "frozenset"
| "int"
| "list"
| "map"
| "memoryview"
| "object"
| "property"
| "range"
| "reversed"
| "set"
| "slice"
| "staticmethod"
| "str"
| "super"
| "tuple"
| "type"
| "zip"
| "ArithmeticError"
| "AssertionError"
| "AttributeError"
| "BaseException"
| "BlockingIOError"
| "BrokenPipeError"
| "BufferError"
| "BytesWarning"
| "ChildProcessError"
| "ConnectionAbortedError"
| "ConnectionError"
| "ConnectionRefusedError"
| "ConnectionResetError"
| "DeprecationWarning"
| "EnvironmentError"
| "EOFError"
| "Exception"
| "FileExistsError"
| "FileNotFoundError"
| "FloatingPointError"
| "FutureWarning"
| "GeneratorExit"
| "ImportError"
| "ImportWarning"
| "IndentationError"
| "IndexError"
| "InterruptedError"
| "IOError"
| "IsADirectoryError"
| "KeyboardInterrupt"
| "KeyError"
| "LookupError"
| "MemoryError"
| "ModuleNotFoundError"
| "NameError"
| "NotADirectoryError"
| "NotImplementedError"
| "OSError"
| "OverflowError"
| "PendingDeprecationWarning"
| "PermissionError"
| "ProcessLookupError"
| "RecursionError"
| "ReferenceError"
| "ResourceWarning"
| "RuntimeError"
| "RuntimeWarning"
| "StopAsyncIteration"
| "StopIteration"
| "SyntaxError"
| "SyntaxWarning"
| "SystemError"
| "SystemExit"
| "TabError"
| "TimeoutError"
| "TypeError"
| "UnboundLocalError"
| "UnicodeDecodeError"
| "UnicodeEncodeError"
| "UnicodeError"
| "UnicodeTranslateError"
| "UnicodeWarning"
| "UserWarning"
| "ValueError"
| "Warning"
| "ZeroDivisionError"
) && semantic.is_builtin(id)
}
_ => false,

View File

@@ -92,6 +92,8 @@ E70.py:71:4: E703 [*] Statement ends with an unnecessary semicolon
70 | a = \
71 | 5;
| ^ E703
72 | #:
73 | with x(y) as z: ...
|
= help: Remove unnecessary semicolon
@@ -101,5 +103,7 @@ E70.py:71:4: E703 [*] Statement ends with an unnecessary semicolon
70 70 | a = \
71 |- 5;
71 |+ 5
72 72 | #:
73 73 | with x(y) as z: ...

View File

@@ -17,144 +17,154 @@ E721.py:5:4: E721 Do not compare types, use `isinstance()`
5 | if type(res) != type(""):
| ^^^^^^^^^^^^^^^^^^^^^ E721
6 | pass
7 | #: Okay
7 | #: E721
|
E721.py:15:4: E721 Do not compare types, use `isinstance()`
E721.py:8:4: E721 Do not compare types, use `isinstance()`
|
13 | import types
14 |
15 | if type(res) is not types.ListType:
6 | pass
7 | #: E721
8 | if type(res) == memoryview:
| ^^^^^^^^^^^^^^^^^^^^^^^ E721
9 | pass
10 | #: Okay
|
E721.py:18:4: E721 Do not compare types, use `isinstance()`
|
16 | import types
17 |
18 | if type(res) is not types.ListType:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E721
16 | pass
17 | #: E721
19 | pass
20 | #: E721
|
E721.py:18:8: E721 Do not compare types, use `isinstance()`
E721.py:21:8: E721 Do not compare types, use `isinstance()`
|
16 | pass
17 | #: E721
18 | assert type(res) == type(False) or type(res) == type(None)
19 | pass
20 | #: E721
21 | assert type(res) == type(False) or type(res) == type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721
19 | #: E721
20 | assert type(res) == type([])
22 | #: E721
23 | assert type(res) == type([])
|
E721.py:20:8: E721 Do not compare types, use `isinstance()`
E721.py:23:8: E721 Do not compare types, use `isinstance()`
|
18 | assert type(res) == type(False) or type(res) == type(None)
19 | #: E721
20 | assert type(res) == type([])
21 | assert type(res) == type(False) or type(res) == type(None)
22 | #: E721
23 | assert type(res) == type([])
| ^^^^^^^^^^^^^^^^^^^^^ E721
21 | #: E721
22 | assert type(res) == type(())
24 | #: E721
25 | assert type(res) == type(())
|
E721.py:22:8: E721 Do not compare types, use `isinstance()`
E721.py:25:8: E721 Do not compare types, use `isinstance()`
|
20 | assert type(res) == type([])
21 | #: E721
22 | assert type(res) == type(())
23 | assert type(res) == type([])
24 | #: E721
25 | assert type(res) == type(())
| ^^^^^^^^^^^^^^^^^^^^^ E721
23 | #: E721
24 | assert type(res) == type((0,))
26 | #: E721
27 | assert type(res) == type((0,))
|
E721.py:24:8: E721 Do not compare types, use `isinstance()`
E721.py:27:8: E721 Do not compare types, use `isinstance()`
|
22 | assert type(res) == type(())
23 | #: E721
24 | assert type(res) == type((0,))
25 | assert type(res) == type(())
26 | #: E721
27 | assert type(res) == type((0,))
| ^^^^^^^^^^^^^^^^^^^^^^^ E721
25 | #: E721
26 | assert type(res) == type((0))
28 | #: E721
29 | assert type(res) == type((0))
|
E721.py:26:8: E721 Do not compare types, use `isinstance()`
E721.py:29:8: E721 Do not compare types, use `isinstance()`
|
24 | assert type(res) == type((0,))
25 | #: E721
26 | assert type(res) == type((0))
27 | assert type(res) == type((0,))
28 | #: E721
29 | assert type(res) == type((0))
| ^^^^^^^^^^^^^^^^^^^^^^ E721
27 | #: E721
28 | assert type(res) != type((1, ))
30 | #: E721
31 | assert type(res) != type((1, ))
|
E721.py:28:8: E721 Do not compare types, use `isinstance()`
E721.py:31:8: E721 Do not compare types, use `isinstance()`
|
26 | assert type(res) == type((0))
27 | #: E721
28 | assert type(res) != type((1, ))
29 | assert type(res) == type((0))
30 | #: E721
31 | assert type(res) != type((1, ))
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721
29 | #: Okay
30 | assert type(res) is type((1, ))
32 | #: Okay
33 | assert type(res) is type((1, ))
|
E721.py:30:8: E721 Do not compare types, use `isinstance()`
E721.py:33:8: E721 Do not compare types, use `isinstance()`
|
28 | assert type(res) != type((1, ))
29 | #: Okay
30 | assert type(res) is type((1, ))
31 | assert type(res) != type((1, ))
32 | #: Okay
33 | assert type(res) is type((1, ))
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721
31 | #: Okay
32 | assert type(res) is not type((1, ))
34 | #: Okay
35 | assert type(res) is not type((1, ))
|
E721.py:32:8: E721 Do not compare types, use `isinstance()`
E721.py:35:8: E721 Do not compare types, use `isinstance()`
|
30 | assert type(res) is type((1, ))
31 | #: Okay
32 | assert type(res) is not type((1, ))
33 | assert type(res) is type((1, ))
34 | #: Okay
35 | assert type(res) is not type((1, ))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E721
33 | #: E211 E721
34 | assert type(res) == type ([2, ])
36 | #: E211 E721
37 | assert type(res) == type ([2, ])
|
E721.py:34:8: E721 Do not compare types, use `isinstance()`
E721.py:37:8: E721 Do not compare types, use `isinstance()`
|
32 | assert type(res) is not type((1, ))
33 | #: E211 E721
34 | assert type(res) == type ([2, ])
35 | assert type(res) is not type((1, ))
36 | #: E211 E721
37 | assert type(res) == type ([2, ])
| ^^^^^^^^^^^^^^^^^^^^^^^^^ E721
35 | #: E201 E201 E202 E721
36 | assert type(res) == type( ( ) )
38 | #: E201 E201 E202 E721
39 | assert type(res) == type( ( ) )
|
E721.py:36:8: E721 Do not compare types, use `isinstance()`
E721.py:39:8: E721 Do not compare types, use `isinstance()`
|
34 | assert type(res) == type ([2, ])
35 | #: E201 E201 E202 E721
36 | assert type(res) == type( ( ) )
37 | assert type(res) == type ([2, ])
38 | #: E201 E201 E202 E721
39 | assert type(res) == type( ( ) )
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721
37 | #: E201 E202 E721
38 | assert type(res) == type( (0, ) )
40 | #: E201 E202 E721
41 | assert type(res) == type( (0, ) )
|
E721.py:38:8: E721 Do not compare types, use `isinstance()`
E721.py:41:8: E721 Do not compare types, use `isinstance()`
|
36 | assert type(res) == type( ( ) )
37 | #: E201 E202 E721
38 | assert type(res) == type( (0, ) )
39 | assert type(res) == type( ( ) )
40 | #: E201 E202 E721
41 | assert type(res) == type( (0, ) )
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E721
39 | #:
42 | #:
|
E721.py:96:12: E721 Do not compare types, use `isinstance()`
|
94 | def asdf(self, value: str | None):
95 | #: E721
96 | if type(value) is str:
| ^^^^^^^^^^^^^^^^^^ E721
97 | ...
|
E721.py:106:12: E721 Do not compare types, use `isinstance()`
E721.py:107:12: E721 Do not compare types, use `isinstance()`
|
104 | def asdf(self, value: str | None):
105 | #: E721
106 | if type(value) is str:
105 | def asdf(self, value: str | None):
106 | #: E721
107 | if type(value) is str:
| ^^^^^^^^^^^^^^^^^^ E721
107 | ...
108 | ...
|
E721.py:117:12: E721 Do not compare types, use `isinstance()`
|
115 | def asdf(self, value: str | None):
116 | #: E721
117 | if type(value) is str:
| ^^^^^^^^^^^^^^^^^^ E721
118 | ...
|

View File

@@ -17,96 +17,116 @@ E721.py:5:4: E721 Use `is` and `is not` for type comparisons, or `isinstance()`
5 | if type(res) != type(""):
| ^^^^^^^^^^^^^^^^^^^^^ E721
6 | pass
7 | #: Okay
7 | #: E721
|
E721.py:18:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
E721.py:8:4: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
16 | pass
17 | #: E721
18 | assert type(res) == type(False) or type(res) == type(None)
6 | pass
7 | #: E721
8 | if type(res) == memoryview:
| ^^^^^^^^^^^^^^^^^^^^^^^ E721
9 | pass
10 | #: Okay
|
E721.py:21:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
19 | pass
20 | #: E721
21 | assert type(res) == type(False) or type(res) == type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721
19 | #: E721
20 | assert type(res) == type([])
22 | #: E721
23 | assert type(res) == type([])
|
E721.py:20:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
E721.py:23:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
18 | assert type(res) == type(False) or type(res) == type(None)
19 | #: E721
20 | assert type(res) == type([])
21 | assert type(res) == type(False) or type(res) == type(None)
22 | #: E721
23 | assert type(res) == type([])
| ^^^^^^^^^^^^^^^^^^^^^ E721
21 | #: E721
22 | assert type(res) == type(())
24 | #: E721
25 | assert type(res) == type(())
|
E721.py:22:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
E721.py:25:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
20 | assert type(res) == type([])
21 | #: E721
22 | assert type(res) == type(())
23 | assert type(res) == type([])
24 | #: E721
25 | assert type(res) == type(())
| ^^^^^^^^^^^^^^^^^^^^^ E721
23 | #: E721
24 | assert type(res) == type((0,))
26 | #: E721
27 | assert type(res) == type((0,))
|
E721.py:24:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
E721.py:27:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
22 | assert type(res) == type(())
23 | #: E721
24 | assert type(res) == type((0,))
25 | assert type(res) == type(())
26 | #: E721
27 | assert type(res) == type((0,))
| ^^^^^^^^^^^^^^^^^^^^^^^ E721
25 | #: E721
26 | assert type(res) == type((0))
28 | #: E721
29 | assert type(res) == type((0))
|
E721.py:26:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
E721.py:29:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
24 | assert type(res) == type((0,))
25 | #: E721
26 | assert type(res) == type((0))
27 | assert type(res) == type((0,))
28 | #: E721
29 | assert type(res) == type((0))
| ^^^^^^^^^^^^^^^^^^^^^^ E721
27 | #: E721
28 | assert type(res) != type((1, ))
30 | #: E721
31 | assert type(res) != type((1, ))
|
E721.py:28:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
E721.py:31:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
26 | assert type(res) == type((0))
27 | #: E721
28 | assert type(res) != type((1, ))
29 | assert type(res) == type((0))
30 | #: E721
31 | assert type(res) != type((1, ))
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721
29 | #: Okay
30 | assert type(res) is type((1, ))
32 | #: Okay
33 | assert type(res) is type((1, ))
|
E721.py:34:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
E721.py:37:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
32 | assert type(res) is not type((1, ))
33 | #: E211 E721
34 | assert type(res) == type ([2, ])
35 | assert type(res) is not type((1, ))
36 | #: E211 E721
37 | assert type(res) == type ([2, ])
| ^^^^^^^^^^^^^^^^^^^^^^^^^ E721
35 | #: E201 E201 E202 E721
36 | assert type(res) == type( ( ) )
38 | #: E201 E201 E202 E721
39 | assert type(res) == type( ( ) )
|
E721.py:36:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
E721.py:39:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
34 | assert type(res) == type ([2, ])
35 | #: E201 E201 E202 E721
36 | assert type(res) == type( ( ) )
37 | assert type(res) == type ([2, ])
38 | #: E201 E201 E202 E721
39 | assert type(res) == type( ( ) )
| ^^^^^^^^^^^^^^^^^^^^^^^^ E721
37 | #: E201 E202 E721
38 | assert type(res) == type( (0, ) )
40 | #: E201 E202 E721
41 | assert type(res) == type( (0, ) )
|
E721.py:38:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
E721.py:41:8: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
36 | assert type(res) == type( ( ) )
37 | #: E201 E202 E721
38 | assert type(res) == type( (0, ) )
39 | assert type(res) == type( ( ) )
40 | #: E201 E202 E721
41 | assert type(res) == type( (0, ) )
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E721
39 | #:
42 | #:
|
E721.py:59:4: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
57 | pass
58 | #: E721
59 | if type(res) == type:
| ^^^^^^^^^^^^^^^^^ E721
60 | pass
61 | #: Okay
|

View File

@@ -1,6 +1,6 @@
use memchr::memchr_iter;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
@@ -46,14 +46,16 @@ use crate::docstrings::Docstring;
#[violation]
pub struct EscapeSequenceInDocstring;
impl AlwaysFixableViolation for EscapeSequenceInDocstring {
impl Violation for EscapeSequenceInDocstring {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!(r#"Use `r"""` if any backslashes in a docstring"#)
}
fn fix_title(&self) -> String {
format!(r#"Add `r` prefix"#)
fn fix_title(&self) -> Option<String> {
Some(format!(r#"Add `r` prefix"#))
}
}
@@ -74,10 +76,12 @@ pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) {
}) {
let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range());
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
"r".to_owned() + docstring.contents,
docstring.range(),
)));
if !docstring.leading_quote().contains(['u', 'U']) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
"r".to_owned() + docstring.contents,
docstring.range(),
)));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -17,4 +17,12 @@ D301.py:2:5: D301 [*] Use `r"""` if any backslashes in a docstring
4 4 |
5 5 | def double_quotes_backslash_raw():
D301.py:37:5: D301 Use `r"""` if any backslashes in a docstring
|
36 | def shouldnt_add_raw_here2():
37 | u"Sum\\mary."
| ^^^^^^^^^^^^^ D301
|
= help: Add `r` prefix

View File

@@ -26,6 +26,7 @@ mod tests {
use crate::linter::{check_path, LinterResult};
use crate::registry::{AsRule, Linter, Rule};
use crate::rules::pyflakes;
use crate::settings::types::PreviewMode;
use crate::settings::{flags, LinterSettings};
use crate::source_kind::SourceKind;
use crate::test::{test_path, test_snippet};
@@ -145,6 +146,7 @@ mod tests {
#[test_case(Rule::UnusedVariable, Path::new("F841_1.py"))]
#[test_case(Rule::UnusedVariable, Path::new("F841_2.py"))]
#[test_case(Rule::UnusedVariable, Path::new("F841_3.py"))]
#[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))]
#[test_case(Rule::UnusedAnnotation, Path::new("F842.py"))]
#[test_case(Rule::RaiseNotImplemented, Path::new("F901.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
@@ -157,6 +159,24 @@ mod tests {
Ok(())
}
#[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("pyflakes").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn f841_dummy_variable_rgx() -> Result<()> {
let diagnostics = test_path(
@@ -1126,7 +1146,8 @@ mod tests {
#[test]
fn used_as_star_unpack() {
// Star names in unpack are used if RHS is not a tuple/list literal.
// In stable, starred names in unpack are used if RHS is not a tuple/list literal.
// In preview, these should be marked as unused.
flakes(
r#"
def f():

View File

@@ -12,6 +12,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
use crate::fix::edits::delete_stmt;
use crate::settings::types::PreviewMode;
/// ## What it does
/// Checks for the presence of unused variables in function scopes.
@@ -24,6 +25,9 @@ use crate::fix::edits::delete_stmt;
/// prefixed with an underscore, or some other value that adheres to the
/// [`dummy-variable-rgx`] pattern.
///
/// Under [preview mode](https://docs.astral.sh/ruff/preview), this rule also
/// triggers on unused unpacked assignments (for example, `x, y = foo()`).
///
/// ## Example
/// ```python
/// def foo():
@@ -318,7 +322,10 @@ pub(crate) fn unused_variable(checker: &Checker, scope: &Scope, diagnostics: &mu
.bindings()
.map(|(name, binding_id)| (name, checker.semantic().binding(binding_id)))
.filter_map(|(name, binding)| {
if (binding.kind.is_assignment() || binding.kind.is_named_expr_assignment())
if (binding.kind.is_assignment()
|| binding.kind.is_named_expr_assignment()
|| (matches!(checker.settings.preview, PreviewMode::Enabled)
&& binding.kind.is_unpacked_assignment()))
&& !binding.is_nonlocal()
&& !binding.is_global()
&& !binding.is_used()

View File

@@ -20,7 +20,7 @@ F841_1.py:6:8: F841 Local variable `y` is assigned to but never used
F841_1.py:16:14: F841 [*] Local variable `coords` is assigned to but never used
|
15 | def f():
16 | (x, y) = coords = 1, 2 # this triggers F841 on coords
16 | (x, y) = coords = 1, 2
| ^^^^^^ F841
|
= help: Remove assignment to unused variable `coords`
@@ -29,8 +29,8 @@ F841_1.py:16:14: F841 [*] Local variable `coords` is assigned to but never used
13 13 |
14 14 |
15 15 | def f():
16 |- (x, y) = coords = 1, 2 # this triggers F841 on coords
16 |+ (x, y) = 1, 2 # this triggers F841 on coords
16 |- (x, y) = coords = 1, 2
16 |+ (x, y) = 1, 2
17 17 |
18 18 |
19 19 | def f():
@@ -38,7 +38,7 @@ F841_1.py:16:14: F841 [*] Local variable `coords` is assigned to but never used
F841_1.py:20:5: F841 [*] Local variable `coords` is assigned to but never used
|
19 | def f():
20 | coords = (x, y) = 1, 2 # this triggers F841 on coords
20 | coords = (x, y) = 1, 2
| ^^^^^^ F841
|
= help: Remove assignment to unused variable `coords`
@@ -47,8 +47,8 @@ F841_1.py:20:5: F841 [*] Local variable `coords` is assigned to but never used
17 17 |
18 18 |
19 19 | def f():
20 |- coords = (x, y) = 1, 2 # this triggers F841 on coords
20 |+ (x, y) = 1, 2 # this triggers F841 on coords
20 |- coords = (x, y) = 1, 2
20 |+ (x, y) = 1, 2
21 21 |
22 22 |
23 23 | def f():

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F841_4.py:12:5: F841 [*] Local variable `a` is assigned to but never used
|
11 | def bar():
12 | a = foo()
| ^ F841
13 | b, c = foo()
|
= help: Remove assignment to unused variable `a`
Suggested fix
9 9 |
10 10 |
11 11 | def bar():
12 |- a = foo()
12 |+ foo()
13 13 | b, c = foo()
14 14 |
15 15 |

View File

@@ -0,0 +1,41 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F841_4.py:12:5: F841 [*] Local variable `a` is assigned to but never used
|
11 | def bar():
12 | a = foo()
| ^ F841
13 | b, c = foo()
|
= help: Remove assignment to unused variable `a`
Suggested fix
9 9 |
10 10 |
11 11 | def bar():
12 |- a = foo()
12 |+ foo()
13 13 | b, c = foo()
14 14 |
15 15 |
F841_4.py:13:5: F841 Local variable `b` is assigned to but never used
|
11 | def bar():
12 | a = foo()
13 | b, c = foo()
| ^ F841
|
= help: Remove assignment to unused variable `b`
F841_4.py:13:8: F841 Local variable `c` is assigned to but never used
|
11 | def bar():
12 | a = foo()
13 | b, c = foo()
| ^ F841
|
= help: Remove assignment to unused variable `c`

View File

@@ -138,7 +138,7 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign)
stmt.range(),
);
// The fix is only safe in a type stub because new-style aliases have different runtime behavior
// The fix is only safe in a type stub because new-style aliases have different runtime behavior
// See https://github.com/astral-sh/ruff/issues/6434
let fix = if checker.source_type.is_stub() {
Fix::safe_edit(edit)

View File

@@ -34,3 +34,30 @@ pub(super) fn generate_method_call(name: &str, method: &str, generator: Generato
};
generator.stmt(&stmt.into())
}
/// Format a code snippet comparing `name` to `None` (e.g., `name is None`).
pub(super) fn generate_none_identity_comparison(
name: &str,
negate: bool,
generator: Generator,
) -> String {
// Construct `name`.
let var = ast::ExprName {
id: name.to_string(),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Construct `name is None` or `name is not None`.
let op = if negate {
ast::CmpOp::IsNot
} else {
ast::CmpOp::Is
};
let compare = ast::ExprCompare {
left: Box::new(var.into()),
ops: vec![op],
comparators: vec![ast::Expr::NoneLiteral(ast::ExprNoneLiteral::default())],
range: TextRange::default(),
};
generator.expr(&compare.into())
}

View File

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

View File

@@ -97,9 +97,9 @@ pub(crate) fn check_and_remove_from_set(checker: &mut Checker, if_stmt: &ast::St
// Check if what we assume is set is indeed a set.
if !checker
.semantic()
.resolve_name(check_set)
.map(|binding_id| checker.semantic().binding(binding_id))
.map_or(false, |binding| is_set(binding, checker.semantic()))
.only_binding(check_set)
.map(|id| checker.semantic().binding(id))
.is_some_and(|binding| is_set(binding, checker.semantic()))
{
return;
};

View File

@@ -2,7 +2,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::{is_dict, is_list};
use ruff_python_semantic::{Binding, SemanticModel};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -70,7 +70,7 @@ pub(crate) fn delete_full_slice(checker: &mut Checker, delete: &ast::StmtDelete)
// Fix is only supported for single-target deletions.
if delete.targets.len() == 1 {
let replacement = generate_method_call(name, "clear", checker.generator());
let replacement = generate_method_call(&name.id, "clear", checker.generator());
diagnostic.set_fix(Fix::unsafe_edit(Edit::replacement(
replacement,
delete.start(),
@@ -83,7 +83,7 @@ pub(crate) fn delete_full_slice(checker: &mut Checker, delete: &ast::StmtDelete)
}
/// Match `del expr[:]` where `expr` is a list or a dict.
fn match_full_slice<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a str> {
fn match_full_slice<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a ast::ExprName> {
// Check that it is `del expr[...]`.
let subscript = expr.as_subscript_expr()?;
@@ -100,22 +100,9 @@ fn match_full_slice<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a
return None;
}
// Check that it is del var[:]
let ast::ExprName { id: name, .. } = subscript.value.as_name_expr()?;
// Let's find definition for var
let scope = semantic.current_scope();
let bindings: Vec<&Binding> = scope
.get_all(name)
.map(|binding_id| semantic.binding(binding_id))
.collect();
// NOTE: Maybe it is too strict of a limitation, but it seems reasonable.
let [binding] = bindings.as_slice() else {
return None;
};
// It should only apply to variables that are known to be lists or dicts.
let name = subscript.value.as_name_expr()?;
let binding = semantic.binding(semantic.only_binding(name)?);
if !(is_dict(binding, semantic) || is_list(binding, semantic)) {
return None;
}

View File

@@ -2,11 +2,11 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_codegen::Generator;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
use crate::rules::refurb::helpers::generate_none_identity_comparison;
/// ## What it does
/// Checks for uses of `isinstance` that check if an object is of type `None`.
@@ -69,7 +69,8 @@ pub(crate) fn isinstance_type_none(checker: &mut Checker, call: &ast::ExprCall)
return;
};
let mut diagnostic = Diagnostic::new(IsinstanceTypeNone, call.range());
let replacement = generate_replacement(object_name, checker.generator());
let replacement =
generate_none_identity_comparison(object_name, false, checker.generator());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
pad(replacement, call.range(), checker.locator()),
call.range(),
@@ -117,21 +118,3 @@ fn is_none(expr: &Expr) -> bool {
}
inner(expr, false)
}
/// Format a code snippet comparing `name` to `None` (e.g., `name is None`).
fn generate_replacement(name: &str, generator: Generator) -> String {
// Construct `name`.
let var = ast::ExprName {
id: name.to_string(),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Construct `name is None`.
let compare = ast::ExprCompare {
left: Box::new(var.into()),
ops: vec![ast::CmpOp::Is],
comparators: vec![ast::Expr::NoneLiteral(ast::ExprNoneLiteral::default())],
range: TextRange::default(),
};
generator.expr(&compare.into())
}

View File

@@ -8,6 +8,7 @@ pub(crate) use reimplemented_starmap::*;
pub(crate) use repeated_append::*;
pub(crate) use single_item_membership_test::*;
pub(crate) use slice_copy::*;
pub(crate) use type_none_comparison::*;
pub(crate) use unnecessary_enumerate::*;
mod check_and_remove_from_set;
@@ -20,4 +21,5 @@ mod reimplemented_starmap;
mod repeated_append;
mod single_item_membership_test;
mod slice_copy;
mod type_none_comparison;
mod unnecessary_enumerate;

View File

@@ -76,7 +76,7 @@ impl Violation for RepeatedAppend {
/// FURB113
pub(crate) fn repeated_append(checker: &mut Checker, stmt: &Stmt) {
let Some(appends) = match_consecutive_appends(checker.semantic(), stmt) else {
let Some(appends) = match_consecutive_appends(stmt, checker.semantic()) else {
return;
};
@@ -163,8 +163,8 @@ impl Ranged for AppendGroup<'_> {
/// Match consecutive calls to `append` on list variables starting from the given statement.
fn match_consecutive_appends<'a>(
semantic: &'a SemanticModel,
stmt: &'a Stmt,
semantic: &'a SemanticModel,
) -> Option<Vec<Append<'a>>> {
// Match the current statement, to see if it's an append.
let append = match_append(semantic, stmt)?;

View File

@@ -0,0 +1,153 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
use crate::rules::refurb::helpers::generate_none_identity_comparison;
/// ## What it does
/// Checks for uses of `type` that compare the type of an object to the type of
/// `None`.
///
/// ## Why is this bad?
/// There is only ever one instance of `None`, so it is more efficient and
/// readable to use the `is` operator to check if an object is `None`.
///
/// ## Example
/// ```python
/// type(obj) is type(None)
/// ```
///
/// Use instead:
/// ```python
/// obj is None
/// ```
///
/// ## References
/// - [Python documentation: `isinstance`](https://docs.python.org/3/library/functions.html#isinstance)
/// - [Python documentation: `None`](https://docs.python.org/3/library/constants.html#None)
/// - [Python documentation: `type`](https://docs.python.org/3/library/functions.html#type)
/// - [Python documentation: Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not)
#[violation]
pub struct TypeNoneComparison {
object: String,
comparison: Comparison,
}
impl Violation for TypeNoneComparison {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let TypeNoneComparison { object, .. } = self;
format!("Compare the identities of `{object}` and `None` instead of their respective types")
}
fn fix_title(&self) -> Option<String> {
let TypeNoneComparison { object, comparison } = self;
match comparison {
Comparison::Is | Comparison::Eq => Some(format!("Replace with `{object} is None`")),
Comparison::IsNot | Comparison::NotEq => {
Some(format!("Replace with `{object} is not None`"))
}
}
}
}
/// FURB169
pub(crate) fn type_none_comparison(checker: &mut Checker, compare: &ast::ExprCompare) {
let ([op], [right]) = (compare.ops.as_slice(), compare.comparators.as_slice()) else {
return;
};
// Ensure that the comparison is an identity or equality test.
let comparison = match op {
CmpOp::Is => Comparison::Is,
CmpOp::IsNot => Comparison::IsNot,
CmpOp::Eq => Comparison::Eq,
CmpOp::NotEq => Comparison::NotEq,
_ => return,
};
// Get the objects whose types are being compared.
let Some(left_arg) = type_call_arg(&compare.left, checker.semantic()) else {
return;
};
let Some(right_arg) = type_call_arg(right, checker.semantic()) else {
return;
};
// If one of the objects is `None`, get the other object; else, return.
let other_arg = match (
left_arg.is_none_literal_expr(),
right_arg.is_none_literal_expr(),
) {
(true, false) => right_arg,
(false, true) => left_arg,
// If both are `None`, just pick one.
(true, true) => left_arg,
_ => return,
};
// Get the name of the other object (or `None` if both were `None`).
let other_arg_name = match other_arg {
Expr::Name(ast::ExprName { id, .. }) => id.as_str(),
Expr::NoneLiteral { .. } => "None",
_ => return,
};
let mut diagnostic = Diagnostic::new(
TypeNoneComparison {
object: other_arg_name.to_string(),
comparison,
},
compare.range(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
pad(
match comparison {
Comparison::Is | Comparison::Eq => {
generate_none_identity_comparison(other_arg_name, false, checker.generator())
}
Comparison::IsNot | Comparison::NotEq => {
generate_none_identity_comparison(other_arg_name, true, checker.generator())
}
},
compare.range(),
checker.locator(),
),
compare.range(),
)));
checker.diagnostics.push(diagnostic);
}
/// Returns the object passed to the function, if the expression is a call to
/// `type` with a single argument.
fn type_call_arg<'a>(expr: &'a Expr, semantic: &'a SemanticModel) -> Option<&'a Expr> {
// The expression must be a single-argument call to `type`.
let ast::ExprCall {
func, arguments, ..
} = expr.as_call_expr()?;
if arguments.len() != 1 {
return None;
}
// The function itself must be the builtin `type`.
let ast::ExprName { id, .. } = func.as_name_expr()?;
if id.as_str() != "type" || !semantic.is_builtin(id) {
return None;
}
arguments.find_positional(0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Comparison {
Is,
IsNot,
Eq,
NotEq,
}

View File

@@ -6,7 +6,7 @@ use ruff_python_ast as ast;
use ruff_python_ast::{Arguments, Expr, Int};
use ruff_python_codegen::Generator;
use ruff_python_semantic::analyze::typing::{is_dict, is_list, is_set, is_tuple};
use ruff_python_semantic::Binding;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -114,7 +114,7 @@ pub(crate) fn unnecessary_enumerate(checker: &mut Checker, stmt_for: &ast::StmtF
};
// Get the first argument, which is the sequence to iterate over.
let Some(Expr::Name(ast::ExprName { id: sequence, .. })) = arguments.args.first() else {
let Some(Expr::Name(sequence)) = arguments.args.first() else {
return;
};
@@ -138,7 +138,8 @@ pub(crate) fn unnecessary_enumerate(checker: &mut Checker, stmt_for: &ast::StmtF
);
// The index is unused, so replace with `for value in sequence`.
let replace_iter = Edit::range_replacement(sequence.into(), stmt_for.iter.range());
let replace_iter =
Edit::range_replacement(sequence.id.to_string(), stmt_for.iter.range());
let replace_target = Edit::range_replacement(
pad(
checker.locator().slice(value).to_string(),
@@ -154,12 +155,11 @@ pub(crate) fn unnecessary_enumerate(checker: &mut Checker, stmt_for: &ast::StmtF
(false, true) => {
// Ensure the sequence object works with `len`. If it doesn't, the
// fix is unclear.
let scope = checker.semantic().current_scope();
let bindings: Vec<&Binding> = scope
.get_all(sequence)
.map(|binding_id| checker.semantic().binding(binding_id))
.collect();
let [binding] = bindings.as_slice() else {
let Some(binding) = checker
.semantic()
.only_binding(sequence)
.map(|id| checker.semantic().binding(id))
else {
return;
};
// This will lead to a lot of false negatives, but it is the best
@@ -193,7 +193,7 @@ pub(crate) fn unnecessary_enumerate(checker: &mut Checker, stmt_for: &ast::StmtF
)
}) {
let replace_iter = Edit::range_replacement(
generate_range_len_call(sequence, checker.generator()),
generate_range_len_call(&sequence.id, checker.generator()),
stmt_for.iter.range(),
);

View File

@@ -127,6 +127,27 @@ FURB131.py:43:5: FURB131 [*] Prefer `clear` over deleting a full slice
43 |+ x.clear()
44 44 |
45 45 |
46 46 | # these should not
46 46 | def yes_five(x: Dict[int, str]):
FURB131.py:48:5: FURB131 [*] Prefer `clear` over deleting a full slice
|
46 | def yes_five(x: Dict[int, str]):
47 | # FURB131
48 | del x[:]
| ^^^^^^^^ FURB131
49 |
50 | x = 1
|
= help: Replace with `clear()`
Suggested fix
45 45 |
46 46 | def yes_five(x: Dict[int, str]):
47 47 | # FURB131
48 |- del x[:]
48 |+ x.clear()
49 49 |
50 50 | x = 1
51 51 |

View File

@@ -0,0 +1,256 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB169.py:5:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
3 | # Error.
4 |
5 | type(foo) is type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
6 |
7 | type(None) is type(foo)
|
= help: Replace with `foo is None`
Fix
2 2 |
3 3 | # Error.
4 4 |
5 |-type(foo) is type(None)
5 |+foo is None
6 6 |
7 7 | type(None) is type(foo)
8 8 |
FURB169.py:7:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
5 | type(foo) is type(None)
6 |
7 | type(None) is type(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
8 |
9 | type(None) is type(None)
|
= help: Replace with `foo is None`
Fix
4 4 |
5 5 | type(foo) is type(None)
6 6 |
7 |-type(None) is type(foo)
7 |+foo is None
8 8 |
9 9 | type(None) is type(None)
10 10 |
FURB169.py:9:1: FURB169 [*] Compare the identities of `None` and `None` instead of their respective types
|
7 | type(None) is type(foo)
8 |
9 | type(None) is type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
10 |
11 | type(foo) is not type(None)
|
= help: Replace with `None is None`
Fix
6 6 |
7 7 | type(None) is type(foo)
8 8 |
9 |-type(None) is type(None)
9 |+None is None
10 10 |
11 11 | type(foo) is not type(None)
12 12 |
FURB169.py:11:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
9 | type(None) is type(None)
10 |
11 | type(foo) is not type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
12 |
13 | type(None) is not type(foo)
|
= help: Replace with `foo is not None`
Fix
8 8 |
9 9 | type(None) is type(None)
10 10 |
11 |-type(foo) is not type(None)
11 |+foo is not None
12 12 |
13 13 | type(None) is not type(foo)
14 14 |
FURB169.py:13:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
11 | type(foo) is not type(None)
12 |
13 | type(None) is not type(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
14 |
15 | type(None) is not type(None)
|
= help: Replace with `foo is not None`
Fix
10 10 |
11 11 | type(foo) is not type(None)
12 12 |
13 |-type(None) is not type(foo)
13 |+foo is not None
14 14 |
15 15 | type(None) is not type(None)
16 16 |
FURB169.py:15:1: FURB169 [*] Compare the identities of `None` and `None` instead of their respective types
|
13 | type(None) is not type(foo)
14 |
15 | type(None) is not type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
16 |
17 | type(foo) == type(None)
|
= help: Replace with `None is not None`
Fix
12 12 |
13 13 | type(None) is not type(foo)
14 14 |
15 |-type(None) is not type(None)
15 |+None is not None
16 16 |
17 17 | type(foo) == type(None)
18 18 |
FURB169.py:17:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
15 | type(None) is not type(None)
16 |
17 | type(foo) == type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
18 |
19 | type(None) == type(foo)
|
= help: Replace with `foo is None`
Fix
14 14 |
15 15 | type(None) is not type(None)
16 16 |
17 |-type(foo) == type(None)
17 |+foo is None
18 18 |
19 19 | type(None) == type(foo)
20 20 |
FURB169.py:19:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
17 | type(foo) == type(None)
18 |
19 | type(None) == type(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
20 |
21 | type(None) == type(None)
|
= help: Replace with `foo is None`
Fix
16 16 |
17 17 | type(foo) == type(None)
18 18 |
19 |-type(None) == type(foo)
19 |+foo is None
20 20 |
21 21 | type(None) == type(None)
22 22 |
FURB169.py:21:1: FURB169 [*] Compare the identities of `None` and `None` instead of their respective types
|
19 | type(None) == type(foo)
20 |
21 | type(None) == type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
22 |
23 | type(foo) != type(None)
|
= help: Replace with `None is None`
Fix
18 18 |
19 19 | type(None) == type(foo)
20 20 |
21 |-type(None) == type(None)
21 |+None is None
22 22 |
23 23 | type(foo) != type(None)
24 24 |
FURB169.py:23:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
21 | type(None) == type(None)
22 |
23 | type(foo) != type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
24 |
25 | type(None) != type(foo)
|
= help: Replace with `foo is not None`
Fix
20 20 |
21 21 | type(None) == type(None)
22 22 |
23 |-type(foo) != type(None)
23 |+foo is not None
24 24 |
25 25 | type(None) != type(foo)
26 26 |
FURB169.py:25:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
23 | type(foo) != type(None)
24 |
25 | type(None) != type(foo)
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
26 |
27 | type(None) != type(None)
|
= help: Replace with `foo is not None`
Fix
22 22 |
23 23 | type(foo) != type(None)
24 24 |
25 |-type(None) != type(foo)
25 |+foo is not None
26 26 |
27 27 | type(None) != type(None)
28 28 |
FURB169.py:27:1: FURB169 [*] Compare the identities of `None` and `None` instead of their respective types
|
25 | type(None) != type(foo)
26 |
27 | type(None) != type(None)
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
28 |
29 | # Ok.
|
= help: Replace with `None is not None`
Fix
24 24 |
25 25 | type(None) != type(foo)
26 26 |
27 |-type(None) != type(None)
27 |+None is not None
28 28 |
29 29 | # Ok.
30 30 |

View File

@@ -25,7 +25,7 @@ use crate::importer::ImportRequest;
/// lists:
///
/// - `functools.reduce(operator.iconcat, lists, [])`
/// - `list(itertools.chain.from_iterable(lists)`
/// - `list(itertools.chain.from_iterable(lists))`
/// - `[item for sublist in lists for item in sublist]`
///
/// ## Example

View File

@@ -20,15 +20,6 @@ use crate::fix::snippet::SourceCodeSnippet;
/// element of the collection, you can use `next(...)` or `next(iter(...)` to
/// lazily fetch the first element.
///
/// Note that migrating from `list(...)[0]` to `next(iter(...))` can change
/// the behavior of your program in two ways:
///
/// 1. First, `list(...)` will eagerly evaluate the entire collection, while
/// `next(iter(...))` will only evaluate the first element. As such, any
/// side effects that occur during iteration will be delayed.
/// 2. Second, `list(...)[0]` will raise `IndexError` if the collection is
/// empty, while `next(iter(...))` will raise `StopIteration`.
///
/// ## Example
/// ```python
/// head = list(x)[0]
@@ -41,6 +32,16 @@ use crate::fix::snippet::SourceCodeSnippet;
/// head = next(x * x for x in range(10))
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as migrating from `list(...)[0]` to
/// `next(iter(...))` can change the behavior of your program in two ways:
///
/// 1. First, `list(...)` will eagerly evaluate the entire collection, while
/// `next(iter(...))` will only evaluate the first element. As such, any
/// side effects that occur during iteration will be delayed.
/// 2. Second, `list(...)[0]` will raise `IndexError` if the collection is
/// empty, while `next(iter(...))` will raise `StopIteration`.
///
/// ## References
/// - [Iterators and Iterables in Python: Run Efficient Iterations](https://realpython.com/python-iterators-iterables/#when-to-use-an-iterator-in-python)
#[violation]

View File

@@ -31,6 +31,10 @@ use crate::checkers::ast::Checker;
/// except ValueError:
/// raise
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it doesn't properly handle bound
/// exceptions that are shadowed between the `except` and `raise` statements.
#[violation]
pub struct VerboseRaise;

View File

@@ -10,6 +10,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::call_path::CallPath;
use crate::parenthesize::parenthesized_range;
use crate::statement_visitor::{walk_body, walk_stmt, StatementVisitor};
use crate::visitor::Visitor;
use crate::AnyNodeRef;
use crate::{
self as ast, Arguments, CmpOp, ExceptHandler, Expr, MatchCase, Pattern, Stmt, TypeParam,
@@ -931,6 +932,29 @@ where
}
}
/// A [`Visitor`] that detects the presence of `await` expressions in the current scope.
#[derive(Debug, Default)]
pub struct AwaitVisitor {
pub seen_await: bool,
}
impl Visitor<'_> for AwaitVisitor {
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => (),
_ => crate::visitor::walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &Expr) {
if let Expr::Await(ast::ExprAwait { .. }) = expr {
self.seen_await = true;
} else {
crate::visitor::walk_expr(self, expr);
}
}
}
/// Return `true` if a `Stmt` is a docstring.
pub fn is_docstring_stmt(stmt: &Stmt) -> bool {
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {

View File

@@ -203,3 +203,28 @@ lambda: ( # comment
y:
z
)
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: d
# Regression tests for https://github.com/astral-sh/ruff/issues/8179
def a():
return b(
c,
d,
e,
f=lambda self, *args, **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(
*args, **kwargs
),
)
def a():
return b(
c,
d,
e,
f=lambda self, araa, kkkwargs,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
args,kwargs,
e=1, f=2, g=2: d,
g = 10
)

View File

@@ -22,8 +22,6 @@ def foo():
pass
(yield a, b) = (1, 2)
# some comment
for e in l : yield e # some comment

View File

@@ -102,7 +102,15 @@ impl FormatNodeRule<Parameters> for FormatParameters {
dangling.split_at(parenthesis_comments_end);
let format_inner = format_with(|f: &mut PyFormatter| {
let separator = format_with(|f| write!(f, [token(","), soft_line_break_or_space()]));
let separator = format_with(|f: &mut PyFormatter| {
token(",").fmt(f)?;
if f.context().node_level().is_parenthesized() {
soft_line_break_or_space().fmt(f)
} else {
space().fmt(f)
}
});
let mut joiner = f.join_with(separator);
let mut last_node: Option<AnyNodeRef> = None;
@@ -232,8 +240,6 @@ impl FormatNodeRule<Parameters> for FormatParameters {
Ok(())
});
let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f);
let num_parameters = posonlyargs.len()
+ args.len()
+ usize::from(vararg.is_some())
@@ -243,12 +249,14 @@ impl FormatNodeRule<Parameters> for FormatParameters {
if self.parentheses == ParametersParentheses::Never {
write!(f, [group(&format_inner), dangling_comments(dangling)])
} else if num_parameters == 0 {
let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f);
// No parameters, format any dangling comments between `()`
write!(f, [empty_parenthesized("(", dangling, ")")])
} else {
// Intentionally avoid `parenthesized`, which groups the entire formatted contents.
// We want parameters to be grouped alongside return types, one level up, so we
// format them "inline" here.
let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f);
write!(
f,
[

View File

@@ -209,6 +209,31 @@ lambda: ( # comment
y:
z
)
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: d
# Regression tests for https://github.com/astral-sh/ruff/issues/8179
def a():
return b(
c,
d,
e,
f=lambda self, *args, **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(
*args, **kwargs
),
)
def a():
return b(
c,
d,
e,
f=lambda self, araa, kkkwargs,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
args,kwargs,
e=1, f=2, g=2: d,
g = 10
)
```
## Output
@@ -413,6 +438,40 @@ lambda: ( # comment
# comment
y: z
)
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(
*args, **kwargs
), e=1, f=2, g=2: d
# Regression tests for https://github.com/astral-sh/ruff/issues/8179
def a():
return b(
c,
d,
e,
f=lambda self,
*args,
**kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs),
)
def a():
return b(
c,
d,
e,
f=lambda self,
araa,
kkkwargs,
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
args,
kwargs,
e=1,
f=2,
g=2: d,
g=10,
)
```

View File

@@ -28,8 +28,6 @@ def foo():
pass
(yield a, b) = (1, 2)
# some comment
for e in l : yield e # some comment
@@ -151,8 +149,6 @@ def foo():
# comment
pass
(yield a, b) = (1, 2)
# some comment
for e in l:
yield e # some comment

View File

@@ -0,0 +1,733 @@
/*!
Defines some helper routines for rejecting invalid Python programs.
These routines are named in a way that supports qualified use. For example,
`invalid::assignment_targets`.
*/
use {ruff_python_ast::Expr, ruff_text_size::TextSize};
use crate::lexer::{LexicalError, LexicalErrorType};
/// Returns an error for invalid assignment targets.
///
/// # Errors
///
/// This returns an error when any of the given expressions are themselves
/// or contain an expression that is invalid on the left hand side of an
/// assignment. For example, all literal expressions are invalid assignment
/// targets.
pub(crate) fn assignment_targets(targets: &[Expr]) -> Result<(), LexicalError> {
for t in targets {
assignment_target(t)?;
}
Ok(())
}
/// Returns an error if the given target is invalid for the left hand side of
/// an assignment.
///
/// # Errors
///
/// This returns an error when the given expression is itself or contains an
/// expression that is invalid on the left hand side of an assignment. For
/// example, all literal expressions are invalid assignment targets.
pub(crate) fn assignment_target(target: &Expr) -> Result<(), LexicalError> {
// Allowing a glob import here because of its limited scope.
#[allow(clippy::enum_glob_use)]
use self::Expr::*;
let err = |location: TextSize| -> LexicalError {
let error = LexicalErrorType::AssignmentError;
LexicalError { error, location }
};
match *target {
BoolOp(ref e) => Err(err(e.range.start())),
NamedExpr(ref e) => Err(err(e.range.start())),
BinOp(ref e) => Err(err(e.range.start())),
UnaryOp(ref e) => Err(err(e.range.start())),
Lambda(ref e) => Err(err(e.range.start())),
IfExp(ref e) => Err(err(e.range.start())),
Dict(ref e) => Err(err(e.range.start())),
Set(ref e) => Err(err(e.range.start())),
ListComp(ref e) => Err(err(e.range.start())),
SetComp(ref e) => Err(err(e.range.start())),
DictComp(ref e) => Err(err(e.range.start())),
GeneratorExp(ref e) => Err(err(e.range.start())),
Await(ref e) => Err(err(e.range.start())),
Yield(ref e) => Err(err(e.range.start())),
YieldFrom(ref e) => Err(err(e.range.start())),
Compare(ref e) => Err(err(e.range.start())),
Call(ref e) => Err(err(e.range.start())),
FormattedValue(ref e) => Err(err(e.range.start())),
// FString is recursive, but all its forms are invalid as an
// assignment target, so we can reject it without exploring it.
FString(ref e) => Err(err(e.range.start())),
StringLiteral(ref e) => Err(err(e.range.start())),
BytesLiteral(ref e) => Err(err(e.range.start())),
NumberLiteral(ref e) => Err(err(e.range.start())),
BooleanLiteral(ref e) => Err(err(e.range.start())),
NoneLiteral(ref e) => Err(err(e.range.start())),
EllipsisLiteral(ref e) => Err(err(e.range.start())),
// This isn't in the Python grammar but is Jupyter notebook specific.
// It seems like this should be an error. It does also seem like the
// parser prevents this from ever appearing as an assignment target
// anyway. ---AG
IpyEscapeCommand(ref e) => Err(err(e.range.start())),
// The only nested expressions allowed as an assignment target
// are star exprs, lists and tuples.
Starred(ref e) => assignment_target(&e.value),
List(ref e) => assignment_targets(&e.elts),
Tuple(ref e) => assignment_targets(&e.elts),
// Subscript is recursive and can be invalid, but aren't syntax errors.
// For example, `5[1] = 42` is a type error.
Subscript(_) => Ok(()),
// Similar to Subscript, e.g., `5[1:2] = [42]` is a type error.
Slice(_) => Ok(()),
// Similar to Subscript, e.g., `"foo".y = 42` is an attribute error.
Attribute(_) => Ok(()),
// These are always valid as assignment targets.
Name(_) => Ok(()),
}
}
#[cfg(test)]
mod tests {
use crate::parse_suite;
// First we test, broadly, that various kinds of assignments are now
// rejected by the parser. e.g., `5 = 3`, `5 += 3`, `(5): int = 3`.
// Regression test: https://github.com/astral-sh/ruff/issues/6895
#[test]
fn err_literal_assignment() {
let ast = parse_suite(r"5 = 3", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
// This test previously passed before the assignment operator checking
// above, but we include it here for good measure.
#[test]
fn err_assignment_expr() {
let ast = parse_suite(r"(5 := 3)", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: UnrecognizedToken(
ColonEqual,
None,
),
offset: 3,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_literal_augment_assignment() {
let ast = parse_suite(r"5 += 3", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_literal_annotation_assignment() {
let ast = parse_suite(r"(5): int = 3", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 1,
source_path: "<test>",
},
)
"#);
}
// Now we exhaustively test all possible cases where assignment can fail.
#[test]
fn err_bool_op() {
let ast = parse_suite(r"x or y = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_named_expr() {
let ast = parse_suite(r"(x := 5) = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 1,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_bin_op() {
let ast = parse_suite(r"x + y = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_unary_op() {
let ast = parse_suite(r"-x = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_lambda() {
let ast = parse_suite(r"(lambda _: 1) = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 1,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_if_exp() {
let ast = parse_suite(r"a if b else c = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_dict() {
let ast = parse_suite(r"{'a':5} = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_set() {
let ast = parse_suite(r"{a} = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_list_comp() {
let ast = parse_suite(r"[x for x in xs] = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_set_comp() {
let ast = parse_suite(r"{x for x in xs} = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_dict_comp() {
let ast = parse_suite(r"{x: x*2 for x in xs} = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_generator_exp() {
let ast = parse_suite(r"(x for x in xs) = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_await() {
let ast = parse_suite(r"await x = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_yield() {
let ast = parse_suite(r"(yield x) = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 1,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_yield_from() {
let ast = parse_suite(r"(yield from xs) = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 1,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_compare() {
let ast = parse_suite(r"a < b < c = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_call() {
let ast = parse_suite(r"foo() = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_formatted_value() {
// N.B. It looks like the parser can't generate a top-level
// FormattedValue, where as the official Python AST permits
// representing a single f-string containing just a variable as a
// FormattedValue directly.
//
// Bottom line is that because of this, this test is (at present)
// duplicative with the `fstring` test. That is, in theory these tests
// could fail independently, but in practice their failure or success
// is coupled.
//
// See: https://docs.python.org/3/library/ast.html#ast.FormattedValue
let ast = parse_suite(r#"f"{quux}" = 42"#, "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_fstring() {
let ast = parse_suite(r#"f"{foo} and {bar}" = 42"#, "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_string_literal() {
let ast = parse_suite(r#""foo" = 42"#, "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_bytes_literal() {
let ast = parse_suite(r#"b"foo" = 42"#, "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_number_literal() {
let ast = parse_suite(r"123 = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_boolean_literal() {
let ast = parse_suite(r"True = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_none_literal() {
let ast = parse_suite(r"None = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_ellipsis_literal() {
let ast = parse_suite(r"... = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 0,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_starred() {
let ast = parse_suite(r"*foo() = 42", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 1,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_list() {
let ast = parse_suite(r"[x, foo(), y] = [42, 42, 42]", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 4,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_list_nested() {
let ast = parse_suite(r"[[a, b], [[42]], d] = [[1, 2], [[3]], 4]", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 11,
source_path: "<test>",
},
)
"#);
}
#[test]
fn err_tuple() {
let ast = parse_suite(r"(x, foo(), y) = (42, 42, 42)", "<test>");
insta::assert_debug_snapshot!(ast, @r#"
Err(
ParseError {
error: Lexical(
AssignmentError,
),
offset: 4,
source_path: "<test>",
},
)
"#);
}
// This last group of tests checks that assignments we expect to be parsed
// (including some interesting ones) continue to be parsed successfully.
#[test]
fn ok_starred() {
let ast = parse_suite(r"*foo = 42", "<test>");
insta::assert_debug_snapshot!(ast);
}
#[test]
fn ok_list() {
let ast = parse_suite(r"[x, y, z] = [1, 2, 3]", "<test>");
insta::assert_debug_snapshot!(ast);
}
#[test]
fn ok_tuple() {
let ast = parse_suite(r"(x, y, z) = (1, 2, 3)", "<test>");
insta::assert_debug_snapshot!(ast);
}
#[test]
fn ok_subscript_normal() {
let ast = parse_suite(r"x[0] = 42", "<test>");
insta::assert_debug_snapshot!(ast);
}
// This is actually a type error, not a syntax error. So check that it
// doesn't fail parsing.
#[test]
fn ok_subscript_weird() {
let ast = parse_suite(r"5[0] = 42", "<test>");
insta::assert_debug_snapshot!(ast);
}
#[test]
fn ok_slice_normal() {
let ast = parse_suite(r"x[1:2] = [42]", "<test>");
insta::assert_debug_snapshot!(ast);
}
// This is actually a type error, not a syntax error. So check that it
// doesn't fail parsing.
#[test]
fn ok_slice_weird() {
let ast = parse_suite(r"5[1:2] = [42]", "<test>");
insta::assert_debug_snapshot!(ast);
}
#[test]
fn ok_attribute_normal() {
let ast = parse_suite(r"foo.bar = 42", "<test>");
insta::assert_debug_snapshot!(ast);
}
// This is actually an attribute error, not a syntax error. So check that
// it doesn't fail parsing.
#[test]
fn ok_attribute_weird() {
let ast = parse_suite(r#""foo".y = 42"#, "<test>");
insta::assert_debug_snapshot!(ast);
}
#[test]
fn ok_name() {
let ast = parse_suite(r"foo = 42", "<test>");
insta::assert_debug_snapshot!(ast);
}
// This is a sanity test for what looks like an ipython directive being
// assigned to. Although this doesn't actually parse as an assignment
// statement, but rather, a directive whose value is `foo = 42`.
#[test]
fn ok_ipy_escape_command() {
use crate::Mode;
let src = r"!foo = 42";
let tokens = crate::lexer::lex(src, Mode::Ipython);
let ast = crate::parse_tokens(tokens, src, Mode::Ipython, "<test>");
insta::assert_debug_snapshot!(ast);
}
#[test]
fn ok_assignment_expr() {
let ast = parse_suite(r"(x := 5)", "<test>");
insta::assert_debug_snapshot!(ast);
}
}

View File

@@ -1344,6 +1344,8 @@ pub enum LexicalErrorType {
LineContinuationError,
/// An unexpected end of file was encountered.
Eof,
/// Occurs when a syntactically invalid assignment was encountered.
AssignmentError,
/// An unexpected error occurred.
OtherError(String),
}
@@ -1389,6 +1391,7 @@ impl std::fmt::Display for LexicalErrorType {
write!(f, "unexpected character after line continuation character")
}
LexicalErrorType::Eof => write!(f, "unexpected EOF while parsing"),
LexicalErrorType::AssignmentError => write!(f, "invalid assignment target"),
LexicalErrorType::OtherError(msg) => write!(f, "{msg}"),
}
}

View File

@@ -123,6 +123,7 @@ use crate::lexer::LexResult;
mod function;
// Skip flattening lexer to distinguish from full ruff_python_parser
mod context;
mod invalid;
pub mod lexer;
mod parser;
mod soft_keywords;

View File

@@ -13,6 +13,7 @@ use crate::{
context::set_context,
string::{StringType, concatenate_strings, parse_fstring_middle, parse_string_literal},
token::{self, StringKind},
invalid,
};
use lalrpop_util::ParseError;
@@ -108,12 +109,12 @@ DelStatement: ast::Stmt = {
};
ExpressionStatement: ast::Stmt = {
<location:@L> <expression:TestOrStarExprList> <suffix:AssignSuffix*> <end_location:@R> => {
<location:@L> <expression:TestOrStarExprList> <suffix:AssignSuffix*> <end_location:@R> =>? {
// Just an expression, no assignment:
if suffix.is_empty() {
ast::Stmt::Expr(
Ok(ast::Stmt::Expr(
ast::StmtExpr { value: Box::new(expression.into()), range: (location..end_location).into() }
)
))
} else {
let mut targets = vec![set_context(expression.into(), ast::ExprContext::Store)];
let mut values = suffix;
@@ -123,25 +124,27 @@ ExpressionStatement: ast::Stmt = {
for target in values {
targets.push(set_context(target.into(), ast::ExprContext::Store));
}
ast::Stmt::Assign(
invalid::assignment_targets(&targets)?;
Ok(ast::Stmt::Assign(
ast::StmtAssign { targets, value, range: (location..end_location).into() }
)
))
}
},
<location:@L> <target:TestOrStarExprList> <op:AugAssign> <rhs:TestListOrYieldExpr> <end_location:@R> => {
ast::Stmt::AugAssign(
<location:@L> <target:TestOrStarExprList> <op:AugAssign> <rhs:TestListOrYieldExpr> <end_location:@R> =>? {
invalid::assignment_target(&target.expr)?;
Ok(ast::Stmt::AugAssign(
ast::StmtAugAssign {
target: Box::new(set_context(target.into(), ast::ExprContext::Store)),
op,
value: Box::new(rhs.into()),
range: (location..end_location).into()
},
)
))
},
<location:@L> <target:Test<"all">> ":" <annotation:Test<"all">> <rhs:AssignSuffix?> <end_location:@R> => {
<location:@L> <target:Test<"all">> ":" <annotation:Test<"all">> <rhs:AssignSuffix?> <end_location:@R> =>? {
let simple = target.expr.is_name_expr();
ast::Stmt::AnnAssign(
invalid::assignment_target(&target.expr)?;
Ok(ast::Stmt::AnnAssign(
ast::StmtAnnAssign {
target: Box::new(set_context(target.into(), ast::ExprContext::Store)),
annotation: Box::new(annotation.into()),
@@ -149,7 +152,7 @@ ExpressionStatement: ast::Stmt = {
simple,
range: (location..end_location).into()
},
)
))
},
};

View File

@@ -1,5 +1,5 @@
// auto-generated: "lalrpop 0.20.0"
// sha3: c798bc6e7bd9950e88dd5d950470865a75b5ff0352f4fc7fb51f13147de6ba6c
// sha3: b8ac4a859b69d580e50733d39c96a3fe018f568e71e532ebb3153a19902e64e5
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_python_ast::{self as ast, Int, IpyEscapeKind};
use crate::{
@@ -10,6 +10,7 @@ use crate::{
context::set_context,
string::{StringType, concatenate_strings, parse_fstring_middle, parse_string_literal},
token::{self, StringKind},
invalid,
};
use lalrpop_util::ParseError;
#[allow(unused_extern_crates)]
@@ -33,6 +34,7 @@ mod __parse__Top {
context::set_context,
string::{StringType, concatenate_strings, parse_fstring_middle, parse_string_literal},
token::{self, StringKind},
invalid,
};
use lalrpop_util::ParseError;
#[allow(unused_extern_crates)]
@@ -13725,19 +13727,76 @@ mod __parse__Top {
__reduce356(source_code, mode, __lookahead_start, __symbols, core::marker::PhantomData::<()>)
}
357 => {
__reduce357(source_code, mode, __lookahead_start, __symbols, core::marker::PhantomData::<()>)
// ExpressionStatement = GenericList<TestOrStarExpr> => ActionFn(1752);
let __sym0 = __pop_Variant15(__symbols);
let __start = __sym0.0;
let __end = __sym0.2;
let __nt = match super::__action1752::<>(source_code, mode, __sym0) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
__symbols.push((__start, __Symbol::Variant37(__nt), __end));
(1, 137)
}
358 => {
__reduce358(source_code, mode, __lookahead_start, __symbols, core::marker::PhantomData::<()>)
// ExpressionStatement = GenericList<TestOrStarExpr>, AssignSuffix+ => ActionFn(1753);
assert!(__symbols.len() >= 2);
let __sym1 = __pop_Variant17(__symbols);
let __sym0 = __pop_Variant15(__symbols);
let __start = __sym0.0;
let __end = __sym1.2;
let __nt = match super::__action1753::<>(source_code, mode, __sym0, __sym1) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
__symbols.push((__start, __Symbol::Variant37(__nt), __end));
(2, 137)
}
359 => {
__reduce359(source_code, mode, __lookahead_start, __symbols, core::marker::PhantomData::<()>)
// ExpressionStatement = GenericList<TestOrStarExpr>, AugAssign, TestListOrYieldExpr => ActionFn(1754);
assert!(__symbols.len() >= 3);
let __sym2 = __pop_Variant15(__symbols);
let __sym1 = __pop_Variant49(__symbols);
let __sym0 = __pop_Variant15(__symbols);
let __start = __sym0.0;
let __end = __sym2.2;
let __nt = match super::__action1754::<>(source_code, mode, __sym0, __sym1, __sym2) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
__symbols.push((__start, __Symbol::Variant37(__nt), __end));
(3, 137)
}
360 => {
__reduce360(source_code, mode, __lookahead_start, __symbols, core::marker::PhantomData::<()>)
// ExpressionStatement = Test<"all">, ":", Test<"all">, AssignSuffix => ActionFn(1531);
assert!(__symbols.len() >= 4);
let __sym3 = __pop_Variant15(__symbols);
let __sym2 = __pop_Variant15(__symbols);
let __sym1 = __pop_Variant0(__symbols);
let __sym0 = __pop_Variant15(__symbols);
let __start = __sym0.0;
let __end = __sym3.2;
let __nt = match super::__action1531::<>(source_code, mode, __sym0, __sym1, __sym2, __sym3) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
__symbols.push((__start, __Symbol::Variant37(__nt), __end));
(4, 137)
}
361 => {
__reduce361(source_code, mode, __lookahead_start, __symbols, core::marker::PhantomData::<()>)
// ExpressionStatement = Test<"all">, ":", Test<"all"> => ActionFn(1532);
assert!(__symbols.len() >= 3);
let __sym2 = __pop_Variant15(__symbols);
let __sym1 = __pop_Variant0(__symbols);
let __sym0 = __pop_Variant15(__symbols);
let __start = __sym0.0;
let __end = __sym2.2;
let __nt = match super::__action1532::<>(source_code, mode, __sym0, __sym1, __sym2) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
__symbols.push((__start, __Symbol::Variant37(__nt), __end));
(3, 137)
}
362 => {
// FStringConversion = "!", name => ActionFn(800);
@@ -24718,103 +24777,6 @@ mod __parse__Top {
__symbols.push((__start, __Symbol::Variant15(__nt), __end));
(1, 136)
}
pub(crate) fn __reduce357<
>(
source_code: &str,
mode: Mode,
__lookahead_start: Option<&TextSize>,
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>,
_: core::marker::PhantomData<()>,
) -> (usize, usize)
{
// ExpressionStatement = GenericList<TestOrStarExpr> => ActionFn(1752);
let __sym0 = __pop_Variant15(__symbols);
let __start = __sym0.0;
let __end = __sym0.2;
let __nt = super::__action1752::<>(source_code, mode, __sym0);
__symbols.push((__start, __Symbol::Variant37(__nt), __end));
(1, 137)
}
pub(crate) fn __reduce358<
>(
source_code: &str,
mode: Mode,
__lookahead_start: Option<&TextSize>,
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>,
_: core::marker::PhantomData<()>,
) -> (usize, usize)
{
// ExpressionStatement = GenericList<TestOrStarExpr>, AssignSuffix+ => ActionFn(1753);
assert!(__symbols.len() >= 2);
let __sym1 = __pop_Variant17(__symbols);
let __sym0 = __pop_Variant15(__symbols);
let __start = __sym0.0;
let __end = __sym1.2;
let __nt = super::__action1753::<>(source_code, mode, __sym0, __sym1);
__symbols.push((__start, __Symbol::Variant37(__nt), __end));
(2, 137)
}
pub(crate) fn __reduce359<
>(
source_code: &str,
mode: Mode,
__lookahead_start: Option<&TextSize>,
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>,
_: core::marker::PhantomData<()>,
) -> (usize, usize)
{
// ExpressionStatement = GenericList<TestOrStarExpr>, AugAssign, TestListOrYieldExpr => ActionFn(1754);
assert!(__symbols.len() >= 3);
let __sym2 = __pop_Variant15(__symbols);
let __sym1 = __pop_Variant49(__symbols);
let __sym0 = __pop_Variant15(__symbols);
let __start = __sym0.0;
let __end = __sym2.2;
let __nt = super::__action1754::<>(source_code, mode, __sym0, __sym1, __sym2);
__symbols.push((__start, __Symbol::Variant37(__nt), __end));
(3, 137)
}
pub(crate) fn __reduce360<
>(
source_code: &str,
mode: Mode,
__lookahead_start: Option<&TextSize>,
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>,
_: core::marker::PhantomData<()>,
) -> (usize, usize)
{
// ExpressionStatement = Test<"all">, ":", Test<"all">, AssignSuffix => ActionFn(1531);
assert!(__symbols.len() >= 4);
let __sym3 = __pop_Variant15(__symbols);
let __sym2 = __pop_Variant15(__symbols);
let __sym1 = __pop_Variant0(__symbols);
let __sym0 = __pop_Variant15(__symbols);
let __start = __sym0.0;
let __end = __sym3.2;
let __nt = super::__action1531::<>(source_code, mode, __sym0, __sym1, __sym2, __sym3);
__symbols.push((__start, __Symbol::Variant37(__nt), __end));
(4, 137)
}
pub(crate) fn __reduce361<
>(
source_code: &str,
mode: Mode,
__lookahead_start: Option<&TextSize>,
__symbols: &mut alloc::vec::Vec<(TextSize,__Symbol<>,TextSize)>,
_: core::marker::PhantomData<()>,
) -> (usize, usize)
{
// ExpressionStatement = Test<"all">, ":", Test<"all"> => ActionFn(1532);
assert!(__symbols.len() >= 3);
let __sym2 = __pop_Variant15(__symbols);
let __sym1 = __pop_Variant0(__symbols);
let __sym0 = __pop_Variant15(__symbols);
let __start = __sym0.0;
let __end = __sym2.2;
let __nt = super::__action1532::<>(source_code, mode, __sym0, __sym1, __sym2);
__symbols.push((__start, __Symbol::Variant37(__nt), __end));
(3, 137)
}
pub(crate) fn __reduce363<
>(
source_code: &str,
@@ -32789,14 +32751,14 @@ fn __action26<
(_, expression, _): (TextSize, ast::ParenthesizedExpr, TextSize),
(_, suffix, _): (TextSize, alloc::vec::Vec<ast::ParenthesizedExpr>, TextSize),
(_, end_location, _): (TextSize, TextSize, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
{
// Just an expression, no assignment:
if suffix.is_empty() {
ast::Stmt::Expr(
Ok(ast::Stmt::Expr(
ast::StmtExpr { value: Box::new(expression.into()), range: (location..end_location).into() }
)
))
} else {
let mut targets = vec![set_context(expression.into(), ast::ExprContext::Store)];
let mut values = suffix;
@@ -32806,10 +32768,10 @@ fn __action26<
for target in values {
targets.push(set_context(target.into(), ast::ExprContext::Store));
}
ast::Stmt::Assign(
invalid::assignment_targets(&targets)?;
Ok(ast::Stmt::Assign(
ast::StmtAssign { targets, value, range: (location..end_location).into() }
)
))
}
}
}
@@ -32825,17 +32787,18 @@ fn __action27<
(_, op, _): (TextSize, ast::Operator, TextSize),
(_, rhs, _): (TextSize, ast::ParenthesizedExpr, TextSize),
(_, end_location, _): (TextSize, TextSize, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
{
ast::Stmt::AugAssign(
invalid::assignment_target(&target.expr)?;
Ok(ast::Stmt::AugAssign(
ast::StmtAugAssign {
target: Box::new(set_context(target.into(), ast::ExprContext::Store)),
op,
value: Box::new(rhs.into()),
range: (location..end_location).into()
},
)
))
}
}
@@ -32851,11 +32814,12 @@ fn __action28<
(_, annotation, _): (TextSize, ast::ParenthesizedExpr, TextSize),
(_, rhs, _): (TextSize, core::option::Option<ast::ParenthesizedExpr>, TextSize),
(_, end_location, _): (TextSize, TextSize, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
{
let simple = target.expr.is_name_expr();
ast::Stmt::AnnAssign(
invalid::assignment_target(&target.expr)?;
Ok(ast::Stmt::AnnAssign(
ast::StmtAnnAssign {
target: Box::new(set_context(target.into(), ast::ExprContext::Store)),
annotation: Box::new(annotation.into()),
@@ -32863,7 +32827,7 @@ fn __action28<
simple,
range: (location..end_location).into()
},
)
))
}
}
@@ -48215,7 +48179,7 @@ fn __action797<
__0: (TextSize, ast::ParenthesizedExpr, TextSize),
__1: (TextSize, alloc::vec::Vec<ast::ParenthesizedExpr>, TextSize),
__2: (TextSize, TextSize, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __0.0;
let __end0 = __0.0;
@@ -48246,7 +48210,7 @@ fn __action798<
__1: (TextSize, ast::Operator, TextSize),
__2: (TextSize, ast::ParenthesizedExpr, TextSize),
__3: (TextSize, TextSize, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __0.0;
let __end0 = __0.0;
@@ -48279,7 +48243,7 @@ fn __action799<
__2: (TextSize, ast::ParenthesizedExpr, TextSize),
__3: (TextSize, core::option::Option<ast::ParenthesizedExpr>, TextSize),
__4: (TextSize, TextSize, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __0.0;
let __end0 = __0.0;
@@ -64278,7 +64242,7 @@ fn __action1309<
mode: Mode,
__0: (TextSize, ast::ParenthesizedExpr, TextSize),
__1: (TextSize, alloc::vec::Vec<ast::ParenthesizedExpr>, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __1.2;
let __end0 = __1.2;
@@ -64307,7 +64271,7 @@ fn __action1310<
__0: (TextSize, ast::ParenthesizedExpr, TextSize),
__1: (TextSize, ast::Operator, TextSize),
__2: (TextSize, ast::ParenthesizedExpr, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __2.2;
let __end0 = __2.2;
@@ -64338,7 +64302,7 @@ fn __action1311<
__1: (TextSize, token::Tok, TextSize),
__2: (TextSize, ast::ParenthesizedExpr, TextSize),
__3: (TextSize, core::option::Option<ast::ParenthesizedExpr>, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __3.2;
let __end0 = __3.2;
@@ -71035,7 +70999,7 @@ fn __action1529<
source_code: &str,
mode: Mode,
__0: (TextSize, ast::ParenthesizedExpr, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __0.2;
let __end0 = __0.2;
@@ -71062,7 +71026,7 @@ fn __action1530<
mode: Mode,
__0: (TextSize, ast::ParenthesizedExpr, TextSize),
__1: (TextSize, alloc::vec::Vec<ast::ParenthesizedExpr>, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __1.0;
let __end0 = __1.2;
@@ -71090,7 +71054,7 @@ fn __action1531<
__1: (TextSize, token::Tok, TextSize),
__2: (TextSize, ast::ParenthesizedExpr, TextSize),
__3: (TextSize, ast::ParenthesizedExpr, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __3.0;
let __end0 = __3.2;
@@ -71119,7 +71083,7 @@ fn __action1532<
__0: (TextSize, ast::ParenthesizedExpr, TextSize),
__1: (TextSize, token::Tok, TextSize),
__2: (TextSize, ast::ParenthesizedExpr, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __2.2;
let __end0 = __2.2;
@@ -78391,7 +78355,7 @@ fn __action1752<
source_code: &str,
mode: Mode,
__0: (TextSize, ast::ParenthesizedExpr, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __0.0;
let __end0 = __0.2;
@@ -78416,7 +78380,7 @@ fn __action1753<
mode: Mode,
__0: (TextSize, ast::ParenthesizedExpr, TextSize),
__1: (TextSize, alloc::vec::Vec<ast::ParenthesizedExpr>, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __0.0;
let __end0 = __0.2;
@@ -78443,7 +78407,7 @@ fn __action1754<
__0: (TextSize, ast::ParenthesizedExpr, TextSize),
__1: (TextSize, ast::Operator, TextSize),
__2: (TextSize, ast::ParenthesizedExpr, TextSize),
) -> ast::Stmt
) -> Result<ast::Stmt,__lalrpop_util::ParseError<TextSize,token::Tok,LexicalError>>
{
let __start0 = __0.0;
let __end0 = __0.2;

View File

@@ -0,0 +1,33 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Expr(
StmtExpr {
range: 0..8,
value: NamedExpr(
ExprNamedExpr {
range: 1..7,
target: Name(
ExprName {
range: 1..2,
id: "x",
ctx: Store,
},
),
value: NumberLiteral(
ExprNumberLiteral {
range: 6..7,
value: Int(
5,
),
},
),
},
),
},
),
],
)

View File

@@ -0,0 +1,40 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Assign(
StmtAssign {
range: 0..12,
targets: [
Attribute(
ExprAttribute {
range: 0..7,
value: Name(
ExprName {
range: 0..3,
id: "foo",
ctx: Load,
},
),
attr: Identifier {
id: "bar",
range: 4..7,
},
ctx: Store,
},
),
],
value: NumberLiteral(
ExprNumberLiteral {
range: 10..12,
value: Int(
42,
),
},
),
},
),
],
)

View File

@@ -0,0 +1,41 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Assign(
StmtAssign {
range: 0..12,
targets: [
Attribute(
ExprAttribute {
range: 0..7,
value: StringLiteral(
ExprStringLiteral {
range: 0..5,
value: "foo",
unicode: false,
implicit_concatenated: false,
},
),
attr: Identifier {
id: "y",
range: 6..7,
},
ctx: Store,
},
),
],
value: NumberLiteral(
ExprNumberLiteral {
range: 10..12,
value: Int(
42,
),
},
),
},
),
],
)

View File

@@ -0,0 +1,20 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
Module(
ModModule {
range: 0..9,
body: [
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 0..9,
kind: Shell,
value: "foo = 42",
},
),
],
},
),
)

View File

@@ -0,0 +1,76 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Assign(
StmtAssign {
range: 0..21,
targets: [
List(
ExprList {
range: 0..9,
elts: [
Name(
ExprName {
range: 1..2,
id: "x",
ctx: Store,
},
),
Name(
ExprName {
range: 4..5,
id: "y",
ctx: Store,
},
),
Name(
ExprName {
range: 7..8,
id: "z",
ctx: Store,
},
),
],
ctx: Store,
},
),
],
value: List(
ExprList {
range: 12..21,
elts: [
NumberLiteral(
ExprNumberLiteral {
range: 13..14,
value: Int(
1,
),
},
),
NumberLiteral(
ExprNumberLiteral {
range: 16..17,
value: Int(
2,
),
},
),
NumberLiteral(
ExprNumberLiteral {
range: 19..20,
value: Int(
3,
),
},
),
],
ctx: Load,
},
),
},
),
],
)

View File

@@ -0,0 +1,30 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Assign(
StmtAssign {
range: 0..8,
targets: [
Name(
ExprName {
range: 0..3,
id: "foo",
ctx: Store,
},
),
],
value: NumberLiteral(
ExprNumberLiteral {
range: 6..8,
value: Int(
42,
),
},
),
},
),
],
)

View File

@@ -0,0 +1,70 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Assign(
StmtAssign {
range: 0..13,
targets: [
Subscript(
ExprSubscript {
range: 0..6,
value: Name(
ExprName {
range: 0..1,
id: "x",
ctx: Load,
},
),
slice: Slice(
ExprSlice {
range: 2..5,
lower: Some(
NumberLiteral(
ExprNumberLiteral {
range: 2..3,
value: Int(
1,
),
},
),
),
upper: Some(
NumberLiteral(
ExprNumberLiteral {
range: 4..5,
value: Int(
2,
),
},
),
),
step: None,
},
),
ctx: Store,
},
),
],
value: List(
ExprList {
range: 9..13,
elts: [
NumberLiteral(
ExprNumberLiteral {
range: 10..12,
value: Int(
42,
),
},
),
],
ctx: Load,
},
),
},
),
],
)

View File

@@ -0,0 +1,71 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Assign(
StmtAssign {
range: 0..13,
targets: [
Subscript(
ExprSubscript {
range: 0..6,
value: NumberLiteral(
ExprNumberLiteral {
range: 0..1,
value: Int(
5,
),
},
),
slice: Slice(
ExprSlice {
range: 2..5,
lower: Some(
NumberLiteral(
ExprNumberLiteral {
range: 2..3,
value: Int(
1,
),
},
),
),
upper: Some(
NumberLiteral(
ExprNumberLiteral {
range: 4..5,
value: Int(
2,
),
},
),
),
step: None,
},
),
ctx: Store,
},
),
],
value: List(
ExprList {
range: 9..13,
elts: [
NumberLiteral(
ExprNumberLiteral {
range: 10..12,
value: Int(
42,
),
},
),
],
ctx: Load,
},
),
},
),
],
)

View File

@@ -0,0 +1,36 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Assign(
StmtAssign {
range: 0..9,
targets: [
Starred(
ExprStarred {
range: 0..4,
value: Name(
ExprName {
range: 1..4,
id: "foo",
ctx: Store,
},
),
ctx: Store,
},
),
],
value: NumberLiteral(
ExprNumberLiteral {
range: 7..9,
value: Int(
42,
),
},
),
},
),
],
)

View File

@@ -0,0 +1,44 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Assign(
StmtAssign {
range: 0..9,
targets: [
Subscript(
ExprSubscript {
range: 0..4,
value: Name(
ExprName {
range: 0..1,
id: "x",
ctx: Load,
},
),
slice: NumberLiteral(
ExprNumberLiteral {
range: 2..3,
value: Int(
0,
),
},
),
ctx: Store,
},
),
],
value: NumberLiteral(
ExprNumberLiteral {
range: 7..9,
value: Int(
42,
),
},
),
},
),
],
)

View File

@@ -0,0 +1,45 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Assign(
StmtAssign {
range: 0..9,
targets: [
Subscript(
ExprSubscript {
range: 0..4,
value: NumberLiteral(
ExprNumberLiteral {
range: 0..1,
value: Int(
5,
),
},
),
slice: NumberLiteral(
ExprNumberLiteral {
range: 2..3,
value: Int(
0,
),
},
),
ctx: Store,
},
),
],
value: NumberLiteral(
ExprNumberLiteral {
range: 7..9,
value: Int(
42,
),
},
),
},
),
],
)

View File

@@ -0,0 +1,76 @@
---
source: crates/ruff_python_parser/src/invalid.rs
expression: ast
---
Ok(
[
Assign(
StmtAssign {
range: 0..21,
targets: [
Tuple(
ExprTuple {
range: 0..9,
elts: [
Name(
ExprName {
range: 1..2,
id: "x",
ctx: Store,
},
),
Name(
ExprName {
range: 4..5,
id: "y",
ctx: Store,
},
),
Name(
ExprName {
range: 7..8,
id: "z",
ctx: Store,
},
),
],
ctx: Store,
},
),
],
value: Tuple(
ExprTuple {
range: 12..21,
elts: [
NumberLiteral(
ExprNumberLiteral {
range: 13..14,
value: Int(
1,
),
},
),
NumberLiteral(
ExprNumberLiteral {
range: 16..17,
value: Int(
2,
),
},
),
NumberLiteral(
ExprNumberLiteral {
range: 19..20,
value: Int(
3,
),
},
),
],
ctx: Load,
},
),
},
),
],
)

View File

@@ -616,6 +616,16 @@ impl<'a> SemanticModel<'a> {
self.resolved_names.get(&name.into()).copied()
}
/// Resolves the [`ast::ExprName`] to the [`BindingId`] of the symbol it refers to, if it's the
/// only binding to that name in its scope.
pub fn only_binding(&self, name: &ast::ExprName) -> Option<BindingId> {
self.resolve_name(name).filter(|id| {
let binding = self.binding(*id);
let scope = &self.scopes[binding.scope];
scope.shadowed_binding(*id).is_none()
})
}
/// Resolves the [`Expr`] to a fully-qualified symbol-name, if `value` resolves to an imported
/// or builtin symbol.
///

View File

@@ -36,7 +36,7 @@ console_log = { version = "1.0.0" }
log = { workspace = true }
serde = { workspace = true }
serde-wasm-bindgen = { version = "0.6.0" }
serde-wasm-bindgen = { version = "0.6.1" }
wasm-bindgen = { version = "0.2.84" }
js-sys = { version = "0.3.61" }

View File

@@ -298,10 +298,18 @@ pub struct Options {
/// For example, to represent supporting Python >=3.10 or ==3.10
/// specify `target-version = "py310"`.
///
/// If omitted, and Ruff is configured via a `pyproject.toml` file, the
/// target version will be inferred from its `project.requires-python`
/// field (e.g., `requires-python = ">=3.8"`). If Ruff is configured via
/// `ruff.toml` or `.ruff.toml`, no such inference will be performed.
/// If you're already using a `pyproject.toml` file, we recommend
/// `project.requires-python` instead, as it's based on Python packaging
/// standards, and will be respected by other tools. For example, Ruff
/// treats the following as identical to `target-version = "py38"`:
///
/// ```toml
/// [project]
/// requires-python = ">=3.8"
/// ```
///
/// If both are specified, `target-version` takes precedence over
/// `requires-python`.
#[option(
default = r#""py38""#,
value_type = r#""py37" | "py38" | "py39" | "py310" | "py311" | "py312""#,

View File

@@ -7,147 +7,220 @@ semantics are the same.
For a complete enumeration of the available configuration options, see [_Settings_](settings.md).
## Using `pyproject.toml`
If left unspecified, Ruff's default configuration is equivalent to:
```toml
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
=== "pyproject.toml"
# Same as Black.
line-length = 88
indent-width = 4
```toml
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
# Assume Python 3.8
target-version = "py38"
# Same as Black.
line-length = 88
indent-width = 4
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F"]
ignore = []
# Assume Python 3.8
target-version = "py38"
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F"]
ignore = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
```
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
```
=== "ruff.toml"
```toml
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
# Same as Black.
line-length = 88
indent-width = 4
# Assume Python 3.8
target-version = "py38"
[lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F"]
ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
```
As an example, the following would configure Ruff to:
```toml
[tool.ruff.lint]
# 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults.
select = ["E4", "E7", "E9", "F", "B"]
=== "pyproject.toml"
# 2. Avoid enforcing line-length violations (`E501`)
ignore = ["E501"]
```toml
[tool.ruff.lint]
# 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults.
select = ["E4", "E7", "E9", "F", "B"]
# 3. Avoid trying to fix flake8-bugbear (`B`) violations.
unfixable = ["B"]
# 2. Avoid enforcing line-length violations (`E501`)
ignore = ["E501"]
# 4. Ignore `E402` (import violations) in all `__init__.py` files, and in select subdirectories.
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["E402"]
"**/{tests,docs,tools}/*" = ["E402"]
# 3. Avoid trying to fix flake8-bugbear (`B`) violations.
unfixable = ["B"]
[tool.ruff.format]
# 5. Use single quotes for non-triple-quoted strings.
quote-style = "single"
```
# 4. Ignore `E402` (import violations) in all `__init__.py` files, and in select subdirectories.
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["E402"]
"**/{tests,docs,tools}/*" = ["E402"]
[tool.ruff.format]
# 5. Use single quotes for non-triple-quoted strings.
quote-style = "single"
```
=== "ruff.toml"
```toml
[lint]
# 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults.
select = ["E4", "E7", "E9", "F", "B"]
# 2. Avoid enforcing line-length violations (`E501`)
ignore = ["E501"]
# 3. Avoid trying to fix flake8-bugbear (`B`) violations.
unfixable = ["B"]
# 4. Ignore `E402` (import violations) in all `__init__.py` files, and in select subdirectories.
[lint.per-file-ignores]
"__init__.py" = ["E402"]
"**/{tests,docs,tools}/*" = ["E402"]
[format]
# 5. Use single quotes for non-triple-quoted strings.
quote-style = "single"
```
Linter plugin configurations are expressed as subsections, e.g.:
```toml
[tool.ruff.lint]
# Add "Q" to the list of enabled codes.
select = ["E4", "E7", "E9", "F", "Q"]
=== "pyproject.toml"
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
```
```toml
[tool.ruff.lint]
# Add "Q" to the list of enabled codes.
select = ["E4", "E7", "E9", "F", "Q"]
For a complete enumeration of the available configuration options, see [_Settings_](settings.md).
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
```
## Using `ruff.toml`
=== "ruff.toml"
As an alternative to `pyproject.toml`, Ruff will also respect a `ruff.toml` (or `.ruff.toml`) file,
which implements an equivalent schema (though in the `ruff.toml` and `.ruff.toml` versions, the
```toml
[lint]
# Add "Q" to the list of enabled codes.
select = ["E4", "E7", "E9", "F", "Q"]
[lint.flake8-quotes]
docstring-quotes = "double"
```
Ruff respects `pyproject.toml`, `ruff.toml`, and `.ruff.toml` files. All three implement
an equivalent schema (though in the `ruff.toml` and `.ruff.toml` versions, the
`[tool.ruff]` header is omitted).
For example, the `pyproject.toml` described above would be represented via the following
`ruff.toml` (or `.ruff.toml`):
```toml
[lint]
# Enable flake8-bugbear (`B`) rules.
select = ["E4", "E7", "E9", "F", "B"]
# Never enforce `E501` (line length violations).
ignore = ["E501"]
# Avoid trying to fix flake8-bugbear (`B`) violations.
unfixable = ["B"]
# Ignore `E402` (import violations) in all `__init__.py` files, and in select subdirectories.
[lint.per-file-ignores]
"__init__.py" = ["E402"]
"**/{tests,docs,tools}/*" = ["E402"]
[format]
# Use single quotes for non-triple-quoted strings.
quote-style = "single"
```
For a complete enumeration of the available configuration options, see [_Settings_](settings.md).
## `pyproject.toml` discovery
## Config file discovery
Similar to [ESLint](https://eslint.org/docs/latest/user-guide/configuring/configuration-files#cascading-and-hierarchy),
Ruff supports hierarchical configuration, such that the "closest" `pyproject.toml` file in the
directory hierarchy is used for every individual file, with all paths in the `pyproject.toml` file
Ruff supports hierarchical configuration, such that the "closest" config file in the
directory hierarchy is used for every individual file, with all paths in the config file
(e.g., `exclude` globs, `src` paths) being resolved relative to the directory containing that
`pyproject.toml` file.
config file.
There are a few exceptions to these rules:
@@ -156,47 +229,69 @@ There are a few exceptions to these rules:
1. If a configuration file is passed directly via `--config`, those settings are used for _all_
analyzed files, and any relative paths in that configuration file (like `exclude` globs or
`src` paths) are resolved relative to the _current_ working directory.
1. If no `pyproject.toml` file is found in the filesystem hierarchy, Ruff will fall back to using
1. If no config file is found in the filesystem hierarchy, Ruff will fall back to using
a default configuration. If a user-specific configuration file exists
at `${config_dir}/ruff/pyproject.toml`, that file will be used instead of the default
configuration, with `${config_dir}` being determined via the [`dirs`](https://docs.rs/dirs/4.0.0/dirs/fn.config_dir.html)
crate, and all relative paths being again resolved relative to the _current working directory_.
1. Any `pyproject.toml`-supported settings that are provided on the command-line (e.g., via
1. Any config-file-supported settings that are provided on the command-line (e.g., via
`--select`) will override the settings in _every_ resolved configuration file.
Unlike [ESLint](https://eslint.org/docs/latest/user-guide/configuring/configuration-files#cascading-and-hierarchy),
Ruff does not merge settings across configuration files; instead, the "closest" configuration file
is used, and any parent configuration files are ignored. In lieu of this implicit cascade, Ruff
supports an [`extend`](settings.md#extend) field, which allows you to inherit the settings from another
`pyproject.toml` file, like so:
config file, like so:
```toml
[tool.ruff]
# Extend the `pyproject.toml` file in the parent directory...
extend = "../pyproject.toml"
=== "pyproject.toml"
# ...but use a different line length.
line-length = 100
```
```toml
[tool.ruff]
# Extend the `pyproject.toml` file in the parent directory...
extend = "../pyproject.toml"
All of the above rules apply equivalently to `ruff.toml` and `.ruff.toml` files. If Ruff detects
multiple configuration files in the same directory, the `.ruff.toml` file will take precedence over
the `ruff.toml` file, and the `ruff.toml` file will take precedence over the `pyproject.toml` file.
# ...but use a different line length.
line-length = 100
```
=== "ruff.toml"
```toml
# Extend the `ruff.toml` file in the parent directory...
extend = "../ruff.toml"
# ...but use a different line length.
line-length = 100
```
All of the above rules apply equivalently to `pyproject.toml`, `ruff.toml`, and `.ruff.toml` files.
If Ruff detects multiple configuration files in the same directory, the `.ruff.toml` file will take
precedence over the `ruff.toml` file, and the `ruff.toml` file will take precedence over
the `pyproject.toml` file.
## Python file discovery
When passed a path on the command-line, Ruff will automatically discover all Python files in that
path, taking into account the [`exclude`](settings.md#exclude) and [`extend-exclude`](settings.md#extend-exclude)
settings in each directory's `pyproject.toml` file.
settings in each directory's configuration file.
Files can also be selectively excluded from linting or formatting by scoping the `exclude` setting
to the tool-specific configuration tables. For example, the following would prevent `ruff` from
formatting `.pyi` files, but would continue to include them in linting:
```toml
[tool.ruff.format]
exclude = ["*.pyi"]
```
=== "pyproject.toml"
```toml
[tool.ruff.format]
exclude = ["*.pyi"]
```
=== "ruff.toml"
```toml
[format]
exclude = ["*.pyi"]
```
By default, Ruff will also skip any files that are omitted via `.ignore`, `.gitignore`,
`.git/info/exclude`, and global `gitignore` files (see: [`respect-gitignore`](settings.md#respect-gitignore)).
@@ -211,10 +306,18 @@ Ruff has built-in support for [Jupyter Notebooks](https://jupyter.org/).
To opt in to linting and formatting Jupyter Notebook (`.ipynb`) files, add the `*.ipynb` pattern to
your [`extend-include`](settings.md#extend-include) setting, like so:
```toml
[tool.ruff]
extend-include = ["*.ipynb"]
```
=== "pyproject.toml"
```toml
[tool.ruff]
extend-include = ["*.ipynb"]
```
=== "ruff.toml"
```toml
extend-include = ["*.ipynb"]
```
This will prompt Ruff to discover Jupyter Notebook (`.ipynb`) files in any specified
directories, then lint and format them accordingly.
@@ -245,10 +348,6 @@ Alternatively, pass the notebook file(s) to `ruff` on the command-line directly.
`ruff check /path/to/notebook.ipynb` will always lint `notebook.ipynb`. Similarly,
`ruff format /path/to/notebook.ipynb` will always format `notebook.ipynb`.
All of the above rules apply equivalently to `ruff.toml` and `.ruff.toml` files. If Ruff detects
multiple configuration files in the same directory, the `.ruff.toml` file will take precedence over
the `ruff.toml` file, and the `ruff.toml` file will take precedence over the `pyproject.toml` file.
## Command-line interface
Some configuration options can be provided via the command-line, such as those related to rule
@@ -367,7 +466,7 @@ File selection:
Miscellaneous:
-n, --no-cache
Disable cache reads
Disable cache reads [env: RUFF_NO_CACHE=]
--isolated
Ignore all configuration files
--cache-dir <CACHE_DIR>
@@ -414,7 +513,7 @@ Options:
Print help
Miscellaneous:
-n, --no-cache Disable cache reads
-n, --no-cache Disable cache reads [env: RUFF_NO_CACHE=]
--cache-dir <CACHE_DIR> Path to the cache directory [env: RUFF_CACHE_DIR=]
--isolated Ignore all configuration files
--stdin-filename <STDIN_FILENAME> The name of the file when passing it through stdin

View File

@@ -265,24 +265,47 @@ them. You can find the supported settings in the [API reference](settings.md#iso
For example, you can set [`known-first-party`](settings.md#known-first-party--isort-known-first-party-)
like so:
```toml
[tool.ruff.lint]
select = [
# Pyflakes
"F",
# Pycodestyle
"E",
"W",
# isort
"I001"
]
=== "pyproject.toml"
# Note: Ruff supports a top-level `src` option in lieu of isort's `src_paths` setting.
src = ["src", "tests"]
```toml
[tool.ruff.lint]
select = [
# Pyflakes
"F",
# Pycodestyle
"E",
"W",
# isort
"I001"
]
[tool.ruff.lint.isort]
known-first-party = ["my_module1", "my_module2"]
```
# Note: Ruff supports a top-level `src` option in lieu of isort's `src_paths` setting.
src = ["src", "tests"]
[tool.ruff.lint.isort]
known-first-party = ["my_module1", "my_module2"]
```
=== "ruff.toml"
```toml
[lint]
select = [
# Pyflakes
"F",
# Pycodestyle
"E",
"W",
# isort
"I001"
]
# Note: Ruff supports a top-level `src` option in lieu of isort's `src_paths` setting.
src = ["src", "tests"]
[lint.isort]
known-first-party = ["my_module1", "my_module2"]
```
## How does Ruff determine which of my imports are first-party, third-party, etc.?
@@ -315,24 +338,42 @@ the `--config` option, in which case, the current working directory is used as t
In this case, Ruff would only check the top-level directory. Instead, we can configure Ruff to
consider `src` as a first-party source like so:
```toml
[tool.ruff]
# All paths are relative to the project root, which is the directory containing the pyproject.toml.
src = ["src"]
```
=== "pyproject.toml"
```toml
[tool.ruff]
# All paths are relative to the project root, which is the directory containing the pyproject.toml.
src = ["src"]
```
=== "ruff.toml"
```toml
# All paths are relative to the project root, which is the directory containing the pyproject.toml.
src = ["src"]
```
If your `pyproject.toml`, `ruff.toml`, or `.ruff.toml` extends another configuration file, Ruff
will still use the directory containing your `pyproject.toml`, `ruff.toml`, or `.ruff.toml` file as
the project root (as opposed to the directory of the file pointed to via the `extends` option).
For example, if you add a `ruff.toml` to the `tests` directory in the above example, you'll want to
explicitly set the `src` option in the extended configuration file:
For example, if you add a configuration file to the `tests` directory in the above example, you'll
want to explicitly set the `src` option in the extended configuration file:
```toml
# tests/ruff.toml
extend = "../pyproject.toml"
src = ["../src"]
```
=== "pyproject.toml"
```toml
[tool.ruff]
extend = "../pyproject.toml"
src = ["../src"]
```
=== "ruff.toml"
```toml
extend = "../pyproject.toml"
src = ["../src"]
```
Beyond this `src`-based detection, Ruff will also attempt to determine the current Python package
for a given Python file, and mark imports from within the same package as first-party. For example,
@@ -349,10 +390,18 @@ Ruff has built-in support for linting [Jupyter Notebooks](https://jupyter.org/).
To opt in to linting Jupyter Notebook (`.ipynb`) files, add the `*.ipynb` pattern to your
[`extend-include`](settings.md#extend-include) setting, like so:
```toml
[tool.ruff]
extend-include = ["*.ipynb"]
```
=== "pyproject.toml"
```toml
[tool.ruff]
extend-include = ["*.ipynb"]
```
=== "ruff.toml"
```toml
extend-include = ["*.ipynb"]
```
This will prompt Ruff to discover Jupyter Notebook (`.ipynb`) files in any specified
directories, then lint and format them accordingly.
@@ -378,12 +427,21 @@ Found 3 errors.
## Does Ruff support NumPy- or Google-style docstrings?
Yes! To enforce a docstring convention, add a [`convention`](settings.md#convention--pydocstyle-convention-)
setting following to your `pyproject.toml`:
setting following to your configuration file:
```toml
[tool.ruff.lint.pydocstyle]
convention = "google" # Accepts: "google", "numpy", or "pep257".
```
=== "pyproject.toml"
```toml
[tool.ruff.lint.pydocstyle]
convention = "google" # Accepts: "google", "numpy", or "pep257".
```
=== "ruff.toml"
```toml
[lint.pydocstyle]
convention = "google" # Accepts: "google", "numpy", or "pep257".
```
For example, if you're coming from flake8-docstrings, and your originating configuration uses
`--docstring-convention=numpy`, you'd instead set `convention = "numpy"` in your `pyproject.toml`,
@@ -392,15 +450,29 @@ as above.
Alongside [`convention`](settings.md#convention--pydocstyle-convention-), you'll want to
explicitly enable the `D` rule code prefix, since the `D` rules are not enabled by default:
```toml
[tool.ruff.lint]
select = [
"D",
]
=== "pyproject.toml"
[tool.ruff.lint.pydocstyle]
convention = "google"
```
```toml
[tool.ruff.lint]
select = [
"D",
]
[tool.ruff.lint.pydocstyle]
convention = "google"
```
=== "ruff.toml"
```toml
[lint]
select = [
"D",
]
[lint.pydocstyle]
convention = "google"
```
Setting a [`convention`](settings.md#convention--pydocstyle-convention-) force-disables any rules
that are incompatible with that convention, no matter how they're provided, which avoids accidental
@@ -419,11 +491,11 @@ Run `ruff check /path/to/code.py --show-settings` to view the resolved settings
## I want to use Ruff, but I don't want to use `pyproject.toml`. What are my options?
Yes! In lieu of a `pyproject.toml` file, you can use a `ruff.toml` file for configuration. The two
In lieu of a `pyproject.toml` file, you can use a `ruff.toml` file for configuration. The two
files are functionally equivalent and have an identical schema, with the exception that a `ruff.toml`
file can omit the `[tool.ruff]` section header.
file can omit the `[tool.ruff]` section header. For example:
For example, given this `pyproject.toml`:
=== "pyproject.toml"
```toml
[tool.ruff]
@@ -433,7 +505,7 @@ line-length = 88
convention = "google"
```
You could instead use a `ruff.toml` file like so:
=== "ruff.toml"
```toml
line-length = 88

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