Compare commits
66 Commits
deps/parse
...
charlie/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6d41b9560 | ||
|
|
bcec2f0c4c | ||
|
|
2cde9b8aa6 | ||
|
|
c948dcc203 | ||
|
|
b866cbb33d | ||
|
|
d351761f5d | ||
|
|
ccc6bd5df0 | ||
|
|
eeb8a5fe0a | ||
|
|
c2b7b46717 | ||
|
|
6fd8574a0b | ||
|
|
76e9ce6dc0 | ||
|
|
8c5f8a8aef | ||
|
|
92f471a666 | ||
|
|
029fe05a5f | ||
|
|
9e32585cb1 | ||
|
|
fe7505b738 | ||
|
|
266e684192 | ||
|
|
4bba0bcab8 | ||
|
|
d35cb6942f | ||
|
|
a37d91529b | ||
|
|
963f240e46 | ||
|
|
23cde4d1f5 | ||
|
|
9834c69c98 | ||
|
|
b27f0fa433 | ||
|
|
a459d8ffc7 | ||
|
|
598549d24e | ||
|
|
e1d76b60cc | ||
|
|
6f96acfd27 | ||
|
|
5a4317c688 | ||
|
|
5f3da9955a | ||
|
|
a75a6de577 | ||
|
|
a227775f62 | ||
|
|
a51606a10a | ||
|
|
63ed7a31e8 | ||
|
|
46a17d11f3 | ||
|
|
9ed7ceeb0a | ||
|
|
9fb8d6e999 | ||
|
|
38678142ed | ||
|
|
5d68ad9008 | ||
|
|
cda90d071c | ||
|
|
7e6b472c5b | ||
|
|
1181d25e5a | ||
|
|
626d8dc2cc | ||
|
|
7ffcd93afd | ||
|
|
2d505e2b04 | ||
|
|
a93254f026 | ||
|
|
c577045f2e | ||
|
|
4204fc002d | ||
|
|
41da52a61b | ||
|
|
d5c43a45b3 | ||
|
|
cdfed3d50e | ||
|
|
68097e34e6 | ||
|
|
f47443014e | ||
|
|
0eab4b3c22 | ||
|
|
c868def374 | ||
|
|
5d41c832ad | ||
|
|
0c7c81aa31 | ||
|
|
3b32e3a8fe | ||
|
|
1aa851796e | ||
|
|
730e6b2b4c | ||
|
|
167b9356fa | ||
|
|
d098256c96 | ||
|
|
ef58287c16 | ||
|
|
9ddf40455d | ||
|
|
a4e5e3205f | ||
|
|
17ee80363a |
8
.github/workflows/benchmark.yaml
vendored
8
.github/workflows/benchmark.yaml
vendored
@@ -2,6 +2,14 @@ name: Benchmark
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'rust-toolchain'
|
||||
- 'crates/**'
|
||||
- '!crates/ruff_dev'
|
||||
- '!crates/ruff_shrinking'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
||||
51
.github/workflows/ci.yaml
vendored
51
.github/workflows/ci.yaml
vendored
@@ -19,6 +19,41 @@ env:
|
||||
PYTHON_VERSION: "3.11" # to build abi3 wheels
|
||||
|
||||
jobs:
|
||||
determine_changes:
|
||||
name: "Determine changes"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
linter: ${{ steps.linter.outputs.any_changed }}
|
||||
formatter: ${{ steps.formatter.outputs.any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: tj-actions/changed-files@v37
|
||||
id: linter
|
||||
with:
|
||||
files: |
|
||||
Cargo.toml
|
||||
Cargo.lock
|
||||
crates/**
|
||||
!crates/ruff_python_formatter/**
|
||||
!crates/ruff_formatter/**
|
||||
!crates/ruff_dev/**
|
||||
!crates/ruff_shrinking/**
|
||||
|
||||
- uses: tj-actions/changed-files@v37
|
||||
id: formatter
|
||||
with:
|
||||
files: |
|
||||
Cargo.toml
|
||||
Cargo.lock
|
||||
crates/ruff_python_formatter/**
|
||||
crates/ruff_formatter/**
|
||||
crates/ruff_python_trivia/**
|
||||
crates/ruff_python_ast/**
|
||||
|
||||
|
||||
cargo-fmt:
|
||||
name: "cargo fmt"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -53,10 +88,12 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
# cargo insta 1.30.0 fails for some reason (https://github.com/mitsuhiko/insta/issues/392)
|
||||
- run: cargo install cargo-insta@=1.29.0
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- run: pip install black[d]==23.1.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests (Ubuntu)"
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
run: cargo insta test --all --all-features --unreferenced reject
|
||||
@@ -135,9 +172,11 @@ jobs:
|
||||
ecosystem:
|
||||
name: "ecosystem"
|
||||
runs-on: ubuntu-latest
|
||||
needs: cargo-test
|
||||
needs:
|
||||
- cargo-test
|
||||
- determine_changes
|
||||
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request' && needs.determine_changes.outputs.linter == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
@@ -285,6 +324,8 @@ jobs:
|
||||
check-formatter-stability:
|
||||
name: "Check formatter stability"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.formatter == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Install Rust toolchain"
|
||||
|
||||
@@ -133,8 +133,8 @@ At time of writing, the repository includes the following crates:
|
||||
refer to?"
|
||||
- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data, e.g.
|
||||
the names of all built-in exceptions and which standard library types are immutable.
|
||||
- `crates/ruff_python_whitespace`: library crate containing Python-specific whitespace analysis
|
||||
logic (indentation and newlines).
|
||||
- `crates/ruff_python_trivia`: library crate containing Python-specific trivia utilities (e.g.,
|
||||
for analyzing indentation, newlines, etc.).
|
||||
- `crates/ruff_rustpython`: library crate containing `RustPython`-specific utilities.
|
||||
- `crates/ruff_textwrap`: library crate to indent and dedent Python source code.
|
||||
- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module. Powers the
|
||||
|
||||
408
Cargo.lock
generated
408
Cargo.lock
generated
@@ -14,6 +14,15 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.0.2"
|
||||
@@ -111,9 +120,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.72"
|
||||
version = "1.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
|
||||
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
|
||||
[[package]]
|
||||
name = "argfile"
|
||||
@@ -126,9 +135,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "assert_cmd"
|
||||
version = "2.0.12"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6"
|
||||
checksum = "86d6b683edf8d1119fe420a94f8a7e389239666aa72e65495d91c00462510151"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"bstr",
|
||||
@@ -179,7 +188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-automata 0.3.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -270,9 +279,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.3.14"
|
||||
version = "4.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98330784c494e49850cb23b8e2afcca13587d2500b2e3f1f78ae20248059c9be"
|
||||
checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -281,9 +290,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.3.14"
|
||||
version = "4.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e182eb5f2562a67dda37e2c57af64d720a9e010c5e860ed87c056586aeafa52e"
|
||||
checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -334,14 +343,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.3.12"
|
||||
version = "4.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
|
||||
checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -526,10 +535,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.3"
|
||||
name = "ctor"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
|
||||
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
@@ -537,27 +556,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.3"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
|
||||
checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.3"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
|
||||
checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -627,9 +646,9 @@ checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.12"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272"
|
||||
checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
@@ -658,9 +677,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
@@ -757,6 +776,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs-err"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0845fa252299212f0389d64ba26f34fa32cfe41588355f21ed507c59a0f64541"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@@ -785,11 +810,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.11"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
|
||||
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"aho-corasick 0.7.20",
|
||||
"bstr",
|
||||
"fnv",
|
||||
"log",
|
||||
@@ -980,6 +1005,7 @@ dependencies = [
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"linked-hash-map",
|
||||
"regex",
|
||||
"similar",
|
||||
"walkdir",
|
||||
"yaml-rust",
|
||||
@@ -1020,12 +1046,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.9"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
||||
checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"rustix 0.38.4",
|
||||
"rustix 0.38.3",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
@@ -1040,9 +1066,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.9"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
@@ -1179,6 +1205,15 @@ version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.10"
|
||||
@@ -1294,6 +1329,16 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.3"
|
||||
@@ -1368,10 +1413,25 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
name = "output_vt100"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||
checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35"
|
||||
|
||||
[[package]]
|
||||
name = "path-absolutize"
|
||||
@@ -1498,7 +1558,7 @@ dependencies = [
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1557,9 +1617,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.4.1"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edc55135a600d700580e406b4de0d59cb9ad25e344a3a091a97ded2622ec4ec6"
|
||||
checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794"
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
@@ -1591,11 +1651,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.0"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
|
||||
checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755"
|
||||
dependencies = [
|
||||
"ctor",
|
||||
"diff",
|
||||
"output_vt100",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
@@ -1625,9 +1687,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.66"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
|
||||
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1647,12 +1709,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-junit"
|
||||
version = "0.3.3"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bf780b59d590c25f8c59b44c124166a2a93587868b619fb8f5b47fb15e9ed6d"
|
||||
checksum = "05b909fe9bf2abb1e3d6a97c9189a37c8105c61d03dca9ce6aace023e7d682bd"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"indexmap 2.0.0",
|
||||
"indexmap 1.9.3",
|
||||
"nextest-workspace-hack",
|
||||
"quick-xml",
|
||||
"thiserror",
|
||||
@@ -1661,18 +1723,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.29.0"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
|
||||
checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.31"
|
||||
version = "1.0.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
|
||||
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -1745,32 +1807,47 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.9.1"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
|
||||
checksum = "89089e897c013b3deb627116ae56a6955a72b8bed395c9526af31c9fe528b484"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"aho-corasick 1.0.2",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
"regex-automata 0.3.0",
|
||||
"regex-syntax 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.3"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56"
|
||||
dependencies = [
|
||||
"aho-corasick 1.0.2",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.4"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
|
||||
|
||||
[[package]]
|
||||
name = "result-like"
|
||||
@@ -1852,7 +1929,7 @@ dependencies = [
|
||||
"ruff_python_ast",
|
||||
"ruff_python_semantic",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_python_whitespace",
|
||||
"ruff_python_trivia",
|
||||
"ruff_rustpython",
|
||||
"ruff_text_size",
|
||||
"ruff_textwrap",
|
||||
@@ -1869,6 +1946,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror",
|
||||
"toml",
|
||||
@@ -1927,6 +2005,7 @@ dependencies = [
|
||||
"filetime",
|
||||
"glob",
|
||||
"ignore",
|
||||
"insta",
|
||||
"itertools",
|
||||
"itoa",
|
||||
"log",
|
||||
@@ -1949,6 +2028,7 @@ dependencies = [
|
||||
"shellexpand",
|
||||
"similar",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"tikv-jemallocator",
|
||||
"ureq",
|
||||
"walkdir",
|
||||
@@ -2031,7 +2111,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_textwrap",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2048,7 +2128,7 @@ dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"ruff_python_whitespace",
|
||||
"ruff_python_trivia",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"rustpython-ast",
|
||||
@@ -2072,7 +2152,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ruff_formatter",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_whitespace",
|
||||
"ruff_python_trivia",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"rustpython-parser",
|
||||
@@ -2081,7 +2161,6 @@ dependencies = [
|
||||
"similar",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"unic-ucd-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2116,11 +2195,14 @@ name = "ruff_python_stdlib"
|
||||
version = "0.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "ruff_python_whitespace"
|
||||
name = "ruff_python_trivia"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"insta",
|
||||
"memchr",
|
||||
"ruff_text_size",
|
||||
"smallvec",
|
||||
"unic-ucd-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2131,10 +2213,26 @@ dependencies = [
|
||||
"rustpython-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"fs-err",
|
||||
"regex",
|
||||
"ruff_python_ast",
|
||||
"ruff_rustpython",
|
||||
"rustpython-ast",
|
||||
"shlex",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_text_size"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -2144,7 +2242,7 @@ dependencies = [
|
||||
name = "ruff_textwrap"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"ruff_python_whitespace",
|
||||
"ruff_python_trivia",
|
||||
"ruff_text_size",
|
||||
]
|
||||
|
||||
@@ -2159,6 +2257,7 @@ dependencies = [
|
||||
"ruff",
|
||||
"ruff_diagnostics",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_rustpython",
|
||||
"rustpython-parser",
|
||||
"serde",
|
||||
@@ -2199,9 +2298,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.4"
|
||||
version = "0.38.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
|
||||
checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4"
|
||||
dependencies = [
|
||||
"bitflags 2.3.3",
|
||||
"errno",
|
||||
@@ -2212,13 +2311,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.5"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36"
|
||||
checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki 0.101.1",
|
||||
"rustls-webpki",
|
||||
"sct",
|
||||
]
|
||||
|
||||
@@ -2232,20 +2331,10 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustpython-ast"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
|
||||
dependencies = [
|
||||
"is-macro",
|
||||
"num-bigint",
|
||||
@@ -2256,7 +2345,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-format"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
|
||||
dependencies = [
|
||||
"bitflags 2.3.3",
|
||||
"itertools",
|
||||
@@ -2268,7 +2357,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-literal"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
|
||||
dependencies = [
|
||||
"hexf-parse",
|
||||
"is-macro",
|
||||
@@ -2280,7 +2369,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-parser"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"is-macro",
|
||||
@@ -2303,7 +2392,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-parser-core"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
|
||||
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
|
||||
dependencies = [
|
||||
"is-macro",
|
||||
"memchr",
|
||||
@@ -2312,15 +2401,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
||||
checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.15"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
@@ -2363,9 +2452,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
@@ -2379,15 +2468,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.18"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
|
||||
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.171"
|
||||
version = "1.0.166"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
|
||||
checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -2405,13 +2494,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.171"
|
||||
version = "1.0.166"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
|
||||
checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2427,9 +2516,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.103"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
|
||||
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -2447,9 +2536,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.1.0"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3"
|
||||
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
@@ -2458,19 +2547,28 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time 0.3.23",
|
||||
"time 0.3.22",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.1.0"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65"
|
||||
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2482,6 +2580,12 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.2.1"
|
||||
@@ -2496,9 +2600,9 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.11.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
@@ -2553,9 +2657,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.26"
|
||||
version = "2.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
|
||||
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2665,7 +2769,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2711,9 +2815,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.23"
|
||||
version = "0.3.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
|
||||
checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
@@ -2729,9 +2833,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.10"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
|
||||
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
@@ -2772,9 +2876,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.6"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
|
||||
checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@@ -2793,9 +2897,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.14"
|
||||
version = "0.19.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
||||
checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
|
||||
dependencies = [
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
@@ -2825,7 +2929,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2835,6 +2939,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2915,9 +3049,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.11"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
|
||||
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -2959,7 +3093,7 @@ dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-webpki 0.100.1",
|
||||
"rustls-webpki",
|
||||
"url",
|
||||
"webpki-roots",
|
||||
]
|
||||
@@ -2984,9 +3118,15 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.1"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
||||
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
@@ -3046,7 +3186,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -3080,7 +3220,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.26",
|
||||
"syn 2.0.23",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -3131,7 +3271,7 @@ version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
|
||||
dependencies = [
|
||||
"rustls-webpki 0.100.1",
|
||||
"rustls-webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3328,9 +3468,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.0"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
|
||||
checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -21,7 +21,7 @@ filetime = { version = "0.2.20" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.10" }
|
||||
ignore = { version = "0.4.20" }
|
||||
insta = { version = "1.30.0" }
|
||||
insta = { version = "1.31.0", feature = ["filters", "glob"] }
|
||||
is-macro = { version = "0.2.2" }
|
||||
itertools = { version = "0.10.5" }
|
||||
log = { version = "0.4.17" }
|
||||
@@ -52,14 +52,11 @@ wsl = { version = "0.1.0" }
|
||||
# v1.0.1
|
||||
libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "3cacca1a1029f05707e50703b49fe3dd860aa839", default-features = false }
|
||||
|
||||
# Please tag the RustPython version every time you update its revision here and in fuzz/Cargo.toml
|
||||
# Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork.
|
||||
# Current tag: v0.0.7
|
||||
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "b996b21ffca562ecb2086f632a6a0b05c245c24a" }
|
||||
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "b996b21ffca562ecb2086f632a6a0b05c245c24a" , default-features = false, features = ["num-bigint"]}
|
||||
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "b996b21ffca562ecb2086f632a6a0b05c245c24a", default-features = false, features = ["num-bigint"] }
|
||||
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "b996b21ffca562ecb2086f632a6a0b05c245c24a", default-features = false }
|
||||
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "b996b21ffca562ecb2086f632a6a0b05c245c24a" , default-features = false, features = ["full-lexer", "num-bigint"] }
|
||||
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "db04fd415774032e1e2ceb03bcbf5305e0d22c8c" }
|
||||
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "db04fd415774032e1e2ceb03bcbf5305e0d22c8c" , default-features = false, features = ["num-bigint"]}
|
||||
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "db04fd415774032e1e2ceb03bcbf5305e0d22c8c", default-features = false, features = ["num-bigint"] }
|
||||
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "db04fd415774032e1e2ceb03bcbf5305e0d22c8c", default-features = false }
|
||||
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "db04fd415774032e1e2ceb03bcbf5305e0d22c8c" , default-features = false, features = ["full-lexer", "num-bigint"] }
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
|
||||
@@ -397,7 +397,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [PyTorch](https://github.com/pytorch/pytorch)
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
- [Pynecone](https://github.com/pynecone-io/pynecone)
|
||||
- [Reflex](https://github.com/reflex-dev/reflex)
|
||||
- [Robyn](https://github.com/sansyrox/robyn)
|
||||
- Scale AI ([Launch SDK](https://github.com/scaleapi/launch-python-client))
|
||||
- Snowflake ([SnowCLI](https://github.com/Snowflake-Labs/snowcli))
|
||||
|
||||
@@ -19,7 +19,7 @@ ruff_cache = { path = "../ruff_cache" }
|
||||
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
|
||||
ruff_index = { path = "../ruff_index" }
|
||||
ruff_macros = { path = "../ruff_macros" }
|
||||
ruff_python_whitespace = { path = "../ruff_python_whitespace" }
|
||||
ruff_python_trivia = { path = "../ruff_python_trivia" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
|
||||
ruff_python_semantic = { path = "../ruff_python_semantic" }
|
||||
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
|
||||
@@ -86,6 +86,7 @@ pretty_assertions = "1.3.0"
|
||||
test-case = { workspace = true }
|
||||
# Disable colored output in tests
|
||||
colored = { workspace = true, features = ["no-color"] }
|
||||
tempfile = "3.6.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -97,3 +97,10 @@ def f():
|
||||
# variable name).
|
||||
for line_ in range(self.header_lines):
|
||||
fp.readline()
|
||||
|
||||
# Regression test: visitor didn't walk the elif test
|
||||
for key, value in current_crawler_tags.items():
|
||||
if key:
|
||||
pass
|
||||
elif wanted_tag_value != value:
|
||||
pass
|
||||
|
||||
14
crates/ruff/resources/test/fixtures/flake8_pyi/PYI017.py
vendored
Normal file
14
crates/ruff/resources/test/fixtures/flake8_pyi/PYI017.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
var: int
|
||||
a = var # OK
|
||||
|
||||
b = c = int # OK
|
||||
|
||||
a.b = int # OK
|
||||
|
||||
d, e = int, str # OK
|
||||
|
||||
f, g, h = int, str, TypeVar("T") # OK
|
||||
|
||||
i: TypeAlias = int | str # OK
|
||||
|
||||
j: TypeAlias = int # OK
|
||||
14
crates/ruff/resources/test/fixtures/flake8_pyi/PYI017.pyi
vendored
Normal file
14
crates/ruff/resources/test/fixtures/flake8_pyi/PYI017.pyi
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
var: int
|
||||
a = var # OK
|
||||
|
||||
b = c = int # PYI017
|
||||
|
||||
a.b = int # PYI017
|
||||
|
||||
d, e = int, str # PYI017
|
||||
|
||||
f, g, h = int, str, TypeVar("T") # PYI017
|
||||
|
||||
i: TypeAlias = int | str # OK
|
||||
|
||||
j: TypeAlias = int # OK
|
||||
19
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.py
vendored
Normal file
19
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import typing
|
||||
from typing import TypeAlias, Literal, Any
|
||||
|
||||
NewAny = Any
|
||||
OptionalStr = typing.Optional[str]
|
||||
Foo = Literal["foo"]
|
||||
IntOrStr = int | str
|
||||
AliasNone = None
|
||||
|
||||
NewAny: typing.TypeAlias = Any
|
||||
OptionalStr: TypeAlias = typing.Optional[str]
|
||||
Foo: typing.TypeAlias = Literal["foo"]
|
||||
IntOrStr: TypeAlias = int | str
|
||||
IntOrFloat: Foo = int | float
|
||||
AliasNone: typing.TypeAlias = None
|
||||
|
||||
# these are ok
|
||||
VarAlias = str
|
||||
AliasFoo = Foo
|
||||
18
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi
vendored
Normal file
18
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import Literal, Any
|
||||
|
||||
NewAny = Any
|
||||
OptionalStr = typing.Optional[str]
|
||||
Foo = Literal["foo"]
|
||||
IntOrStr = int | str
|
||||
AliasNone = None
|
||||
|
||||
NewAny: typing.TypeAlias = Any
|
||||
OptionalStr: TypeAlias = typing.Optional[str]
|
||||
Foo: typing.TypeAlias = Literal["foo"]
|
||||
IntOrStr: TypeAlias = int | str
|
||||
IntOrFloat: Foo = int | float
|
||||
AliasNone: typing.TypeAlias = None
|
||||
|
||||
# these are ok
|
||||
VarAlias = str
|
||||
AliasFoo = Foo
|
||||
@@ -100,6 +100,14 @@ if node.module0123456789:
|
||||
):
|
||||
print("Bad module!")
|
||||
|
||||
# SIM102
|
||||
# Regression test for https://github.com/apache/airflow/blob/145b16caaa43f0c42bffd97344df916c602cddde/airflow/configuration.py#L1161
|
||||
if a:
|
||||
if b:
|
||||
if c:
|
||||
print("if")
|
||||
elif d:
|
||||
print("elif")
|
||||
|
||||
# OK
|
||||
if a:
|
||||
|
||||
@@ -23,7 +23,7 @@ elif a:
|
||||
else:
|
||||
b = 2
|
||||
|
||||
# OK (false negative)
|
||||
# SIM108
|
||||
if True:
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -94,3 +94,10 @@ if result.eofs == "F":
|
||||
errors = 1
|
||||
else:
|
||||
errors = 1
|
||||
|
||||
if a:
|
||||
# Ignore branches with diverging comments because it means we're repeating
|
||||
# the bodies because we have different reasons for each branch
|
||||
x = 1
|
||||
elif c:
|
||||
x = 1
|
||||
|
||||
@@ -84,3 +84,15 @@ elif func_name == "remove":
|
||||
return "D"
|
||||
elif func_name == "move":
|
||||
return "MV"
|
||||
|
||||
# OK
|
||||
def no_return_in_else(platform):
|
||||
if platform == "linux":
|
||||
return "auditwheel repair -w {dest_dir} {wheel}"
|
||||
elif platform == "macos":
|
||||
return "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"
|
||||
elif platform == "windows":
|
||||
return ""
|
||||
else:
|
||||
msg = f"Unknown platform: {platform!r}"
|
||||
raise ValueError(msg)
|
||||
|
||||
@@ -38,6 +38,15 @@ if key in a_dict:
|
||||
else:
|
||||
vars[idx] = "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789"
|
||||
|
||||
# SIM401
|
||||
if foo():
|
||||
pass
|
||||
else:
|
||||
if key in a_dict:
|
||||
vars[idx] = a_dict[key]
|
||||
else:
|
||||
vars[idx] = "default"
|
||||
|
||||
###
|
||||
# Negative cases
|
||||
###
|
||||
@@ -105,12 +114,3 @@ elif key in a_dict:
|
||||
vars[idx] = a_dict[key]
|
||||
else:
|
||||
vars[idx] = "default"
|
||||
|
||||
# OK (false negative for nested else)
|
||||
if foo():
|
||||
pass
|
||||
else:
|
||||
if key in a_dict:
|
||||
vars[idx] = a_dict[key]
|
||||
else:
|
||||
vars[idx] = "default"
|
||||
|
||||
14
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH201.py
vendored
Normal file
14
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH201.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
from pathlib import Path, PurePath
|
||||
from pathlib import Path as pth
|
||||
|
||||
# match
|
||||
_ = Path(".")
|
||||
_ = pth(".")
|
||||
_ = PurePath(".")
|
||||
|
||||
# no match
|
||||
_ = Path()
|
||||
print(".")
|
||||
Path("file.txt")
|
||||
Path(".", "folder")
|
||||
PurePath(".", "folder")
|
||||
14
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH202.py
vendored
Normal file
14
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH202.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import os.path
|
||||
from pathlib import Path
|
||||
from os.path import getsize
|
||||
|
||||
|
||||
os.path.getsize("filename")
|
||||
os.path.getsize(b"filename")
|
||||
os.path.getsize(Path("filename"))
|
||||
os.path.getsize(__file__)
|
||||
|
||||
getsize("filename")
|
||||
getsize(b"filename")
|
||||
getsize(Path("filename"))
|
||||
getsize(__file__)
|
||||
12
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH203.py
vendored
Normal file
12
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH203.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import os.path
|
||||
from pathlib import Path
|
||||
from os.path import getatime
|
||||
|
||||
os.path.getatime("filename")
|
||||
os.path.getatime(b"filename")
|
||||
os.path.getatime(Path("filename"))
|
||||
|
||||
|
||||
getatime("filename")
|
||||
getatime(b"filename")
|
||||
getatime(Path("filename"))
|
||||
13
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH204.py
vendored
Normal file
13
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH204.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import os.path
|
||||
from pathlib import Path
|
||||
from os.path import getmtime
|
||||
|
||||
|
||||
os.path.getmtime("filename")
|
||||
os.path.getmtime(b"filename")
|
||||
os.path.getmtime(Path("filename"))
|
||||
|
||||
|
||||
getmtime("filename")
|
||||
getmtime(b"filename")
|
||||
getmtime(Path("filename"))
|
||||
12
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH205.py
vendored
Normal file
12
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH205.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import os.path
|
||||
from pathlib import Path
|
||||
from os.path import getctime
|
||||
|
||||
|
||||
os.path.getctime("filename")
|
||||
os.path.getctime(b"filename")
|
||||
os.path.getctime(Path("filename"))
|
||||
|
||||
getctime("filename")
|
||||
getctime(b"filename")
|
||||
getctime(Path("filename"))
|
||||
@@ -1,6 +1,11 @@
|
||||
if (1, 2):
|
||||
pass
|
||||
|
||||
if (3, 4):
|
||||
pass
|
||||
elif foo:
|
||||
pass
|
||||
|
||||
for _ in range(5):
|
||||
if True:
|
||||
pass
|
||||
|
||||
15
crates/ruff/resources/test/fixtures/pyflakes/F811_25.py
vendored
Normal file
15
crates/ruff/resources/test/fixtures/pyflakes/F811_25.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Regression test for branch detection from
|
||||
# https://github.com/pypa/build/blob/5800521541e5e749d4429617420d1ef8cdb40b46/src/build/_importlib.py
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
import importlib_metadata as metadata
|
||||
elif sys.version_info < (3, 9, 10) or (3, 10, 0) <= sys.version_info < (3, 10, 2):
|
||||
try:
|
||||
import importlib_metadata as metadata
|
||||
except ModuleNotFoundError:
|
||||
from importlib import metadata
|
||||
else:
|
||||
from importlib import metadata
|
||||
|
||||
__all__ = ["metadata"]
|
||||
6
crates/ruff/resources/test/fixtures/pyflakes/F811_26.py
vendored
Normal file
6
crates/ruff/resources/test/fixtures/pyflakes/F811_26.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
class Class:
|
||||
def func(self):
|
||||
pass
|
||||
|
||||
def func(self):
|
||||
pass
|
||||
@@ -25,3 +25,17 @@ def dec(x):
|
||||
def f():
|
||||
dec = 1
|
||||
return dec
|
||||
|
||||
|
||||
class Class:
|
||||
def f(self):
|
||||
print(my_var)
|
||||
my_var = 1
|
||||
|
||||
|
||||
class Class:
|
||||
my_var = 0
|
||||
|
||||
def f(self):
|
||||
print(my_var)
|
||||
my_var = 1
|
||||
|
||||
@@ -47,3 +47,17 @@ def not_ok1():
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
# Regression test for https://github.com/apache/airflow/blob/f1e1cdcc3b2826e68ba133f350300b5065bbca33/airflow/models/dag.py#L1737
|
||||
def not_ok2():
|
||||
if True:
|
||||
print(1)
|
||||
elif True:
|
||||
print(2)
|
||||
else:
|
||||
if True:
|
||||
print(3)
|
||||
else:
|
||||
print(4)
|
||||
|
||||
|
||||
@@ -62,6 +62,16 @@ print("foo {} ".format(x))
|
||||
1111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
)
|
||||
|
||||
"""
|
||||
{}
|
||||
""".format(1)
|
||||
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """
|
||||
{}
|
||||
""".format(
|
||||
111111
|
||||
)
|
||||
|
||||
###
|
||||
# Non-errors
|
||||
###
|
||||
@@ -99,6 +109,21 @@ r'"\N{snowman} {}".format(a)'
|
||||
11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
)
|
||||
|
||||
"""
|
||||
{}
|
||||
{}
|
||||
{}
|
||||
""".format(
|
||||
1,
|
||||
2,
|
||||
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
)
|
||||
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
""".format(
|
||||
111111
|
||||
)
|
||||
|
||||
async def c():
|
||||
return "{}".format(await 3)
|
||||
|
||||
|
||||
@@ -7,20 +7,20 @@ if True:
|
||||
|
||||
if True:
|
||||
if foo:
|
||||
pass
|
||||
print()
|
||||
elif sys.version_info < (3, 3):
|
||||
cmd = [sys.executable, "-m", "test.regrtest"]
|
||||
|
||||
if True:
|
||||
if foo:
|
||||
pass
|
||||
print()
|
||||
elif sys.version_info < (3, 3):
|
||||
cmd = [sys.executable, "-m", "test.regrtest"]
|
||||
elif foo:
|
||||
cmd = [sys.executable, "-m", "test", "-j0"]
|
||||
|
||||
if foo:
|
||||
pass
|
||||
print()
|
||||
elif sys.version_info < (3, 3):
|
||||
cmd = [sys.executable, "-m", "test.regrtest"]
|
||||
|
||||
@@ -28,7 +28,7 @@ if True:
|
||||
cmd = [sys.executable, "-m", "test.regrtest"]
|
||||
|
||||
if foo:
|
||||
pass
|
||||
print()
|
||||
elif sys.version_info < (3, 3):
|
||||
cmd = [sys.executable, "-m", "test.regrtest"]
|
||||
else:
|
||||
|
||||
@@ -230,6 +230,15 @@ def incorrect_multi_conditional(arg1, arg2):
|
||||
raise Exception("...") # should be typeerror
|
||||
|
||||
|
||||
def multiple_is_instance_checks(some_arg):
|
||||
if isinstance(some_arg, str):
|
||||
pass
|
||||
elif isinstance(some_arg, int):
|
||||
pass
|
||||
else:
|
||||
raise Exception("...") # should be typeerror
|
||||
|
||||
|
||||
class MyCustomTypeValidation(Exception):
|
||||
pass
|
||||
|
||||
@@ -296,6 +305,17 @@ def multiple_ifs(some_args):
|
||||
pass
|
||||
|
||||
|
||||
def else_body(obj):
|
||||
if isinstance(obj, datetime.timedelta):
|
||||
return "TimeDelta"
|
||||
elif isinstance(obj, relativedelta.relativedelta):
|
||||
return "RelativeDelta"
|
||||
elif isinstance(obj, CronExpression):
|
||||
return "CronExpression"
|
||||
else:
|
||||
raise Exception(f"Unknown object type: {obj.__class__.__name__}")
|
||||
|
||||
|
||||
def early_return():
|
||||
if isinstance(this, some_type):
|
||||
if x in this:
|
||||
|
||||
@@ -7,7 +7,7 @@ use rustpython_parser::{lexer, Mode};
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::helpers;
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
use ruff_python_whitespace::{is_python_whitespace, NewlineWithTrailingNewline, PythonWhitespace};
|
||||
use ruff_python_trivia::{is_python_whitespace, NewlineWithTrailingNewline, PythonWhitespace};
|
||||
|
||||
use crate::autofix::codemods;
|
||||
|
||||
@@ -190,12 +190,24 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
|
||||
}
|
||||
Stmt::For(ast::StmtFor { body, orelse, .. })
|
||||
| Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. })
|
||||
| Stmt::While(ast::StmtWhile { body, orelse, .. })
|
||||
| Stmt::If(ast::StmtIf { body, orelse, .. }) => {
|
||||
| Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
|
||||
if is_only(body, child) || is_only(orelse, child) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::If(ast::StmtIf {
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
}) => {
|
||||
if is_only(body, child)
|
||||
|| elif_else_clauses
|
||||
.iter()
|
||||
.any(|ast::ElifElseClause { body, .. }| is_only(body, child))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
//! `NoQA` enforcement and validation.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use itertools::Itertools;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use rustpython_parser::ast::Ranged;
|
||||
@@ -16,6 +18,7 @@ use crate::settings::Settings;
|
||||
|
||||
pub(crate) fn check_noqa(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
comment_ranges: &[TextRange],
|
||||
noqa_line_for: &NoqaMapping,
|
||||
@@ -23,10 +26,10 @@ pub(crate) fn check_noqa(
|
||||
settings: &Settings,
|
||||
) -> Vec<usize> {
|
||||
// Identify any codes that are globally exempted (within the current file).
|
||||
let exemption = FileExemption::try_extract(locator.contents(), comment_ranges, locator);
|
||||
let exemption = FileExemption::try_extract(locator.contents(), comment_ranges, path, locator);
|
||||
|
||||
// Extract all `noqa` directives.
|
||||
let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, locator);
|
||||
let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator);
|
||||
|
||||
// Indices of diagnostics that were ignored by a `noqa` directive.
|
||||
let mut ignored_diagnostics = vec![];
|
||||
|
||||
@@ -5,7 +5,7 @@ use ruff_text_size::TextSize;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
use ruff_python_whitespace::UniversalNewlines;
|
||||
use ruff_python_trivia::UniversalNewlines;
|
||||
|
||||
use crate::comments::shebang::ShebangDirective;
|
||||
use crate::registry::Rule;
|
||||
|
||||
@@ -629,10 +629,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Pyi, "014") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ArgumentDefaultInStub),
|
||||
(Flake8Pyi, "015") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::AssignmentDefaultInStub),
|
||||
(Flake8Pyi, "016") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::DuplicateUnionMember),
|
||||
(Flake8Pyi, "017") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ComplexAssignmentInStub),
|
||||
(Flake8Pyi, "020") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::QuotedAnnotationInStub),
|
||||
(Flake8Pyi, "021") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::DocstringInStub),
|
||||
(Flake8Pyi, "024") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::CollectionsNamedTuple),
|
||||
(Flake8Pyi, "025") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnaliasedCollectionsAbcSetImport),
|
||||
(Flake8Pyi, "026") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TypeAliasWithoutAnnotation),
|
||||
(Flake8Pyi, "029") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StrOrReprDefinedInStub),
|
||||
(Flake8Pyi, "030") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnnecessaryLiteralUnion),
|
||||
(Flake8Pyi, "032") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::AnyEqNeAnnotation),
|
||||
@@ -747,6 +749,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8UsePathlib, "122") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::violations::OsPathSplitext),
|
||||
(Flake8UsePathlib, "123") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::violations::BuiltinOpen),
|
||||
(Flake8UsePathlib, "124") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::violations::PyPath),
|
||||
(Flake8UsePathlib, "201") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::PathConstructorCurrentDirectory),
|
||||
(Flake8UsePathlib, "202") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetsize),
|
||||
(Flake8UsePathlib, "202") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetsize),
|
||||
(Flake8UsePathlib, "203") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetatime),
|
||||
(Flake8UsePathlib, "204") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetmtime),
|
||||
(Flake8UsePathlib, "205") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetctime),
|
||||
|
||||
// flake8-logging-format
|
||||
(Flake8LoggingFormat, "001") => (RuleGroup::Unspecified, rules::flake8_logging_format::violations::LoggingStringFormat),
|
||||
@@ -782,7 +790,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "011") => (RuleGroup::Unspecified, rules::ruff::rules::StaticKeyDictComprehension),
|
||||
(Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault),
|
||||
(Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional),
|
||||
#[cfg(feature = "unreachable-code")]
|
||||
#[cfg(feature = "unreachable-code")] // When removing this feature gate, also update rules_selector.rs
|
||||
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
|
||||
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
|
||||
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_python_whitespace::{is_python_whitespace, Cursor};
|
||||
use ruff_python_trivia::{is_python_whitespace, Cursor};
|
||||
use ruff_text_size::{TextLen, TextSize};
|
||||
|
||||
/// A shebang directive (e.g., `#!/usr/bin/env python3`).
|
||||
|
||||
@@ -10,7 +10,7 @@ use rustpython_parser::Tok;
|
||||
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor};
|
||||
use ruff_python_whitespace::UniversalNewlineIterator;
|
||||
use ruff_python_trivia::UniversalNewlineIterator;
|
||||
|
||||
/// Extract doc lines (standalone comments) from a token sequence.
|
||||
pub(crate) fn doc_lines_from_tokens(lxr: &[LexResult]) -> DocLines {
|
||||
|
||||
@@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use ruff_python_whitespace::{Line, UniversalNewlineIterator, UniversalNewlines};
|
||||
use ruff_python_trivia::{Line, UniversalNewlineIterator, UniversalNewlines};
|
||||
|
||||
use crate::docstrings::styles::SectionStyle;
|
||||
use crate::docstrings::{Docstring, DocstringBody};
|
||||
|
||||
@@ -8,7 +8,7 @@ use rustpython_parser::{lexer, Mode, Tok};
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
use ruff_python_whitespace::{PythonWhitespace, UniversalNewlineIterator};
|
||||
use ruff_python_trivia::{PythonWhitespace, UniversalNewlineIterator};
|
||||
use ruff_textwrap::indent;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -305,7 +305,7 @@ mod tests {
|
||||
use rustpython_parser::Parse;
|
||||
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
use ruff_python_whitespace::LineEnding;
|
||||
use ruff_python_trivia::LineEnding;
|
||||
|
||||
use super::Insertion;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use serde::Serialize;
|
||||
use serde_json::error::Category;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_whitespace::{NewlineWithTrailingNewline, UniversalNewlineIterator};
|
||||
use ruff_python_trivia::{NewlineWithTrailingNewline, UniversalNewlineIterator};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::autofix::source_map::{SourceMap, SourceMarker};
|
||||
|
||||
@@ -214,6 +214,7 @@ pub fn check_path(
|
||||
{
|
||||
let ignored = check_noqa(
|
||||
&mut diagnostics,
|
||||
path,
|
||||
locator,
|
||||
indexer.comment_ranges(),
|
||||
&directives.noqa_line_for,
|
||||
@@ -320,6 +321,7 @@ pub fn lint_only(
|
||||
package: Option<&Path>,
|
||||
settings: &Settings,
|
||||
noqa: flags::Noqa,
|
||||
source_kind: Option<&SourceKind>,
|
||||
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
|
||||
// Tokenize once.
|
||||
let tokens: Vec<LexResult> = ruff_rustpython::tokenize(contents);
|
||||
@@ -352,7 +354,7 @@ pub fn lint_only(
|
||||
&directives,
|
||||
settings,
|
||||
noqa,
|
||||
None,
|
||||
source_kind,
|
||||
);
|
||||
|
||||
result.map(|(diagnostics, imports)| {
|
||||
|
||||
@@ -13,9 +13,10 @@ use rustpython_parser::ast::Ranged;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_whitespace::LineEnding;
|
||||
use ruff_python_trivia::LineEnding;
|
||||
|
||||
use crate::codes::NoqaCode;
|
||||
use crate::fs::relativize_path;
|
||||
use crate::registry::{AsRule, Rule, RuleSet};
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
|
||||
@@ -225,6 +226,7 @@ impl FileExemption {
|
||||
pub(crate) fn try_extract(
|
||||
contents: &str,
|
||||
comment_ranges: &[TextRange],
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
) -> Option<Self> {
|
||||
let mut exempt_codes: Vec<NoqaCode> = vec![];
|
||||
@@ -234,7 +236,8 @@ impl FileExemption {
|
||||
Err(err) => {
|
||||
#[allow(deprecated)]
|
||||
let line = locator.compute_line_index(range.start());
|
||||
warn!("Invalid `# noqa` directive on line {line}: {err}");
|
||||
let path_display = relativize_path(path);
|
||||
warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}");
|
||||
}
|
||||
Ok(Some(ParsedFileExemption::All)) => {
|
||||
return Some(Self::All);
|
||||
@@ -437,6 +440,7 @@ pub(crate) fn add_noqa(
|
||||
line_ending: LineEnding,
|
||||
) -> Result<usize> {
|
||||
let (count, output) = add_noqa_inner(
|
||||
path,
|
||||
diagnostics,
|
||||
locator,
|
||||
commented_lines,
|
||||
@@ -448,6 +452,7 @@ pub(crate) fn add_noqa(
|
||||
}
|
||||
|
||||
fn add_noqa_inner(
|
||||
path: &Path,
|
||||
diagnostics: &[Diagnostic],
|
||||
locator: &Locator,
|
||||
commented_ranges: &[TextRange],
|
||||
@@ -460,8 +465,8 @@ fn add_noqa_inner(
|
||||
|
||||
// Whether the file is exempted from all checks.
|
||||
// Codes that are globally exempted (within the current file).
|
||||
let exemption = FileExemption::try_extract(locator.contents(), commented_ranges, locator);
|
||||
let directives = NoqaDirectives::from_commented_ranges(commented_ranges, locator);
|
||||
let exemption = FileExemption::try_extract(locator.contents(), commented_ranges, path, locator);
|
||||
let directives = NoqaDirectives::from_commented_ranges(commented_ranges, path, locator);
|
||||
|
||||
// Mark any non-ignored diagnostics.
|
||||
for diagnostic in diagnostics {
|
||||
@@ -625,6 +630,7 @@ pub(crate) struct NoqaDirectives<'a> {
|
||||
impl<'a> NoqaDirectives<'a> {
|
||||
pub(crate) fn from_commented_ranges(
|
||||
comment_ranges: &[TextRange],
|
||||
path: &Path,
|
||||
locator: &'a Locator<'a>,
|
||||
) -> Self {
|
||||
let mut directives = Vec::new();
|
||||
@@ -634,7 +640,8 @@ impl<'a> NoqaDirectives<'a> {
|
||||
Err(err) => {
|
||||
#[allow(deprecated)]
|
||||
let line = locator.compute_line_index(range.start());
|
||||
warn!("Invalid `# noqa` directive on line {line}: {err}");
|
||||
let path_display = relativize_path(path);
|
||||
warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}");
|
||||
}
|
||||
Ok(Some(directive)) => {
|
||||
// noqa comments are guaranteed to be single line.
|
||||
@@ -758,12 +765,14 @@ impl FromIterator<TextRange> for NoqaMapping {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use insta::assert_debug_snapshot;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_whitespace::LineEnding;
|
||||
use ruff_python_trivia::LineEnding;
|
||||
|
||||
use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption};
|
||||
use crate::rules::pycodestyle::rules::AmbiguousVariableName;
|
||||
@@ -946,9 +955,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn modification() {
|
||||
let path = Path::new("/tmp/foo.txt");
|
||||
|
||||
let contents = "x = 1";
|
||||
let noqa_line_for = NoqaMapping::default();
|
||||
let (count, output) = add_noqa_inner(
|
||||
path,
|
||||
&[],
|
||||
&Locator::new(contents),
|
||||
&[],
|
||||
@@ -968,6 +980,7 @@ mod tests {
|
||||
let contents = "x = 1";
|
||||
let noqa_line_for = NoqaMapping::default();
|
||||
let (count, output) = add_noqa_inner(
|
||||
path,
|
||||
&diagnostics,
|
||||
&Locator::new(contents),
|
||||
&[],
|
||||
@@ -992,6 +1005,7 @@ mod tests {
|
||||
let contents = "x = 1 # noqa: E741\n";
|
||||
let noqa_line_for = NoqaMapping::default();
|
||||
let (count, output) = add_noqa_inner(
|
||||
path,
|
||||
&diagnostics,
|
||||
&Locator::new(contents),
|
||||
&[TextRange::new(TextSize::from(7), TextSize::from(19))],
|
||||
@@ -1016,6 +1030,7 @@ mod tests {
|
||||
let contents = "x = 1 # noqa";
|
||||
let noqa_line_for = NoqaMapping::default();
|
||||
let (count, output) = add_noqa_inner(
|
||||
path,
|
||||
&diagnostics,
|
||||
&Locator::new(contents),
|
||||
&[TextRange::new(TextSize::from(7), TextSize::from(13))],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use colored::Colorize;
|
||||
use log::warn;
|
||||
use pyproject_toml::{BuildSystem, Project};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -22,34 +23,38 @@ struct PyProjectToml {
|
||||
project: Option<Project>,
|
||||
}
|
||||
|
||||
pub fn lint_pyproject_toml(source_file: SourceFile, settings: &Settings) -> Result<Vec<Message>> {
|
||||
let mut messages = vec![];
|
||||
|
||||
let err = match toml::from_str::<PyProjectToml>(source_file.source_text()) {
|
||||
Ok(_) => return Ok(messages),
|
||||
Err(err) => err,
|
||||
pub fn lint_pyproject_toml(source_file: SourceFile, settings: &Settings) -> Vec<Message> {
|
||||
let Some(err) = toml::from_str::<PyProjectToml>(source_file.source_text()).err() else {
|
||||
return Vec::default();
|
||||
};
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let range = match err.span() {
|
||||
// This is bad but sometimes toml and/or serde just don't give us spans
|
||||
// TODO(konstin,micha): https://github.com/astral-sh/ruff/issues/4571
|
||||
None => TextRange::default(),
|
||||
Some(range) => {
|
||||
let Ok(end) = TextSize::try_from(range.end) else {
|
||||
let message = format!(
|
||||
"{} is larger than 4GB, but ruff assumes all files to be smaller",
|
||||
source_file.name(),
|
||||
);
|
||||
if settings.rules.enabled(Rule::IOError) {
|
||||
let diagnostic = Diagnostic::new(
|
||||
IOError {
|
||||
message: "pyproject.toml is larger than 4GB".to_string(),
|
||||
},
|
||||
TextRange::default(),
|
||||
);
|
||||
let diagnostic = Diagnostic::new(IOError { message }, TextRange::default());
|
||||
messages.push(Message::from_diagnostic(
|
||||
diagnostic,
|
||||
source_file,
|
||||
TextSize::default(),
|
||||
));
|
||||
} else {
|
||||
warn!(
|
||||
"{}{}{} {message}",
|
||||
"Failed to lint ".bold(),
|
||||
source_file.name().bold(),
|
||||
":".bold()
|
||||
);
|
||||
}
|
||||
return Ok(messages);
|
||||
return messages;
|
||||
};
|
||||
TextRange::new(
|
||||
// start <= end, so if end < 4GB follows start < 4GB
|
||||
@@ -69,5 +74,5 @@ pub fn lint_pyproject_toml(source_file: SourceFile, settings: &Settings) -> Resu
|
||||
));
|
||||
}
|
||||
|
||||
Ok(messages)
|
||||
messages
|
||||
}
|
||||
|
||||
@@ -245,6 +245,7 @@ impl Renamer {
|
||||
| BindingKind::NamedExprAssignment
|
||||
| BindingKind::UnpackedAssignment
|
||||
| BindingKind::Assignment
|
||||
| BindingKind::BoundException
|
||||
| BindingKind::LoopVar
|
||||
| BindingKind::Global
|
||||
| BindingKind::Nonlocal(_)
|
||||
|
||||
@@ -330,9 +330,12 @@ pub fn python_files_in_path(
|
||||
}
|
||||
|
||||
if result.as_ref().map_or(true, |entry| {
|
||||
if entry.depth() == 0 {
|
||||
// Ignore directories
|
||||
if entry.file_type().map_or(true, |ft| ft.is_dir()) {
|
||||
false
|
||||
} else if entry.depth() == 0 {
|
||||
// Accept all files that are passed-in directly.
|
||||
entry.file_type().map_or(false, |ft| ft.is_file())
|
||||
true
|
||||
} else {
|
||||
// Otherwise, check if the file is included.
|
||||
let path = entry.path();
|
||||
|
||||
@@ -249,6 +249,9 @@ mod schema {
|
||||
(!prefix.is_empty()).then(|| prefix.to_string())
|
||||
})),
|
||||
)
|
||||
// Filter out rule gated behind `#[cfg(feature = "unreachable-code")]`, which is
|
||||
// off-by-default
|
||||
.filter(|prefix| prefix != "RUF014")
|
||||
.sorted()
|
||||
.map(Value::String)
|
||||
.collect(),
|
||||
@@ -342,24 +345,33 @@ mod clap_completion {
|
||||
let prefix = l.common_prefix();
|
||||
(!prefix.is_empty()).then(|| PossibleValue::new(prefix).help(l.name()))
|
||||
})
|
||||
.chain(RuleCodePrefix::iter().map(|p| {
|
||||
let prefix = p.linter().common_prefix();
|
||||
let code = p.short_code();
|
||||
.chain(
|
||||
RuleCodePrefix::iter()
|
||||
// Filter out rule gated behind `#[cfg(feature = "unreachable-code")]`, which is
|
||||
// off-by-default
|
||||
.filter(|p| {
|
||||
format!("{}{}", p.linter().common_prefix(), p.short_code())
|
||||
!= "RUF014"
|
||||
})
|
||||
.map(|p| {
|
||||
let prefix = p.linter().common_prefix();
|
||||
let code = p.short_code();
|
||||
|
||||
let mut rules_iter = p.rules();
|
||||
let rule1 = rules_iter.next();
|
||||
let rule2 = rules_iter.next();
|
||||
let mut rules_iter = p.rules();
|
||||
let rule1 = rules_iter.next();
|
||||
let rule2 = rules_iter.next();
|
||||
|
||||
let value = PossibleValue::new(format!("{prefix}{code}"));
|
||||
let value = PossibleValue::new(format!("{prefix}{code}"));
|
||||
|
||||
if rule2.is_none() {
|
||||
let rule1 = rule1.unwrap();
|
||||
let name: &'static str = rule1.into();
|
||||
value.help(name)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
})),
|
||||
if rule2.is_none() {
|
||||
let rule1 = rule1.unwrap();
|
||||
let name: &'static str = rule1.into();
|
||||
value.help(name)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ pub(super) fn match_function_def(
|
||||
}) => (
|
||||
name,
|
||||
args,
|
||||
returns.as_ref().map(|expr| &**expr),
|
||||
returns.as_ref().map(AsRef::as_ref),
|
||||
body,
|
||||
decorator_list,
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_python_ast::helpers::find_keyword;
|
||||
use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
@@ -37,12 +38,7 @@ pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, func: &Expr, keywor
|
||||
matches!(call_path.as_slice(), ["jinja2", "Environment"])
|
||||
})
|
||||
{
|
||||
if let Some(keyword) = keywords.iter().find(|keyword| {
|
||||
keyword
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |arg| arg.as_str() == "autoescape")
|
||||
}) {
|
||||
if let Some(keyword) = find_keyword(keywords, "autoescape") {
|
||||
match &keyword.value {
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Bool(true),
|
||||
|
||||
@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_const_false;
|
||||
use ruff_python_ast::helpers::{find_keyword, is_const_false};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -63,12 +63,7 @@ pub(crate) fn request_with_no_cert_validation(
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
if let Some(keyword) = keywords.iter().find(|keyword| {
|
||||
keyword
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |arg| arg.as_str() == "verify")
|
||||
}) {
|
||||
if let Some(keyword) = find_keyword(keywords, "verify") {
|
||||
if is_const_false(&keyword.value) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
RequestWithNoCertValidation {
|
||||
|
||||
@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_const_none;
|
||||
use ruff_python_ast::helpers::{find_keyword, is_const_none};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -63,12 +63,7 @@ pub(crate) fn request_without_timeout(checker: &mut Checker, func: &Expr, keywor
|
||||
)
|
||||
})
|
||||
{
|
||||
if let Some(keyword) = keywords.iter().find(|keyword| {
|
||||
keyword
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |arg| arg.as_str() == "timeout")
|
||||
}) {
|
||||
if let Some(keyword) = find_keyword(keywords, "timeout") {
|
||||
if is_const_none(&keyword.value) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
RequestWithoutTimeout { implicit: false },
|
||||
|
||||
@@ -45,7 +45,7 @@ use crate::checkers::ast::Checker;
|
||||
/// - `flake8-bugbear.extend-immutable-calls`
|
||||
#[violation]
|
||||
pub struct FunctionCallInDefaultArgument {
|
||||
pub name: Option<String>,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
impl Violation for FunctionCallInDefaultArgument {
|
||||
@@ -96,14 +96,16 @@ where
|
||||
}
|
||||
visitor::walk_expr(self, expr);
|
||||
}
|
||||
Expr::Lambda(_) => {}
|
||||
Expr::Lambda(_) => {
|
||||
// Don't recurse.
|
||||
}
|
||||
_ => visitor::walk_expr(self, expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// B008
|
||||
pub(crate) fn function_call_argument_default(checker: &mut Checker, arguments: &Arguments) {
|
||||
pub(crate) fn function_call_in_argument_default(checker: &mut Checker, arguments: &Arguments) {
|
||||
// Map immutable calls to (module, member) format.
|
||||
let extend_immutable_calls: Vec<CallPath> = checker
|
||||
.settings
|
||||
@@ -8,7 +8,7 @@ pub(crate) use duplicate_value::*;
|
||||
pub(crate) use except_with_empty_tuple::*;
|
||||
pub(crate) use except_with_non_exception_classes::*;
|
||||
pub(crate) use f_string_docstring::*;
|
||||
pub(crate) use function_call_argument_default::*;
|
||||
pub(crate) use function_call_in_argument_default::*;
|
||||
pub(crate) use function_uses_loop_variable::*;
|
||||
pub(crate) use getattr_with_constant::*;
|
||||
pub(crate) use jump_statement_in_finally::*;
|
||||
@@ -42,7 +42,7 @@ mod duplicate_value;
|
||||
mod except_with_empty_tuple;
|
||||
mod except_with_non_exception_classes;
|
||||
mod f_string_docstring;
|
||||
mod function_call_argument_default;
|
||||
mod function_call_in_argument_default;
|
||||
mod function_uses_loop_variable;
|
||||
mod getattr_with_constant;
|
||||
mod jump_statement_in_finally;
|
||||
|
||||
@@ -13,8 +13,8 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Passing `count`, `maxsplit`, or `flags` as positional arguments to
|
||||
/// `re.sub`, re.subn`, or `re.split` can lead to confusion, as most methods in
|
||||
/// the `re` module accepts `flags` as the third positional argument, while
|
||||
/// `re.sub`, `re.subn`, or `re.split` can lead to confusion, as most methods in
|
||||
/// the `re` module accept `flags` as the third positional argument, while
|
||||
/// `re.sub`, `re.subn`, and `re.split` have different signatures.
|
||||
///
|
||||
/// Instead, pass `count`, `maxsplit`, and `flags` as keyword arguments.
|
||||
|
||||
@@ -55,8 +55,6 @@ struct GroupNameFinder<'a> {
|
||||
/// A flag indicating that the `group_name` variable has been overridden
|
||||
/// during the visit.
|
||||
overridden: bool,
|
||||
/// A stack of `if` statements.
|
||||
parent_ifs: Vec<&'a Stmt>,
|
||||
/// A stack of counters where each counter is itself a list of usage count.
|
||||
/// This is used specifically for mutually exclusive statements such as an
|
||||
/// `if` or `match`.
|
||||
@@ -77,7 +75,6 @@ impl<'a> GroupNameFinder<'a> {
|
||||
usage_count: 0,
|
||||
nested: false,
|
||||
overridden: false,
|
||||
parent_ifs: Vec::new(),
|
||||
counter_stack: Vec::new(),
|
||||
exprs: Vec::new(),
|
||||
}
|
||||
@@ -146,56 +143,28 @@ where
|
||||
Stmt::If(ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
elif_else_clauses,
|
||||
range: _,
|
||||
}) => {
|
||||
// Determine whether we're on an `if` arm (as opposed to an `elif`).
|
||||
let is_if_arm = !self.parent_ifs.iter().any(|parent| {
|
||||
if let Stmt::If(ast::StmtIf { orelse, .. }) = parent {
|
||||
orelse.len() == 1 && &orelse[0] == stmt
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
// base if plus branches
|
||||
let mut if_stack = Vec::with_capacity(1 + elif_else_clauses.len());
|
||||
// Initialize the vector with the count for the if branch.
|
||||
if_stack.push(0);
|
||||
self.counter_stack.push(if_stack);
|
||||
|
||||
if is_if_arm {
|
||||
// Initialize the vector with the count for current branch.
|
||||
self.counter_stack.push(vec![0]);
|
||||
} else {
|
||||
// SAFETY: `unwrap` is safe because we're either in `elif` or
|
||||
// `else` branch which can come only after an `if` branch.
|
||||
// When inside an `if` branch, a new vector will be pushed
|
||||
// onto the stack.
|
||||
self.visit_expr(test);
|
||||
self.visit_body(body);
|
||||
|
||||
for clause in elif_else_clauses {
|
||||
self.counter_stack.last_mut().unwrap().push(0);
|
||||
self.visit_elif_else_clause(clause);
|
||||
}
|
||||
|
||||
let has_else = orelse
|
||||
.first()
|
||||
.map_or(false, |expr| !matches!(expr, Stmt::If(_)));
|
||||
|
||||
self.parent_ifs.push(stmt);
|
||||
if has_else {
|
||||
// There's no `Stmt::Else`; instead, the `else` contents are directly on
|
||||
// the `orelse` of the `Stmt::If` node. We want to add a new counter for
|
||||
// the `orelse` branch, but first, we need to visit the `if` body manually.
|
||||
self.visit_expr(test);
|
||||
self.visit_body(body);
|
||||
|
||||
// Now, we're in an `else` block.
|
||||
self.counter_stack.last_mut().unwrap().push(0);
|
||||
self.visit_body(orelse);
|
||||
} else {
|
||||
visitor::walk_stmt(self, stmt);
|
||||
}
|
||||
self.parent_ifs.pop();
|
||||
|
||||
if is_if_arm {
|
||||
if let Some(last) = self.counter_stack.pop() {
|
||||
// This is the max number of group usage from all the
|
||||
// branches of this `if` statement.
|
||||
let max_count = last.into_iter().max().unwrap_or(0);
|
||||
self.increment_usage_count(max_count);
|
||||
}
|
||||
if let Some(last) = self.counter_stack.pop() {
|
||||
// This is the max number of group usage from all the
|
||||
// branches of this `if` statement.
|
||||
let max_count = last.into_iter().max().unwrap_or(0);
|
||||
self.increment_usage_count(max_count);
|
||||
}
|
||||
}
|
||||
Stmt::Match(ast::StmtMatch {
|
||||
|
||||
@@ -22,9 +22,12 @@ pub struct Options {
|
||||
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
|
||||
"#
|
||||
)]
|
||||
/// Additional callable functions to consider "immutable" when evaluating,
|
||||
/// e.g., the `no-mutable-default-argument` rule (`B006`) or
|
||||
/// `no-function-call-in-dataclass-defaults` rule (`RUF009`).
|
||||
/// Additional callable functions to consider "immutable" when evaluating, e.g., the
|
||||
/// `function-call-in-default-argument` rule (`B008`) or `function-call-in-dataclass-defaults`
|
||||
/// rule (`RUF009`).
|
||||
///
|
||||
/// Expects to receive a list of fully-qualified names (e.g., `fastapi.Query`, rather than
|
||||
/// `Query`).
|
||||
pub extend_immutable_calls: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,39 +3,114 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::directives::{TodoComment, TodoDirectiveKind};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for "TODO" comments.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// "TODO" comments are used to describe an issue that should be resolved
|
||||
/// (usually, a missing feature, optimization, or refactoring opportunity).
|
||||
///
|
||||
/// Consider resolving the issue before deploying the code.
|
||||
///
|
||||
/// Note that if you use "TODO" comments as a form of documentation (e.g.,
|
||||
/// to [provide context for future work](https://gist.github.com/dmnd/ed5d8ef8de2e4cfea174bd5dafcda382)),
|
||||
/// this rule may not be appropriate for your project.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def greet(name):
|
||||
/// return f"Hello, {name}!" # TODO: Add support for custom greetings.
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct LineContainsTodo;
|
||||
impl Violation for LineContainsTodo {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Line contains TODO")
|
||||
format!("Line contains TODO, consider resolving the issue")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for "FIXME" comments.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// "FIXME" comments are used to describe an issue that should be resolved
|
||||
/// (usually, a bug or unexpected behavior).
|
||||
///
|
||||
/// Consider resolving the issue before deploying the code.
|
||||
///
|
||||
/// Note that if you use "FIXME" comments as a form of documentation, this
|
||||
/// rule may not be appropriate for your project.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def speed(distance, time):
|
||||
/// return distance / time # FIXME: Raises ZeroDivisionError for time = 0.
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct LineContainsFixme;
|
||||
impl Violation for LineContainsFixme {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Line contains FIXME")
|
||||
format!("Line contains FIXME, consider resolving the issue")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for "XXX" comments.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// "XXX" comments are used to describe an issue that should be resolved.
|
||||
///
|
||||
/// Consider resolving the issue before deploying the code, or, at minimum,
|
||||
/// using a more descriptive comment tag (e.g, "TODO").
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def speed(distance, time):
|
||||
/// return distance / time # XXX: Raises ZeroDivisionError for time = 0.
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct LineContainsXxx;
|
||||
impl Violation for LineContainsXxx {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Line contains XXX")
|
||||
format!("Line contains XXX, consider resolving the issue")
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for "HACK" comments.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// "HACK" comments are used to describe an issue that should be resolved
|
||||
/// (usually, a suboptimal solution or temporary workaround).
|
||||
///
|
||||
/// Consider resolving the issue before deploying the code.
|
||||
///
|
||||
/// Note that if you use "HACK" comments as a form of documentation, this
|
||||
/// rule may not be appropriate for your project.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
///
|
||||
/// def running_windows(): # HACK: Use platform module instead.
|
||||
/// try:
|
||||
/// os.mkdir("C:\\Windows\\System32\\")
|
||||
/// except FileExistsError:
|
||||
/// return True
|
||||
/// else:
|
||||
/// os.rmdir("C:\\Windows\\System32\\")
|
||||
/// return False
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct LineContainsHack;
|
||||
impl Violation for LineContainsHack {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Line contains HACK")
|
||||
format!("Line contains HACK, consider resolving the issue")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_fixme/mod.rs
|
||||
---
|
||||
T00.py:7:3: FIX001 Line contains FIXME
|
||||
T00.py:7:3: FIX001 Line contains FIXME, consider resolving the issue
|
||||
|
|
||||
5 | # HACK: hack
|
||||
6 | # hack: hack
|
||||
@@ -10,7 +10,7 @@ T00.py:7:3: FIX001 Line contains FIXME
|
||||
8 | # fixme: fixme
|
||||
|
|
||||
|
||||
T00.py:8:3: FIX001 Line contains FIXME
|
||||
T00.py:8:3: FIX001 Line contains FIXME, consider resolving the issue
|
||||
|
|
||||
6 | # hack: hack
|
||||
7 | # FIXME: fixme
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_fixme/mod.rs
|
||||
---
|
||||
T00.py:5:3: FIX004 Line contains HACK
|
||||
T00.py:5:3: FIX004 Line contains HACK, consider resolving the issue
|
||||
|
|
||||
3 | # XXX: xxx
|
||||
4 | # xxx: xxx
|
||||
@@ -11,7 +11,7 @@ T00.py:5:3: FIX004 Line contains HACK
|
||||
7 | # FIXME: fixme
|
||||
|
|
||||
|
||||
T00.py:6:3: FIX004 Line contains HACK
|
||||
T00.py:6:3: FIX004 Line contains HACK, consider resolving the issue
|
||||
|
|
||||
4 | # xxx: xxx
|
||||
5 | # HACK: hack
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_fixme/mod.rs
|
||||
---
|
||||
T00.py:1:3: FIX002 Line contains TODO
|
||||
T00.py:1:3: FIX002 Line contains TODO, consider resolving the issue
|
||||
|
|
||||
1 | # TODO: todo
|
||||
| ^^^^ FIX002
|
||||
@@ -9,7 +9,7 @@ T00.py:1:3: FIX002 Line contains TODO
|
||||
3 | # XXX: xxx
|
||||
|
|
||||
|
||||
T00.py:2:3: FIX002 Line contains TODO
|
||||
T00.py:2:3: FIX002 Line contains TODO, consider resolving the issue
|
||||
|
|
||||
1 | # TODO: todo
|
||||
2 | # todo: todo
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_fixme/mod.rs
|
||||
---
|
||||
T00.py:3:3: FIX003 Line contains XXX
|
||||
T00.py:3:3: FIX003 Line contains XXX, consider resolving the issue
|
||||
|
|
||||
1 | # TODO: todo
|
||||
2 | # todo: todo
|
||||
@@ -11,7 +11,7 @@ T00.py:3:3: FIX003 Line contains XXX
|
||||
5 | # HACK: hack
|
||||
|
|
||||
|
||||
T00.py:4:3: FIX003 Line contains XXX
|
||||
T00.py:4:3: FIX003 Line contains XXX, consider resolving the issue
|
||||
|
|
||||
2 | # todo: todo
|
||||
3 | # XXX: xxx
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] | None = []
|
||||
| ^^^^^^^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
|
||||
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
1 | def main() -> None:
|
||||
@@ -17,11 +9,12 @@ no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annot
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
|
||||
no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
6 | def hello(y: dict[str, int] | None) -> None:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ FA102
|
||||
7 | del y
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] | None = []
|
||||
| ^^^^^^^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
|
||||
no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
@@ -31,4 +24,11 @@ no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annot
|
||||
7 | del y
|
||||
|
|
||||
|
||||
no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
6 | def hello(y: dict[str, int] | None) -> None:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ FA102
|
||||
7 | del y
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Scope;
|
||||
use ruff_python_semantic::Binding;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
@@ -53,42 +53,38 @@ impl Violation for UnconventionalImportAlias {
|
||||
/// ICN001
|
||||
pub(crate) fn unconventional_import_alias(
|
||||
checker: &Checker,
|
||||
scope: &Scope,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
binding: &Binding,
|
||||
conventions: &FxHashMap<String, String>,
|
||||
) -> Option<Diagnostic> {
|
||||
for (name, binding_id) in scope.all_bindings() {
|
||||
let binding = checker.semantic().binding(binding_id);
|
||||
let Some(qualified_name) = binding.qualified_name() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Some(qualified_name) = binding.qualified_name() else {
|
||||
continue;
|
||||
};
|
||||
let Some(expected_alias) = conventions.get(qualified_name) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Some(expected_alias) = conventions.get(qualified_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if binding.is_alias() && name == expected_alias {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnconventionalImportAlias {
|
||||
name: qualified_name.to_string(),
|
||||
asname: expected_alias.to_string(),
|
||||
},
|
||||
binding.range,
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if checker.semantic().is_available(expected_alias) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (edit, rest) =
|
||||
Renamer::rename(name, expected_alias, scope, checker.semantic())?;
|
||||
Ok(Fix::suggested_edits(edit, rest))
|
||||
});
|
||||
}
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
let name = binding.name(checker.locator);
|
||||
if binding.is_alias() && name == expected_alias {
|
||||
return None;
|
||||
}
|
||||
None
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnconventionalImportAlias {
|
||||
name: qualified_name.to_string(),
|
||||
asname: expected_alias.to_string(),
|
||||
},
|
||||
binding.range,
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if checker.semantic().is_available(expected_alias) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let scope = &checker.semantic().scopes[binding.scope];
|
||||
let (edit, rest) =
|
||||
Renamer::rename(name, expected_alias, scope, checker.semantic())?;
|
||||
Ok(Fix::suggested_edits(edit, rest))
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(diagnostic)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ mod tests {
|
||||
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))]
|
||||
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))]
|
||||
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))]
|
||||
#[test_case(Rule::ComplexAssignmentInStub, Path::new("PYI017.py"))]
|
||||
#[test_case(Rule::ComplexAssignmentInStub, Path::new("PYI017.pyi"))]
|
||||
#[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.py"))]
|
||||
#[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.pyi"))]
|
||||
#[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))]
|
||||
@@ -87,6 +89,8 @@ mod tests {
|
||||
#[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.pyi"))]
|
||||
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.py"))]
|
||||
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.pyi"))]
|
||||
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.py"))]
|
||||
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.pyi"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
use rustpython_parser::ast::{Expr, StmtAssign};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for assignments with multiple or non-name targets in stub files.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// In general, stub files should be thought of as "data files" for a type
|
||||
/// checker, and are not intended to be executed. As such, it's useful to
|
||||
/// enforce that only a subset of Python syntax is allowed in a stub file, to
|
||||
/// ensure that everything in the stub is unambiguous for the type checker.
|
||||
///
|
||||
/// The need to perform multi-assignment, or assignment to a non-name target,
|
||||
/// likely indicates a misunderstanding of how stub files are intended to be
|
||||
/// used.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// a = b = int
|
||||
/// a.b = int
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// a: TypeAlias = int
|
||||
/// b: TypeAlias = int
|
||||
///
|
||||
///
|
||||
/// class a:
|
||||
/// b: int
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct ComplexAssignmentInStub;
|
||||
|
||||
impl Violation for ComplexAssignmentInStub {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Stubs should not contain assignments to attributes or multiple targets")
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI017
|
||||
pub(crate) fn complex_assignment_in_stub(checker: &mut Checker, stmt: &StmtAssign) {
|
||||
if matches!(stmt.targets.as_slice(), [Expr::Name(_)]) {
|
||||
return;
|
||||
}
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(ComplexAssignmentInStub, stmt.range));
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub(crate) use any_eq_ne_annotation::*;
|
||||
pub(crate) use bad_version_info_comparison::*;
|
||||
pub(crate) use collections_named_tuple::*;
|
||||
pub(crate) use complex_assignment_in_stub::*;
|
||||
pub(crate) use complex_if_statement_in_stub::*;
|
||||
pub(crate) use docstring_in_stubs::*;
|
||||
pub(crate) use duplicate_union_member::*;
|
||||
@@ -31,6 +32,7 @@ pub(crate) use unrecognized_version_info::*;
|
||||
mod any_eq_ne_annotation;
|
||||
mod bad_version_info_comparison;
|
||||
mod collections_named_tuple;
|
||||
mod complex_assignment_in_stub;
|
||||
mod complex_if_statement_in_stub;
|
||||
mod docstring_in_stubs;
|
||||
mod duplicate_union_member;
|
||||
|
||||
@@ -9,6 +9,7 @@ use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_semantic::{ScopeKind, SemanticModel};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
#[violation]
|
||||
@@ -97,6 +98,47 @@ impl Violation for UnassignedSpecialVariableInStub {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for type alias definitions that are not annotated with
|
||||
/// `typing.TypeAlias`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// In Python, a type alias is defined by assigning a type to a variable (e.g.,
|
||||
/// `Vector = list[float]`).
|
||||
///
|
||||
/// It's best to annotate type aliases with the `typing.TypeAlias` type to
|
||||
/// make it clear that the statement is a type alias declaration, as opposed
|
||||
/// to a normal variable assignment.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// Vector = list[float]
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from typing import TypeAlias
|
||||
///
|
||||
/// Vector: TypeAlias = list[float]
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct TypeAliasWithoutAnnotation {
|
||||
name: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl AlwaysAutofixableViolation for TypeAliasWithoutAnnotation {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let TypeAliasWithoutAnnotation { name, value } = self;
|
||||
format!("Use `typing.TypeAlias` for type alias, e.g., `{name}: typing.TypeAlias = {value}`")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
"Add `typing.TypeAlias` annotation".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_allowed_negated_math_attribute(call_path: &CallPath) -> bool {
|
||||
matches!(call_path.as_slice(), ["math", "inf" | "e" | "pi" | "tau"])
|
||||
}
|
||||
@@ -234,22 +276,39 @@ fn is_valid_default_value_with_annotation(
|
||||
|
||||
/// Returns `true` if an [`Expr`] appears to be a valid PEP 604 union. (e.g. `int | None`)
|
||||
fn is_valid_pep_604_union(annotation: &Expr) -> bool {
|
||||
match annotation {
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op: Operator::BitOr,
|
||||
right,
|
||||
range: _,
|
||||
}) => is_valid_pep_604_union(left) && is_valid_pep_604_union(right),
|
||||
Expr::Name(_)
|
||||
| Expr::Subscript(_)
|
||||
| Expr::Attribute(_)
|
||||
| Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::None,
|
||||
..
|
||||
}) => true,
|
||||
_ => false,
|
||||
/// Returns `true` if an [`Expr`] appears to be a valid PEP 604 union member.
|
||||
fn is_valid_pep_604_union_member(value: &Expr) -> bool {
|
||||
match value {
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op: Operator::BitOr,
|
||||
right,
|
||||
range: _,
|
||||
}) => is_valid_pep_604_union_member(left) && is_valid_pep_604_union_member(right),
|
||||
Expr::Name(_)
|
||||
| Expr::Subscript(_)
|
||||
| Expr::Attribute(_)
|
||||
| Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::None,
|
||||
..
|
||||
}) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// The top-level expression must be a bit-or operation.
|
||||
let Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op: Operator::BitOr,
|
||||
right,
|
||||
range: _,
|
||||
}) = annotation
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// The left and right operands must be valid union members.
|
||||
is_valid_pep_604_union_member(left) && is_valid_pep_604_union_member(right)
|
||||
}
|
||||
|
||||
/// Returns `true` if an [`Expr`] appears to be a valid default value without an annotation.
|
||||
@@ -323,6 +382,23 @@ fn is_enum(bases: &[Expr], semantic: &SemanticModel) -> bool {
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns `true` if an [`Expr`] is a value that should be annotated with `typing.TypeAlias`.
|
||||
///
|
||||
/// This is relatively conservative, as it's hard to reliably detect whether a right-hand side is a
|
||||
/// valid type alias. In particular, this function checks for uses of `typing.Any`, `None`,
|
||||
/// parameterized generics, and PEP 604-style unions.
|
||||
fn is_annotatable_type_alias(value: &Expr, semantic: &SemanticModel) -> bool {
|
||||
matches!(
|
||||
value,
|
||||
Expr::Subscript(_)
|
||||
| Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::None,
|
||||
..
|
||||
}),
|
||||
) || is_valid_pep_604_union(value)
|
||||
|| semantic.match_typing_expr(value, "Any")
|
||||
}
|
||||
|
||||
/// PYI011
|
||||
pub(crate) fn typed_argument_simple_defaults(checker: &mut Checker, arguments: &Arguments) {
|
||||
for ArgWithDefault {
|
||||
@@ -523,3 +599,40 @@ pub(crate) fn unassigned_special_variable_in_stub(
|
||||
stmt.range(),
|
||||
));
|
||||
}
|
||||
|
||||
/// PIY026
|
||||
pub(crate) fn type_alias_without_annotation(checker: &mut Checker, value: &Expr, targets: &[Expr]) {
|
||||
let [target] = targets else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Expr::Name(ast::ExprName { id, .. }) = target else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !is_annotatable_type_alias(value, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
TypeAliasWithoutAnnotation {
|
||||
name: id.to_string(),
|
||||
value: checker.generator().expr(value),
|
||||
},
|
||||
target.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer.get_or_import_symbol(
|
||||
&ImportRequest::import("typing", "TypeAlias"),
|
||||
target.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
Ok(Fix::suggested_edits(
|
||||
Edit::range_replacement(format!("{id}: {binding}"), target.range()),
|
||||
[import_edit],
|
||||
))
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::{BindingKind, FromImport, Scope};
|
||||
use ruff_python_semantic::{Binding, BindingKind, FromImport};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
@@ -48,31 +48,29 @@ impl Violation for UnaliasedCollectionsAbcSetImport {
|
||||
/// PYI025
|
||||
pub(crate) fn unaliased_collections_abc_set_import(
|
||||
checker: &Checker,
|
||||
scope: &Scope,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
for (name, binding_id) in scope.all_bindings() {
|
||||
let binding = checker.semantic().binding(binding_id);
|
||||
let BindingKind::FromImport(FromImport { qualified_name }) = &binding.kind else {
|
||||
continue;
|
||||
};
|
||||
if qualified_name.as_str() != "collections.abc.Set" {
|
||||
continue;
|
||||
}
|
||||
if name == "AbstractSet" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UnaliasedCollectionsAbcSetImport, binding.range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if checker.semantic().is_available("AbstractSet") {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (edit, rest) =
|
||||
Renamer::rename(name, "AbstractSet", scope, checker.semantic())?;
|
||||
Ok(Fix::suggested_edits(edit, rest))
|
||||
});
|
||||
}
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
binding: &Binding,
|
||||
) -> Option<Diagnostic> {
|
||||
let BindingKind::FromImport(FromImport { qualified_name }) = &binding.kind else {
|
||||
return None;
|
||||
};
|
||||
if qualified_name.as_str() != "collections.abc.Set" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = binding.name(checker.locator);
|
||||
if name == "AbstractSet" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UnaliasedCollectionsAbcSetImport, binding.range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if checker.semantic().is_available("AbstractSet") {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let scope = &checker.semantic().scopes[binding.scope];
|
||||
let (edit, rest) = Renamer::rename(name, "AbstractSet", scope, checker.semantic())?;
|
||||
Ok(Fix::suggested_edits(edit, rest))
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(diagnostic)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI017.pyi:4:1: PYI017 Stubs should not contain assignments to attributes or multiple targets
|
||||
|
|
||||
2 | a = var # OK
|
||||
3 |
|
||||
4 | b = c = int # PYI017
|
||||
| ^^^^^^^^^^^ PYI017
|
||||
5 |
|
||||
6 | a.b = int # PYI017
|
||||
|
|
||||
|
||||
PYI017.pyi:6:1: PYI017 Stubs should not contain assignments to attributes or multiple targets
|
||||
|
|
||||
4 | b = c = int # PYI017
|
||||
5 |
|
||||
6 | a.b = int # PYI017
|
||||
| ^^^^^^^^^ PYI017
|
||||
7 |
|
||||
8 | d, e = int, str # PYI017
|
||||
|
|
||||
|
||||
PYI017.pyi:8:1: PYI017 Stubs should not contain assignments to attributes or multiple targets
|
||||
|
|
||||
6 | a.b = int # PYI017
|
||||
7 |
|
||||
8 | d, e = int, str # PYI017
|
||||
| ^^^^^^^^^^^^^^^ PYI017
|
||||
9 |
|
||||
10 | f, g, h = int, str, TypeVar("T") # PYI017
|
||||
|
|
||||
|
||||
PYI017.pyi:10:1: PYI017 Stubs should not contain assignments to attributes or multiple targets
|
||||
|
|
||||
8 | d, e = int, str # PYI017
|
||||
9 |
|
||||
10 | f, g, h = int, str, TypeVar("T") # PYI017
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI017
|
||||
11 |
|
||||
12 | i: TypeAlias = int | str # OK
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI026.pyi:3:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `NewAny: typing.TypeAlias = Any`
|
||||
|
|
||||
1 | from typing import Literal, Any
|
||||
2 |
|
||||
3 | NewAny = Any
|
||||
| ^^^^^^ PYI026
|
||||
4 | OptionalStr = typing.Optional[str]
|
||||
5 | Foo = Literal["foo"]
|
||||
|
|
||||
= help: Add `typing.TypeAlias` annotation
|
||||
|
||||
ℹ Suggested fix
|
||||
1 |-from typing import Literal, Any
|
||||
1 |+from typing import Literal, Any, TypeAlias
|
||||
2 2 |
|
||||
3 |-NewAny = Any
|
||||
3 |+NewAny: TypeAlias = Any
|
||||
4 4 | OptionalStr = typing.Optional[str]
|
||||
5 5 | Foo = Literal["foo"]
|
||||
6 6 | IntOrStr = int | str
|
||||
|
||||
PYI026.pyi:4:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `OptionalStr: typing.TypeAlias = typing.Optional[str]`
|
||||
|
|
||||
3 | NewAny = Any
|
||||
4 | OptionalStr = typing.Optional[str]
|
||||
| ^^^^^^^^^^^ PYI026
|
||||
5 | Foo = Literal["foo"]
|
||||
6 | IntOrStr = int | str
|
||||
|
|
||||
= help: Add `typing.TypeAlias` annotation
|
||||
|
||||
ℹ Suggested fix
|
||||
1 |-from typing import Literal, Any
|
||||
1 |+from typing import Literal, Any, TypeAlias
|
||||
2 2 |
|
||||
3 3 | NewAny = Any
|
||||
4 |-OptionalStr = typing.Optional[str]
|
||||
4 |+OptionalStr: TypeAlias = typing.Optional[str]
|
||||
5 5 | Foo = Literal["foo"]
|
||||
6 6 | IntOrStr = int | str
|
||||
7 7 | AliasNone = None
|
||||
|
||||
PYI026.pyi:5:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `Foo: typing.TypeAlias = Literal["foo"]`
|
||||
|
|
||||
3 | NewAny = Any
|
||||
4 | OptionalStr = typing.Optional[str]
|
||||
5 | Foo = Literal["foo"]
|
||||
| ^^^ PYI026
|
||||
6 | IntOrStr = int | str
|
||||
7 | AliasNone = None
|
||||
|
|
||||
= help: Add `typing.TypeAlias` annotation
|
||||
|
||||
ℹ Suggested fix
|
||||
1 |-from typing import Literal, Any
|
||||
1 |+from typing import Literal, Any, TypeAlias
|
||||
2 2 |
|
||||
3 3 | NewAny = Any
|
||||
4 4 | OptionalStr = typing.Optional[str]
|
||||
5 |-Foo = Literal["foo"]
|
||||
5 |+Foo: TypeAlias = Literal["foo"]
|
||||
6 6 | IntOrStr = int | str
|
||||
7 7 | AliasNone = None
|
||||
8 8 |
|
||||
|
||||
PYI026.pyi:6:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `IntOrStr: typing.TypeAlias = int | str`
|
||||
|
|
||||
4 | OptionalStr = typing.Optional[str]
|
||||
5 | Foo = Literal["foo"]
|
||||
6 | IntOrStr = int | str
|
||||
| ^^^^^^^^ PYI026
|
||||
7 | AliasNone = None
|
||||
|
|
||||
= help: Add `typing.TypeAlias` annotation
|
||||
|
||||
ℹ Suggested fix
|
||||
1 |-from typing import Literal, Any
|
||||
1 |+from typing import Literal, Any, TypeAlias
|
||||
2 2 |
|
||||
3 3 | NewAny = Any
|
||||
4 4 | OptionalStr = typing.Optional[str]
|
||||
5 5 | Foo = Literal["foo"]
|
||||
6 |-IntOrStr = int | str
|
||||
6 |+IntOrStr: TypeAlias = int | str
|
||||
7 7 | AliasNone = None
|
||||
8 8 |
|
||||
9 9 | NewAny: typing.TypeAlias = Any
|
||||
|
||||
PYI026.pyi:7:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `AliasNone: typing.TypeAlias = None`
|
||||
|
|
||||
5 | Foo = Literal["foo"]
|
||||
6 | IntOrStr = int | str
|
||||
7 | AliasNone = None
|
||||
| ^^^^^^^^^ PYI026
|
||||
8 |
|
||||
9 | NewAny: typing.TypeAlias = Any
|
||||
|
|
||||
= help: Add `typing.TypeAlias` annotation
|
||||
|
||||
ℹ Suggested fix
|
||||
1 |-from typing import Literal, Any
|
||||
1 |+from typing import Literal, Any, TypeAlias
|
||||
2 2 |
|
||||
3 3 | NewAny = Any
|
||||
4 4 | OptionalStr = typing.Optional[str]
|
||||
5 5 | Foo = Literal["foo"]
|
||||
6 6 | IntOrStr = int | str
|
||||
7 |-AliasNone = None
|
||||
7 |+AliasNone: TypeAlias = None
|
||||
8 8 |
|
||||
9 9 | NewAny: typing.TypeAlias = Any
|
||||
10 10 | OptionalStr: TypeAlias = typing.Optional[str]
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Constant, Decorator, Expr, Keyword};
|
||||
use ruff_python_ast::call_path::{collect_call_path, CallPath};
|
||||
use ruff_python_ast::helpers::map_callable;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_whitespace::PythonWhitespace;
|
||||
use ruff_python_trivia::PythonWhitespace;
|
||||
|
||||
pub(super) fn get_mark_decorators(
|
||||
decorators: &[Decorator],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_python_ast::helpers::find_keyword;
|
||||
use rustpython_parser::ast::{self, Expr, Keyword, Ranged, Stmt, WithItem};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
@@ -74,13 +75,7 @@ pub(crate) fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], key
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::PytestRaisesTooBroad) {
|
||||
let match_keyword = keywords.iter().find(|keyword| {
|
||||
keyword
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |arg| arg.as_str() == "match")
|
||||
});
|
||||
|
||||
let match_keyword = find_keyword(keywords, "match");
|
||||
if let Some(exception) = args.first() {
|
||||
if let Some(match_keyword) = match_keyword {
|
||||
if is_empty_or_null_string(&match_keyword.value) {
|
||||
|
||||
@@ -3,7 +3,7 @@ use rustpython_parser::ast;
|
||||
use rustpython_parser::ast::{Expr, Ranged, Stmt};
|
||||
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_whitespace::UniversalNewlines;
|
||||
use ruff_python_trivia::UniversalNewlines;
|
||||
|
||||
/// Return `true` if a function's return statement include at least one
|
||||
/// non-`None` value.
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::ops::Add;
|
||||
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustpython_parser::ast::{self, Expr, Ranged, Stmt};
|
||||
use rustpython_parser::ast::{self, ElifElseClause, Expr, Ranged, Stmt};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_const_none;
|
||||
use ruff_python_ast::helpers::{elif_else_range, is_const_false, is_const_true};
|
||||
use ruff_python_ast::helpers::{is_const_false, is_const_true};
|
||||
use ruff_python_ast::stmt_if::elif_else_range;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::whitespace::indentation;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
@@ -387,13 +388,25 @@ fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
/// RET503
|
||||
fn implicit_return(checker: &mut Checker, stmt: &Stmt) {
|
||||
match stmt {
|
||||
Stmt::If(ast::StmtIf { body, orelse, .. }) => {
|
||||
Stmt::If(ast::StmtIf {
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
}) => {
|
||||
if let Some(last_stmt) = body.last() {
|
||||
implicit_return(checker, last_stmt);
|
||||
}
|
||||
if let Some(last_stmt) = orelse.last() {
|
||||
implicit_return(checker, last_stmt);
|
||||
} else {
|
||||
for clause in elif_else_clauses {
|
||||
if let Some(last_stmt) = clause.body.last() {
|
||||
implicit_return(checker, last_stmt);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we don't have an else clause
|
||||
if matches!(
|
||||
elif_else_clauses.last(),
|
||||
None | Some(ast::ElifElseClause { test: Some(_), .. })
|
||||
) {
|
||||
let mut diagnostic = Diagnostic::new(ImplicitReturn, stmt.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(indent) = indentation(checker.locator, stmt) {
|
||||
@@ -564,13 +577,21 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) {
|
||||
}
|
||||
|
||||
/// RET505, RET506, RET507, RET508
|
||||
fn superfluous_else_node(checker: &mut Checker, stmt: &ast::StmtIf, branch: Branch) -> bool {
|
||||
let ast::StmtIf { body, .. } = stmt;
|
||||
for child in body {
|
||||
fn superfluous_else_node(
|
||||
checker: &mut Checker,
|
||||
if_elif_body: &[Stmt],
|
||||
elif_else: &ElifElseClause,
|
||||
) -> bool {
|
||||
let branch = if elif_else.test.is_some() {
|
||||
Branch::Elif
|
||||
} else {
|
||||
Branch::Else
|
||||
};
|
||||
for child in if_elif_body {
|
||||
if child.is_return_stmt() {
|
||||
let diagnostic = Diagnostic::new(
|
||||
SuperfluousElseReturn { branch },
|
||||
elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()),
|
||||
elif_else_range(elif_else, checker.locator).unwrap_or_else(|| elif_else.range()),
|
||||
);
|
||||
if checker.enabled(diagnostic.kind.rule()) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
@@ -579,7 +600,7 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &ast::StmtIf, branch: Bran
|
||||
} else if child.is_break_stmt() {
|
||||
let diagnostic = Diagnostic::new(
|
||||
SuperfluousElseBreak { branch },
|
||||
elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()),
|
||||
elif_else_range(elif_else, checker.locator).unwrap_or_else(|| elif_else.range()),
|
||||
);
|
||||
if checker.enabled(diagnostic.kind.rule()) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
@@ -588,7 +609,7 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &ast::StmtIf, branch: Bran
|
||||
} else if child.is_raise_stmt() {
|
||||
let diagnostic = Diagnostic::new(
|
||||
SuperfluousElseRaise { branch },
|
||||
elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()),
|
||||
elif_else_range(elif_else, checker.locator).unwrap_or_else(|| elif_else.range()),
|
||||
);
|
||||
if checker.enabled(diagnostic.kind.rule()) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
@@ -597,7 +618,7 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &ast::StmtIf, branch: Bran
|
||||
} else if child.is_continue_stmt() {
|
||||
let diagnostic = Diagnostic::new(
|
||||
SuperfluousElseContinue { branch },
|
||||
elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()),
|
||||
elif_else_range(elif_else, checker.locator).unwrap_or_else(|| elif_else.range()),
|
||||
);
|
||||
if checker.enabled(diagnostic.kind.rule()) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
@@ -609,16 +630,9 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &ast::StmtIf, branch: Bran
|
||||
}
|
||||
|
||||
/// RET505, RET506, RET507, RET508
|
||||
fn superfluous_elif(checker: &mut Checker, stack: &Stack) {
|
||||
for stmt in &stack.elifs {
|
||||
superfluous_else_node(checker, stmt, Branch::Elif);
|
||||
}
|
||||
}
|
||||
|
||||
/// RET505, RET506, RET507, RET508
|
||||
fn superfluous_else(checker: &mut Checker, stack: &Stack) {
|
||||
for stmt in &stack.elses {
|
||||
superfluous_else_node(checker, stmt, Branch::Else);
|
||||
fn superfluous_elif_else(checker: &mut Checker, stack: &Stack) {
|
||||
for (if_elif_body, elif_else) in &stack.elifs_elses {
|
||||
superfluous_else_node(checker, if_elif_body, elif_else);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,8 +669,7 @@ pub(crate) fn function(checker: &mut Checker, body: &[Stmt], returns: Option<&Ex
|
||||
Rule::SuperfluousElseContinue,
|
||||
Rule::SuperfluousElseBreak,
|
||||
]) {
|
||||
superfluous_elif(checker, &stack);
|
||||
superfluous_else(checker, &stack);
|
||||
superfluous_elif_else(checker, &stack);
|
||||
}
|
||||
|
||||
// Skip any functions without return statements.
|
||||
|
||||
@@ -70,4 +70,13 @@ RET508.py:82:9: RET508 Unnecessary `else` after `break` statement
|
||||
84 | return
|
||||
|
|
||||
|
||||
RET508.py:158:13: RET508 Unnecessary `else` after `break` statement
|
||||
|
|
||||
156 | if i > w:
|
||||
157 | break
|
||||
158 | else:
|
||||
| ^^^^ RET508
|
||||
159 | a = z
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
use rustpython_parser::ast::{self, Expr, Identifier, Stmt};
|
||||
use rustpython_parser::ast::{self, ElifElseClause, Expr, Identifier, Stmt};
|
||||
|
||||
use ruff_python_ast::visitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
@@ -8,10 +8,8 @@ use ruff_python_ast::visitor::Visitor;
|
||||
pub(super) struct Stack<'a> {
|
||||
/// The `return` statements in the current function.
|
||||
pub(super) returns: Vec<&'a ast::StmtReturn>,
|
||||
/// The `else` statements in the current function.
|
||||
pub(super) elses: Vec<&'a ast::StmtIf>,
|
||||
/// The `elif` statements in the current function.
|
||||
pub(super) elifs: Vec<&'a ast::StmtIf>,
|
||||
/// The `elif` or `else` statements in the current function.
|
||||
pub(super) elifs_elses: Vec<(&'a [Stmt], &'a ElifElseClause)>,
|
||||
/// The non-local variables in the current function.
|
||||
pub(super) non_locals: FxHashSet<&'a str>,
|
||||
/// Whether the current function is a generator.
|
||||
@@ -117,27 +115,13 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
|
||||
|
||||
self.stack.returns.push(stmt_return);
|
||||
}
|
||||
Stmt::If(stmt_if) => {
|
||||
let is_elif_arm = self.parents.iter().any(|parent| {
|
||||
if let Stmt::If(ast::StmtIf { orelse, .. }) = parent {
|
||||
orelse.len() == 1 && &orelse[0] == stmt
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if !is_elif_arm {
|
||||
let has_elif =
|
||||
stmt_if.orelse.len() == 1 && stmt_if.orelse.first().unwrap().is_if_stmt();
|
||||
let has_else = !stmt_if.orelse.is_empty();
|
||||
|
||||
if has_elif {
|
||||
// `stmt` is an `if` block followed by an `elif` clause.
|
||||
self.stack.elifs.push(stmt_if);
|
||||
} else if has_else {
|
||||
// `stmt` is an `if` block followed by an `else` clause.
|
||||
self.stack.elses.push(stmt_if);
|
||||
}
|
||||
Stmt::If(ast::StmtIf {
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
}) => {
|
||||
if let Some(first) = elif_else_clauses.first() {
|
||||
self.stack.elifs_elses.push((body, first));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
use log::error;
|
||||
use ruff_text_size::TextRange;
|
||||
use rustc_hash::FxHashSet;
|
||||
use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Identifier, Ranged, Stmt};
|
||||
use rustpython_parser::ast::{
|
||||
self, CmpOp, Constant, ElifElseClause, Expr, ExprContext, Identifier, Ranged, Stmt, StmtIf,
|
||||
};
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr, ComparableStmt};
|
||||
use ruff_python_ast::helpers::{any_over_expr, contains_effect, first_colon_range, has_comments};
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_ast::stmt_if::if_elif_branches;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_whitespace::UniversalNewlines;
|
||||
use ruff_python_trivia::UniversalNewlines;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::line_width::LineWidth;
|
||||
@@ -23,16 +27,6 @@ fn compare_stmt(stmt1: &ComparableStmt, stmt2: &ComparableStmt) -> bool {
|
||||
stmt1.eq(stmt2)
|
||||
}
|
||||
|
||||
fn compare_body(body1: &[Stmt], body2: &[Stmt]) -> bool {
|
||||
if body1.len() != body2.len() {
|
||||
return false;
|
||||
}
|
||||
body1
|
||||
.iter()
|
||||
.zip(body2.iter())
|
||||
.all(|(stmt1, stmt2)| compare_stmt(&stmt1.into(), &stmt2.into()))
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for nested `if` statements that can be collapsed into a single `if`
|
||||
/// statement.
|
||||
@@ -287,7 +281,7 @@ fn is_main_check(expr: &Expr) -> bool {
|
||||
}
|
||||
|
||||
/// Find the last nested if statement and return the test expression and the
|
||||
/// first statement.
|
||||
/// last statement.
|
||||
///
|
||||
/// ```python
|
||||
/// if xxx:
|
||||
@@ -301,13 +295,13 @@ fn find_last_nested_if(body: &[Stmt]) -> Option<(&Expr, &Stmt)> {
|
||||
let [Stmt::If(ast::StmtIf {
|
||||
test,
|
||||
body: inner_body,
|
||||
orelse,
|
||||
elif_else_clauses,
|
||||
..
|
||||
})] = body
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if !orelse.is_empty() {
|
||||
if !elif_else_clauses.is_empty() {
|
||||
return None;
|
||||
}
|
||||
find_last_nested_if(inner_body).or_else(|| {
|
||||
@@ -318,30 +312,36 @@ fn find_last_nested_if(body: &[Stmt]) -> Option<(&Expr, &Stmt)> {
|
||||
})
|
||||
}
|
||||
|
||||
/// SIM102
|
||||
pub(crate) fn nested_if_statements(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
test: &Expr,
|
||||
body: &[Stmt],
|
||||
orelse: &[Stmt],
|
||||
parent: Option<&Stmt>,
|
||||
) {
|
||||
// If the parent could contain a nested if-statement, abort.
|
||||
if let Some(Stmt::If(ast::StmtIf { body, orelse, .. })) = parent {
|
||||
if orelse.is_empty() && body.len() == 1 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
fn nested_if_body(stmt_if: &StmtIf) -> Option<(&[Stmt], TextRange)> {
|
||||
let StmtIf {
|
||||
test,
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
} = stmt_if;
|
||||
|
||||
// If this if-statement has an else clause, or more than one child, abort.
|
||||
if !(orelse.is_empty() && body.len() == 1) {
|
||||
return;
|
||||
// It must be the last condition, otherwise there could be another `elif` or `else` that only
|
||||
// depends on the outer of the two conditions
|
||||
let (test, body, range) = if let Some(clause) = elif_else_clauses.last() {
|
||||
if let Some(test) = &clause.test {
|
||||
(test, &clause.body, clause.range())
|
||||
} else {
|
||||
// The last condition is an `else` (different rule)
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
(test.as_ref(), body, stmt_if.range())
|
||||
};
|
||||
|
||||
// The nested if must be the only child, otherwise there is at least one more statement that
|
||||
// only depends on the outer condition
|
||||
if body.len() > 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Allow `if __name__ == "__main__":` statements.
|
||||
if is_main_check(test) {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Allow `if True:` and `if False:` statements.
|
||||
@@ -352,9 +352,18 @@ pub(crate) fn nested_if_statements(
|
||||
..
|
||||
})
|
||||
) {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((body, range))
|
||||
}
|
||||
|
||||
/// SIM102
|
||||
pub(crate) fn nested_if_statements(checker: &mut Checker, stmt_if: &StmtIf, parent: Option<&Stmt>) {
|
||||
let Some((body, range)) = nested_if_body(stmt_if) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Find the deepest nested if-statement, to inform the range.
|
||||
let Some((test, first_stmt)) = find_last_nested_if(body) else {
|
||||
return;
|
||||
@@ -365,12 +374,22 @@ pub(crate) fn nested_if_statements(
|
||||
checker.locator,
|
||||
);
|
||||
|
||||
// Check if the parent is already emitting a larger diagnostic including this if statement
|
||||
if let Some(Stmt::If(stmt_if)) = parent {
|
||||
if let Some((body, _range)) = nested_if_body(stmt_if) {
|
||||
// In addition to repeating the `nested_if_body` and `find_last_nested_if` check, we
|
||||
// also need to be the first child in the parent
|
||||
if matches!(&body[0], Stmt::If(inner) if inner == stmt_if)
|
||||
&& find_last_nested_if(body).is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
CollapsibleIf,
|
||||
colon.map_or_else(
|
||||
|| stmt.range(),
|
||||
|colon| TextRange::new(stmt.start(), colon.end()),
|
||||
),
|
||||
colon.map_or(range, |colon| TextRange::new(range.start(), colon.end())),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
// The fixer preserves comments in the nested body, but removes comments between
|
||||
@@ -379,9 +398,9 @@ pub(crate) fn nested_if_statements(
|
||||
if !checker
|
||||
.indexer
|
||||
.comment_ranges()
|
||||
.intersects(TextRange::new(stmt.start(), nested_if.start()))
|
||||
.intersects(TextRange::new(range.start(), nested_if.start()))
|
||||
{
|
||||
match fix_if::fix_nested_if_statements(checker.locator, checker.stylist, stmt) {
|
||||
match fix_if::fix_nested_if_statements(checker.locator, checker.stylist, range) {
|
||||
Ok(edit) => {
|
||||
if edit
|
||||
.content()
|
||||
@@ -437,17 +456,43 @@ fn is_one_line_return_bool(stmts: &[Stmt]) -> Option<Bool> {
|
||||
/// SIM103
|
||||
pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
|
||||
let Stmt::If(ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
test: if_test,
|
||||
body: if_body,
|
||||
elif_else_clauses,
|
||||
range: _,
|
||||
}) = stmt
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Extract an `if` or `elif` (that returns) followed by an else (that returns the same value)
|
||||
let (if_test, if_body, else_body, range) = match elif_else_clauses.as_slice() {
|
||||
// if-else case
|
||||
[ElifElseClause {
|
||||
body: else_body,
|
||||
test: None,
|
||||
..
|
||||
}] => (if_test.as_ref(), if_body, else_body, stmt.range()),
|
||||
// elif-else case
|
||||
[.., ElifElseClause {
|
||||
body: elif_body,
|
||||
test: Some(elif_test),
|
||||
range: elif_range,
|
||||
}, ElifElseClause {
|
||||
body: else_body,
|
||||
test: None,
|
||||
range: else_range,
|
||||
}] => (
|
||||
elif_test,
|
||||
elif_body,
|
||||
else_body,
|
||||
TextRange::new(elif_range.start(), else_range.end()),
|
||||
),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let (Some(if_return), Some(else_return)) = (
|
||||
is_one_line_return_bool(body),
|
||||
is_one_line_return_bool(orelse),
|
||||
is_one_line_return_bool(if_body),
|
||||
is_one_line_return_bool(else_body),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
@@ -458,23 +503,23 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
|
||||
return;
|
||||
}
|
||||
|
||||
let condition = checker.generator().expr(test);
|
||||
let mut diagnostic = Diagnostic::new(NeedlessBool { condition }, stmt.range());
|
||||
let condition = checker.generator().expr(if_test);
|
||||
let mut diagnostic = Diagnostic::new(NeedlessBool { condition }, range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if matches!(if_return, Bool::True)
|
||||
&& matches!(else_return, Bool::False)
|
||||
&& !has_comments(stmt, checker.locator, checker.indexer)
|
||||
&& (test.is_compare_expr() || checker.semantic().is_builtin("bool"))
|
||||
&& !has_comments(&range, checker.locator, checker.indexer)
|
||||
&& (if_test.is_compare_expr() || checker.semantic().is_builtin("bool"))
|
||||
{
|
||||
if test.is_compare_expr() {
|
||||
if if_test.is_compare_expr() {
|
||||
// If the condition is a comparison, we can replace it with the condition.
|
||||
let node = ast::StmtReturn {
|
||||
value: Some(test.clone()),
|
||||
value: Some(Box::new(if_test.clone())),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
|
||||
checker.generator().stmt(&node.into()),
|
||||
stmt.range(),
|
||||
range,
|
||||
)));
|
||||
} else {
|
||||
// Otherwise, we need to wrap the condition in a call to `bool`. (We've already
|
||||
@@ -486,7 +531,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
|
||||
};
|
||||
let node1 = ast::ExprCall {
|
||||
func: Box::new(node.into()),
|
||||
args: vec![(**test).clone()],
|
||||
args: vec![if_test.clone()],
|
||||
keywords: vec![],
|
||||
range: TextRange::default(),
|
||||
};
|
||||
@@ -496,7 +541,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
|
||||
};
|
||||
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
|
||||
checker.generator().stmt(&node2.into()),
|
||||
stmt.range(),
|
||||
range,
|
||||
)));
|
||||
};
|
||||
}
|
||||
@@ -520,99 +565,71 @@ fn ternary(target_var: &Expr, body_value: &Expr, test: &Expr, orelse_value: &Exp
|
||||
node1.into()
|
||||
}
|
||||
|
||||
/// Return `true` if the `Expr` contains a reference to `${module}.${target}`.
|
||||
fn contains_call_path(expr: &Expr, target: &[&str], semantic: &SemanticModel) -> bool {
|
||||
/// Return `true` if the `Expr` contains a reference to any of the given `${module}.${target}`.
|
||||
fn contains_call_path(expr: &Expr, targets: &[&[&str]], semantic: &SemanticModel) -> bool {
|
||||
any_over_expr(expr, &|expr| {
|
||||
semantic
|
||||
.resolve_call_path(expr)
|
||||
.map_or(false, |call_path| call_path.as_slice() == target)
|
||||
semantic.resolve_call_path(expr).map_or(false, |call_path| {
|
||||
targets.iter().any(|target| &call_path.as_slice() == target)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// SIM108
|
||||
pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) {
|
||||
pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
|
||||
let Stmt::If(ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
elif_else_clauses,
|
||||
range: _,
|
||||
}) = stmt
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if body.len() != 1 || orelse.len() != 1 {
|
||||
// `test: None` to only match an `else` clause
|
||||
let [ElifElseClause {
|
||||
body: else_body,
|
||||
test: None,
|
||||
..
|
||||
}] = elif_else_clauses.as_slice()
|
||||
else {
|
||||
return;
|
||||
}
|
||||
let Stmt::Assign(ast::StmtAssign {
|
||||
};
|
||||
let [Stmt::Assign(ast::StmtAssign {
|
||||
targets: body_targets,
|
||||
value: body_value,
|
||||
..
|
||||
}) = &body[0]
|
||||
})] = body.as_slice()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Stmt::Assign(ast::StmtAssign {
|
||||
targets: orelse_targets,
|
||||
value: orelse_value,
|
||||
let [Stmt::Assign(ast::StmtAssign {
|
||||
targets: else_targets,
|
||||
value: else_value,
|
||||
..
|
||||
}) = &orelse[0]
|
||||
})] = else_body.as_slice()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if body_targets.len() != 1 || orelse_targets.len() != 1 {
|
||||
return;
|
||||
}
|
||||
let Expr::Name(ast::ExprName { id: body_id, .. }) = &body_targets[0] else {
|
||||
let ([body_target], [else_target]) = (body_targets.as_slice(), else_targets.as_slice()) else {
|
||||
return;
|
||||
};
|
||||
let Expr::Name(ast::ExprName { id: orelse_id, .. }) = &orelse_targets[0] else {
|
||||
let Expr::Name(ast::ExprName { id: body_id, .. }) = body_target else {
|
||||
return;
|
||||
};
|
||||
if body_id != orelse_id {
|
||||
let Expr::Name(ast::ExprName { id: else_id, .. }) = else_target else {
|
||||
return;
|
||||
};
|
||||
if body_id != else_id {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
|
||||
if contains_call_path(test, &["sys", "version_info"], checker.semantic()) {
|
||||
// Avoid suggesting ternary for `if sys.version_info >= ...`-style and
|
||||
// `if sys.platform.startswith("...")`-style checks.
|
||||
let ignored_call_paths: &[&[&str]] = &[&["sys", "version_info"], &["sys", "platform"]];
|
||||
if contains_call_path(test, ignored_call_paths, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if sys.platform.startswith("...")`-style
|
||||
// checks.
|
||||
if contains_call_path(test, &["sys", "platform"], checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's part of a bigger if-elif block:
|
||||
// https://github.com/MartinThoma/flake8-simplify/issues/115
|
||||
if let Some(Stmt::If(ast::StmtIf {
|
||||
orelse: parent_orelse,
|
||||
..
|
||||
})) = parent
|
||||
{
|
||||
if parent_orelse.len() == 1 && stmt == &parent_orelse[0] {
|
||||
// TODO(charlie): These two cases have the same AST:
|
||||
//
|
||||
// if True:
|
||||
// pass
|
||||
// elif a:
|
||||
// b = 1
|
||||
// else:
|
||||
// b = 2
|
||||
//
|
||||
// if True:
|
||||
// pass
|
||||
// else:
|
||||
// if a:
|
||||
// b = 1
|
||||
// else:
|
||||
// b = 2
|
||||
//
|
||||
// We want to flag the latter, but not the former. Right now, we flag neither.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid suggesting ternary for `if (yield ...)`-style checks.
|
||||
// TODO(charlie): Fix precedence handling for yields in generator.
|
||||
if matches!(
|
||||
@@ -622,14 +639,14 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O
|
||||
return;
|
||||
}
|
||||
if matches!(
|
||||
orelse_value.as_ref(),
|
||||
else_value.as_ref(),
|
||||
Expr::Yield(_) | Expr::YieldFrom(_) | Expr::Await(_)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target_var = &body_targets[0];
|
||||
let ternary = ternary(target_var, body_value, test, orelse_value);
|
||||
let target_var = &body_target;
|
||||
let ternary = ternary(target_var, body_value, test, else_value);
|
||||
let contents = checker.generator().stmt(&ternary);
|
||||
|
||||
// Don't flag if the resulting expression would exceed the maximum line length.
|
||||
@@ -659,135 +676,85 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
fn get_if_body_pairs<'a>(
|
||||
test: &'a Expr,
|
||||
body: &'a [Stmt],
|
||||
orelse: &'a [Stmt],
|
||||
) -> Vec<(&'a Expr, &'a [Stmt])> {
|
||||
let mut pairs = vec![(test, body)];
|
||||
let mut orelse = orelse;
|
||||
loop {
|
||||
if orelse.len() != 1 {
|
||||
break;
|
||||
}
|
||||
let Stmt::If(ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
orelse: orelse_orelse,
|
||||
range: _,
|
||||
}) = &orelse[0]
|
||||
else {
|
||||
break;
|
||||
};
|
||||
pairs.push((test, body));
|
||||
orelse = orelse_orelse;
|
||||
}
|
||||
pairs
|
||||
}
|
||||
|
||||
/// SIM114
|
||||
pub(crate) fn if_with_same_arms(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) {
|
||||
let Stmt::If(ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
range: _,
|
||||
}) = stmt
|
||||
else {
|
||||
return;
|
||||
};
|
||||
pub(crate) fn if_with_same_arms(checker: &mut Checker, locator: &Locator, stmt_if: &StmtIf) {
|
||||
let mut branches_iter = if_elif_branches(stmt_if).peekable();
|
||||
while let Some(current_branch) = branches_iter.next() {
|
||||
let Some(following_branch) = branches_iter.peek() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// It's part of a bigger if-elif block:
|
||||
// https://github.com/MartinThoma/flake8-simplify/issues/115
|
||||
if let Some(Stmt::If(ast::StmtIf {
|
||||
orelse: parent_orelse,
|
||||
..
|
||||
})) = parent
|
||||
{
|
||||
if parent_orelse.len() == 1 && stmt == &parent_orelse[0] {
|
||||
// TODO(charlie): These two cases have the same AST:
|
||||
//
|
||||
// if True:
|
||||
// pass
|
||||
// elif a:
|
||||
// b = 1
|
||||
// else:
|
||||
// b = 2
|
||||
//
|
||||
// if True:
|
||||
// pass
|
||||
// else:
|
||||
// if a:
|
||||
// b = 1
|
||||
// else:
|
||||
// b = 2
|
||||
//
|
||||
// We want to flag the latter, but not the former. Right now, we flag neither.
|
||||
return;
|
||||
// The bodies must have the same code ...
|
||||
if current_branch.body.len() != following_branch.body.len() {
|
||||
continue;
|
||||
}
|
||||
if !current_branch
|
||||
.body
|
||||
.iter()
|
||||
.zip(following_branch.body.iter())
|
||||
.all(|(stmt1, stmt2)| compare_stmt(&stmt1.into(), &stmt2.into()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let if_body_pairs = get_if_body_pairs(test, body, orelse);
|
||||
for i in 0..(if_body_pairs.len() - 1) {
|
||||
let (test, body) = &if_body_pairs[i];
|
||||
let (.., next_body) = &if_body_pairs[i + 1];
|
||||
if compare_body(body, next_body) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
IfWithSameArms,
|
||||
TextRange::new(
|
||||
if i == 0 { stmt.start() } else { test.start() },
|
||||
next_body.last().unwrap().end(),
|
||||
),
|
||||
));
|
||||
// ...and the same comments
|
||||
let first_comments: Vec<_> = checker
|
||||
.indexer
|
||||
.comments_in_range(current_branch.range, locator)
|
||||
.collect();
|
||||
let second_comments: Vec<_> = checker
|
||||
.indexer
|
||||
.comments_in_range(following_branch.range, locator)
|
||||
.collect();
|
||||
if first_comments != second_comments {
|
||||
continue;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
IfWithSameArms,
|
||||
TextRange::new(
|
||||
current_branch.range.start(),
|
||||
following_branch.body.last().unwrap().end(),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// SIM116
|
||||
pub(crate) fn manual_dict_lookup(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
test: &Expr,
|
||||
body: &[Stmt],
|
||||
orelse: &[Stmt],
|
||||
parent: Option<&Stmt>,
|
||||
) {
|
||||
pub(crate) fn manual_dict_lookup(checker: &mut Checker, stmt_if: &StmtIf) {
|
||||
// Throughout this rule:
|
||||
// * Each if-statement's test must consist of a constant equality check with the same variable.
|
||||
// * Each if-statement's body must consist of a single `return`.
|
||||
// * Each if-statement's orelse must be either another if-statement or empty.
|
||||
// * The final if-statement's orelse must be empty, or a single `return`.
|
||||
// * Each if or elif statement's test must consist of a constant equality check with the same variable.
|
||||
// * Each if or elif statement's body must consist of a single `return`.
|
||||
// * The else clause must be empty, or a single `return`.
|
||||
let StmtIf {
|
||||
body,
|
||||
test,
|
||||
elif_else_clauses,
|
||||
..
|
||||
} = stmt_if;
|
||||
|
||||
let Expr::Compare(ast::ExprCompare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
range: _,
|
||||
}) = &test
|
||||
}) = test.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Expr::Name(ast::ExprName { id: target, .. }) = left.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if body.len() != 1 {
|
||||
if ops != &[CmpOp::Eq] {
|
||||
return;
|
||||
}
|
||||
if orelse.len() != 1 {
|
||||
return;
|
||||
}
|
||||
if !(ops.len() == 1 && ops[0] == CmpOp::Eq) {
|
||||
return;
|
||||
}
|
||||
if comparators.len() != 1 {
|
||||
return;
|
||||
}
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
let [Expr::Constant(ast::ExprConstant {
|
||||
value: constant, ..
|
||||
}) = &comparators[0]
|
||||
})] = comparators.as_slice()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Stmt::Return(ast::StmtReturn { value, range: _ }) = &body[0] else {
|
||||
let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else {
|
||||
return;
|
||||
};
|
||||
if value.as_ref().map_or(false, |value| {
|
||||
@@ -796,99 +763,60 @@ pub(crate) fn manual_dict_lookup(
|
||||
return;
|
||||
}
|
||||
|
||||
// It's part of a bigger if-elif block:
|
||||
// https://github.com/MartinThoma/flake8-simplify/issues/115
|
||||
if let Some(Stmt::If(ast::StmtIf {
|
||||
orelse: parent_orelse,
|
||||
..
|
||||
})) = parent
|
||||
{
|
||||
if parent_orelse.len() == 1 && stmt == &parent_orelse[0] {
|
||||
// TODO(charlie): These two cases have the same AST:
|
||||
//
|
||||
// if True:
|
||||
// pass
|
||||
// elif a:
|
||||
// b = 1
|
||||
// else:
|
||||
// b = 2
|
||||
//
|
||||
// if True:
|
||||
// pass
|
||||
// else:
|
||||
// if a:
|
||||
// b = 1
|
||||
// else:
|
||||
// b = 2
|
||||
//
|
||||
// We want to flag the latter, but not the former. Right now, we flag neither.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mut constants: FxHashSet<ComparableConstant> = FxHashSet::default();
|
||||
constants.insert(constant.into());
|
||||
|
||||
let mut child: Option<&Stmt> = orelse.get(0);
|
||||
while let Some(current) = child.take() {
|
||||
let Stmt::If(ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
range: _,
|
||||
}) = ¤t
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if body.len() != 1 {
|
||||
return;
|
||||
}
|
||||
if orelse.len() > 1 {
|
||||
return;
|
||||
}
|
||||
let Expr::Compare(ast::ExprCompare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
range: _,
|
||||
}) = test.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if !(id == target && matches!(ops.as_slice(), [CmpOp::Eq])) {
|
||||
return;
|
||||
}
|
||||
let [Expr::Constant(ast::ExprConstant {
|
||||
value: constant, ..
|
||||
})] = comparators.as_slice()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Stmt::Return(ast::StmtReturn { value, range: _ }) = &body[0] else {
|
||||
return;
|
||||
};
|
||||
if value.as_ref().map_or(false, |value| {
|
||||
contains_effect(value, |id| checker.semantic().is_builtin(id))
|
||||
}) {
|
||||
for clause in elif_else_clauses {
|
||||
let ElifElseClause { test, body, .. } = clause;
|
||||
let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else {
|
||||
return;
|
||||
};
|
||||
|
||||
constants.insert(constant.into());
|
||||
if let Some(orelse) = orelse.first() {
|
||||
match orelse {
|
||||
Stmt::If(_) => {
|
||||
child = Some(orelse);
|
||||
}
|
||||
Stmt::Return(_) => {
|
||||
child = None;
|
||||
}
|
||||
_ => return,
|
||||
match test.as_ref() {
|
||||
// `else`
|
||||
None => {
|
||||
// The else must also be a single effect-free return statement
|
||||
let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else {
|
||||
return;
|
||||
};
|
||||
if value.as_ref().map_or(false, |value| {
|
||||
contains_effect(value, |id| checker.semantic().is_builtin(id))
|
||||
}) {
|
||||
return;
|
||||
};
|
||||
}
|
||||
// `elif`
|
||||
Some(Expr::Compare(ast::ExprCompare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
range: _,
|
||||
})) => {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if id != target || ops != &[CmpOp::Eq] {
|
||||
return;
|
||||
}
|
||||
let [Expr::Constant(ast::ExprConstant {
|
||||
value: constant, ..
|
||||
})] = comparators.as_slice()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if value.as_ref().map_or(false, |value| {
|
||||
contains_effect(value, |id| checker.semantic().is_builtin(id))
|
||||
}) {
|
||||
return;
|
||||
};
|
||||
|
||||
constants.insert(constant.into());
|
||||
}
|
||||
// Different `elif`
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
child = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,27 +826,38 @@ pub(crate) fn manual_dict_lookup(
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
IfElseBlockInsteadOfDictLookup,
|
||||
stmt.range(),
|
||||
stmt_if.range(),
|
||||
));
|
||||
}
|
||||
|
||||
/// SIM401
|
||||
pub(crate) fn use_dict_get_with_default(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
test: &Expr,
|
||||
body: &[Stmt],
|
||||
orelse: &[Stmt],
|
||||
parent: Option<&Stmt>,
|
||||
) {
|
||||
if body.len() != 1 || orelse.len() != 1 {
|
||||
pub(crate) fn use_dict_get_with_default(checker: &mut Checker, stmt_if: &StmtIf) {
|
||||
let StmtIf {
|
||||
test,
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
} = stmt_if;
|
||||
|
||||
let [body_stmt] = body.as_slice() else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let [ElifElseClause {
|
||||
body: else_body,
|
||||
test: None,
|
||||
..
|
||||
}] = elif_else_clauses.as_slice()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let [else_body_stmt] = else_body.as_slice() else {
|
||||
return;
|
||||
};
|
||||
let Stmt::Assign(ast::StmtAssign {
|
||||
targets: body_var,
|
||||
value: body_value,
|
||||
..
|
||||
}) = &body[0]
|
||||
}) = &body_stmt
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -929,7 +868,7 @@ pub(crate) fn use_dict_get_with_default(
|
||||
targets: orelse_var,
|
||||
value: orelse_value,
|
||||
..
|
||||
}) = &orelse[0]
|
||||
}) = &else_body_stmt
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -941,7 +880,7 @@ pub(crate) fn use_dict_get_with_default(
|
||||
ops,
|
||||
comparators: test_dict,
|
||||
range: _,
|
||||
}) = &test
|
||||
}) = test.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -949,8 +888,18 @@ pub(crate) fn use_dict_get_with_default(
|
||||
return;
|
||||
}
|
||||
let (expected_var, expected_value, default_var, default_value) = match ops[..] {
|
||||
[CmpOp::In] => (&body_var[0], body_value, &orelse_var[0], orelse_value),
|
||||
[CmpOp::NotIn] => (&orelse_var[0], orelse_value, &body_var[0], body_value),
|
||||
[CmpOp::In] => (
|
||||
&body_var[0],
|
||||
body_value,
|
||||
&orelse_var[0],
|
||||
orelse_value.as_ref(),
|
||||
),
|
||||
[CmpOp::NotIn] => (
|
||||
&orelse_var[0],
|
||||
orelse_value,
|
||||
&body_var[0],
|
||||
body_value.as_ref(),
|
||||
),
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
@@ -979,37 +928,7 @@ pub(crate) fn use_dict_get_with_default(
|
||||
return;
|
||||
}
|
||||
|
||||
// It's part of a bigger if-elif block:
|
||||
// https://github.com/MartinThoma/flake8-simplify/issues/115
|
||||
if let Some(Stmt::If(ast::StmtIf {
|
||||
orelse: parent_orelse,
|
||||
..
|
||||
})) = parent
|
||||
{
|
||||
if parent_orelse.len() == 1 && stmt == &parent_orelse[0] {
|
||||
// TODO(charlie): These two cases have the same AST:
|
||||
//
|
||||
// if True:
|
||||
// pass
|
||||
// elif a:
|
||||
// b = 1
|
||||
// else:
|
||||
// b = 2
|
||||
//
|
||||
// if True:
|
||||
// pass
|
||||
// else:
|
||||
// if a:
|
||||
// b = 1
|
||||
// else:
|
||||
// b = 2
|
||||
//
|
||||
// We want to flag the latter, but not the former. Right now, we flag neither.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let node = *default_value.clone();
|
||||
let node = default_value.clone();
|
||||
let node1 = *test_key.clone();
|
||||
let node2 = ast::ExprAttribute {
|
||||
value: expected_subscript.clone(),
|
||||
@@ -1033,9 +952,9 @@ pub(crate) fn use_dict_get_with_default(
|
||||
let contents = checker.generator().stmt(&node5.into());
|
||||
|
||||
// Don't flag if the resulting expression would exceed the maximum line length.
|
||||
let line_start = checker.locator.line_start(stmt.start());
|
||||
let line_start = checker.locator.line_start(stmt_if.start());
|
||||
if LineWidth::new(checker.settings.tab_size)
|
||||
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())])
|
||||
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt_if.start())])
|
||||
.add_str(&contents)
|
||||
> checker.settings.line_length
|
||||
{
|
||||
@@ -1046,13 +965,13 @@ pub(crate) fn use_dict_get_with_default(
|
||||
IfElseBlockInsteadOfDictGet {
|
||||
contents: contents.clone(),
|
||||
},
|
||||
stmt.range(),
|
||||
stmt_if.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if !has_comments(stmt, checker.locator, checker.indexer) {
|
||||
if !has_comments(stmt_if, checker.locator, checker.indexer) {
|
||||
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
|
||||
contents,
|
||||
stmt.range(),
|
||||
stmt_if.range(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,13 +127,7 @@ fn is_dunder_method(name: &str) -> bool {
|
||||
}
|
||||
|
||||
fn is_exception_check(stmt: &Stmt) -> bool {
|
||||
let Stmt::If(ast::StmtIf {
|
||||
test: _,
|
||||
body,
|
||||
orelse: _,
|
||||
range: _,
|
||||
}) = stmt
|
||||
else {
|
||||
let Stmt::If(ast::StmtIf { body, .. }) = stmt else {
|
||||
return false;
|
||||
};
|
||||
matches!(body.as_slice(), [Stmt::Raise(_)])
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_diagnostics::{AutofixKind, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::first_colon_range;
|
||||
use ruff_python_whitespace::UniversalNewlines;
|
||||
use ruff_python_trivia::UniversalNewlines;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::line_width::LineWidth;
|
||||
|
||||
@@ -5,14 +5,13 @@ use libcst_native::{
|
||||
BooleanOp, BooleanOperation, CompoundStatement, Expression, If, LeftParen,
|
||||
ParenthesizableWhitespace, ParenthesizedNode, RightParen, SimpleWhitespace, Statement, Suite,
|
||||
};
|
||||
use rustpython_parser::ast::Ranged;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::autofix::codemods::CodegenStylist;
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
use ruff_python_ast::whitespace;
|
||||
use ruff_python_whitespace::PythonWhitespace;
|
||||
|
||||
use crate::autofix::codemods::CodegenStylist;
|
||||
use crate::cst::matchers::{match_function_def, match_if, match_indented_block, match_statement};
|
||||
|
||||
fn parenthesize_and_operand(expr: Expression) -> Expression {
|
||||
@@ -34,21 +33,19 @@ fn parenthesize_and_operand(expr: Expression) -> Expression {
|
||||
pub(crate) fn fix_nested_if_statements(
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
stmt: &rustpython_parser::ast::Stmt,
|
||||
range: TextRange,
|
||||
) -> Result<Edit> {
|
||||
// Infer the indentation of the outer block.
|
||||
let Some(outer_indent) = whitespace::indentation(locator, stmt) else {
|
||||
let Some(outer_indent) = whitespace::indentation(locator, &range) else {
|
||||
bail!("Unable to fix multiline statement");
|
||||
};
|
||||
|
||||
// Extract the module text.
|
||||
let contents = locator.lines(stmt.range());
|
||||
|
||||
// Handle `elif` blocks differently; detect them upfront.
|
||||
let is_elif = contents.trim_whitespace_start().starts_with("elif");
|
||||
let contents = locator.lines(range);
|
||||
|
||||
// If this is an `elif`, we have to remove the `elif` keyword for now. (We'll
|
||||
// restore the `el` later on.)
|
||||
let is_elif = contents.starts_with("elif");
|
||||
let module_text = if is_elif {
|
||||
Cow::Owned(contents.replacen("elif", "if", 1))
|
||||
} else {
|
||||
@@ -128,6 +125,6 @@ pub(crate) fn fix_nested_if_statements(
|
||||
Cow::Borrowed(module_text)
|
||||
};
|
||||
|
||||
let range = locator.lines_range(stmt.range());
|
||||
let range = locator.lines_range(range);
|
||||
Ok(Edit::range_replacement(contents.to_string(), range))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::{
|
||||
self, CmpOp, Comprehension, Constant, Expr, ExprContext, Ranged, Stmt, UnaryOp,
|
||||
};
|
||||
@@ -7,10 +7,11 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::any_over_expr;
|
||||
use ruff_python_ast::source_code::Generator;
|
||||
use ruff_python_ast::traversal;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::line_width::LineWidth;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `for` loops that can be replaced with a builtin function, like
|
||||
@@ -38,7 +39,7 @@ use crate::registry::{AsRule, Rule};
|
||||
/// - [Python documentation: `all`](https://docs.python.org/3/library/functions.html#all)
|
||||
#[violation]
|
||||
pub struct ReimplementedBuiltin {
|
||||
repl: String,
|
||||
replacement: String,
|
||||
}
|
||||
|
||||
impl Violation for ReimplementedBuiltin {
|
||||
@@ -46,207 +47,229 @@ impl Violation for ReimplementedBuiltin {
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let ReimplementedBuiltin { repl } = self;
|
||||
format!("Use `{repl}` instead of `for` loop")
|
||||
let ReimplementedBuiltin { replacement } = self;
|
||||
format!("Use `{replacement}` instead of `for` loop")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> Option<String> {
|
||||
let ReimplementedBuiltin { repl } = self;
|
||||
Some(format!("Replace with `{repl}`"))
|
||||
let ReimplementedBuiltin { replacement } = self;
|
||||
Some(format!("Replace with `{replacement}`"))
|
||||
}
|
||||
}
|
||||
|
||||
/// SIM110, SIM111
|
||||
pub(crate) fn convert_for_loop_to_any_all(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
sibling: Option<&Stmt>,
|
||||
) {
|
||||
// There are two cases to consider:
|
||||
pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
|
||||
if !checker.semantic().scope().kind.is_any_function() {
|
||||
return;
|
||||
}
|
||||
|
||||
// The `for` loop itself must consist of an `if` with a `return`.
|
||||
let Some(loop_) = match_loop(stmt) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Afterwards, there are two cases to consider:
|
||||
// - `for` loop with an `else: return True` or `else: return False`.
|
||||
// - `for` loop followed by `return True` or `return False`
|
||||
if let Some(loop_info) = return_values_for_else(stmt)
|
||||
.or_else(|| sibling.and_then(|sibling| return_values_for_siblings(stmt, sibling)))
|
||||
{
|
||||
// Check if loop_info.target, loop_info.iter, or loop_info.test contains `await`.
|
||||
if contains_await(loop_info.target)
|
||||
|| contains_await(loop_info.iter)
|
||||
|| contains_await(loop_info.test)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if loop_info.return_value && !loop_info.next_return_value {
|
||||
if checker.enabled(Rule::ReimplementedBuiltin) {
|
||||
let contents = return_stmt(
|
||||
"any",
|
||||
loop_info.test,
|
||||
loop_info.target,
|
||||
loop_info.iter,
|
||||
checker.generator(),
|
||||
);
|
||||
// - `for` loop followed by `return True` or `return False`.
|
||||
let Some(terminal) = match_else_return(stmt).or_else(|| {
|
||||
let parent = checker.semantic().stmt_parent()?;
|
||||
let suite = traversal::suite(stmt, parent)?;
|
||||
let sibling = traversal::next_sibling(stmt, suite)?;
|
||||
match_sibling_return(stmt, sibling)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Don't flag if the resulting expression would exceed the maximum line length.
|
||||
let line_start = checker.locator.line_start(stmt.start());
|
||||
if LineWidth::new(checker.settings.tab_size)
|
||||
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())])
|
||||
.add_str(&contents)
|
||||
> checker.settings.line_length
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Check if any of the expressions contain an `await` expression.
|
||||
if contains_await(loop_.target) || contains_await(loop_.iter) || contains_await(loop_.test) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
ReimplementedBuiltin {
|
||||
repl: contents.clone(),
|
||||
},
|
||||
TextRange::new(stmt.start(), loop_info.terminal),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("any") {
|
||||
diagnostic.set_fix(Fix::suggested(Edit::replacement(
|
||||
contents,
|
||||
stmt.start(),
|
||||
loop_info.terminal,
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
match (loop_.return_value, terminal.return_value) {
|
||||
// Replace with `any`.
|
||||
(true, false) => {
|
||||
let contents = return_stmt(
|
||||
"any",
|
||||
loop_.test,
|
||||
loop_.target,
|
||||
loop_.iter,
|
||||
checker.generator(),
|
||||
);
|
||||
|
||||
// Don't flag if the resulting expression would exceed the maximum line length.
|
||||
let line_start = checker.locator.line_start(stmt.start());
|
||||
if LineWidth::new(checker.settings.tab_size)
|
||||
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())])
|
||||
.add_str(&contents)
|
||||
> checker.settings.line_length
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !loop_info.return_value && loop_info.next_return_value {
|
||||
if checker.enabled(Rule::ReimplementedBuiltin) {
|
||||
// Invert the condition.
|
||||
let test = {
|
||||
if let Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: UnaryOp::Not,
|
||||
operand,
|
||||
range: _,
|
||||
}) = &loop_info.test
|
||||
{
|
||||
*operand.clone()
|
||||
} else if let Expr::Compare(ast::ExprCompare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
range: _,
|
||||
}) = &loop_info.test
|
||||
{
|
||||
if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) {
|
||||
let op = match op {
|
||||
CmpOp::Eq => CmpOp::NotEq,
|
||||
CmpOp::NotEq => CmpOp::Eq,
|
||||
CmpOp::Lt => CmpOp::GtE,
|
||||
CmpOp::LtE => CmpOp::Gt,
|
||||
CmpOp::Gt => CmpOp::LtE,
|
||||
CmpOp::GtE => CmpOp::Lt,
|
||||
CmpOp::Is => CmpOp::IsNot,
|
||||
CmpOp::IsNot => CmpOp::Is,
|
||||
CmpOp::In => CmpOp::NotIn,
|
||||
CmpOp::NotIn => CmpOp::In,
|
||||
};
|
||||
let node = ast::ExprCompare {
|
||||
left: left.clone(),
|
||||
ops: vec![op],
|
||||
comparators: vec![comparator.clone()],
|
||||
range: TextRange::default(),
|
||||
};
|
||||
node.into()
|
||||
} else {
|
||||
let node = ast::ExprUnaryOp {
|
||||
op: UnaryOp::Not,
|
||||
operand: Box::new(loop_info.test.clone()),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
node.into()
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
ReimplementedBuiltin {
|
||||
replacement: contents.to_string(),
|
||||
},
|
||||
TextRange::new(stmt.start(), terminal.stmt.end()),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("any") {
|
||||
diagnostic.set_fix(Fix::suggested(Edit::replacement(
|
||||
contents,
|
||||
stmt.start(),
|
||||
terminal.stmt.end(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
// Replace with `all`.
|
||||
(false, true) => {
|
||||
// Invert the condition.
|
||||
let test = {
|
||||
if let Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: UnaryOp::Not,
|
||||
operand,
|
||||
range: _,
|
||||
}) = &loop_.test
|
||||
{
|
||||
*operand.clone()
|
||||
} else if let Expr::Compare(ast::ExprCompare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
range: _,
|
||||
}) = &loop_.test
|
||||
{
|
||||
if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) {
|
||||
let op = match op {
|
||||
CmpOp::Eq => CmpOp::NotEq,
|
||||
CmpOp::NotEq => CmpOp::Eq,
|
||||
CmpOp::Lt => CmpOp::GtE,
|
||||
CmpOp::LtE => CmpOp::Gt,
|
||||
CmpOp::Gt => CmpOp::LtE,
|
||||
CmpOp::GtE => CmpOp::Lt,
|
||||
CmpOp::Is => CmpOp::IsNot,
|
||||
CmpOp::IsNot => CmpOp::Is,
|
||||
CmpOp::In => CmpOp::NotIn,
|
||||
CmpOp::NotIn => CmpOp::In,
|
||||
};
|
||||
let node = ast::ExprCompare {
|
||||
left: left.clone(),
|
||||
ops: vec![op],
|
||||
comparators: vec![comparator.clone()],
|
||||
range: TextRange::default(),
|
||||
};
|
||||
node.into()
|
||||
} else {
|
||||
let node = ast::ExprUnaryOp {
|
||||
op: UnaryOp::Not,
|
||||
operand: Box::new(loop_info.test.clone()),
|
||||
operand: Box::new(loop_.test.clone()),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
node.into()
|
||||
}
|
||||
};
|
||||
let contents = return_stmt(
|
||||
"all",
|
||||
&test,
|
||||
loop_info.target,
|
||||
loop_info.iter,
|
||||
checker.generator(),
|
||||
);
|
||||
|
||||
// Don't flag if the resulting expression would exceed the maximum line length.
|
||||
let line_start = checker.locator.line_start(stmt.start());
|
||||
if LineWidth::new(checker.settings.tab_size)
|
||||
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())])
|
||||
.add_str(&contents)
|
||||
> checker.settings.line_length
|
||||
{
|
||||
return;
|
||||
} else {
|
||||
let node = ast::ExprUnaryOp {
|
||||
op: UnaryOp::Not,
|
||||
operand: Box::new(loop_.test.clone()),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
node.into()
|
||||
}
|
||||
};
|
||||
let contents = return_stmt("all", &test, loop_.target, loop_.iter, checker.generator());
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
ReimplementedBuiltin {
|
||||
repl: contents.clone(),
|
||||
},
|
||||
TextRange::new(stmt.start(), loop_info.terminal),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("all") {
|
||||
diagnostic.set_fix(Fix::suggested(Edit::replacement(
|
||||
contents,
|
||||
stmt.start(),
|
||||
loop_info.terminal,
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
// Don't flag if the resulting expression would exceed the maximum line length.
|
||||
let line_start = checker.locator.line_start(stmt.start());
|
||||
if LineWidth::new(checker.settings.tab_size)
|
||||
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())])
|
||||
.add_str(&contents)
|
||||
> checker.settings.line_length
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
ReimplementedBuiltin {
|
||||
replacement: contents.to_string(),
|
||||
},
|
||||
TextRange::new(stmt.start(), terminal.stmt.end()),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("all") {
|
||||
diagnostic.set_fix(Fix::suggested(Edit::replacement(
|
||||
contents,
|
||||
stmt.start(),
|
||||
terminal.stmt.end(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a `for` loop with a conditional `return`, like:
|
||||
/// ```python
|
||||
/// for x in y:
|
||||
/// if x == 0:
|
||||
/// return True
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
struct Loop<'a> {
|
||||
/// The `return` value of the loop.
|
||||
return_value: bool,
|
||||
next_return_value: bool,
|
||||
/// The test condition in the loop.
|
||||
test: &'a Expr,
|
||||
/// The target of the loop.
|
||||
target: &'a Expr,
|
||||
/// The iterator of the loop.
|
||||
iter: &'a Expr,
|
||||
terminal: TextSize,
|
||||
}
|
||||
|
||||
/// Extract the returned boolean values a `Stmt::For` with an `else` body.
|
||||
fn return_values_for_else(stmt: &Stmt) -> Option<Loop> {
|
||||
/// Represents a `return` statement following a `for` loop, like:
|
||||
/// ```python
|
||||
/// for x in y:
|
||||
/// if x == 0:
|
||||
/// return True
|
||||
/// return False
|
||||
/// ```
|
||||
///
|
||||
/// Or:
|
||||
/// ```python
|
||||
/// for x in y:
|
||||
/// if x == 0:
|
||||
/// return True
|
||||
/// else:
|
||||
/// return False
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
struct Terminal<'a> {
|
||||
return_value: bool,
|
||||
stmt: &'a Stmt,
|
||||
}
|
||||
|
||||
fn match_loop(stmt: &Stmt) -> Option<Loop> {
|
||||
let Stmt::For(ast::StmtFor {
|
||||
body,
|
||||
target,
|
||||
iter,
|
||||
orelse,
|
||||
..
|
||||
body, target, iter, ..
|
||||
}) = stmt
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// The loop itself should contain a single `if` statement, with an `else`
|
||||
// containing a single `return True` or `return False`.
|
||||
if body.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
if orelse.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let Stmt::If(ast::StmtIf {
|
||||
// The loop itself should contain a single `if` statement, with a single `return` statement in
|
||||
// the body.
|
||||
let [Stmt::If(ast::StmtIf {
|
||||
body: nested_body,
|
||||
test: nested_test,
|
||||
orelse: nested_orelse,
|
||||
elif_else_clauses: nested_elif_else_clauses,
|
||||
range: _,
|
||||
}) = &body[0]
|
||||
})] = body.as_slice()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if nested_body.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
if !nested_orelse.is_empty() {
|
||||
if !nested_elif_else_clauses.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let Stmt::Return(ast::StmtReturn { value, range: _ }) = &nested_body[0] else {
|
||||
@@ -263,15 +286,35 @@ fn return_values_for_else(stmt: &Stmt) -> Option<Loop> {
|
||||
return None;
|
||||
};
|
||||
|
||||
// The `else` block has to contain a single `return True` or `return False`.
|
||||
let Stmt::Return(ast::StmtReturn {
|
||||
value: next_value,
|
||||
range: _,
|
||||
}) = &orelse[0]
|
||||
else {
|
||||
Some(Loop {
|
||||
return_value: *value,
|
||||
test: nested_test,
|
||||
target,
|
||||
iter,
|
||||
})
|
||||
}
|
||||
|
||||
/// If a `Stmt::For` contains an `else` with a single boolean `return`, return the [`Terminal`]
|
||||
/// representing that `return`.
|
||||
///
|
||||
/// For example, matches the `return` in:
|
||||
/// ```python
|
||||
/// for x in y:
|
||||
/// if x == 0:
|
||||
/// return True
|
||||
/// return False
|
||||
/// ```
|
||||
fn match_else_return(stmt: &Stmt) -> Option<Terminal> {
|
||||
let Stmt::For(ast::StmtFor { orelse, .. }) = stmt else {
|
||||
return None;
|
||||
};
|
||||
let Some(next_value) = next_value else {
|
||||
|
||||
// The `else` block has to contain a single `return True` or `return False`.
|
||||
let [Stmt::Return(ast::StmtReturn {
|
||||
value: Some(next_value),
|
||||
range: _,
|
||||
})] = orelse.as_slice()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
@@ -282,78 +325,41 @@ fn return_values_for_else(stmt: &Stmt) -> Option<Loop> {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(Loop {
|
||||
return_value: *value,
|
||||
next_return_value: *next_value,
|
||||
test: nested_test,
|
||||
target,
|
||||
iter,
|
||||
terminal: stmt.end(),
|
||||
Some(Terminal {
|
||||
return_value: *next_value,
|
||||
stmt,
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract the returned boolean values from subsequent `Stmt::For` and
|
||||
/// `Stmt::Return` statements, or `None`.
|
||||
fn return_values_for_siblings<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option<Loop<'a>> {
|
||||
let Stmt::For(ast::StmtFor {
|
||||
body,
|
||||
target,
|
||||
iter,
|
||||
orelse,
|
||||
..
|
||||
}) = stmt
|
||||
else {
|
||||
/// If a `Stmt::For` is followed by a boolean `return`, return the [`Terminal`] representing that
|
||||
/// `return`.
|
||||
///
|
||||
/// For example, matches the `return` in:
|
||||
/// ```python
|
||||
/// for x in y:
|
||||
/// if x == 0:
|
||||
/// return True
|
||||
/// else:
|
||||
/// return False
|
||||
/// ```
|
||||
fn match_sibling_return<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option<Terminal<'a>> {
|
||||
let Stmt::For(ast::StmtFor { orelse, .. }) = stmt else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// The loop itself should contain a single `if` statement, with a single `return
|
||||
// True` or `return False`.
|
||||
if body.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
// The loop itself shouldn't have an `else` block.
|
||||
if !orelse.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let Stmt::If(ast::StmtIf {
|
||||
body: nested_body,
|
||||
test: nested_test,
|
||||
orelse: nested_orelse,
|
||||
range: _,
|
||||
}) = &body[0]
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if nested_body.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
if !nested_orelse.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let Stmt::Return(ast::StmtReturn { value, range: _ }) = &nested_body[0] else {
|
||||
return None;
|
||||
};
|
||||
let Some(value) = value else {
|
||||
return None;
|
||||
};
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Bool(value),
|
||||
..
|
||||
}) = value.as_ref()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// The next statement has to be a `return True` or `return False`.
|
||||
let Stmt::Return(ast::StmtReturn {
|
||||
value: next_value,
|
||||
value: Some(next_value),
|
||||
range: _,
|
||||
}) = &sibling
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let Some(next_value) = next_value else {
|
||||
return None;
|
||||
};
|
||||
let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Bool(next_value),
|
||||
..
|
||||
@@ -362,13 +368,9 @@ fn return_values_for_siblings<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option<L
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(Loop {
|
||||
return_value: *value,
|
||||
next_return_value: *next_value,
|
||||
test: nested_test,
|
||||
target,
|
||||
iter,
|
||||
terminal: sibling.end(),
|
||||
Some(Terminal {
|
||||
return_value: *next_value,
|
||||
stmt: sibling,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,31 @@ SIM102.py:7:1: SIM102 [*] Use a single `if` statement instead of nested `if` sta
|
||||
12 11 | # SIM102
|
||||
13 12 | if a:
|
||||
|
||||
SIM102.py:8:5: SIM102 [*] Use a single `if` statement instead of nested `if` statements
|
||||
|
|
||||
6 | # SIM102
|
||||
7 | if a:
|
||||
8 | if b:
|
||||
| _____^
|
||||
9 | | if c:
|
||||
| |_____________^ SIM102
|
||||
10 | d
|
||||
|
|
||||
= help: Combine `if` statements using `and`
|
||||
|
||||
ℹ Suggested fix
|
||||
5 5 |
|
||||
6 6 | # SIM102
|
||||
7 7 | if a:
|
||||
8 |- if b:
|
||||
9 |- if c:
|
||||
10 |- d
|
||||
8 |+ if b and c:
|
||||
9 |+ d
|
||||
11 10 |
|
||||
12 11 | # SIM102
|
||||
13 12 | if a:
|
||||
|
||||
SIM102.py:15:1: SIM102 [*] Use a single `if` statement instead of nested `if` statements
|
||||
|
|
||||
13 | if a:
|
||||
@@ -255,30 +280,56 @@ SIM102.py:97:1: SIM102 Use a single `if` statement instead of nested `if` statem
|
||||
|
|
||||
= help: Combine `if` statements using `and`
|
||||
|
||||
SIM102.py:124:5: SIM102 [*] Use a single `if` statement instead of nested `if` statements
|
||||
SIM102.py:106:5: SIM102 [*] Use a single `if` statement instead of nested `if` statements
|
||||
|
|
||||
122 | if a:
|
||||
123 | # SIM 102
|
||||
124 | if b:
|
||||
104 | # Regression test for https://github.com/apache/airflow/blob/145b16caaa43f0c42bffd97344df916c602cddde/airflow/configuration.py#L1161
|
||||
105 | if a:
|
||||
106 | if b:
|
||||
| _____^
|
||||
125 | | if c:
|
||||
107 | | if c:
|
||||
| |_____________^ SIM102
|
||||
126 | print("foo")
|
||||
127 | else:
|
||||
108 | print("if")
|
||||
109 | elif d:
|
||||
|
|
||||
= help: Combine `if` statements using `and`
|
||||
|
||||
ℹ Suggested fix
|
||||
121 121 | # OK
|
||||
122 122 | if a:
|
||||
123 123 | # SIM 102
|
||||
124 |- if b:
|
||||
125 |- if c:
|
||||
126 |- print("foo")
|
||||
124 |+ if b and c:
|
||||
125 |+ print("foo")
|
||||
127 126 | else:
|
||||
128 127 | print("bar")
|
||||
129 128 |
|
||||
103 103 | # SIM102
|
||||
104 104 | # Regression test for https://github.com/apache/airflow/blob/145b16caaa43f0c42bffd97344df916c602cddde/airflow/configuration.py#L1161
|
||||
105 105 | if a:
|
||||
106 |- if b:
|
||||
107 |- if c:
|
||||
108 |- print("if")
|
||||
106 |+ if b and c:
|
||||
107 |+ print("if")
|
||||
109 108 | elif d:
|
||||
110 109 | print("elif")
|
||||
111 110 |
|
||||
|
||||
SIM102.py:132:5: SIM102 [*] Use a single `if` statement instead of nested `if` statements
|
||||
|
|
||||
130 | if a:
|
||||
131 | # SIM 102
|
||||
132 | if b:
|
||||
| _____^
|
||||
133 | | if c:
|
||||
| |_____________^ SIM102
|
||||
134 | print("foo")
|
||||
135 | else:
|
||||
|
|
||||
= help: Combine `if` statements using `and`
|
||||
|
||||
ℹ Suggested fix
|
||||
129 129 | # OK
|
||||
130 130 | if a:
|
||||
131 131 | # SIM 102
|
||||
132 |- if b:
|
||||
133 |- if c:
|
||||
134 |- print("foo")
|
||||
132 |+ if b and c:
|
||||
133 |+ print("foo")
|
||||
135 134 | else:
|
||||
136 135 | print("bar")
|
||||
137 136 |
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,32 @@ SIM108.py:2:1: SIM108 [*] Use ternary operator `b = c if a else d` instead of `i
|
||||
7 4 | # OK
|
||||
8 5 | b = c if a else d
|
||||
|
||||
SIM108.py:30:5: SIM108 [*] Use ternary operator `b = 1 if a else 2` instead of `if`-`else`-block
|
||||
|
|
||||
28 | pass
|
||||
29 | else:
|
||||
30 | if a:
|
||||
| _____^
|
||||
31 | | b = 1
|
||||
32 | | else:
|
||||
33 | | b = 2
|
||||
| |_____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `b = 1 if a else 2`
|
||||
|
||||
ℹ Suggested fix
|
||||
27 27 | if True:
|
||||
28 28 | pass
|
||||
29 29 | else:
|
||||
30 |- if a:
|
||||
31 |- b = 1
|
||||
32 |- else:
|
||||
33 |- b = 2
|
||||
30 |+ b = 1 if a else 2
|
||||
34 31 |
|
||||
35 32 |
|
||||
36 33 | import sys
|
||||
|
||||
SIM108.py:58:1: SIM108 Use ternary operator `abc = x if x > 0 else -x` instead of `if`-`else`-block
|
||||
|
|
||||
57 | # SIM108 (without fix due to comments)
|
||||
|
||||
@@ -127,12 +127,11 @@ SIM114.py:38:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
58 | if result.eofs == "O":
|
||||
|
|
||||
|
||||
SIM114.py:62:6: SIM114 Combine `if` branches using logical `or` operator
|
||||
SIM114.py:62:1: SIM114 Combine `if` branches using logical `or` operator
|
||||
|
|
||||
60 | elif result.eofs == "S":
|
||||
61 | skipped = 1
|
||||
62 | elif result.eofs == "F":
|
||||
| ______^
|
||||
62 | / elif result.eofs == "F":
|
||||
63 | | errors = 1
|
||||
64 | | elif result.eofs == "E":
|
||||
65 | | errors = 1
|
||||
|
||||
@@ -105,6 +105,8 @@ SIM116.py:79:1: SIM116 Use a dictionary instead of consecutive `if` statements
|
||||
85 | | elif func_name == "move":
|
||||
86 | | return "MV"
|
||||
| |_______________^ SIM116
|
||||
87 |
|
||||
88 | # OK
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ SIM401.py:36:1: SIM401 [*] Use `vars[idx] = a_dict.get(key, "defaultß9💣2ℝ6
|
||||
39 | | vars[idx] = "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789"
|
||||
| |___________________________________________________________________________^ SIM401
|
||||
40 |
|
||||
41 | ###
|
||||
41 | # SIM401
|
||||
|
|
||||
= help: Replace with `vars[idx] = a_dict.get(key, "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789")`
|
||||
|
||||
@@ -128,7 +128,35 @@ SIM401.py:36:1: SIM401 [*] Use `vars[idx] = a_dict.get(key, "defaultß9💣2ℝ6
|
||||
39 |- vars[idx] = "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789"
|
||||
36 |+vars[idx] = a_dict.get(key, "defaultß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789ß9💣2ℝ6789")
|
||||
40 37 |
|
||||
41 38 | ###
|
||||
42 39 | # Negative cases
|
||||
41 38 | # SIM401
|
||||
42 39 | if foo():
|
||||
|
||||
SIM401.py:45:5: SIM401 [*] Use `vars[idx] = a_dict.get(key, "default")` instead of an `if` block
|
||||
|
|
||||
43 | pass
|
||||
44 | else:
|
||||
45 | if key in a_dict:
|
||||
| _____^
|
||||
46 | | vars[idx] = a_dict[key]
|
||||
47 | | else:
|
||||
48 | | vars[idx] = "default"
|
||||
| |_____________________________^ SIM401
|
||||
49 |
|
||||
50 | ###
|
||||
|
|
||||
= help: Replace with `vars[idx] = a_dict.get(key, "default")`
|
||||
|
||||
ℹ Suggested fix
|
||||
42 42 | if foo():
|
||||
43 43 | pass
|
||||
44 44 | else:
|
||||
45 |- if key in a_dict:
|
||||
46 |- vars[idx] = a_dict[key]
|
||||
47 |- else:
|
||||
48 |- vars[idx] = "default"
|
||||
45 |+ vars[idx] = a_dict.get(key, "default")
|
||||
49 46 |
|
||||
50 47 | ###
|
||||
51 48 | # Negative cases
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::{NodeId, ReferenceId, Scope};
|
||||
use ruff_python_semantic::{NodeId, ResolvedReferenceId, Scope};
|
||||
|
||||
use crate::autofix;
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -180,7 +180,7 @@ struct Import<'a> {
|
||||
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
|
||||
qualified_name: &'a str,
|
||||
/// The first reference to the imported symbol.
|
||||
reference_id: ReferenceId,
|
||||
reference_id: ResolvedReferenceId,
|
||||
/// The trimmed range of the import (e.g., `List` in `from typing import List`).
|
||||
range: TextRange,
|
||||
/// The range of the import's parent statement.
|
||||
|
||||
@@ -4,7 +4,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, DiagnosticKind, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::{Binding, NodeId, ReferenceId, Scope};
|
||||
use ruff_python_semantic::{Binding, NodeId, ResolvedReferenceId, Scope};
|
||||
|
||||
use crate::autofix;
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -357,7 +357,7 @@ struct Import<'a> {
|
||||
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
|
||||
qualified_name: &'a str,
|
||||
/// The first reference to the imported symbol.
|
||||
reference_id: ReferenceId,
|
||||
reference_id: ResolvedReferenceId,
|
||||
/// The trimmed range of the import (e.g., `List` in `from typing import List`).
|
||||
range: TextRange,
|
||||
/// The range of the import's parent statement.
|
||||
|
||||
@@ -56,6 +56,11 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::PyPath, Path::new("py_path_1.py"))]
|
||||
#[test_case(Rule::PyPath, Path::new("py_path_2.py"))]
|
||||
#[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))]
|
||||
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
|
||||
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]
|
||||
#[test_case(Rule::OsPathGetmtime, Path::new("PTH204.py"))]
|
||||
#[test_case(Rule::OsPathGetctime, Path::new("PTH205.py"))]
|
||||
fn rules_pypath(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
pub(crate) use os_path_getatime::*;
|
||||
pub(crate) use os_path_getctime::*;
|
||||
pub(crate) use os_path_getmtime::*;
|
||||
pub(crate) use os_path_getsize::*;
|
||||
pub(crate) use path_constructor_current_directory::*;
|
||||
pub(crate) use replaceable_by_pathlib::*;
|
||||
|
||||
mod os_path_getatime;
|
||||
mod os_path_getctime;
|
||||
mod os_path_getmtime;
|
||||
mod os_path_getsize;
|
||||
mod path_constructor_current_directory;
|
||||
mod replaceable_by_pathlib;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.getatime`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`.
|
||||
///
|
||||
/// When possible, using `Path` object methods such as `Path.stat()` can
|
||||
/// improve readability over the `os` module's counterparts (e.g.,
|
||||
/// `os.path.getsize()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// os.path.getsize(__file__)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// Path(__file__).stat().st_size
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[violation]
|
||||
pub struct OsPathGetatime;
|
||||
|
||||
impl Violation for OsPathGetatime {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`os.path.getatime` should be replaced by `Path.stat().st_atime`")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.getatime`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`.
|
||||
///
|
||||
/// When possible, using `Path` object methods such as `Path.stat()` can
|
||||
/// improve readability over the `os` module's counterparts (e.g.,
|
||||
/// `os.path.getsize()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// os.path.getsize(__file__)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// Path(__file__).stat().st_size
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[violation]
|
||||
pub struct OsPathGetctime;
|
||||
|
||||
impl Violation for OsPathGetctime {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`os.path.getctime` should be replaced by `Path.stat().st_ctime`")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.getatime`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`.
|
||||
///
|
||||
/// When possible, using `Path` object methods such as `Path.stat()` can
|
||||
/// improve readability over the `os` module's counterparts (e.g.,
|
||||
/// `os.path.getsize()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// os.path.getsize(__file__)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// Path(__file__).stat().st_size
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[violation]
|
||||
pub struct OsPathGetmtime;
|
||||
|
||||
impl Violation for OsPathGetmtime {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`os.path.getmtime` should be replaced by `Path.stat().st_mtime`")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.getsize`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`.
|
||||
///
|
||||
/// When possible, using `Path` object methods such as `Path.stat()` can
|
||||
/// improve readability over the `os` module's counterparts (e.g.,
|
||||
/// `os.path.getsize()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// os.path.getsize(__file__)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// Path(__file__).stat().st_size
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[violation]
|
||||
pub struct OsPathGetsize;
|
||||
|
||||
impl Violation for OsPathGetsize {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`os.path.getsize` should be replaced by `Path.stat().st_size`")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use rustpython_parser::ast::{Constant, Expr, ExprCall, ExprConstant};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `pathlib.Path` objects that are initialized with the current
|
||||
/// directory.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `Path()` constructor defaults to the current directory, so passing it
|
||||
/// in explicitly (as `"."`) is unnecessary.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// _ = Path(".")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// _ = Path()
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
|
||||
#[violation]
|
||||
pub struct PathConstructorCurrentDirectory;
|
||||
|
||||
impl AlwaysAutofixableViolation for PathConstructorCurrentDirectory {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Do not pass the current directory explicitly to `Path`")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
"Remove the current directory argument".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// PTH201
|
||||
pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &Expr, func: &Expr) {
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
matches!(call_path.as_slice(), ["pathlib", "Path" | "PurePath"])
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let Expr::Call(ExprCall { args, keywords, .. }) = expr else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !keywords.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let [Expr::Constant(ExprConstant {
|
||||
value: Constant::Str(value),
|
||||
kind: _,
|
||||
range,
|
||||
})] = args.as_slice()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if value != "." {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, *range);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_deletion(*range)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -4,6 +4,9 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::flake8_use_pathlib::rules::{
|
||||
OsPathGetatime, OsPathGetctime, OsPathGetmtime, OsPathGetsize,
|
||||
};
|
||||
use crate::rules::flake8_use_pathlib::violations::{
|
||||
BuiltinOpen, OsChmod, OsGetcwd, OsMakedirs, OsMkdir, OsPathAbspath, OsPathBasename,
|
||||
OsPathDirname, OsPathExists, OsPathExpanduser, OsPathIsabs, OsPathIsdir, OsPathIsfile,
|
||||
@@ -41,6 +44,14 @@ pub(crate) fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) {
|
||||
["os", "path", "dirname"] => Some(OsPathDirname.into()),
|
||||
["os", "path", "samefile"] => Some(OsPathSamefile.into()),
|
||||
["os", "path", "splitext"] => Some(OsPathSplitext.into()),
|
||||
// PTH202
|
||||
["os", "path", "getsize"] => Some(OsPathGetsize.into()),
|
||||
// PTH203
|
||||
["os", "path", "getatime"] => Some(OsPathGetatime.into()),
|
||||
// PTH204
|
||||
["os", "path", "getmtime"] => Some(OsPathGetmtime.into()),
|
||||
// PTH205
|
||||
["os", "path", "getctime"] => Some(OsPathGetctime.into()),
|
||||
["", "open"] => Some(BuiltinOpen.into()),
|
||||
["py", "path", "local"] => Some(PyPath.into()),
|
||||
// Python 3.9+
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
|
||||
---
|
||||
PTH201.py:5:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||
|
|
||||
4 | # match
|
||||
5 | _ = Path(".")
|
||||
| ^^^ PTH201
|
||||
6 | _ = pth(".")
|
||||
7 | _ = PurePath(".")
|
||||
|
|
||||
= help: Remove the current directory argument
|
||||
|
||||
ℹ Fix
|
||||
2 2 | from pathlib import Path as pth
|
||||
3 3 |
|
||||
4 4 | # match
|
||||
5 |-_ = Path(".")
|
||||
5 |+_ = Path()
|
||||
6 6 | _ = pth(".")
|
||||
7 7 | _ = PurePath(".")
|
||||
8 8 |
|
||||
|
||||
PTH201.py:6:9: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||
|
|
||||
4 | # match
|
||||
5 | _ = Path(".")
|
||||
6 | _ = pth(".")
|
||||
| ^^^ PTH201
|
||||
7 | _ = PurePath(".")
|
||||
|
|
||||
= help: Remove the current directory argument
|
||||
|
||||
ℹ Fix
|
||||
3 3 |
|
||||
4 4 | # match
|
||||
5 5 | _ = Path(".")
|
||||
6 |-_ = pth(".")
|
||||
6 |+_ = pth()
|
||||
7 7 | _ = PurePath(".")
|
||||
8 8 |
|
||||
9 9 | # no match
|
||||
|
||||
PTH201.py:7:14: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||
|
|
||||
5 | _ = Path(".")
|
||||
6 | _ = pth(".")
|
||||
7 | _ = PurePath(".")
|
||||
| ^^^ PTH201
|
||||
8 |
|
||||
9 | # no match
|
||||
|
|
||||
= help: Remove the current directory argument
|
||||
|
||||
ℹ Fix
|
||||
4 4 | # match
|
||||
5 5 | _ = Path(".")
|
||||
6 6 | _ = pth(".")
|
||||
7 |-_ = PurePath(".")
|
||||
7 |+_ = PurePath()
|
||||
8 8 |
|
||||
9 9 | # no match
|
||||
10 10 | _ = Path()
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
|
||||
---
|
||||
PTH202.py:6:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
||||
|
|
||||
6 | os.path.getsize("filename")
|
||||
| ^^^^^^^^^^^^^^^ PTH202
|
||||
7 | os.path.getsize(b"filename")
|
||||
8 | os.path.getsize(Path("filename"))
|
||||
|
|
||||
|
||||
PTH202.py:7:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
||||
|
|
||||
6 | os.path.getsize("filename")
|
||||
7 | os.path.getsize(b"filename")
|
||||
| ^^^^^^^^^^^^^^^ PTH202
|
||||
8 | os.path.getsize(Path("filename"))
|
||||
9 | os.path.getsize(__file__)
|
||||
|
|
||||
|
||||
PTH202.py:8:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
||||
|
|
||||
6 | os.path.getsize("filename")
|
||||
7 | os.path.getsize(b"filename")
|
||||
8 | os.path.getsize(Path("filename"))
|
||||
| ^^^^^^^^^^^^^^^ PTH202
|
||||
9 | os.path.getsize(__file__)
|
||||
|
|
||||
|
||||
PTH202.py:9:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
||||
|
|
||||
7 | os.path.getsize(b"filename")
|
||||
8 | os.path.getsize(Path("filename"))
|
||||
9 | os.path.getsize(__file__)
|
||||
| ^^^^^^^^^^^^^^^ PTH202
|
||||
10 |
|
||||
11 | getsize("filename")
|
||||
|
|
||||
|
||||
PTH202.py:11:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
||||
|
|
||||
9 | os.path.getsize(__file__)
|
||||
10 |
|
||||
11 | getsize("filename")
|
||||
| ^^^^^^^ PTH202
|
||||
12 | getsize(b"filename")
|
||||
13 | getsize(Path("filename"))
|
||||
|
|
||||
|
||||
PTH202.py:12:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
||||
|
|
||||
11 | getsize("filename")
|
||||
12 | getsize(b"filename")
|
||||
| ^^^^^^^ PTH202
|
||||
13 | getsize(Path("filename"))
|
||||
14 | getsize(__file__)
|
||||
|
|
||||
|
||||
PTH202.py:13:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
||||
|
|
||||
11 | getsize("filename")
|
||||
12 | getsize(b"filename")
|
||||
13 | getsize(Path("filename"))
|
||||
| ^^^^^^^ PTH202
|
||||
14 | getsize(__file__)
|
||||
|
|
||||
|
||||
PTH202.py:14:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
||||
|
|
||||
12 | getsize(b"filename")
|
||||
13 | getsize(Path("filename"))
|
||||
14 | getsize(__file__)
|
||||
| ^^^^^^^ PTH202
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
|
||||
---
|
||||
PTH203.py:5:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
||||
|
|
||||
3 | from os.path import getatime
|
||||
4 |
|
||||
5 | os.path.getatime("filename")
|
||||
| ^^^^^^^^^^^^^^^^ PTH203
|
||||
6 | os.path.getatime(b"filename")
|
||||
7 | os.path.getatime(Path("filename"))
|
||||
|
|
||||
|
||||
PTH203.py:6:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
||||
|
|
||||
5 | os.path.getatime("filename")
|
||||
6 | os.path.getatime(b"filename")
|
||||
| ^^^^^^^^^^^^^^^^ PTH203
|
||||
7 | os.path.getatime(Path("filename"))
|
||||
|
|
||||
|
||||
PTH203.py:7:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
||||
|
|
||||
5 | os.path.getatime("filename")
|
||||
6 | os.path.getatime(b"filename")
|
||||
7 | os.path.getatime(Path("filename"))
|
||||
| ^^^^^^^^^^^^^^^^ PTH203
|
||||
|
|
||||
|
||||
PTH203.py:10:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
||||
|
|
||||
10 | getatime("filename")
|
||||
| ^^^^^^^^ PTH203
|
||||
11 | getatime(b"filename")
|
||||
12 | getatime(Path("filename"))
|
||||
|
|
||||
|
||||
PTH203.py:11:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
||||
|
|
||||
10 | getatime("filename")
|
||||
11 | getatime(b"filename")
|
||||
| ^^^^^^^^ PTH203
|
||||
12 | getatime(Path("filename"))
|
||||
|
|
||||
|
||||
PTH203.py:12:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
||||
|
|
||||
10 | getatime("filename")
|
||||
11 | getatime(b"filename")
|
||||
12 | getatime(Path("filename"))
|
||||
| ^^^^^^^^ PTH203
|
||||
|
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user