Compare commits

...

30 Commits

Author SHA1 Message Date
Charlie Marsh
55f8f3b2cc Bump version to v0.1.12 (#9475) 2024-01-11 22:13:00 +00:00
trag1c
eb4ed2471b [flake8-simplify] Implement SIM911 (#9460)
## Summary

Closes #9319, implements the [`SIM911` rule from
`flake8-simplify`](https://github.com/MartinThoma/flake8-simplify/pull/183).


#### Note
I wasn't sure whether or not to include
```rs
if checker.settings.preview.is_disabled() {
    return;
}
```
at the beginning of the function with violation logic if the rule's
already declared as part of `RuleGroup::Preview`.
I've seen both variants, so I'd appreciate some feedback on that :)
2024-01-11 14:42:43 -05:00
Micha Reiser
f192c72596 Remove type parameter from parse_* methods (#9466) 2024-01-11 19:41:19 +01:00
Charlie Marsh
25bafd2d66 Restrict builtin-attribute-shadowing to actual shadowed references (#9462)
## Summary

This PR attempts to improve `builtin-attribute-shadowing` (`A003`), a
rule which has been repeatedly criticized, but _does_ have value (just
not in the current form).

Historically, this rule would flag cases like:

```python
class Class:
    id: int
```

This led to an increasing number of exceptions and special-cases to the
rule over time to try and improve it's specificity (e.g., ignore
`TypedDict`, ignore `@override`).

The crux of the issue is that given the above, referencing `id` will
never resolve to `Class.id`, so the shadowing is actually fine. There's
one exception, however:

```python
class Class:
    id: int

    def do_thing() -> id:
        pass
```

Here, `id` actually resolves to the `id` attribute on the class, not the
`id` builtin.

So this PR completely reworks the rule around this _much_ more targeted
case, which will almost always be a mistake: when you reference a class
member from within the class, and that member shadows a builtin.

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

Closes https://github.com/astral-sh/ruff/issues/7806.
2024-01-11 12:59:40 -05:00
Randy Syring
7fc51d29c5 docs: fix typo in formatter.md (#9473)
I believe there was a typo in the formatter docs and I've attempted to
fix it according to what I think was originally intended.
2024-01-11 12:59:01 -05:00
Juan Orduz
2d362e9366 Add numpyro and pymc to ruff users (#9468)
- NumPyro: https://github.com/pyro-ppl/numpyro/pull/1700
- PyMC: https://github.com/pymc-devs/pymc/pull/7091
2024-01-11 10:16:15 -05:00
Micha Reiser
501bc1c270 Avoid allocating during implicit concatenated string formatting (#9469) 2024-01-11 15:58:49 +01:00
manunio
14d3fe6bfa Add a idempotent fuzz_target for ruff_python_formatter (#9448)
Co-authored-by: Addison Crump <addison.crump@cispa.de>
Co-authored-by: Addison Crump <me@addisoncrump.info>
2024-01-11 08:55:59 +01:00
Charlie Marsh
4a3bb67b5f Move pyproject_config into Resolver (#9453)
## Summary

Sort of a random PR to make the coupling between `pyproject_config` and
`resolver` more explicit by passing it to the `Resolver`, rather than
threading it through to each individual method.
2024-01-10 17:58:53 -05:00
Micha Reiser
79f4abbb8d Import Black's type aliases test (#9456) 2024-01-10 12:37:17 +00:00
Micha Reiser
58fcd96ac1 Update Black Tests (#9455) 2024-01-10 12:09:34 +00:00
Micha Reiser
ac02d3aedd Hug multiline-strings preview style (#9243) 2024-01-10 12:47:34 +01:00
Alex Waygood
6be73322da [RUF021]: Add an autofix (#9449)
## Summary

This adds an autofix for the newly added RUF021 (see #9440).
2024-01-09 17:55:33 +00:00
Charlie Marsh
ad1ca72a35 Add some additional Python 3.12 typing members to deprecated-import (#9445)
Closes https://github.com/astral-sh/ruff/issues/9443.
2024-01-09 12:52:01 -05:00
Charlie Marsh
381811b4a6 Skip extra settings resolution when namespace packages are empty (#9446)
Saves 2% on Airflow:

```shell
❯ hyperfine --warmup 20 -i "./target/release/main format ../airflow" "./target/release/ruff format ../airflow"
Benchmark 1: ./target/release/main format ../airflow
  Time (mean ± σ):      72.7 ms ±   0.4 ms    [User: 48.7 ms, System: 75.5 ms]
  Range (min … max):    72.0 ms …  73.7 ms    40 runs

Benchmark 2: ./target/release/ruff format ../airflow
  Time (mean ± σ):      71.4 ms ±   0.6 ms    [User: 46.2 ms, System: 76.2 ms]
  Range (min … max):    70.3 ms …  73.8 ms    41 runs

Summary
  './target/release/ruff format ../airflow' ran
    1.02 ± 0.01 times faster than './target/release/main format ../airflow'
```
2024-01-09 08:33:22 -05:00
Charlie Marsh
20af5a774f Allow Hashable = None in type annotations (#9442)
Closes https://github.com/astral-sh/ruff/issues/9441.
2024-01-08 22:38:34 -05:00
Alex Waygood
86b1ae9383 Add rule to enforce parentheses in a or b and c (#9440)
Fixes #8721

## Summary

This implements the rule proposed in #8721, as RUF021. `and` always
binds more tightly than `or` when chaining the two together.

(This should definitely be autofixable, but I'm leaving that to a
followup PR for now.)

## Test Plan

`cargo test` / `cargo insta review`
2024-01-08 20:28:03 -05:00
Charlie Marsh
84ab21f073 Add a fix for redefinition-while-unused (#9419)
## Summary

This PR enables Ruff to remove redefined imports, as in:

```python
import os
import os

print(os)
```

Previously, Ruff would flag `F811` on the second `import os`, but
couldn't fix it.

For now, this fix is marked as safe, but only available in preview.

Closes https://github.com/astral-sh/ruff/issues/3477.
2024-01-09 00:29:20 +00:00
Charlie Marsh
985f1d10f6 Don't flag redefined-while-unused in if branches (#9418)
## Summary

On `main`, we flag redefinitions in cases like:

```python
import os

x = 1

if x > 0:
    import os
```

That is, we consider these to be in the "same branch", since they're not
in disjoint branches. This matches Flake8's behavior, but it seems to
lead to false positives.
2024-01-08 17:06:55 -05:00
Charlie Marsh
f419af494f Allow Boolean positionals in setters (#9429)
## Summary

Ignores Boolean trap enforcement for methods that appear to be setters
(as in the Qt and pygame APIs).

Closes https://github.com/astral-sh/ruff/issues/9287.
Closes https://github.com/astral-sh/ruff/issues/8923.
2024-01-08 13:02:16 -05:00
Micha Reiser
94968fedd5 Use Rust 1.75 toolchain (#9437) 2024-01-08 18:03:16 +01:00
Charlie Marsh
ba71772d93 Parenthesize breaking named expressions in match guards (#9396)
## Summary

This is an attempt to solve
https://github.com/astral-sh/ruff/issues/9394 by avoiding breaks in
named expressions when invalid.
2024-01-08 14:47:01 +00:00
Micha Reiser
b1a5df8694 Move locate_cmp_ops to invalid_literal_comparisons (#9438) 2024-01-08 13:15:36 +01:00
dependabot[bot]
0c84782060 Bump tempfile from 3.8.1 to 3.9.0 (#9434)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 08:57:24 +00:00
dependabot[bot]
d9fc9702cf Bump serde from 1.0.193 to 1.0.195 (#9430)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 08:57:02 +00:00
dependabot[bot]
c58d1aa87d Bump anyhow from 1.0.76 to 1.0.79 (#9432)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 08:56:55 +00:00
dependabot[bot]
1d1824787b Bump clap from 4.4.12 to 4.4.13 (#9431)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 08:56:49 +00:00
Charlie Marsh
04afdf177b Disambiguate argument descriptors from section headers (#9427)
## Summary

Given a docstring like:

```python
def func(x: int, args: tuple[int]):
    """Toggle the gizmo.

    Args:
        x: Some argument.
        args: Some other arguments.
    """
```

We were considering the `args:` descriptor to be an indented docstring
section header (since `Args:`) is a valid header name. This led to very
confusing diagnostics.

This PR makes the parsing a bit more lax in this case, such that if we
see a nested header that's more deeply indented than the preceding
header, and the preceding section allows sub-items (like `Args:`), we
avoid treating the nested item as a section header.

Closes https://github.com/astral-sh/ruff/issues/9426.
2024-01-07 22:41:00 -05:00
Alex Waygood
d5a439cbd3 [flake8-pyi] PYI053: Exclude string literals that are the first argument to warnings.deprecated or typing_extensions.deprecated (#9423)
Fixes #9420
2024-01-07 18:41:14 -05:00
Charlie Marsh
63953431a6 Include subscripts and attributes in static key rule (#9416) 2024-01-06 17:28:57 -05:00
200 changed files with 3948 additions and 1313 deletions

View File

@@ -1,5 +1,57 @@
# Changelog
## 0.1.12
### Preview features
- Formatter: Hug multiline-strings in preview style ([#9243](https://github.com/astral-sh/ruff/pull/9243))
- \[`flake8-bandit`\] Add `ssl-with-no-version` (`S504`) ([#9384](https://github.com/astral-sh/ruff/pull/9384))
- \[`flake8-bandit`\] Implement `ssl-insecure-version` (`S502`) ([#9390](https://github.com/astral-sh/ruff/pull/9390))
- \[`flake8-bandit`\] Implement `ssl-with-bad-defaults` (`S503`) ([#9391](https://github.com/astral-sh/ruff/pull/9391))
- \[`flake8-bandit`\] Implement suspicious import rules (`S4XX`) ([#8831](https://github.com/astral-sh/ruff/pull/8831))
- \[`flake8-simplify`\] Implement `zip-dict-keys-and-values` (`SIM911`) ([#9460](https://github.com/astral-sh/ruff/pull/9460))
- \[`pyflakes`\] Add a fix for `redefined-while-unused` (`F811`) ([#9419](https://github.com/astral-sh/ruff/pull/9419))
- \[`pylint`\] Implement `unnecessary-dunder-call` (`C2801`) ([#9166](https://github.com/astral-sh/ruff/pull/9166))
- \[`ruff`\] Add `parenthesize-chained-operators` (`RUF021`) to enforce parentheses in `a or b and c` ([#9440](https://github.com/astral-sh/ruff/pull/9440))
### Rule changes
- \[`flake8-boolean-trap`\] Allow Boolean positional arguments in setters ([#9429](https://github.com/astral-sh/ruff/pull/9429))
- \[`flake8-builtins`\] Restrict `builtin-attribute-shadowing` (`A003`) to actual shadowed references ([#9462](https://github.com/astral-sh/ruff/pull/9462))
- \[`flake8-pyi`\] Add fix for `generator-return-from-iter-method` (`PYI058`) ([#9355](https://github.com/astral-sh/ruff/pull/9355))
- \[`pyflakes`\] Don't flag `redefined-while-unused` (`F811`) in `if` branches ([#9418](https://github.com/astral-sh/ruff/pull/9418))
- \[`pyupgrade`\] Add some additional Python 3.12 typing members to `deprecated-import` ([#9445](https://github.com/astral-sh/ruff/pull/9445))
- \[`ruff`\] Add fix for `parenthesize-chained-operators` (`RUF021`) ([#9449](https://github.com/astral-sh/ruff/pull/9449))
- \[`ruff`\] Include subscripts and attributes in static key rule (`RUF011`) ([#9416](https://github.com/astral-sh/ruff/pull/9416))
- \[`ruff`\] Support variable keys in static dictionary key rule (`RUF011`) ([#9411](https://github.com/astral-sh/ruff/pull/9411))
### Formatter
- Generate deterministic IDs when formatting notebooks ([#9359](https://github.com/astral-sh/ruff/pull/9359))
- Allow `# fmt: skip` with interspersed same-line comments ([#9395](https://github.com/astral-sh/ruff/pull/9395))
- Parenthesize breaking named expressions in match guards ([#9396](https://github.com/astral-sh/ruff/pull/9396))
### Bug fixes
- Add cell indexes to all diagnostics ([#9387](https://github.com/astral-sh/ruff/pull/9387))
- Avoid infinite loop in constant vs. `None` comparisons ([#9376](https://github.com/astral-sh/ruff/pull/9376))
- Handle raises with implicit alternate branches ([#9377](https://github.com/astral-sh/ruff/pull/9377))
- Ignore trailing quotes for unclosed l-brace errors ([#9388](https://github.com/astral-sh/ruff/pull/9388))
- Respect multi-segment submodule imports when resolving qualified names ([#9382](https://github.com/astral-sh/ruff/pull/9382))
- Use `DisplayParseError` for stdin parser errors ([#9409](https://github.com/astral-sh/ruff/pull/9409))
- Use `comment_ranges` for isort directive extraction ([#9414](https://github.com/astral-sh/ruff/pull/9414))
- Use transformed source code for diagnostic locations ([#9408](https://github.com/astral-sh/ruff/pull/9408))
- \[`flake8-pyi`\] Exclude `warnings.deprecated` and `typing_extensions.deprecated` arguments ([#9423](https://github.com/astral-sh/ruff/pull/9423))
- \[`flake8-pyi`\] Fix false negative for `unused-private-protocol` (`PYI046`) with unused generic protocols ([#9405](https://github.com/astral-sh/ruff/pull/9405))
- \[`pydocstyle`\] Disambiguate argument descriptors from section headers ([#9427](https://github.com/astral-sh/ruff/pull/9427))
- \[`pylint`\] Homogenize `PLR0914` message to match other `PLR09XX` rules ([#9399](https://github.com/astral-sh/ruff/pull/9399))
- \[`ruff`\] Allow `Hashable = None` in type annotations (`RUF013`) ([#9442](https://github.com/astral-sh/ruff/pull/9442))
### Documentation
- Fix admonition hyperlink colouring ([#9385](https://github.com/astral-sh/ruff/pull/9385))
- Add missing preview link ([#9386](https://github.com/astral-sh/ruff/pull/9386))
## 0.1.11
### Preview features

109
Cargo.lock generated
View File

@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.76"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "argfile"
@@ -312,9 +312,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.12"
version = "4.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d"
checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642"
dependencies = [
"clap_builder",
"clap_derive",
@@ -382,7 +382,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -600,7 +600,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -611,7 +611,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -748,23 +748,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.3"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
"windows-sys 0.52.0",
]
[[package]]
@@ -1107,7 +1096,7 @@ dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -1250,9 +1239,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.149"
version = "0.2.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]]
name = "libcst"
@@ -1297,9 +1286,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.10"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
[[package]]
name = "lock_api"
@@ -1696,7 +1685,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -1781,9 +1770,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.73"
version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1"
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
dependencies = [
"unicode-ident",
]
@@ -1827,9 +1816,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.33"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@@ -1996,7 +1985,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2049,7 +2038,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.1.11"
version = "0.1.12"
dependencies = [
"anyhow",
"argfile",
@@ -2176,7 +2165,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.11"
version = "0.1.12"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2248,7 +2237,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2428,7 +2417,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.11"
version = "0.1.12"
dependencies = [
"anyhow",
"clap",
@@ -2539,15 +2528,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.21"
version = "0.38.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
dependencies = [
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2653,9 +2642,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
version = "1.0.193"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
dependencies = [
"serde_derive",
]
@@ -2673,13 +2662,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.193"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2741,7 +2730,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2845,7 +2834,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2861,9 +2850,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.40"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
@@ -2872,15 +2861,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.8.1"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.4.1",
"rustix",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2941,7 +2930,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -2953,7 +2942,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
"test-case-core",
]
@@ -2974,7 +2963,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -3111,7 +3100,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -3329,7 +3318,7 @@ checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -3423,7 +3412,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
"wasm-bindgen-shared",
]
@@ -3457,7 +3446,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3490,7 +3479,7 @@ checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]
[[package]]
@@ -3817,5 +3806,5 @@ checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
"syn 2.0.48",
]

View File

@@ -14,14 +14,14 @@ license = "MIT"
[workspace.dependencies]
aho-corasick = { version = "1.1.2" }
annotate-snippets = { version = "0.9.2", features = ["color"] }
anyhow = { version = "1.0.76" }
anyhow = { version = "1.0.79" }
argfile = { version = "0.1.6" }
assert_cmd = { version = "2.0.8" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
clap = { version = "4.4.12", features = ["derive"] }
clap = { version = "4.4.13", features = ["derive"] }
clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "2.0.0" }
codspeed-criterion-compat = { version = "2.3.3", default-features = false }
@@ -76,7 +76,7 @@ rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" }
seahash = { version ="4.1.0"}
semver = { version = "1.0.20" }
serde = { version = "1.0.193", features = ["derive"] }
serde = { version = "1.0.195", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.3" }
serde_json = { version = "1.0.109" }
serde_test = { version = "1.0.152" }
@@ -89,7 +89,7 @@ static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.40" }
tempfile = { version ="3.8.1"}
tempfile = { version ="3.9.0"}
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.51" }
tikv-jemallocator = { version ="0.5.0"}

View File

@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.11
rev: v0.1.12
hooks:
# Run the linter.
- id: ruff
@@ -418,6 +418,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
- [Neon](https://github.com/neondatabase/neon)
- [NoneBot](https://github.com/nonebot/nonebot2)
- [NumPyro](https://github.com/pyro-ppl/numpyro)
- [ONNX](https://github.com/onnx/onnx)
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
- [PDM](https://github.com/pdm-project/pdm)
@@ -429,6 +430,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [PostHog](https://github.com/PostHog/posthog)
- Prefect ([Python SDK](https://github.com/PrefectHQ/prefect), [Marvin](https://github.com/PrefectHQ/marvin))
- [PyInstaller](https://github.com/pyinstaller/pyinstaller)
- [PyMC](https://github.com/pymc-devs/pymc/)
- [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing)
- [PyTorch](https://github.com/pytorch/pytorch)
- [Pydantic](https://github.com/pydantic/pydantic)

View File

@@ -55,7 +55,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
&case,
|b, case| {
// Tokenize the source.
let tokens = lexer::lex(case.code(), Mode::Module).collect::<Vec<_>>();
let tokens: Vec<_> = lexer::lex(case.code(), Mode::Module).collect();
// Parse the source.
let ast = parse_program_tokens(tokens.clone(), case.code(), false).unwrap();

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.1.11"
version = "0.1.12"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -25,10 +25,9 @@ use ruff_notebook::NotebookIndex;
use ruff_python_ast::imports::ImportMap;
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy, Resolver};
use ruff_workspace::resolver::Resolver;
use ruff_workspace::Settings;
use crate::cache;
use crate::diagnostics::Diagnostics;
/// [`Path`] that is relative to the package root in [`PackageCache`].
@@ -86,6 +85,7 @@ pub(crate) struct Cache {
changes: Mutex<Vec<Change>>,
/// The "current" timestamp used as cache for the updates of
/// [`FileCache::last_seen`]
#[allow(clippy::struct_field_names)]
last_seen_cache: u64,
}
@@ -442,7 +442,7 @@ pub(super) struct CacheMessage {
pub(crate) trait PackageCaches {
fn get(&self, package_root: &Path) -> Option<&Cache>;
fn persist(self) -> anyhow::Result<()>;
fn persist(self) -> Result<()>;
}
impl<T> PackageCaches for Option<T>
@@ -468,27 +468,17 @@ pub(crate) struct PackageCacheMap<'a>(FxHashMap<&'a Path, Cache>);
impl<'a> PackageCacheMap<'a> {
pub(crate) fn init(
pyproject_config: &PyprojectConfig,
package_roots: &FxHashMap<&'a Path, Option<&'a Path>>,
resolver: &Resolver,
) -> Self {
fn init_cache(path: &Path) {
if let Err(e) = cache::init(path) {
if let Err(e) = init(path) {
error!("Failed to initialize cache at {}: {e:?}", path.display());
}
}
match pyproject_config.strategy {
PyprojectDiscoveryStrategy::Fixed => {
init_cache(&pyproject_config.settings.cache_dir);
}
PyprojectDiscoveryStrategy::Hierarchical => {
for settings in
std::iter::once(&pyproject_config.settings).chain(resolver.settings())
{
init_cache(&settings.cache_dir);
}
}
for settings in resolver.settings() {
init_cache(&settings.cache_dir);
}
Self(
@@ -498,7 +488,7 @@ impl<'a> PackageCacheMap<'a> {
.unique()
.par_bridge()
.map(|cache_root| {
let settings = resolver.resolve(cache_root, pyproject_config);
let settings = resolver.resolve(cache_root);
let cache = Cache::open(cache_root.to_path_buf(), settings);
(cache_root, cache)
})

View File

@@ -38,7 +38,6 @@ pub(crate) fn add_noqa(
.flatten()
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
pyproject_config,
);
let start = Instant::now();
@@ -57,7 +56,7 @@ pub(crate) fn add_noqa(
.parent()
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path, pyproject_config);
let settings = resolver.resolve(path);
let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind,
Ok(None) => return None,

View File

@@ -57,16 +57,11 @@ pub(crate) fn check(
.flatten()
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
pyproject_config,
);
// Load the caches.
let caches = if bool::from(cache) {
Some(PackageCacheMap::init(
pyproject_config,
&package_roots,
&resolver,
))
Some(PackageCacheMap::init(&package_roots, &resolver))
} else {
None
};
@@ -81,7 +76,7 @@ pub(crate) fn check(
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path, pyproject_config);
let settings = resolver.resolve(path);
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion(
@@ -128,7 +123,7 @@ pub(crate) fn check(
Some(result.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
let settings = resolver.resolve(path, pyproject_config);
let settings = resolver.resolve(path);
if settings.linter.rules.enabled(Rule::IOError) {
let dummy =
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use ruff_linter::packaging;
use ruff_linter::settings::flags;
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig, Resolver};
use crate::args::CliOverrides;
use crate::diagnostics::{lint_stdin, Diagnostics};
@@ -18,20 +18,20 @@ pub(crate) fn check_stdin(
noqa: flags::Noqa,
fix_mode: flags::FixMode,
) -> Result<Diagnostics> {
if pyproject_config.settings.file_resolver.force_exclude {
let mut resolver = Resolver::new(pyproject_config);
if resolver.force_exclude() {
if let Some(filename) = filename {
if !python_file_at_path(filename, pyproject_config, overrides)? {
if !python_file_at_path(filename, &mut resolver, overrides)? {
if fix_mode.is_apply() {
parrot_stdin()?;
}
return Ok(Diagnostics::default());
}
let lint_settings = &pyproject_config.settings.linter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
{
if filename.file_name().is_some_and(|name| {
match_exclusion(filename, name, &resolver.base_settings().linter.exclude)
}) {
if fix_mode.is_apply() {
parrot_stdin()?;
}
@@ -41,13 +41,13 @@ pub(crate) fn check_stdin(
}
let stdin = read_from_stdin()?;
let package_root = filename.and_then(Path::parent).and_then(|path| {
packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)
packaging::detect_package_root(path, &resolver.base_settings().linter.namespace_packages)
});
let mut diagnostics = lint_stdin(
filename,
package_root,
stdin,
&pyproject_config.settings,
resolver.base_settings(),
noqa,
fix_mode,
)?;

View File

@@ -25,9 +25,7 @@ use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::resolver::{
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver,
};
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
@@ -79,7 +77,7 @@ pub(crate) fn format(
return Ok(ExitStatus::Success);
}
warn_incompatible_formatter_settings(&pyproject_config, Some(&resolver));
warn_incompatible_formatter_settings(&resolver);
// Discover the package root for each Python file.
let package_roots = resolver.package_roots(
@@ -88,7 +86,6 @@ pub(crate) fn format(
.flatten()
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
&pyproject_config,
);
let caches = if cli.no_cache {
@@ -99,11 +96,7 @@ pub(crate) fn format(
#[cfg(debug_assertions)]
crate::warn_user!("Detected debug build without --no-cache.");
Some(PackageCacheMap::init(
&pyproject_config,
&package_roots,
&resolver,
))
Some(PackageCacheMap::init(&package_roots, &resolver))
};
let start = Instant::now();
@@ -118,7 +111,7 @@ pub(crate) fn format(
return None;
};
let settings = resolver.resolve(path, &pyproject_config);
let settings = resolver.resolve(path);
// Ignore files that are excluded from formatting
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
@@ -723,15 +716,10 @@ impl Display for FormatCommandError {
}
}
pub(super) fn warn_incompatible_formatter_settings(
pyproject_config: &PyprojectConfig,
resolver: Option<&Resolver>,
) {
pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
// First, collect all rules that are incompatible regardless of the linter-specific settings.
let mut incompatible_rules = FxHashSet::default();
for setting in std::iter::once(&pyproject_config.settings)
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
for setting in resolver.settings() {
for rule in [
// The formatter might collapse implicit string concatenation on a single line.
Rule::SingleLineImplicitStringConcatenation,
@@ -760,9 +748,7 @@ pub(super) fn warn_incompatible_formatter_settings(
}
// Next, validate settings-specific incompatibilities.
for setting in std::iter::once(&pyproject_config.settings)
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
for setting in resolver.settings() {
// Validate all rules that rely on tab styles.
if setting.linter.rules.enabled(Rule::TabIndentation)
&& setting.formatter.indent_style.is_tab()

View File

@@ -6,7 +6,7 @@ use log::error;
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
@@ -27,24 +27,23 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
cli.stdin_filename.as_deref(),
)?;
warn_incompatible_formatter_settings(&pyproject_config, None);
let mut resolver = Resolver::new(&pyproject_config);
warn_incompatible_formatter_settings(&resolver);
let mode = FormatMode::from_cli(cli);
if pyproject_config.settings.file_resolver.force_exclude {
if resolver.force_exclude() {
if let Some(filename) = cli.stdin_filename.as_deref() {
if !python_file_at_path(filename, &pyproject_config, overrides)? {
if !python_file_at_path(filename, &mut resolver, overrides)? {
if mode.is_write() {
parrot_stdin()?;
}
return Ok(ExitStatus::Success);
}
let format_settings = &pyproject_config.settings.formatter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
{
if filename.file_name().is_some_and(|name| {
match_exclusion(filename, name, &resolver.base_settings().formatter.exclude)
}) {
if mode.is_write() {
parrot_stdin()?;
}
@@ -63,12 +62,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
};
// Format the file.
match format_source_code(
path,
&pyproject_config.settings.formatter,
source_type,
mode,
) {
match format_source_code(path, &resolver.base_settings().formatter, source_type, mode) {
Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check | FormatMode::Diff => {

View File

@@ -18,6 +18,7 @@ struct Explanation<'a> {
summary: &'a str,
message_formats: &'a [&'a str],
fix: String,
#[allow(clippy::struct_field_names)]
explanation: Option<&'a str>,
preview: bool,
}

View File

@@ -29,7 +29,7 @@ pub(crate) fn show_settings(
bail!("No files found under the given path");
};
let settings = resolver.resolve(&path, pyproject_config);
let settings = resolver.resolve(&path);
writeln!(writer, "Resolved settings for: {path:?}")?;
if let Some(settings_path) = pyproject_config.path.as_ref() {

View File

@@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
use ruff_cli::args::{FormatCommand, LogLevelArgs};
use ruff_cli::args::{CliOverrides, FormatArguments, FormatCommand, LogLevelArgs};
use ruff_cli::resolve::resolve;
use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel;
@@ -38,24 +38,24 @@ use ruff_python_formatter::{
use ruff_python_parser::ParseError;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
#[allow(clippy::type_complexity)]
fn ruff_check_paths(
dirs: &[PathBuf],
) -> anyhow::Result<(
Vec<Result<ResolvedFile, ignore::Error>>,
Resolver,
PyprojectConfig,
)> {
fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, CliOverrides)> {
let args_matches = FormatCommand::command()
.no_binary_name(true)
.get_matches_from(dirs);
let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?;
let (cli, overrides) = arguments.partition();
Ok((cli, overrides))
}
/// Find the [`PyprojectConfig`] to use for formatting.
fn find_pyproject_config(
cli: &FormatArguments,
overrides: &CliOverrides,
) -> anyhow::Result<PyprojectConfig> {
let mut pyproject_config = resolve(
cli.isolated,
cli.config.as_deref(),
&overrides,
overrides,
cli.stdin_filename.as_deref(),
)?;
// We don't want to format pyproject.toml
@@ -64,11 +64,18 @@ fn ruff_check_paths(
FilePattern::Builtin("*.pyi"),
])
.unwrap();
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?;
if paths.is_empty() {
bail!("no python files in {:?}", dirs)
}
Ok((paths, resolver, pyproject_config))
Ok(pyproject_config)
}
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
#[allow(clippy::type_complexity)]
fn ruff_check_paths<'a>(
pyproject_config: &'a PyprojectConfig,
cli: &FormatArguments,
overrides: &CliOverrides,
) -> anyhow::Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> {
let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, overrides)?;
Ok((paths, resolver))
}
/// Collects statistics over the formatted files to compute the Jaccard index or the similarity
@@ -216,6 +223,7 @@ pub(crate) struct Args {
#[arg(long)]
pub(crate) files_with_errors: Option<u32>,
#[clap(flatten)]
#[allow(clippy::struct_field_names)]
pub(crate) log_level_args: LogLevelArgs,
}
@@ -451,11 +459,17 @@ fn format_dev_project(
files[0].display()
);
// TODO(konstin): black excludes
// TODO(konstin): Respect black's excludes.
// Find files to check (or in this case, format twice). Adapted from ruff_cli
// First argument is ignored
let (paths, resolver, pyproject_config) = ruff_check_paths(files)?;
let (cli, overrides) = parse_cli(files)?;
let pyproject_config = find_pyproject_config(&cli, &overrides)?;
let (paths, resolver) = ruff_check_paths(&pyproject_config, &cli, &overrides)?;
if paths.is_empty() {
bail!("No Python files found under the given path(s)");
}
let results = {
let pb_span =
@@ -468,14 +482,7 @@ fn format_dev_project(
#[cfg(feature = "singlethreaded")]
let iter = { paths.into_iter() };
iter.map(|path| {
let result = format_dir_entry(
path,
stability_check,
write,
&black_options,
&resolver,
&pyproject_config,
);
let result = format_dir_entry(path, stability_check, write, &black_options, &resolver);
pb_span.pb_inc(1);
result
})
@@ -525,14 +532,13 @@ fn format_dev_project(
})
}
/// Error handling in between walkdir and `format_dev_file`
/// Error handling in between walkdir and `format_dev_file`.
fn format_dir_entry(
resolved_file: Result<ResolvedFile, ignore::Error>,
stability_check: bool,
write: bool,
options: &BlackOptions,
resolver: &Resolver,
pyproject_config: &PyprojectConfig,
) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> {
let resolved_file = resolved_file.context("Iterating the files in the repository failed")?;
// For some reason it does not filter in the beginning
@@ -543,7 +549,7 @@ fn format_dir_entry(
let path = resolved_file.into_path();
let mut options = options.to_py_format_options(&path);
let settings = resolver.resolve(&path, pyproject_config);
let settings = resolver.resolve(&path);
// That's a bad way of doing this but it's not worth doing something better for format_dev
if settings.formatter.line_width != LineWidth::default() {
options = options.with_line_width(settings.formatter.line_width);

View File

@@ -1472,6 +1472,11 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
}
fn fits_text(&mut self, text: Text, args: PrintElementArgs) -> Fits {
fn exceeds_width(fits: &FitsMeasurer, args: PrintElementArgs) -> bool {
fits.state.line_width > fits.options().line_width.into()
&& !args.measure_mode().allows_text_overflow()
}
let indent = std::mem::take(&mut self.state.pending_indent);
self.state.line_width +=
u32::from(indent.level()) * self.options().indent_width() + u32::from(indent.align());
@@ -1493,7 +1498,13 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
return Fits::No;
}
match args.measure_mode() {
MeasureMode::FirstLine => return Fits::Yes,
MeasureMode::FirstLine => {
return if exceeds_width(self, args) {
Fits::No
} else {
Fits::Yes
};
}
MeasureMode::AllLines
| MeasureMode::AllLinesAllowTextOverflow => {
self.state.line_width = 0;
@@ -1511,9 +1522,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
}
}
if self.state.line_width > self.options().line_width.into()
&& !args.measure_mode().allows_text_overflow()
{
if exceeds_width(self, args) {
return Fits::No;
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.1.11"
version = "0.1.12"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -71,6 +71,8 @@ foo.is_(True)
bar.is_not(False)
next(iter([]), False)
sa.func.coalesce(tbl.c.valid, False)
setVisible(True)
set_visible(True)
class Registry:
@@ -114,3 +116,6 @@ from typing import override
@override
def func(x: bool):
pass
settings(True)

View File

@@ -8,46 +8,14 @@ class MyClass:
self.id = 10
self.dir = "."
def str(self):
pass
from typing import TypedDict
class MyClass(TypedDict):
id: int
from threading import Event
class CustomEvent(Event):
def set(self) -> None:
...
def str(self) -> None:
...
from logging import Filter, LogRecord
class CustomFilter(Filter):
def filter(self, record: LogRecord) -> bool:
...
def str(self) -> None:
...
from typing_extensions import override
class MyClass:
@override
def str(self):
pass
def int(self):
pass
def str(self):
pass
def method_usage(self) -> str:
pass
def attribute_usage(self) -> id:
pass

View File

@@ -1,3 +1,9 @@
import warnings
import typing_extensions
from collections.abc import Callable
from warnings import deprecated
def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None:
...
@@ -45,3 +51,24 @@ class Demo:
def func() -> None:
"""Docstrings are excluded from this rule. Some padding."""
@warnings.deprecated("Veeeeeeeeeeeeeeeeeeeeeeery long deprecation message, but that's okay")
def deprecated_function() -> None: ...
@typing_extensions.deprecated("Another loooooooooooooooooooooong deprecation message, it's still okay")
def another_deprecated_function() -> None: ...
@deprecated("A third loooooooooooooooooooooooooooooong deprecation message")
def a_third_deprecated_function() -> None: ...
def not_warnings_dot_deprecated(
msg: str
) -> Callable[[Callable[[], None]], Callable[[], None]]: ...
@not_warnings_dot_deprecated("Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!")
def not_a_deprecated_function() -> None: ...

View File

@@ -1,3 +1,7 @@
import warnings
import typing_extensions
from typing_extensions import deprecated
def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
def f2(
x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
@@ -38,3 +42,25 @@ class Demo:
def func() -> None:
"""Docstrings are excluded from this rule. Some padding.""" # OK
@warnings.deprecated(
"Veeeeeeeeeeeeeeeeeeeeeeery long deprecation message, but that's okay" # OK
)
def deprecated_function() -> None: ...
@typing_extensions.deprecated(
"Another loooooooooooooooooooooong deprecation message, it's still okay" # OK
)
def another_deprecated_function() -> None: ...
@deprecated("A third loooooooooooooooooooooooooooooong deprecation message") # OK
def a_third_deprecated_function() -> None: ...
def not_warnings_dot_deprecated(
msg: str
) -> Callable[[Callable[[], None]], Callable[[], None]]: ...
@not_warnings_dot_deprecated(
"Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
)
def not_a_deprecated_function() -> None: ...

View File

@@ -0,0 +1,23 @@
def foo(d: dict[str, str]) -> None:
for k, v in zip(d.keys(), d.values()): # SIM911
...
for k, v in zip(d.keys(), d.values(), strict=True): # SIM911
...
for k, v in zip(d.keys(), d.values(), struct=True): # OK
...
d1 = d2 = {}
for k, v in zip(d1.keys(), d2.values()): # OK
...
for k, v in zip(d1.items(), d2.values()): # OK
...
for k, v in zip(d2.keys(), d2.values()): # SIM911
...
items = zip(x.keys(), x.values()) # OK

View File

@@ -150,3 +150,21 @@ class Test:
Args:
arg1: some description of arg
"""
def select_data(
query: str,
args: tuple,
database: str,
auto_save: bool,
) -> None:
"""This function has an argument `args`, which shouldn't be mistaken for a section.
Args:
query:
Query template.
args:
A list of arguments.
database:
Which database to connect to ("origin" or "destination").
"""

View File

@@ -536,9 +536,29 @@ def non_empty_blank_line_before_section(): # noqa: D416
"""Toggle the gizmo.
The function's description.
Returns
-------
A value of some sort.
"""
def lowercase_sub_section_header():
"""Below, `returns:` should _not_ be considered a section header.
Args:
Here's a note.
returns:
"""
def titlecase_sub_section_header():
"""Below, `Returns:` should be considered a section header.
Args:
Here's a note.
Returns:
"""

View File

@@ -0,0 +1,6 @@
import os
x = 1
if x > 0:
import os

View File

@@ -91,3 +91,12 @@ from typing_extensions import dataclass_transform
# UP035
from backports.strenum import StrEnum
# UP035
from typing_extensions import override
# UP035
from typing_extensions import Buffer
# UP035
from typing_extensions import get_original_bases

View File

@@ -7,6 +7,9 @@ constant = 5
{v: v * v for v in range(10)}
{(0, "a", v): v * v for v in range(10)} # Tuple with variable
{constant: value.upper() for value in data for constant in data}
{value.attribute: value.upper() for value in data for constant in data}
{constant[value]: value.upper() for value in data for constant in data}
{value[constant]: value.upper() for value in data for constant in data}
# Errors
{"key": value.upper() for value in data}
@@ -15,3 +18,5 @@ constant = 5
{(1, "a"): value.upper() for value in data} # Constant tuple
{constant: value.upper() for value in data}
{constant + constant: value.upper() for value in data}
{constant.attribute: value.upper() for value in data}
{constant[0]: value.upper() for value in data}

View File

@@ -1,5 +1,5 @@
import typing
from typing import Annotated, Any, Literal, Optional, Tuple, Union
from typing import Annotated, Any, Literal, Optional, Tuple, Union, Hashable
def f(arg: int):
@@ -257,3 +257,13 @@ from custom_typing import MaybeInt
def f(arg: MaybeInt = None):
pass
# Hashable
def f(arg: Hashable = None): # OK
pass
def f(arg: Hashable | int = None): # OK
pass

View File

@@ -0,0 +1,114 @@
# See https://docs.python.org/3/reference/expressions.html#operator-precedence
# for the official docs on operator precedence.
#
# Most importantly, `and` *always* takes precedence over `or`.
#
# `not` (the third boolean/logical operator) takes precedence over both,
# but the rule there is easier to remember,
# so we don't emit a diagnostic if a `not` expression is unparenthesized
# as part of a chain.
a, b, c = 1, 0, 2
x = a or b and c # RUF021: => `a or (b and c)`
x = a or b and c # looooooooooooooooooooooooooooooong comment but it won't prevent an autofix
a, b, c = 0, 1, 2
y = a and b or c # RUF021: => `(a and b) or c`
a, b, c, d = 1, 2, 0, 3
if a or b or c and d: # RUF021: => `a or b or (c and d)`
pass
a, b, c, d = 0, 0, 2, 3
if bool():
pass
elif a or b and c or d: # RUF021: => `a or (b and c) or d`
pass
a, b, c, d = 0, 1, 0, 2
while a and b or c and d: # RUF021: => `(and b) or (c and d)`
pass
b, c, d, e = 2, 3, 0, 4
# RUF021: => `a or b or c or (d and e)`:
z = [a for a in range(5) if a or b or c or d and e]
a, b, c, d = 0, 1, 3, 0
assert not a and b or c or d # RUF021: => `(not a and b) or c or d`
if (not a) and b or c or d: # RUF021: => `((not a) and b) or c or d`
if (not a and b) or c or d: # OK
pass
if (
some_reasonably_long_condition
or some_other_reasonably_long_condition
and some_third_reasonably_long_condition
or some_fourth_reasonably_long_condition
and some_fifth_reasonably_long_condition
# a commment
and some_sixth_reasonably_long_condition
and some_seventh_reasonably_long_condition
# another comment
or some_eighth_reasonably_long_condition
):
pass
#############################################
# If they're all the same operator, it's fine
#############################################
x = not a and c # OK
if a or b or c: # OK
pass
while a and b and c: # OK
pass
###########################################################
# We don't consider `not` as part of a chain as problematic
###########################################################
x = not a or not b or not c # OK
#####################################
# If they're parenthesized, it's fine
#####################################
a, b, c = 1, 0, 2
x = a or (b and c) # OK
x2 = (a or b) and c # OK
x3 = (a or b) or c # OK
x4 = (a and b) and c # OK
a, b, c = 0, 1, 2
y = (a and b) or c # OK
yy = a and (b or c) # OK
a, b, c, d = 1, 2, 0, 3
if a or b or (c and d): # OK
pass
a, b, c, d = 0, 0, 2, 3
if bool():
pass
elif a or (b and c) or d: # OK
pass
a, b, c, d = 0, 1, 0, 2
while (a and b) or (c and d): # OK
pass
b, c, d, e = 2, 3, 0, 4
z = [a for a in range(5) if a or b or c or (d and e)] # OK
a, b = 1, 2
if (not a) or b: # OK
if (not a) and b: # OK
pass
a, b, c, d = 0, 1, 3, 0
assert ((not a) and b) or c or d # OK

View File

@@ -1,12 +1,14 @@
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
use ruff_python_semantic::{Binding, BindingKind, Imported, ScopeKind};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::fix;
use crate::rules::{
flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint, ruff,
flake8_builtins, flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint,
ruff,
};
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
@@ -26,6 +28,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UndefinedLocal,
Rule::UnusedAnnotation,
Rule::UnusedClassMethodArgument,
Rule::BuiltinAttributeShadowing,
Rule::UnusedFunctionArgument,
Rule::UnusedImport,
Rule::UnusedLambdaArgument,
@@ -144,9 +147,9 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
checker.semantic.different_branches(left, right)
})
binding
.source
.map_or(true, |right| !checker.semantic.same_branch(left, right))
}) {
continue;
}
@@ -233,9 +236,9 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
checker.semantic.different_branches(left, right)
})
binding
.source
.map_or(true, |right| !checker.semantic.same_branch(left, right))
}) {
continue;
}
@@ -247,9 +250,33 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
},
binding.range(),
);
if let Some(range) = binding.parent_range(&checker.semantic) {
diagnostic.set_parent(range.start());
}
if checker.settings.preview.is_enabled() {
if let Some(import) = binding.as_any_import() {
if let Some(source) = binding.source {
diagnostic.try_set_fix(|| {
let statement = checker.semantic().statement(source);
let parent = checker.semantic().parent_statement(source);
let edit = fix::edits::remove_unused_imports(
std::iter::once(import.member_name().as_ref()),
statement,
parent,
checker.locator(),
checker.stylist(),
checker.indexer(),
)?;
Ok(Fix::safe_edit(edit).isolate(Checker::isolation(
checker.semantic().parent_statement_id(source),
)))
});
}
}
}
diagnostics.push(diagnostic);
}
}
@@ -272,6 +299,18 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
ruff::rules::asyncio_dangling_binding(scope, &checker.semantic, &mut diagnostics);
}
if let Some(class_def) = scope.kind.as_class() {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker,
scope_id,
scope,
class_def,
&mut diagnostics,
);
}
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);

View File

@@ -242,13 +242,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker, class_def, id, *range,
);
}
} else {
if !checker.semantic.current_scope().kind.is_class() {
if checker.enabled(Rule::BuiltinVariableShadowing) {
flake8_builtins::rules::builtin_variable_shadowing(checker, id, *range);
}
@@ -724,7 +718,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::BooleanPositionalValueInCall) {
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, args, func);
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, call);
}
if checker.enabled(Rule::Debugger) {
flake8_debugger::rules::debugger_call(checker, expr, func);
@@ -869,6 +863,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::DictGetWithNoneDefault) {
flake8_simplify::rules::dict_get_with_none_default(checker, expr);
}
if checker.enabled(Rule::ZipDictKeysAndValues) {
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
}
if checker.any_enabled(&[
Rule::OsPathAbspath,
Rule::OsChmod,
@@ -1492,6 +1489,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryKeyCheck) {
ruff::rules::unnecessary_key_check(checker, expr);
}
if checker.enabled(Rule::ParenthesizeChainedOperators) {
ruff::rules::parenthesize_chained_logical_operators(checker, bool_op);
}
}
Expr::NamedExpr(..) => {
if checker.enabled(Rule::AssignmentInAssert) {

View File

@@ -347,17 +347,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, body);
}
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_method_shadowing(
checker,
class_def,
name,
decorator_list,
name.range(),
);
}
} else {
if !checker.semantic.current_scope().kind.is_class() {
if checker.enabled(Rule::BuiltinVariableShadowing) {
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
}

View File

@@ -472,6 +472,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Simplify, "300") => (RuleGroup::Stable, rules::flake8_simplify::rules::YodaConditions),
(Flake8Simplify, "401") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet),
(Flake8Simplify, "910") => (RuleGroup::Stable, rules::flake8_simplify::rules::DictGetWithNoneDefault),
(Flake8Simplify, "911") => (RuleGroup::Preview, rules::flake8_simplify::rules::ZipDictKeysAndValues),
// flake8-copyright
#[allow(deprecated)]
@@ -923,6 +924,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "018") => (RuleGroup::Preview, rules::ruff::rules::AssignmentInAssert),
(Ruff, "019") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryKeyCheck),
(Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion),
(Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),

View File

@@ -153,13 +153,17 @@ impl<'a> SectionContexts<'a> {
while let Some(line) = lines.next() {
if let Some(section_kind) = suspected_as_section(&line, style) {
let indent = leading_space(&line);
let section_name = leading_words(&line);
let indent_size = indent.text_len();
let section_name_range = TextRange::at(indent.text_len(), section_name.text_len());
let section_name = leading_words(&line);
let section_name_size = section_name.text_len();
if is_docstring_section(
&line,
section_name_range,
indent_size,
section_name_size,
section_kind,
last.as_ref(),
previous_line.as_ref(),
lines.peek(),
) {
@@ -170,7 +174,8 @@ impl<'a> SectionContexts<'a> {
last = Some(SectionContextData {
kind: section_kind,
name_range: section_name_range + line.start(),
indent_size: indent.text_len(),
name_range: TextRange::at(line.start() + indent_size, section_name_size),
range: TextRange::empty(line.start()),
summary_full_end: line.full_end(),
});
@@ -204,8 +209,8 @@ impl<'a> SectionContexts<'a> {
}
impl<'a> IntoIterator for &'a SectionContexts<'a> {
type IntoIter = SectionContextsIter<'a>;
type Item = SectionContext<'a>;
type IntoIter = SectionContextsIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
@@ -257,6 +262,9 @@ impl ExactSizeIterator for SectionContextsIter<'_> {}
struct SectionContextData {
kind: SectionKind,
/// The size of the indentation of the section name.
indent_size: TextSize,
/// Range of the section name, relative to the [`Docstring::body`]
name_range: TextRange,
@@ -401,12 +409,15 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
/// Check if the suspected context is really a section header.
fn is_docstring_section(
line: &Line,
section_name_range: TextRange,
indent_size: TextSize,
section_name_size: TextSize,
section_kind: SectionKind,
previous_section: Option<&SectionContextData>,
previous_line: Option<&Line>,
next_line: Option<&Line>,
) -> bool {
// Determine whether the current line looks like a section header, e.g., "Args:".
let section_name_suffix = line[usize::from(section_name_range.end())..].trim();
let section_name_suffix = line[usize::from(indent_size + section_name_size)..].trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
if !this_looks_like_a_section_name {
@@ -439,5 +450,25 @@ fn is_docstring_section(
return false;
}
// Determine if this is a sub-section within another section, like `args` in:
// ```python
// def func(args: tuple[int]):
// """Toggle the gizmo.
//
// Args:
// args: The arguments to the function.
// """
// ```
// However, if the header is an _exact_ match (like `Returns:`, as opposed to `returns:`), then
// continue to treat it as a section header.
if let Some(previous_section) = previous_section {
if previous_section.indent_size < indent_size {
let verbatim = &line[TextRange::at(indent_size, section_name_size)];
if section_kind.as_str() != verbatim {
return false;
}
}
}
true
}

View File

@@ -51,13 +51,29 @@ pub(super) fn is_allowed_func_def(name: &str) -> bool {
/// Returns `true` if an argument is allowed to use a boolean trap. To return
/// `true`, the function name must be explicitly allowed, and the argument must
/// be either the first or second argument in the call.
pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func {
return is_allowed_func_call(attr);
pub(super) fn allow_boolean_trap(call: &ast::ExprCall) -> bool {
let func_name = match call.func.as_ref() {
Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr.as_str(),
Expr::Name(ast::ExprName { id, .. }) => id.as_str(),
_ => return false,
};
// If the function name is explicitly allowed, then the boolean trap is
// allowed.
if is_allowed_func_call(func_name) {
return true;
}
if let Expr::Name(ast::ExprName { id, .. }) = func {
return is_allowed_func_call(id);
// If the function appears to be a setter (e.g., `set_visible` or `setVisible`), then the
// boolean trap is allowed. We want to avoid raising a violation for cases in which the argument
// is positional-only and third-party, and this tends to be the case for setters.
if call.arguments.args.len() == 1 {
if func_name
.strip_prefix("set")
.is_some_and(|suffix| suffix.starts_with(|c: char| c == '_' || c.is_ascii_uppercase()))
{
return true;
}
}
false

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Expr;
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -45,11 +45,16 @@ impl Violation for BooleanPositionalValueInCall {
}
}
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, args: &[Expr], func: &Expr) {
if allow_boolean_trap(func) {
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, call: &ast::ExprCall) {
if allow_boolean_trap(call) {
return;
}
for arg in args.iter().filter(|arg| arg.is_boolean_literal_expr()) {
for arg in call
.arguments
.args
.iter()
.filter(|arg| arg.is_boolean_literal_expr())
{
checker
.diagnostics
.push(Diagnostic::new(BooleanPositionalValueInCall, arg.range()));

View File

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

View File

@@ -28,14 +28,10 @@ FBT.py:57:17: FBT003 Boolean positional value in function call
59 | mylist.index(True)
|
FBT.py:69:38: FBT003 Boolean positional value in function call
|
67 | os.set_blocking(0, False)
68 | g_action.set_enabled(True)
69 | settings.set_enable_developer_extras(True)
| ^^^^ FBT003
70 | foo.is_(True)
71 | bar.is_not(False)
|
FBT.py:121:10: FBT003 Boolean positional value in function call
|
121 | settings(True)
| ^^^^ FBT003
|

View File

@@ -81,26 +81,26 @@ FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
21 | kwonly_nonvalued_nohint,
|
FBT.py:89:19: FBT001 Boolean-typed positional argument in function definition
FBT.py:91:19: FBT001 Boolean-typed positional argument in function definition
|
88 | # FBT001: Boolean positional arg in function definition
89 | def foo(self, value: bool) -> None:
90 | # FBT001: Boolean positional arg in function definition
91 | def foo(self, value: bool) -> None:
| ^^^^^ FBT001
90 | pass
92 | pass
|
FBT.py:99:10: FBT001 Boolean-typed positional argument in function definition
FBT.py:101:10: FBT001 Boolean-typed positional argument in function definition
|
99 | def func(x: Union[list, Optional[int | str | float | bool]]):
101 | def func(x: Union[list, Optional[int | str | float | bool]]):
| ^ FBT001
100 | pass
102 | pass
|
FBT.py:103:10: FBT001 Boolean-typed positional argument in function definition
FBT.py:105:10: FBT001 Boolean-typed positional argument in function definition
|
103 | def func(x: bool | str):
105 | def func(x: bool | str):
| ^ FBT001
104 | pass
106 | pass
|

View File

@@ -1,11 +1,10 @@
use ruff_python_ast as ast;
use ruff_python_ast::{Arguments, Decorator};
use ruff_text_size::TextRange;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::SemanticModel;
use ruff_python_ast as ast;
use ruff_python_semantic::{BindingKind, Scope, ScopeId};
use ruff_source_file::SourceRow;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_builtins::helpers::shadows_builtin;
@@ -20,6 +19,23 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// non-obvious errors, as readers may mistake the attribute for the
/// builtin and vice versa.
///
/// Since methods and class attributes typically cannot be referenced directly
/// from outside the class scope, this rule only applies to those methods
/// and attributes that both shadow a builtin _and_ are referenced from within
/// the class scope, as in the following example, where the `list[int]` return
/// type annotation resolves to the `list` method, rather than the builtin:
///
/// ```python
/// class Class:
/// @staticmethod
/// def list() -> None:
/// pass
///
/// @staticmethod
/// def repeat(value: int, times: int) -> list[int]:
/// return [value] * times
/// ```
///
/// Builtins can be marked as exceptions to this rule via the
/// [`flake8-builtins.builtins-ignorelist`] configuration option, or
/// converted to the appropriate dunder method. Methods decorated with
@@ -28,135 +44,112 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
///
/// ## Example
/// ```python
/// class Shadow:
/// def int():
/// return 0
/// ```
/// class Class:
/// @staticmethod
/// def list() -> None:
/// pass
///
/// Use instead:
/// ```python
/// class Shadow:
/// def to_int():
/// return 0
/// ```
///
/// Or:
/// ```python
/// class Shadow:
/// # Callable as `int(shadow)`
/// def __int__():
/// return 0
/// @staticmethod
/// def repeat(value: int, times: int) -> list[int]:
/// return [value] * times
/// ```
///
/// ## Options
/// - `flake8-builtins.builtins-ignorelist`
///
/// ## References
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
#[violation]
pub struct BuiltinAttributeShadowing {
kind: Kind,
name: String,
row: SourceRow,
}
impl Violation for BuiltinAttributeShadowing {
#[derive_message_formats]
fn message(&self) -> String {
let BuiltinAttributeShadowing { name } = self;
format!("Class attribute `{name}` is shadowing a Python builtin")
let BuiltinAttributeShadowing { kind, name, row } = self;
match kind {
Kind::Attribute => {
format!("Python builtin is shadowed by class attribute `{name}` from {row}")
}
Kind::Method => {
format!("Python builtin is shadowed by method `{name}` from {row}")
}
}
}
}
/// A003
pub(crate) fn builtin_attribute_shadowing(
checker: &mut Checker,
checker: &Checker,
scope_id: ScopeId,
scope: &Scope,
class_def: &ast::StmtClassDef,
name: &str,
range: TextRange,
diagnostics: &mut Vec<Diagnostic>,
) {
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
// Ignore shadowing within `TypedDict` definitions, since these are only accessible through
// subscripting and not through attribute access.
if class_def
.bases()
.iter()
.any(|base| checker.semantic().match_typing_expr(base, "TypedDict"))
{
return;
}
for (name, binding_id) in scope.all_bindings() {
let binding = checker.semantic().binding(binding_id);
checker.diagnostics.push(Diagnostic::new(
BuiltinAttributeShadowing {
name: name.to_string(),
},
range,
));
// We only care about methods and attributes.
let kind = match binding.kind {
BindingKind::Assignment | BindingKind::Annotation => Kind::Attribute,
BindingKind::FunctionDefinition(_) => Kind::Method,
_ => continue,
};
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
// Ignore explicit overrides.
if class_def.decorator_list.iter().any(|decorator| {
checker
.semantic()
.match_typing_expr(&decorator.expression, "override")
}) {
return;
}
// Class scopes are special, in that you can only reference a binding defined in a
// class scope from within the class scope itself. As such, we can safely ignore
// methods that weren't referenced from within the class scope. In other words, we're
// only trying to identify shadowing as in:
// ```python
// class Class:
// @staticmethod
// def list() -> None:
// pass
//
// @staticmethod
// def repeat(value: int, times: int) -> list[int]:
// return [value] * times
// ```
for reference in binding
.references
.iter()
.map(|reference_id| checker.semantic().reference(*reference_id))
.filter(|reference| {
checker
.semantic()
.first_non_type_parent_scope_id(reference.scope_id())
== Some(scope_id)
})
{
diagnostics.push(Diagnostic::new(
BuiltinAttributeShadowing {
kind,
name: name.to_string(),
row: checker.compute_source_row(binding.start()),
},
reference.range(),
));
}
}
}
}
/// A003
pub(crate) fn builtin_method_shadowing(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
name: &str,
decorator_list: &[Decorator],
range: TextRange,
) {
if shadows_builtin(
name,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.source_type,
) {
// Ignore some standard-library methods. Ideally, we'd ignore all overridden methods, since
// those should be flagged on the superclass, but that's more difficult.
if is_standard_library_override(name, class_def, checker.semantic()) {
return;
}
// Ignore explicit overrides.
if decorator_list.iter().any(|decorator| {
checker
.semantic()
.match_typing_expr(&decorator.expression, "override")
}) {
return;
}
checker.diagnostics.push(Diagnostic::new(
BuiltinAttributeShadowing {
name: name.to_string(),
},
range,
));
}
}
/// Return `true` if an attribute appears to be an override of a standard-library method.
fn is_standard_library_override(
name: &str,
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
) -> bool {
let Some(Arguments { args: bases, .. }) = class_def.arguments.as_deref() else {
return false;
};
match name {
// Ex) `Event.set`
"set" => bases.iter().any(|base| {
semantic
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["threading", "Event"]))
}),
// Ex) `Filter.filter`
"filter" => bases.iter().any(|base| {
semantic
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["logging", "Filter"]))
}),
_ => false,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Kind {
Attribute,
Method,
}

View File

@@ -1,68 +1,22 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
---
A003.py:2:5: A003 Class attribute `ImportError` is shadowing a Python builtin
|
1 | class MyClass:
2 | ImportError = 4
| ^^^^^^^^^^^ A003
3 | id: int
4 | dir = "/"
|
A003.py:3:5: A003 Class attribute `id` is shadowing a Python builtin
|
1 | class MyClass:
2 | ImportError = 4
3 | id: int
| ^^ A003
4 | dir = "/"
|
A003.py:4:5: A003 Class attribute `dir` is shadowing a Python builtin
|
2 | ImportError = 4
3 | id: int
4 | dir = "/"
| ^^^ A003
5 |
6 | def __init__(self):
|
A003.py:11:9: A003 Class attribute `str` is shadowing a Python builtin
A003.py:17:31: A003 Python builtin is shadowed by method `str` from line 14
|
9 | self.dir = "."
10 |
11 | def str(self):
| ^^^ A003
12 | pass
15 | pass
16 |
17 | def method_usage(self) -> str:
| ^^^ A003
18 | pass
|
A003.py:29:9: A003 Class attribute `str` is shadowing a Python builtin
A003.py:20:34: A003 Python builtin is shadowed by class attribute `id` from line 3
|
27 | ...
28 |
29 | def str(self) -> None:
| ^^^ A003
30 | ...
|
A003.py:40:9: A003 Class attribute `str` is shadowing a Python builtin
|
38 | ...
39 |
40 | def str(self) -> None:
| ^^^ A003
41 | ...
|
A003.py:52:9: A003 Class attribute `int` is shadowing a Python builtin
|
50 | pass
51 |
52 | def int(self):
| ^^^ A003
53 | pass
18 | pass
19 |
20 | def attribute_usage(self) -> id:
| ^^ A003
21 | pass
|

View File

@@ -1,49 +1,13 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
---
A003.py:2:5: A003 Class attribute `ImportError` is shadowing a Python builtin
|
1 | class MyClass:
2 | ImportError = 4
| ^^^^^^^^^^^ A003
3 | id: int
4 | dir = "/"
|
A003.py:11:9: A003 Class attribute `str` is shadowing a Python builtin
A003.py:17:31: A003 Python builtin is shadowed by method `str` from line 14
|
9 | self.dir = "."
10 |
11 | def str(self):
| ^^^ A003
12 | pass
|
A003.py:29:9: A003 Class attribute `str` is shadowing a Python builtin
|
27 | ...
28 |
29 | def str(self) -> None:
| ^^^ A003
30 | ...
|
A003.py:40:9: A003 Class attribute `str` is shadowing a Python builtin
|
38 | ...
39 |
40 | def str(self) -> None:
| ^^^ A003
41 | ...
|
A003.py:52:9: A003 Class attribute `int` is shadowing a Python builtin
|
50 | pass
51 |
52 | def int(self):
| ^^^ A003
53 | pass
15 | pass
16 |
17 | def method_usage(self) -> str:
| ^^^ A003
18 | pass
|

View File

@@ -367,7 +367,7 @@ pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Re
comma,
} = element
{
if let Some(Element::Simple { value: key, .. }) = tuple.elements.get(0) {
if let Some(Element::Simple { value: key, .. }) = tuple.elements.first() {
if let Some(Element::Simple { value, .. }) = tuple.elements.get(1) {
return Ok(DictElement::Simple {
key: key.clone(),

View File

@@ -134,7 +134,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
format!("Indices should only contain `{attr_name}` calls")
)
};
args.get(0)
args.first()
.unwrap_or_else(|| panic!("`{attr_name}` should have one argument"))
})
.collect();

View File

@@ -2,6 +2,7 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::{self as ast, StringLike};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -44,8 +45,14 @@ impl AlwaysFixableViolation for StringOrBytesTooLong {
/// PYI053
pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, string: StringLike) {
let semantic = checker.semantic();
// Ignore docstrings.
if is_docstring_stmt(checker.semantic().current_statement()) {
if is_docstring_stmt(semantic.current_statement()) {
return;
}
if is_warnings_dot_deprecated(semantic.current_expression_parent(), semantic) {
return;
}
@@ -67,3 +74,21 @@ pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, string: StringLike
)));
checker.diagnostics.push(diagnostic);
}
fn is_warnings_dot_deprecated(expr: Option<&ast::Expr>, semantic: &SemanticModel) -> bool {
// Does `expr` represent a call to `warnings.deprecated` or `typing_extensions.deprecated`?
let Some(expr) = expr else {
return false;
};
let Some(call) = expr.as_call_expr() else {
return false;
};
semantic
.resolve_call_path(&call.func)
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
["warnings" | "typing_extensions", "deprecated"]
)
})
}

View File

@@ -1,128 +1,148 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI053.pyi:3:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
PYI053.pyi:7:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
1 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
2 | def f2(
3 | x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
5 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
6 | def f2(
7 | x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
4 | ) -> None: ...
5 | def f3(
8 | ) -> None: ...
9 | def f3(
|
= help: Replace with `...`
Safe fix
1 1 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
2 2 | def f2(
3 |- x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
3 |+ x: str = ..., # Error: PYI053
4 4 | ) -> None: ...
5 5 | def f3(
6 6 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK
4 4 |
5 5 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK
6 6 | def f2(
7 |- x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053
7 |+ x: str = ..., # Error: PYI053
8 8 | ) -> None: ...
9 9 | def f3(
10 10 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK
PYI053.pyi:9:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
PYI053.pyi:13:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
7 | ) -> None: ...
8 | def f4(
9 | x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053
11 | ) -> None: ...
12 | def f4(
13 | x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
10 | ) -> None: ...
11 | def f5(
14 | ) -> None: ...
15 | def f5(
|
= help: Replace with `...`
Safe fix
6 6 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK
7 7 | ) -> None: ...
8 8 | def f4(
9 |- x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053
9 |+ x: str = ..., # Error: PYI053
10 10 | ) -> None: ...
11 11 | def f5(
12 12 | x: bytes = b"50 character byte stringgggggggggggggggggggggggggg", # OK
10 10 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK
11 11 | ) -> None: ...
12 12 | def f4(
13 |- x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053
13 |+ x: str = ..., # Error: PYI053
14 14 | ) -> None: ...
15 15 | def f5(
16 16 | x: bytes = b"50 character byte stringgggggggggggggggggggggggggg", # OK
PYI053.pyi:21:16: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
PYI053.pyi:25:16: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
19 | ) -> None: ...
20 | def f8(
21 | x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053
23 | ) -> None: ...
24 | def f8(
25 | x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
22 | ) -> None: ...
26 | ) -> None: ...
|
= help: Replace with `...`
Safe fix
18 18 | x: bytes = b"50 character byte stringggggggggggggggggggggggggg\xff", # OK
19 19 | ) -> None: ...
20 20 | def f8(
21 |- x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053
21 |+ x: bytes = ..., # Error: PYI053
22 22 | ) -> None: ...
23 23 |
24 24 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
PYI053.pyi:26:12: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
24 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
25 |
26 | bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
27 |
28 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
|
= help: Replace with `...`
Safe fix
23 23 |
24 24 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
25 25 |
26 |-bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
26 |+bar: str = ... # Error: PYI053
22 22 | x: bytes = b"50 character byte stringggggggggggggggggggggggggg\xff", # OK
23 23 | ) -> None: ...
24 24 | def f8(
25 |- x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053
25 |+ x: bytes = ..., # Error: PYI053
26 26 | ) -> None: ...
27 27 |
28 28 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
29 29 |
28 28 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
PYI053.pyi:30:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
PYI053.pyi:30:12: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
28 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
28 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
29 |
30 | qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
30 | bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
31 |
32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
|
= help: Replace with `...`
Safe fix
27 27 |
28 28 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
28 28 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK
29 29 |
30 |-qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
30 |+qux: bytes = ... # Error: PYI053
30 |-bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
30 |+bar: str = ... # Error: PYI053
31 31 |
32 32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
32 32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
33 33 |
PYI053.pyi:34:15: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
PYI053.pyi:34:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
33 |
34 | fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
34 | qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
35 |
36 | class Demo:
36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
|
= help: Replace with `...`
Safe fix
31 31 |
32 32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
32 32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
33 33 |
34 |-fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
34 |+fbar: str = f"..." # Error: PYI053
34 |-qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
34 |+qux: bytes = ... # Error: PYI053
35 35 |
36 36 | class Demo:
37 37 | """Docstrings are excluded from this rule. Some padding.""" # OK
36 36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
37 37 |
PYI053.pyi:38:15: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
37 |
38 | fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
39 |
40 | class Demo:
|
= help: Replace with `...`
Safe fix
35 35 |
36 36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
37 37 |
38 |-fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
38 |+fbar: str = f"..." # Error: PYI053
39 39 |
40 40 | class Demo:
41 41 | """Docstrings are excluded from this rule. Some padding.""" # OK
PYI053.pyi:64:5: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
63 | @not_warnings_dot_deprecated(
64 | "Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
65 | )
66 | def not_a_deprecated_function() -> None: ...
|
= help: Replace with `...`
Safe fix
61 61 | ) -> Callable[[Callable[[], None]], Callable[[], None]]: ...
62 62 |
63 63 | @not_warnings_dot_deprecated(
64 |- "Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
64 |+ ... # Error: PYI053
65 65 | )
66 66 | def not_a_deprecated_function() -> None: ...

View File

@@ -379,7 +379,8 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) {
..
}) = &values[indices[0]]
{
args.get(0).expect("`isinstance` should have two arguments")
args.first()
.expect("`isinstance` should have two arguments")
} else {
unreachable!("Indices should only contain `isinstance` calls")
};

View File

@@ -142,7 +142,7 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex
else {
return;
};
let Some(arg) = args.get(0) else {
let Some(arg) = args.first() else {
return;
};
let Expr::StringLiteral(ast::ExprStringLiteral { value: env_var, .. }) = arg else {
@@ -249,7 +249,7 @@ pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) {
if attr != "get" {
return;
}
let Some(key) = args.get(0) else {
let Some(key) = args.first() else {
return;
};
if !(key.is_literal_expr() || key.is_name_expr()) {

View File

@@ -15,6 +15,7 @@ pub(crate) use reimplemented_builtin::*;
pub(crate) use return_in_try_except_finally::*;
pub(crate) use suppressible_exception::*;
pub(crate) use yoda_conditions::*;
pub(crate) use zip_dict_keys_and_values::*;
mod ast_bool_op;
mod ast_expr;
@@ -34,3 +35,4 @@ mod reimplemented_builtin;
mod return_in_try_except_finally;
mod suppressible_exception;
mod yoda_conditions;
mod zip_dict_keys_and_values;

View File

@@ -0,0 +1,130 @@
use ast::{ExprAttribute, ExprName, Identifier};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Expr, ExprCall};
use ruff_text_size::Ranged;
use crate::{checkers::ast::Checker, fix::snippet::SourceCodeSnippet};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_python_semantic::analyze::typing::is_dict;
/// ## What it does
/// Checks for use of `zip()` to iterate over keys and values of a dictionary at once.
///
/// ## Why is this bad?
/// The `dict` type provides an `.items()` method which is faster and more readable.
///
/// ## Example
/// ```python
/// flag_stars = {"USA": 50, "Slovenia": 3, "Panama": 2, "Australia": 6}
///
/// for country, stars in zip(flag_stars.keys(), flag_stars.values()):
/// print(f"{country}'s flag has {stars} stars.")
/// ```
///
/// Use instead:
/// ```python
/// flag_stars = {"USA": 50, "Slovenia": 3, "Panama": 2, "Australia": 6}
///
/// for country, stars in flag_stars.items():
/// print(f"{country}'s flag has {stars} stars.")
/// ```
///
/// ## References
/// - [Python documentation: `dict.items`](https://docs.python.org/3/library/stdtypes.html#dict.items)
#[violation]
pub struct ZipDictKeysAndValues {
expected: SourceCodeSnippet,
actual: SourceCodeSnippet,
}
impl AlwaysFixableViolation for ZipDictKeysAndValues {
#[derive_message_formats]
fn message(&self) -> String {
let ZipDictKeysAndValues { expected, actual } = self;
if let (Some(expected), Some(actual)) = (expected.full_display(), actual.full_display()) {
format!("Use `{expected}` instead of `{actual}`")
} else {
format!("Use `dict.items()` instead of `zip(dict.keys(), dict.values())`")
}
}
fn fix_title(&self) -> String {
let ZipDictKeysAndValues { expected, actual } = self;
if let (Some(expected), Some(actual)) = (expected.full_display(), actual.full_display()) {
format!("Replace `{actual}` with `{expected}`")
} else {
"Replace `zip(dict.keys(), dict.values())` with `dict.items()`".to_string()
}
}
}
/// SIM911
pub(crate) fn zip_dict_keys_and_values(checker: &mut Checker, expr: &ExprCall) {
let ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
} = expr;
match &keywords[..] {
[] => {}
[ast::Keyword {
arg: Some(name), ..
}] if name.as_str() == "strict" => {}
_ => return,
};
if matches!(func.as_ref(), Expr::Name(ExprName { id, .. }) if id != "zip") {
return;
}
let [arg1, arg2] = &args[..] else {
return;
};
let Some((var1, attr1)) = get_var_attr(arg1) else {
return;
};
let Some((var2, attr2)) = get_var_attr(arg2) else {
return;
};
if var1.id != var2.id || attr1 != "keys" || attr2 != "values" {
return;
}
let Some(binding) = checker
.semantic()
.only_binding(var1)
.map(|id| checker.semantic().binding(id))
else {
return;
};
if !is_dict(binding, checker.semantic()) {
return;
}
let expected = format!("{}.items()", checker.locator().slice(var1));
let actual = checker.locator().slice(expr);
let mut diagnostic = Diagnostic::new(
ZipDictKeysAndValues {
expected: SourceCodeSnippet::new(expected.clone()),
actual: SourceCodeSnippet::from_str(actual),
},
expr.range(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
expected,
expr.range(),
)));
checker.diagnostics.push(diagnostic);
}
fn get_var_attr(expr: &Expr) -> Option<(&ExprName, &Identifier)> {
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return None;
};
let Expr::Attribute(ExprAttribute { value, attr, .. }) = func.as_ref() else {
return None;
};
let Expr::Name(var_name) = value.as_ref() else {
return None;
};
Some((var_name, attr))
}

View File

@@ -27,4 +27,27 @@ sections.py:144:5: D214 [*] Section is over-indented ("Returns")
148 148 | A value of some sort.
149 149 |
sections.py:558:5: D214 [*] Section is over-indented ("Returns")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D214
|
= help: Remove over-indentation from "Returns"
Safe fix
560 560 | Args:
561 561 | Here's a note.
562 562 |
563 |- Returns:
563 |+ Returns:
564 564 | """

View File

@@ -498,4 +498,74 @@ sections.py:527:5: D407 [*] Missing dashed underline after section ("Parameters"
531 532 | """
532 533 |
sections.py:548:5: D407 [*] Missing dashed underline after section ("Args")
|
547 | def lowercase_sub_section_header():
548 | """Below, `returns:` should _not_ be considered a section header.
| _____^
549 | |
550 | | Args:
551 | | Here's a note.
552 | |
553 | | returns:
554 | | """
| |_______^ D407
|
= help: Add dashed line under "Args"
Safe fix
548 548 | """Below, `returns:` should _not_ be considered a section header.
549 549 |
550 550 | Args:
551 |+ ----
551 552 | Here's a note.
552 553 |
553 554 | returns:
sections.py:558:5: D407 [*] Missing dashed underline after section ("Args")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D407
|
= help: Add dashed line under "Args"
Safe fix
558 558 | """Below, `Returns:` should be considered a section header.
559 559 |
560 560 | Args:
561 |+ ----
561 562 | Here's a note.
562 563 |
563 564 | Returns:
sections.py:558:5: D407 [*] Missing dashed underline after section ("Returns")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D407
|
= help: Add dashed line under "Returns"
Safe fix
561 561 | Here's a note.
562 562 |
563 563 | Returns:
564 |+ -------
564 565 | """

View File

@@ -97,4 +97,18 @@ sections.py:261:5: D414 Section has no content ("Returns")
| |_______^ D414
|
sections.py:558:5: D414 Section has no content ("Returns")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D414
|

View File

@@ -64,4 +64,12 @@ D417.py:108:5: D417 Missing argument description in the docstring for `f`: `*arg
109 | """Do something.
|
D417.py:155:5: D417 Missing argument description in the docstring for `select_data`: `auto_save`
|
155 | def select_data(
| ^^^^^^^^^^^ D417
156 | query: str,
157 | args: tuple,
|

View File

@@ -64,4 +64,12 @@ D417.py:108:5: D417 Missing argument description in the docstring for `f`: `*arg
109 | """Do something.
|
D417.py:155:5: D417 Missing argument description in the docstring for `select_data`: `auto_save`
|
155 | def select_data(
| ^^^^^^^^^^^ D417
156 | query: str,
157 | args: tuple,
|

View File

@@ -120,6 +120,7 @@ mod tests {
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_24.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_25.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_26.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_27.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_0.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_1.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_2.py"))]
@@ -167,6 +168,34 @@ mod tests {
Ok(())
}
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_0.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_1.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_10.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_11.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_12.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_13.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_14.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_15.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_16.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_17.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_18.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_19.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_2.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_20.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_21.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_22.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_23.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_24.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_25.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_26.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_27.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_3.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_4.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_5.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_6.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_7.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_8.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_9.py"))]
#[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(

View File

@@ -4,8 +4,8 @@ use ruff_python_ast::{CmpOp, Expr};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers;
use ruff_python_parser::locate_cmp_ops;
use ruff_text_size::Ranged;
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
use crate::settings::types::PreviewMode;
@@ -138,3 +138,207 @@ impl From<&CmpOp> for IsCmpOp {
}
}
}
/// Extract all [`CmpOp`] operators from an expression snippet, with appropriate
/// ranges.
///
/// `RustPython` doesn't include line and column information on [`CmpOp`] nodes.
/// `CPython` doesn't either. This method iterates over the token stream and
/// re-identifies [`CmpOp`] nodes, annotating them with valid ranges.
fn locate_cmp_ops(expr: &Expr, source: &str) -> Vec<LocatedCmpOp> {
// If `Expr` is a multi-line expression, we need to parenthesize it to
// ensure that it's lexed correctly.
let contents = &source[expr.range()];
let parenthesized_contents = format!("({contents})");
let mut tok_iter = lexer::lex(&parenthesized_contents, Mode::Expression)
.flatten()
.skip(1)
.map(|(tok, range)| (tok, range - TextSize::from(1)))
.filter(|(tok, _)| !matches!(tok, Tok::NonLogicalNewline | Tok::Comment(_)))
.peekable();
let mut ops: Vec<LocatedCmpOp> = vec![];
// Track the bracket depth.
let mut par_count = 0u32;
let mut sqb_count = 0u32;
let mut brace_count = 0u32;
loop {
let Some((tok, range)) = tok_iter.next() else {
break;
};
match tok {
Tok::Lpar => {
par_count = par_count.saturating_add(1);
}
Tok::Rpar => {
par_count = par_count.saturating_sub(1);
}
Tok::Lsqb => {
sqb_count = sqb_count.saturating_add(1);
}
Tok::Rsqb => {
sqb_count = sqb_count.saturating_sub(1);
}
Tok::Lbrace => {
brace_count = brace_count.saturating_add(1);
}
Tok::Rbrace => {
brace_count = brace_count.saturating_sub(1);
}
_ => {}
}
if par_count > 0 || sqb_count > 0 || brace_count > 0 {
continue;
}
match tok {
Tok::Not => {
if let Some((_, next_range)) = tok_iter.next_if(|(tok, _)| tok.is_in()) {
ops.push(LocatedCmpOp::new(
TextRange::new(range.start(), next_range.end()),
CmpOp::NotIn,
));
}
}
Tok::In => {
ops.push(LocatedCmpOp::new(range, CmpOp::In));
}
Tok::Is => {
let op = if let Some((_, next_range)) = tok_iter.next_if(|(tok, _)| tok.is_not()) {
LocatedCmpOp::new(
TextRange::new(range.start(), next_range.end()),
CmpOp::IsNot,
)
} else {
LocatedCmpOp::new(range, CmpOp::Is)
};
ops.push(op);
}
Tok::NotEqual => {
ops.push(LocatedCmpOp::new(range, CmpOp::NotEq));
}
Tok::EqEqual => {
ops.push(LocatedCmpOp::new(range, CmpOp::Eq));
}
Tok::GreaterEqual => {
ops.push(LocatedCmpOp::new(range, CmpOp::GtE));
}
Tok::Greater => {
ops.push(LocatedCmpOp::new(range, CmpOp::Gt));
}
Tok::LessEqual => {
ops.push(LocatedCmpOp::new(range, CmpOp::LtE));
}
Tok::Less => {
ops.push(LocatedCmpOp::new(range, CmpOp::Lt));
}
_ => {}
}
}
ops
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct LocatedCmpOp {
range: TextRange,
op: CmpOp,
}
impl LocatedCmpOp {
fn new<T: Into<TextRange>>(range: T, op: CmpOp) -> Self {
Self {
range: range.into(),
op,
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff_python_ast::CmpOp;
use ruff_python_parser::parse_expression;
use ruff_text_size::TextSize;
use super::{locate_cmp_ops, LocatedCmpOp};
#[test]
fn extract_cmp_op_location() -> Result<()> {
let contents = "x == 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(4),
CmpOp::Eq
)]
);
let contents = "x != 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(4),
CmpOp::NotEq
)]
);
let contents = "x is 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(4),
CmpOp::Is
)]
);
let contents = "x is not 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(8),
CmpOp::IsNot
)]
);
let contents = "x in 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(4),
CmpOp::In
)]
);
let contents = "x not in 1";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(8),
CmpOp::NotIn
)]
);
let contents = "x != (1 is not 2)";
let expr = parse_expression(contents)?;
assert_eq!(
locate_cmp_ops(&expr, contents),
vec![LocatedCmpOp::new(
TextSize::from(2)..TextSize::from(4),
CmpOp::NotEq
)]
);
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::Violation;
use ruff_diagnostics::{FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_source_file::SourceRow;
@@ -29,9 +29,16 @@ pub struct RedefinedWhileUnused {
}
impl Violation for RedefinedWhileUnused {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let RedefinedWhileUnused { name, row } = self;
format!("Redefinition of unused `{name}` from {row}")
}
fn fix_title(&self) -> Option<String> {
let RedefinedWhileUnused { name, .. } = self;
Some(format!("Remove definition: `{name}`"))
}
}

View File

@@ -7,5 +7,6 @@ F811_0.py:10:5: F811 Redefinition of unused `bar` from line 6
| ^^^ F811
11 | pass
|
= help: Remove definition: `bar`

View File

@@ -6,5 +6,6 @@ F811_1.py:1:25: F811 Redefinition of unused `FU` from line 1
1 | import fu as FU, bar as FU
| ^^ F811
|
= help: Remove definition: `FU`

View File

@@ -9,5 +9,6 @@ F811_12.py:6:20: F811 Redefinition of unused `mixer` from line 2
| ^^^^^ F811
7 | mixer(123)
|
= help: Remove definition: `mixer`

View File

@@ -7,5 +7,6 @@ F811_15.py:4:5: F811 Redefinition of unused `fu` from line 1
| ^^ F811
5 | pass
|
= help: Remove definition: `fu`

View File

@@ -9,5 +9,6 @@ F811_16.py:8:13: F811 Redefinition of unused `fu` from line 3
| ^^ F811
9 | pass
|
= help: Remove definition: `fu`

View File

@@ -9,6 +9,7 @@ F811_17.py:6:12: F811 Redefinition of unused `fu` from line 2
7 |
8 | def baz():
|
= help: Remove definition: `fu`
F811_17.py:9:13: F811 Redefinition of unused `fu` from line 6
|
@@ -17,5 +18,6 @@ F811_17.py:9:13: F811 Redefinition of unused `fu` from line 6
| ^^ F811
10 | pass
|
= help: Remove definition: `fu`

View File

@@ -6,5 +6,6 @@ F811_2.py:1:34: F811 Redefinition of unused `FU` from line 1
1 | from moo import fu as FU, bar as FU
| ^^ F811
|
= help: Remove definition: `FU`

View File

@@ -9,5 +9,6 @@ F811_21.py:32:5: F811 Redefinition of unused `Sequence` from line 26
| ^^^^^^^^ F811
33 | )
|
= help: Remove definition: `Sequence`

View File

@@ -7,5 +7,6 @@ F811_23.py:4:15: F811 Redefinition of unused `foo` from line 3
4 | import bar as foo
| ^^^ F811
|
= help: Remove definition: `foo`

View File

@@ -9,5 +9,6 @@ F811_26.py:5:9: F811 Redefinition of unused `func` from line 2
| ^^^^ F811
6 | pass
|
= help: Remove definition: `func`

View File

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

View File

@@ -6,5 +6,6 @@ F811_3.py:1:12: F811 Redefinition of unused `fu` from line 1
1 | import fu; fu = 3
| ^^ F811
|
= help: Remove definition: `fu`

View File

@@ -6,5 +6,6 @@ F811_4.py:1:12: F811 Redefinition of unused `fu` from line 1
1 | import fu; fu, bar = 3
| ^^ F811
|
= help: Remove definition: `fu`

View File

@@ -6,5 +6,6 @@ F811_5.py:1:13: F811 Redefinition of unused `fu` from line 1
1 | import fu; [fu, bar] = 3
| ^^ F811
|
= help: Remove definition: `fu`

View File

@@ -9,5 +9,6 @@ F811_6.py:6:12: F811 Redefinition of unused `os` from line 5
| ^^ F811
7 | os.path
|
= help: Remove definition: `os`

View File

@@ -10,5 +10,6 @@ F811_8.py:5:12: F811 Redefinition of unused `os` from line 4
6 | except:
7 | pass
|
= help: Remove definition: `os`

View File

@@ -25,5 +25,6 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs
6 |
7 | # Despite this `del`, `import os` in `f` should still be flagged as shadowing an unused
|
= help: Remove definition: `os`

View File

@@ -24,5 +24,6 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs
| ^^ F811
6 | print(os)
|
= help: Remove definition: `os`

View File

@@ -10,5 +10,6 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs
5 |
6 | # Despite this `del`, `import os` should still be flagged as shadowing an unused
|
= help: Remove definition: `os`

View File

@@ -0,0 +1,12 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_0.py:10:5: F811 Redefinition of unused `bar` from line 6
|
10 | def bar():
| ^^^ F811
11 | pass
|
= help: Remove definition: `bar`

View File

@@ -0,0 +1,15 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_1.py:1:25: F811 [*] Redefinition of unused `FU` from line 1
|
1 | import fu as FU, bar as FU
| ^^ F811
|
= help: Remove definition: `FU`
Safe fix
1 |-import fu as FU, bar as FU
1 |+import fu as FU

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_12.py:6:20: F811 [*] Redefinition of unused `mixer` from line 2
|
4 | pass
5 | else:
6 | from bb import mixer
| ^^^^^ F811
7 | mixer(123)
|
= help: Remove definition: `mixer`
Safe fix
3 3 | except ImportError:
4 4 | pass
5 5 | else:
6 |- from bb import mixer
6 |+ pass
7 7 | mixer(123)

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_15.py:4:5: F811 Redefinition of unused `fu` from line 1
|
4 | def fu():
| ^^ F811
5 | pass
|
= help: Remove definition: `fu`

View File

@@ -0,0 +1,14 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_16.py:8:13: F811 Redefinition of unused `fu` from line 3
|
6 | def bar():
7 | def baz():
8 | def fu():
| ^^ F811
9 | pass
|
= help: Remove definition: `fu`

View File

@@ -0,0 +1,32 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_17.py:6:12: F811 [*] Redefinition of unused `fu` from line 2
|
5 | def bar():
6 | import fu
| ^^ F811
7 |
8 | def baz():
|
= help: Remove definition: `fu`
Safe fix
3 3 |
4 4 |
5 5 | def bar():
6 |- import fu
7 6 |
8 7 | def baz():
9 8 | def fu():
F811_17.py:9:13: F811 Redefinition of unused `fu` from line 6
|
8 | def baz():
9 | def fu():
| ^^ F811
10 | pass
|
= help: Remove definition: `fu`

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_2.py:1:34: F811 [*] Redefinition of unused `FU` from line 1
|
1 | from moo import fu as FU, bar as FU
| ^^ F811
|
= help: Remove definition: `FU`
Safe fix
1 |-from moo import fu as FU, bar as FU
1 |+from moo import fu as FU

View File

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

View File

@@ -0,0 +1,25 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_21.py:32:5: F811 [*] Redefinition of unused `Sequence` from line 26
|
30 | from typing import (
31 | List, # noqa: F811
32 | Sequence,
| ^^^^^^^^ F811
33 | )
|
= help: Remove definition: `Sequence`
Safe fix
29 29 | # This should ignore the first error.
30 30 | from typing import (
31 31 | List, # noqa: F811
32 |- Sequence,
33 |-)
32 |+ )
34 33 |
35 34 | # This should ignore both errors.
36 35 | from typing import ( # noqa

View File

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

View File

@@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_23.py:4:15: F811 [*] Redefinition of unused `foo` from line 3
|
3 | import foo as foo
4 | import bar as foo
| ^^^ F811
|
= help: Remove definition: `foo`
Safe fix
1 1 | """Test that shadowing an explicit re-export produces a warning."""
2 2 |
3 3 | import foo as foo
4 |-import bar as foo

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_26.py:5:9: F811 Redefinition of unused `func` from line 2
|
3 | pass
4 |
5 | def func(self):
| ^^^^ F811
6 | pass
|
= help: Remove definition: `func`

View File

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

View File

@@ -0,0 +1,11 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_3.py:1:12: F811 Redefinition of unused `fu` from line 1
|
1 | import fu; fu = 3
| ^^ F811
|
= help: Remove definition: `fu`

View File

@@ -0,0 +1,11 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811_4.py:1:12: F811 Redefinition of unused `fu` from line 1
|
1 | import fu; fu, bar = 3
| ^^ F811
|
= help: Remove definition: `fu`

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