Compare commits

..

2 Commits

Author SHA1 Message Date
Aria Desires
7ef86c9637 fixup 2025-09-18 21:08:49 -04:00
Aria Desires
793d0d0dd4 Implement additional implicit re-exports idiom for __init__.pyi 2025-09-18 15:09:01 -04:00
213 changed files with 1310 additions and 5793 deletions

View File

@@ -88,6 +88,7 @@ jobs:
':!crates/ruff_python_formatter/**' \
':!crates/ruff_formatter/**' \
':!crates/ruff_dev/**' \
':!crates/ruff_db/**' \
':scripts/*' \
':python/**' \
':.github/workflows/ci.yaml' \
@@ -906,13 +907,10 @@ jobs:
run: npm run fmt:check
working-directory: playground
benchmarks-instrumented-ruff:
name: "benchmarks instrumented (ruff)"
benchmarks-instrumented:
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.ref == 'refs/heads/main' ||
(needs.determine_changes.outputs.formatter == 'true' || needs.determine_changes.outputs.linter == 'true')
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- name: "Checkout Branch"
@@ -932,42 +930,7 @@ jobs:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark formatter lexer linter parser
- name: "Run benchmarks"
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1
with:
mode: instrumentation
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
benchmarks-instrumented-ty:
name: "benchmarks instrumented (ty)"
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.ty == 'true'
timeout-minutes: 20
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
with:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark ty
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1

View File

@@ -49,7 +49,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@fc0f612798710b0dd69bb7528bc9b361dc60bd43"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
ecosystem-analyzer \
--verbose \

View File

@@ -3,7 +3,4 @@
"--all-features"
],
"rust-analyzer.check.command": "clippy",
"search.exclude": {
"**/*.snap": true
}
}
}

View File

@@ -1,65 +1,5 @@
# Changelog
## 0.13.1
Released on 2025-09-18.
### Preview features
- \[`flake8-simplify`\] Detect unnecessary `None` default for additional key expression types (`SIM910`) ([#20343](https://github.com/astral-sh/ruff/pull/20343))
- \[`flake8-use-pathlib`\] Add fix for `PTH123` ([#20169](https://github.com/astral-sh/ruff/pull/20169))
- \[`flake8-use-pathlib`\] Fix `PTH101`, `PTH104`, `PTH105`, `PTH121` fixes ([#20143](https://github.com/astral-sh/ruff/pull/20143))
- \[`flake8-use-pathlib`\] Make `PTH111` fix unsafe because it can change behavior ([#20215](https://github.com/astral-sh/ruff/pull/20215))
- \[`pycodestyle`\] Fix `E301` to only trigger for functions immediately within a class ([#19768](https://github.com/astral-sh/ruff/pull/19768))
- \[`refurb`\] Mark `single-item-membership-test` fix as always unsafe (`FURB171`) ([#20279](https://github.com/astral-sh/ruff/pull/20279))
### Bug fixes
- Handle t-strings for token-based rules and suppression comments ([#20357](https://github.com/astral-sh/ruff/pull/20357))
- \[`flake8-bandit`\] Fix truthiness: dict-only `**` displays not truthy for `shell` (`S602`, `S604`, `S609`) ([#20177](https://github.com/astral-sh/ruff/pull/20177))
- \[`flake8-simplify`\] Fix diagnostic to show correct method name for `str.rsplit` calls (`SIM905`) ([#20459](https://github.com/astral-sh/ruff/pull/20459))
- \[`flynt`\] Use triple quotes for joined raw strings with newlines (`FLY002`) ([#20197](https://github.com/astral-sh/ruff/pull/20197))
- \[`pyupgrade`\] Fix false positive when class name is shadowed by local variable (`UP008`) ([#20427](https://github.com/astral-sh/ruff/pull/20427))
- \[`pyupgrade`\] Prevent infinite loop with `I002` and `UP026` ([#20327](https://github.com/astral-sh/ruff/pull/20327))
- \[`ruff`\] Recognize t-strings, generators, and lambdas in `invalid-index-type` (`RUF016`) ([#20213](https://github.com/astral-sh/ruff/pull/20213))
### Rule changes
- \[`RUF102`\] Respect rule redirects in invalid rule code detection ([#20245](https://github.com/astral-sh/ruff/pull/20245))
- \[`flake8-bugbear`\] Mark the fix for `unreliable-callable-check` as always unsafe (`B004`) ([#20318](https://github.com/astral-sh/ruff/pull/20318))
- \[`ruff`\] Allow dataclass attribute value instantiation from nested frozen dataclass (`RUF009`) ([#20352](https://github.com/astral-sh/ruff/pull/20352))
### CLI
- Add fixes to `output-format=sarif` ([#20300](https://github.com/astral-sh/ruff/pull/20300))
- Treat panics as fatal diagnostics, sort panics last ([#20258](https://github.com/astral-sh/ruff/pull/20258))
### Documentation
- \[`ruff`\] Add `analyze.string-imports-min-dots` to settings ([#20375](https://github.com/astral-sh/ruff/pull/20375))
- Update README.md with Albumentations new repository URL ([#20415](https://github.com/astral-sh/ruff/pull/20415))
### Other changes
- Bump MSRV to Rust 1.88 ([#20470](https://github.com/astral-sh/ruff/pull/20470))
- Enable inline noqa for multiline strings in playground ([#20442](https://github.com/astral-sh/ruff/pull/20442))
### Contributors
- [@chirizxc](https://github.com/chirizxc)
- [@danparizher](https://github.com/danparizher)
- [@IDrokin117](https://github.com/IDrokin117)
- [@amyreese](https://github.com/amyreese)
- [@AlexWaygood](https://github.com/AlexWaygood)
- [@dylwil3](https://github.com/dylwil3)
- [@njhearp](https://github.com/njhearp)
- [@woodruffw](https://github.com/woodruffw)
- [@dcreager](https://github.com/dcreager)
- [@TaKO8Ki](https://github.com/TaKO8Ki)
- [@BurntSushi](https://github.com/BurntSushi)
- [@salahelfarissi](https://github.com/salahelfarissi)
- [@MichaReiser](https://github.com/MichaReiser)
## 0.13.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.13.0) for a migration

376
Cargo.lock generated
View File

@@ -23,6 +23,12 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -98,9 +104,9 @@ dependencies = [
[[package]]
name = "anstyle-svg"
version = "0.1.11"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b9ec8c976eada1b0f9747a3d7cc4eae3bef10613e443746e7487f26c872fde"
checksum = "dc03a770ef506fe1396c0e476120ac0e6523cf14b74218dd5f18cd6833326fa9"
dependencies = [
"anstyle",
"anstyle-lossy",
@@ -122,9 +128,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.100"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "approx"
@@ -278,9 +284,9 @@ dependencies = [
[[package]]
name = "boxcar"
version = "0.2.14"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
checksum = "26c4925bc979b677330a8c7fe7a8c94af2dbb4a2d37b4a20a80d884400f46baa"
[[package]]
name = "bstr"
@@ -340,11 +346,10 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.38"
version = "1.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9"
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
@@ -352,9 +357,9 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.3"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "cfg_aliases"
@@ -364,13 +369,14 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.42"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-link 0.2.0",
"windows-link 0.1.3",
]
[[package]]
@@ -402,9 +408,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.48"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
dependencies = [
"clap_builder",
"clap_derive",
@@ -412,9 +418,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.48"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
dependencies = [
"anstream",
"anstyle",
@@ -425,9 +431,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.58"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a"
checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
dependencies = [
"clap",
]
@@ -644,15 +650,15 @@ dependencies = [
[[package]]
name = "console"
version = "0.16.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.1",
"windows-sys 0.61.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -831,9 +837,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.21.3"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
@@ -841,9 +847,9 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.21.3"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
@@ -855,9 +861,9 @@ dependencies = [
[[package]]
name = "darling_macro"
version = "0.21.3"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
@@ -950,7 +956,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -1031,12 +1037,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.61.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1047,11 +1053,12 @@ checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6"
[[package]]
name = "escargot"
version = "0.5.15"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf"
checksum = "83f351750780493fc33fa0ce8ba3c7d61f9736cfa3b3bb9ee2342643ffe40211"
dependencies = [
"log",
"once_cell",
"serde",
"serde_json",
]
@@ -1094,12 +1101,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
[[package]]
name = "flate2"
version = "1.1.2"
@@ -1167,9 +1168,9 @@ dependencies = [
[[package]]
name = "get-size-derive2"
version = "0.7.0"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3814abc7da8ab18d2fd820f5b540b5e39b6af0a32de1bdd7c47576693074843"
checksum = "75a17a226478b2e8294ded60782c03efe54476aa8cd1371d0e5ad9d1071e74e0"
dependencies = [
"attribute-derive",
"quote",
@@ -1178,21 +1179,21 @@ dependencies = [
[[package]]
name = "get-size2"
version = "0.7.0"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfe2cec5b5ce8fb94dcdb16a1708baa4d0609cc3ce305ca5d3f6f2ffb59baed"
checksum = "5697765925a05c9d401dd04a93dfd662d336cc25fdcc3301220385a1ffcfdde5"
dependencies = [
"compact_str",
"get-size-derive2",
"hashbrown 0.16.0",
"hashbrown 0.15.5",
"smallvec",
]
[[package]]
name = "getopts"
version = "0.2.24"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
dependencies = [
"unicode-width 0.2.1",
]
@@ -1218,7 +1219,7 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
"wasi 0.14.7+wasi-0.2.4",
"wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
]
@@ -1279,15 +1280,6 @@ dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
dependencies = [
"equivalent",
]
[[package]]
name = "hashlink"
version = "0.10.0"
@@ -1329,9 +1321,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.64"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -1501,14 +1493,13 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.11.4"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
dependencies = [
"equivalent",
"hashbrown 0.16.0",
"hashbrown 0.15.5",
"serde",
"serde_core",
]
[[package]]
@@ -1517,7 +1508,7 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
dependencies = [
"console 0.16.1",
"console 0.16.0",
"portable-atomic",
"unicode-width 0.2.1",
"unit-prefix",
@@ -1597,9 +1588,9 @@ dependencies = [
[[package]]
name = "inventory"
version = "0.3.21"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83"
dependencies = [
"rustversion",
]
@@ -1728,9 +1719,9 @@ dependencies = [
[[package]]
name = "jobserver"
version = "0.1.34"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
"getrandom 0.3.3",
"libc",
@@ -1744,9 +1735,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24"
[[package]]
name = "js-sys"
version = "0.3.80"
version = "0.3.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e"
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1821,9 +1812,9 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.10"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
dependencies = [
"bitflags 2.9.4",
"libc",
@@ -1844,9 +1835,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litemap"
@@ -2142,13 +2133,12 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordermap"
version = "0.5.12"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b100f7dd605611822d30e182214d3c02fdefce2d801d23993f6b6ba6ca1392af"
checksum = "0dcd63f1ae4b091e314a26627c467dd8810d674ba798abc0e566679955776c63"
dependencies = [
"indexmap",
"serde",
"serde_core",
]
[[package]]
@@ -2299,9 +2289,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.2"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [
"memchr",
"thiserror 2.0.16",
@@ -2310,9 +2300,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.8.2"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663"
checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
dependencies = [
"pest",
"pest_generator",
@@ -2320,9 +2310,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.8.2"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f"
checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
dependencies = [
"pest",
"pest_meta",
@@ -2333,9 +2323,9 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.8.2"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420"
checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
dependencies = [
"pest",
"sha2",
@@ -2408,9 +2398,9 @@ dependencies = [
[[package]]
name = "potential_utf"
version = "0.1.3"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
dependencies = [
"zerovec",
]
@@ -2463,9 +2453,9 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
"toml_edit",
]
@@ -2715,15 +2705,15 @@ dependencies = [
[[package]]
name = "regex-lite"
version = "0.1.7"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ron"
@@ -2738,7 +2728,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.13.1"
version = "0.13.0"
dependencies = [
"anyhow",
"argfile",
@@ -2994,7 +2984,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.13.1"
version = "0.13.0"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3004,7 +2994,7 @@ dependencies = [
"fern",
"glob",
"globset",
"hashbrown 0.16.0",
"hashbrown 0.15.5",
"imperative",
"insta",
"is-macro",
@@ -3348,7 +3338,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.13.1"
version = "0.13.0"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3437,22 +3427,22 @@ checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08"
[[package]]
name = "rustix"
version = "1.1.2"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags 2.9.4",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.0",
"windows-sys 0.59.0",
]
[[package]]
name = "rustversion"
version = "1.0.22"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
@@ -3547,9 +3537,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.226"
version = "1.0.223"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac"
dependencies = [
"serde_core",
"serde_derive",
@@ -3568,18 +3558,18 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.226"
version = "1.0.223"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.226"
version = "1.0.223"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56"
dependencies = [
"proc-macro2",
"quote",
@@ -3623,11 +3613,11 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.2"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
dependencies = [
"serde_core",
"serde",
]
[[package]]
@@ -3641,9 +3631,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.14.1"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
dependencies = [
"serde",
"serde_derive",
@@ -3652,9 +3642,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.14.1"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
dependencies = [
"darling",
"proc-macro2",
@@ -3840,7 +3830,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.61.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -3854,12 +3844,12 @@ dependencies = [
[[package]]
name = "terminal_size"
version = "0.4.3"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
dependencies = [
"rustix",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -4019,9 +4009,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.10.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
dependencies = [
"tinyvec_macros",
]
@@ -4034,14 +4024,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.9.7"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [
"indexmap",
"serde_core",
"serde",
"serde_spanned",
"toml_datetime",
"toml_datetime 0.7.0",
"toml_parser",
"toml_writer",
"winnow",
@@ -4049,39 +4039,44 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.7.2"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
dependencies = [
"serde_core",
"serde",
]
[[package]]
name = "toml_edit"
version = "0.23.6"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"toml_datetime 0.6.11",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.0.3"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.3"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
[[package]]
name = "tracing"
@@ -4291,7 +4286,6 @@ dependencies = [
"tracing",
"ty_combine",
"ty_python_semantic",
"ty_static",
"ty_vendored",
]
@@ -4309,7 +4303,7 @@ dependencies = [
"drop_bomb",
"get-size2",
"glob",
"hashbrown 0.16.0",
"hashbrown 0.15.5",
"indexmap",
"insta",
"itertools 0.14.0",
@@ -4519,9 +4513,9 @@ dependencies = [
[[package]]
name = "unicode-id"
version = "0.3.6"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580"
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
[[package]]
name = "unicode-ident"
@@ -4746,27 +4740,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.7+wasi-0.2.4"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wasip2",
]
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
"wit-bindgen-rt",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.103"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819"
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
dependencies = [
"cfg-if",
"once_cell",
@@ -4777,9 +4762,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.103"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c"
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
dependencies = [
"bumpalo",
"log",
@@ -4791,9 +4776,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.53"
version = "0.4.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67"
checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
dependencies = [
"cfg-if",
"js-sys",
@@ -4804,9 +4789,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.103"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0"
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4814,9 +4799,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.103"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32"
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
dependencies = [
"proc-macro2",
"quote",
@@ -4827,18 +4812,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.103"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf"
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
version = "0.3.53"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aee0a0f5343de9221a0d233b04520ed8dc2e6728dce180b1dcd9288ec9d9fa3c"
checksum = "80cc7f8a4114fdaa0c58383caf973fc126cf004eba25c9dc639bccd3880d55ad"
dependencies = [
"js-sys",
"minicov",
@@ -4849,9 +4834,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.53"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a369369e4360c2884c3168d22bded735c43cccae97bbc147586d4b480edd138d"
checksum = "c5ada2ab788d46d4bda04c9d567702a79c8ced14f51f221646a16ed39d0e6a5d"
dependencies = [
"proc-macro2",
"quote",
@@ -4860,9 +4845,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.80"
version = "0.3.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4900,22 +4885,22 @@ dependencies = [
[[package]]
name = "winapi-util"
version = "0.1.11"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.61.0",
"windows-sys 0.59.0",
]
[[package]]
name = "windows-core"
version = "0.62.0"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.2.0",
"windows-link 0.1.3",
"windows-result",
"windows-strings",
]
@@ -4956,20 +4941,20 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-result"
version = "0.4.0"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link 0.2.0",
"windows-link 0.1.3",
]
[[package]]
name = "windows-strings"
version = "0.5.0"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link 0.2.0",
"windows-link 0.1.3",
]
[[package]]
@@ -5139,9 +5124,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.13"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
dependencies = [
"memchr",
]
@@ -5153,10 +5138,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "wit-bindgen"
version = "0.46.0"
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.4",
]
[[package]]
name = "writeable"
@@ -5205,18 +5193,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.27"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",
@@ -5311,9 +5299,9 @@ dependencies = [
[[package]]
name = "zstd-sys"
version = "2.0.16+zstd.1.5.7"
version = "2.0.15+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
dependencies = [
"cc",
"pkg-config",

View File

@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
# Please update rustfmt.toml when bumping the Rust edition
edition = "2024"
rust-version = "1.88"
rust-version = "1.87"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
@@ -86,7 +86,7 @@ etcetera = { version = "0.10.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
get-size2 = { version = "0.7.0", features = [
get-size2 = { version = "0.6.2", features = [
"derive",
"smallvec",
"hashbrown",
@@ -95,7 +95,7 @@ get-size2 = { version = "0.7.0", features = [
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
globwalk = { version = "0.9.1" }
hashbrown = { version = "0.16.0", default-features = false, features = [
hashbrown = { version = "0.15.0", default-features = false, features = [
"raw-entry",
"equivalent",
"inline-more",
@@ -203,7 +203,7 @@ wild = { version = "2" }
zip = { version = "0.6.6", default-features = false }
[workspace.metadata.cargo-shear]
ignored = ["getrandom", "ruff_options_metadata", "uuid", "get-size2"]
ignored = ["getrandom", "ruff_options_metadata", "uuid"]
[workspace.lints.rust]

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.13.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.13.1/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.13.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.13.0/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.1
rev: v0.13.0
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

@@ -500,35 +500,6 @@ OTHER = "OTHER"
Ok(())
}
/// Regression test for <https://github.com/astral-sh/ruff/issues/20035>
#[test]
fn deduplicate_directory_and_explicit_file() -> Result<()> {
let tempdir = TempDir::new()?;
let root = tempdir.path();
let main = root.join("main.py");
fs::write(&main, "x = 1\n")?;
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.current_dir(root)
.args(["format", "--no-cache", "--check"])
.arg(".")
.arg("main.py"),
@r"
success: false
exit_code: 1
----- stdout -----
Would reformat: main.py
1 file would be reformatted
----- stderr -----
"
);
Ok(())
}
#[test]
fn syntax_error() -> Result<()> {
let tempdir = TempDir::new()?;

View File

@@ -271,50 +271,6 @@ OTHER = "OTHER"
Ok(())
}
/// Regression test for <https://github.com/astral-sh/ruff/issues/20035>
#[test]
fn deduplicate_directory_and_explicit_file() -> Result<()> {
let tempdir = TempDir::new()?;
let root = tempdir.path();
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
exclude = ["main.py"]
"#,
)?;
let main = root.join("main.py");
fs::write(&main, "import os\n")?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.current_dir(root)
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.arg(".")
// Explicitly pass main.py, should be linted regardless of it being excluded by lint.exclude
.arg("main.py"),
@r"
success: false
exit_code: 1
----- stdout -----
main.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"
);
});
Ok(())
}
#[test]
fn exclude_stdin() -> Result<()> {
let tempdir = TempDir::new()?;

View File

@@ -22,30 +22,6 @@ exit_code: 1
{
"results": [
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 1,
"endLine": 2,
"startColumn": 1,
"startLine": 1
}
}
]
}
],
"description": {
"text": "Remove unused import: `os`"
}
}
],
"level": "error",
"locations": [
{

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="182px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="164px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,7 +1,7 @@
<svg width="1356px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="869px" height="236px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="911px" height="236px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,7 +1,7 @@
<svg width="768px" height="290px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="146px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,7 +1,7 @@
<svg width="1196px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="182px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="1196px" height="164px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { fill: #000000 }
.bg { background: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -232,7 +232,7 @@ static STATIC_FRAME: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLo
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
600,
500,
)
});

View File

@@ -58,11 +58,10 @@ impl<'a> FullRenderer<'a> {
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
}
if self.config.show_fix_diff
&& diag.has_applicable_fix(self.config)
&& let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver)
{
write!(f, "{diff}")?;
if self.config.show_fix_diff && diag.has_applicable_fix(self.config) {
if let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver) {
write!(f, "{diff}")?;
}
}
writeln!(f)?;

View File

@@ -459,6 +459,12 @@ impl File {
self.source_type(db).is_stub()
}
/// Returns `true` if the file is an `__init__.py(i)`
pub fn is_init(self, db: &dyn Db) -> bool {
let path = self.path(db).as_str();
path.ends_with("__init__.py") || path.ends_with("__init__.pyi")
}
pub fn source_type(self, db: &dyn Db) -> PySourceType {
match self.path(db) {
FilePath::System(path) => path

View File

@@ -504,8 +504,8 @@ impl ToOwned for SystemPath {
pub struct SystemPathBuf(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Utf8PathBuf);
impl get_size2::GetSize for SystemPathBuf {
fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
(self.0.capacity(), tracker)
fn get_heap_size(&self) -> usize {
self.0.capacity()
}
}

View File

@@ -92,8 +92,8 @@ impl ToOwned for VendoredPath {
pub struct VendoredPathBuf(Utf8PathBuf);
impl get_size2::GetSize for VendoredPathBuf {
fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
(self.0.capacity(), tracker)
fn get_heap_size(&self) -> usize {
self.0.capacity()
}
}

View File

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

View File

@@ -1,104 +0,0 @@
import os
from typing import Optional
from pathlib import Path
## Valid cases:
def os_path_in_foo():
file = "file.txt"
os.path.abspath(file) # OK
os.path.exists(file) # OK
os.path.split() # OK
async def non_io_os_path_methods():
os.path.split() # OK
os.path.dirname() # OK
os.path.basename() # OK
os.path.join() # OK
def pathlib_path_in_foo():
path = Path("src/my_text.txt") # OK
path.exists() # OK
with path.open() as f: # OK
...
path = Path("src/my_text.txt").open() # OK
async def non_io_pathlib_path_methods():
path = Path("src/my_text.txt")
path.is_absolute() # OK
path.is_relative_to() # OK
path.as_posix() # OK
path.relative_to() # OK
def inline_path_method_call():
Path("src/my_text.txt").open() # OK
Path("src/my_text.txt").open().flush() # OK
with Path("src/my_text.txt").open() as f: # OK
...
async def trio_path_in_foo():
from trio import Path
path = Path("src/my_text.txt") # OK
await path.absolute() # OK
await path.exists() # OK
with Path("src/my_text.txt").open() as f: # OK
...
async def anyio_path_in_foo():
from anyio import Path
path = Path("src/my_text.txt") # OK
await path.absolute() # OK
await path.exists() # OK
with Path("src/my_text.txt").open() as f: # OK
...
async def path_open_in_foo():
path = Path("src/my_text.txt") # OK
path.open() # OK, covered by ASYNC230
## Invalid cases:
async def os_path_in_foo():
file = "file.txt"
os.path.abspath(file) # ASYNC240
os.path.exists(file) # ASYNC240
async def pathlib_path_in_foo():
path = Path("src/my_text.txt")
path.exists() # ASYNC240
async def pathlib_path_in_foo():
import pathlib
path = pathlib.Path("src/my_text.txt")
path.exists() # ASYNC240
async def inline_path_method_call():
Path("src/my_text.txt").exists() # ASYNC240
Path("src/my_text.txt").absolute().exists() # ASYNC240
async def aliased_path_in_foo():
from pathlib import Path as PathAlias
path = PathAlias("src/my_text.txt")
path.exists() # ASYNC240
global_path = Path("src/my_text.txt")
async def global_path_in_foo():
global_path.exists() # ASYNC240
async def path_as_simple_parameter_type(path: Path):
path.exists() # ASYNC240
async def path_as_union_parameter_type(path: Path | None):
path.exists() # ASYNC240
async def path_as_optional_parameter_type(path: Optional[Path]):
path.exists() # ASYNC240

View File

@@ -52,21 +52,3 @@ class A:
assert hasattr(A(), "__call__")
assert callable(A()) is False
# https://github.com/astral-sh/ruff/issues/20440
def test_invalid_hasattr_calls():
hasattr(0, "__call__", 0) # 3 args - invalid
hasattr(0, "__call__", x=0) # keyword arg - invalid
hasattr(0, "__call__", 0, x=0) # 3 args + keyword - invalid
hasattr() # no args - invalid
hasattr(0) # 1 arg - invalid
hasattr(*(), "__call__", "extra") # unpacking - invalid
hasattr(*()) # unpacking - invalid
def test_invalid_getattr_calls():
getattr(0, "__call__", None, "extra") # 4 args - invalid
getattr(0, "__call__", default=None) # keyword arg - invalid
getattr() # no args - invalid
getattr(0) # 1 arg - invalid
getattr(*(), "__call__", None, "extra") # unpacking - invalid
getattr(*()) # unpacking - invalid

View File

@@ -1,33 +0,0 @@
from itertools import count, cycle, repeat
# Errors
map(lambda x: x, [1, 2, 3])
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
# Errors (limited iterators).
map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
import builtins
# Still an error even though it uses the qualified name
builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
# OK
map(lambda x: x, [1, 2, 3], strict=True)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=True)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=False)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=True)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True), strict=True)
# OK (single iterable - no strict required)
map(lambda x: x, [1, 2, 3])
# OK (infinite iterators)
map(lambda x, y: x + y, [1, 2, 3], cycle([1, 2, 3]))
map(lambda x, y: x + y, [1, 2, 3], repeat(1))
map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=None))
map(lambda x, y: x + y, [1, 2, 3], count())

View File

@@ -19,18 +19,3 @@ class MyClass:
def attribute_usage(self) -> id:
pass
class C:
@staticmethod
def property(f):
return f
id = 1
@[property][0]
def f(self, x=[id]):
return x
bin = 2
foo = [bin]

View File

@@ -42,6 +42,3 @@ tuple(
x for x in [1,2,3]
}
)
t9 = tuple([1],)
t10 = tuple([1, 2],)

View File

@@ -170,4 +170,3 @@ print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
# leading/trailing whitespace should not count towards maxsplit
" a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
" a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
"a b".split(maxsplit=1) # ["a", "b"]

View File

@@ -125,15 +125,3 @@ os.makedirs("name", 0o777, False)
os.makedirs(name="name", mode=0o777, exist_ok=False)
os.makedirs("name", unknown_kwarg=True)
# https://github.com/astral-sh/ruff/issues/20134
os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
# Only diagnostic
os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)

View File

@@ -16,9 +16,7 @@ nok4 = "a".join([a, a, *a]) # Not OK (not a static length)
nok5 = "a".join([choice("flarp")]) # Not OK (not a simple call)
nok6 = "a".join(x for x in "feefoofum") # Not OK (generator)
nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
# https://github.com/astral-sh/ruff/issues/19887
nok8 = '\n'.join([r'line1','line2'])
nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
# Regression test for: https://github.com/astral-sh/ruff/issues/7197
def create_file_public_url(url, filename):

View File

@@ -271,35 +271,3 @@ class ChildI9(ParentI):
if False: super
if False: __class__
builtins.super(ChildI9, self).f()
# See: https://github.com/astral-sh/ruff/issues/20422
# UP008 should not apply when the class variable is shadowed
class A:
def f(self):
return 1
class B(A):
def f(self):
return 2
class C(B):
def f(self):
C = B # Local variable C shadows the class name
return super(C, self).f() # Should NOT trigger UP008
# See: https://github.com/astral-sh/ruff/issues/20491
# UP008 should not apply when __class__ is a local variable
class A:
def f(self):
return 1
class B(A):
def f(self):
return 2
class C(B):
def f(self):
__class__ = B # Local variable __class__ shadows the implicit __class__
return super(__class__, self).f() # Should NOT trigger UP008

View File

@@ -117,31 +117,3 @@ def _():
# After
] and \
0 < 1: ...
# https://github.com/astral-sh/ruff/issues/20255
import math
# NaN behavior differences
if math.nan in [math.nan]:
print("This is True")
if math.nan in (math.nan,):
print("This is True")
if math.nan in {math.nan}:
print("This is True")
# Potential type differences with custom __eq__ methods
class CustomEq:
def __eq__(self, other):
return "custom"
obj = CustomEq()
if obj in [CustomEq()]:
pass
if obj in (CustomEq(),):
pass
if obj in {CustomEq()}:
pass

View File

@@ -51,13 +51,3 @@ if 1 in set(1,2):
if 1 in set((x for x in range(2))):
pass
# https://github.com/astral-sh/ruff/issues/20255
import math
# set() and frozenset() with NaN
if math.nan in set([math.nan]):
print("This should be marked unsafe")
if math.nan in frozenset([math.nan]):
print("This should be marked unsafe")

View File

@@ -1,39 +0,0 @@
import logging
# %s + str()
logging.info("Hello %s", str("World!"))
logging.log(logging.INFO, "Hello %s", str("World!"))
# %s + repr()
logging.info("Hello %s", repr("World!"))
logging.log(logging.INFO, "Hello %s", repr("World!"))
# %r + str()
logging.info("Hello %r", str("World!"))
logging.log(logging.INFO, "Hello %r", str("World!"))
# %r + repr()
logging.info("Hello %r", repr("World!"))
logging.log(logging.INFO, "Hello %r", repr("World!"))
from logging import info, log
# %s + str()
info("Hello %s", str("World!"))
log(logging.INFO, "Hello %s", str("World!"))
# %s + repr()
info("Hello %s", repr("World!"))
log(logging.INFO, "Hello %s", repr("World!"))
# %r + str()
info("Hello %r", str("World!"))
log(logging.INFO, "Hello %r", str("World!"))
# %r + repr()
info("Hello %r", repr("World!"))
log(logging.INFO, "Hello %r", repr("World!"))
def str(s): return f"str = {s}"
# Don't flag this
logging.info("Hello %s", str("World!"))

View File

@@ -669,9 +669,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) {
flake8_async::rules::blocking_open_call(checker, call);
}
if checker.is_rule_enabled(Rule::BlockingPathMethodInAsyncFunction) {
flake8_async::rules::blocking_os_path(checker, call);
}
if checker.any_rule_enabled(&[
Rule::CreateSubprocessInAsyncFunction,
Rule::RunProcessInAsyncFunction,
@@ -720,9 +717,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_bugbear::rules::re_sub_positional_args(checker, call);
}
if checker.is_rule_enabled(Rule::UnreliableCallableCheck) {
flake8_bugbear::rules::unreliable_callable_check(
checker, expr, func, args, keywords,
);
flake8_bugbear::rules::unreliable_callable_check(checker, expr, func, args);
}
if checker.is_rule_enabled(Rule::StripWithMultiCharacters) {
flake8_bugbear::rules::strip_with_multi_characters(checker, expr, func, args);
@@ -746,11 +741,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_bugbear::rules::zip_without_explicit_strict(checker, call);
}
}
if checker.is_rule_enabled(Rule::MapWithoutExplicitStrict) {
if checker.target_version() >= PythonVersion::PY314 {
flake8_bugbear::rules::map_without_explicit_strict(checker, call);
}
}
if checker.is_rule_enabled(Rule::NoExplicitStacklevel) {
flake8_bugbear::rules::no_explicit_stacklevel(checker, call);
}
@@ -1289,9 +1279,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::UnnecessaryEmptyIterableWithinDequeCall) {
ruff::rules::unnecessary_literal_within_deque_call(checker, call);
}
if checker.is_rule_enabled(Rule::LoggingEagerConversion) {
ruff::rules::logging_eager_conversion(checker, call);
}
if checker.is_rule_enabled(Rule::StarmapZip) {
ruff::rules::starmap_zip(checker, call);
}

View File

@@ -56,7 +56,7 @@ use ruff_python_semantic::{
Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel,
SemanticModelFlags, StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{python_builtins, python_magic_globals};
use ruff_python_stdlib::builtins::{MAGIC_GLOBALS, python_builtins};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -2550,7 +2550,7 @@ impl<'a> Checker<'a> {
for builtin in standard_builtins {
bind_builtin(builtin);
}
for builtin in python_magic_globals(target_version.minor) {
for builtin in MAGIC_GLOBALS {
bind_builtin(builtin);
}
for builtin in &settings.builtins {

View File

@@ -341,7 +341,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),
(Flake8Async, "230") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOpenCallInAsyncFunction),
(Flake8Async, "240") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingPathMethodInAsyncFunction),
(Flake8Async, "250") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingInputInAsyncFunction),
(Flake8Async, "251") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingSleepInAsyncFunction),
@@ -395,7 +394,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict),
(Flake8Bugbear, "909") => (RuleGroup::Preview, rules::flake8_bugbear::rules::LoopIteratorMutation),
(Flake8Bugbear, "911") => (RuleGroup::Stable, rules::flake8_bugbear::rules::BatchedWithoutExplicitStrict),
(Flake8Bugbear, "912") => (RuleGroup::Preview, rules::flake8_bugbear::rules::MapWithoutExplicitStrict),
// flake8-blind-except
(Flake8BlindExcept, "001") => (RuleGroup::Stable, rules::flake8_blind_except::rules::BlindExcept),
@@ -1053,8 +1051,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises),
(Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict),
(Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::NonOctalPermissions),
(Ruff, "065") => (RuleGroup::Preview, rules::ruff::rules::LoggingEagerConversion),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
(Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode),

View File

@@ -65,7 +65,7 @@ pub(crate) fn remove_imports<'a>(
if member == "*" {
found_star = true;
} else {
bail!("Expected \"*\" for unused import (got: \"{member}\")");
bail!("Expected \"*\" for unused import (got: \"{}\")", member);
}
}
if !found_star {

View File

@@ -2,24 +2,17 @@ use std::collections::HashSet;
use std::io::Write;
use anyhow::Result;
use log::warn;
use serde::{Serialize, Serializer};
use serde_json::json;
use ruff_db::diagnostic::{Diagnostic, SecondaryCode};
use ruff_source_file::{OneIndexed, SourceFile};
use ruff_text_size::{Ranged, TextRange};
use ruff_source_file::OneIndexed;
use crate::VERSION;
use crate::fs::normalize_path;
use crate::message::{Emitter, EmitterContext};
use crate::registry::{Linter, RuleNamespace};
/// An emitter for producing SARIF 2.1.0-compliant JSON output.
///
/// Static Analysis Results Interchange Format (SARIF) is a standard format
/// for static analysis results. For full specfification, see:
/// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
pub struct SarifEmitter;
impl Emitter for SarifEmitter {
@@ -36,7 +29,7 @@ impl Emitter for SarifEmitter {
let unique_rules: HashSet<_> = results
.iter()
.filter_map(|result| result.rule_id.as_secondary_code())
.filter_map(|result| result.code.as_secondary_code())
.collect();
let mut rules: Vec<SarifRule> = unique_rules.into_iter().map(SarifRule::from).collect();
rules.sort_by(|a, b| a.code.cmp(b.code));
@@ -141,15 +134,6 @@ impl RuleCode<'_> {
}
}
impl Serialize for RuleCode<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
fn from(code: &'a Diagnostic) -> Self {
match code.secondary_code() {
@@ -159,83 +143,12 @@ impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
}
}
/// Represents a single result in a SARIF 2.1.0 report.
///
/// See the SARIF 2.1.0 specification for details:
/// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(Debug)]
struct SarifResult<'a> {
rule_id: RuleCode<'a>,
code: RuleCode<'a>,
level: String,
message: SarifMessage,
locations: Vec<SarifLocation>,
#[serde(skip_serializing_if = "Vec::is_empty")]
fixes: Vec<SarifFix>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifMessage {
text: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifPhysicalLocation {
artifact_location: SarifArtifactLocation,
region: SarifRegion,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifLocation {
physical_location: SarifPhysicalLocation,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifFix {
description: RuleDescription,
artifact_changes: Vec<SarifArtifactChange>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct RuleDescription {
text: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifArtifactChange {
artifact_location: SarifArtifactLocation,
replacements: Vec<SarifReplacement>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifArtifactLocation {
message: String,
uri: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifReplacement {
deleted_region: SarifRegion,
#[serde(skip_serializing_if = "Option::is_none")]
inserted_content: Option<InsertedContent>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct InsertedContent {
text: String,
}
#[derive(Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "camelCase")]
struct SarifRegion {
start_line: OneIndexed,
start_column: OneIndexed,
end_line: OneIndexed,
@@ -243,108 +156,71 @@ struct SarifRegion {
}
impl<'a> SarifResult<'a> {
fn range_to_sarif_region(source_file: &SourceFile, range: TextRange) -> SarifRegion {
let source_code = source_file.to_source_code();
let start_location = source_code.line_column(range.start());
let end_location = source_code.line_column(range.end());
SarifRegion {
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
}
}
fn fix(diagnostic: &'a Diagnostic, uri: &str) -> Option<SarifFix> {
let fix = diagnostic.fix()?;
let Some(source_file) = diagnostic.ruff_source_file() else {
debug_assert!(
false,
"Omitting the fix for diagnostic with id `{}` because the source file is missing. This is a bug in Ruff, please report an issue.",
diagnostic.id()
);
warn!(
"Omitting the fix for diagnostic with id `{}` because the source file is missing. This is a bug in Ruff, please report an issue.",
diagnostic.id()
);
return None;
};
let fix_description = diagnostic
.first_help_text()
.map(std::string::ToString::to_string);
let replacements: Vec<SarifReplacement> = fix
.edits()
.iter()
.map(|edit| {
let range = edit.range();
let deleted_region = Self::range_to_sarif_region(source_file, range);
SarifReplacement {
deleted_region,
inserted_content: edit.content().map(|content| InsertedContent {
text: content.to_string(),
}),
}
})
.collect();
let artifact_changes = vec![SarifArtifactChange {
artifact_location: SarifArtifactLocation {
uri: uri.to_string(),
},
replacements,
}];
Some(SarifFix {
description: RuleDescription {
text: fix_description,
},
artifact_changes,
})
}
#[allow(clippy::unnecessary_wraps)]
fn uri(diagnostic: &Diagnostic) -> Result<String> {
let path = normalize_path(&*diagnostic.expect_ruff_filename());
#[cfg(not(target_arch = "wasm32"))]
return url::Url::from_file_path(&path)
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))
.map(|u| u.to_string());
#[cfg(target_arch = "wasm32")]
return Ok(format!("file://{}", path.display()));
}
fn from_message(diagnostic: &'a Diagnostic) -> Result<Self> {
let start_location = diagnostic.ruff_start_location().unwrap_or_default();
let end_location = diagnostic.ruff_end_location().unwrap_or_default();
let region = SarifRegion {
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
};
let uri = Self::uri(diagnostic)?;
#[cfg(not(target_arch = "wasm32"))]
fn from_message(message: &'a Diagnostic) -> Result<Self> {
let start_location = message.ruff_start_location().unwrap_or_default();
let end_location = message.ruff_end_location().unwrap_or_default();
let path = normalize_path(&*message.expect_ruff_filename());
Ok(Self {
rule_id: RuleCode::from(diagnostic),
code: RuleCode::from(message),
level: "error".to_string(),
message: SarifMessage {
text: diagnostic.body().to_string(),
},
fixes: Self::fix(diagnostic, &uri).into_iter().collect(),
locations: vec![SarifLocation {
physical_location: SarifPhysicalLocation {
artifact_location: SarifArtifactLocation { uri },
region,
},
}],
message: message.body().to_string(),
uri: url::Url::from_file_path(&path)
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))?
.to_string(),
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
})
}
#[cfg(target_arch = "wasm32")]
#[expect(clippy::unnecessary_wraps)]
fn from_message(message: &'a Diagnostic) -> Result<Self> {
let start_location = message.ruff_start_location().unwrap_or_default();
let end_location = message.ruff_end_location().unwrap_or_default();
let path = normalize_path(&*message.expect_ruff_filename());
Ok(Self {
code: RuleCode::from(message),
level: "error".to_string(),
message: message.body().to_string(),
uri: path.display().to_string(),
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
})
}
}
impl Serialize for SarifResult<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
json!({
"level": self.level,
"message": {
"text": self.message,
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": self.uri,
},
"region": {
"startLine": self.start_line,
"startColumn": self.start_column,
"endLine": self.end_line,
"endColumn": self.end_column,
}
}
}],
"ruleId": self.code.as_str(),
})
.serialize(serializer)
}
}
#[cfg(test)]
@@ -380,7 +256,6 @@ mod tests {
insta::assert_json_snapshot!(value, {
".runs[0].tool.driver.version" => "[VERSION]",
".runs[0].results[].locations[].physicalLocation.artifactLocation.uri" => "[URI]",
".runs[0].results[].fixes[].artifactChanges[].artifactLocation.uri" => "[URI]",
});
}
}

View File

@@ -8,30 +8,6 @@ expression: value
{
"results": [
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "[URI]"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 1,
"endLine": 2,
"startColumn": 1,
"startLine": 1
}
}
]
}
],
"description": {
"text": "Remove unused import: `os`"
}
}
],
"level": "error",
"locations": [
{
@@ -54,30 +30,6 @@ expression: value
"ruleId": "F401"
},
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "[URI]"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 10,
"endLine": 6,
"startColumn": 5,
"startLine": 6
}
}
]
}
],
"description": {
"text": "Remove assignment to unused variable `x`"
}
}
],
"level": "error",
"locations": [
{

View File

@@ -228,10 +228,3 @@ pub(crate) const fn is_sim910_expanded_key_support_enabled(settings: &LinterSett
pub(crate) const fn is_fix_builtin_open_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20178
pub(crate) const fn is_a003_class_scope_shadowing_expansion_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}

View File

@@ -487,6 +487,7 @@ impl<'a> Iterator for PathParamIterator<'a> {
let param_name_end = param_content.find(':').unwrap_or(param_content.len());
let param_name = &param_content[..param_name_end];
#[expect(clippy::range_plus_one)]
return Some((param_name, start..end + 1));
}
}

View File

@@ -28,7 +28,6 @@ mod tests {
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::BlockingOpenCallInAsyncFunction, Path::new("ASYNC230.py"))]
#[test_case(Rule::BlockingPathMethodInAsyncFunction, Path::new("ASYNC240.py"))]
#[test_case(Rule::BlockingInputInAsyncFunction, Path::new("ASYNC250.py"))]
#[test_case(Rule::BlockingSleepInAsyncFunction, Path::new("ASYNC251.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@@ -1,246 +0,0 @@
use crate::Violation;
use crate::checkers::ast::Checker;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_python_semantic::analyze::typing::{TypeChecker, check_type, traverse_union_and_optional};
use ruff_text_size::Ranged;
/// ## What it does
/// Checks that async functions do not call blocking `os.path` or `pathlib.Path`
/// methods.
///
/// ## Why is this bad?
/// Calling some `os.path` or `pathlib.Path` methods in an async function will block
/// the entire event loop, preventing it from executing other tasks while waiting
/// for the operation. This negates the benefits of asynchronous programming.
///
/// Instead, use the methods' async equivalents from `trio.Path` or `anyio.Path`.
///
/// ## Example
/// ```python
/// import os
///
///
/// async def func():
/// path = "my_file.txt"
/// file_exists = os.path.exists(path)
/// ```
///
/// Use instead:
/// ```python
/// import trio
///
///
/// async def func():
/// path = trio.Path("my_file.txt")
/// file_exists = await path.exists()
/// ```
///
/// Non-blocking methods are OK to use:
/// ```python
/// import pathlib
///
///
/// async def func():
/// path = pathlib.Path("my_file.txt")
/// file_dirname = path.dirname()
/// new_path = os.path.join("/tmp/src/", path)
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct BlockingPathMethodInAsyncFunction {
path_library: String,
}
impl Violation for BlockingPathMethodInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Async functions should not use {path_library} methods, use trio.Path or anyio.path",
path_library = self.path_library
)
}
}
/// ASYNC240
pub(crate) fn blocking_os_path(checker: &Checker, call: &ExprCall) {
let semantic = checker.semantic();
if !semantic.in_async_context() {
return;
}
// Check if an expression is calling I/O related os.path method.
// Just initializing pathlib.Path object is OK, we can return
// early in that scenario.
if let Some(qualified_name) = semantic.resolve_qualified_name(call.func.as_ref()) {
let segments = qualified_name.segments();
if !matches!(segments, ["os", "path", _]) {
return;
}
let Some(os_path_method) = segments.last() else {
return;
};
if maybe_calling_io_operation(os_path_method) {
checker.report_diagnostic(
BlockingPathMethodInAsyncFunction {
path_library: "os.path".to_string(),
},
call.func.range(),
);
}
return;
}
let Some(ast::ExprAttribute { value, attr, .. }) = call.func.as_attribute_expr() else {
return;
};
if !maybe_calling_io_operation(attr.id.as_str()) {
return;
}
// Check if an expression is a pathlib.Path constructor that directly
// calls an I/O method.
if PathlibPathChecker::match_initializer(value, semantic) {
checker.report_diagnostic(
BlockingPathMethodInAsyncFunction {
path_library: "pathlib.Path".to_string(),
},
call.func.range(),
);
return;
}
// Lastly, check if a variable is a pathlib.Path instance and it's
// calling an I/O method.
let Some(name) = value.as_name_expr() else {
return;
};
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
return;
};
if check_type::<PathlibPathChecker>(binding, semantic) {
checker.report_diagnostic(
BlockingPathMethodInAsyncFunction {
path_library: "pathlib.Path".to_string(),
},
call.func.range(),
);
}
}
struct PathlibPathChecker;
impl PathlibPathChecker {
fn is_pathlib_path_constructor(
semantic: &ruff_python_semantic::SemanticModel,
expr: &Expr,
) -> bool {
let Some(qualified_name) = semantic.resolve_qualified_name(expr) else {
return false;
};
matches!(
qualified_name.segments(),
[
"pathlib",
"Path"
| "PosixPath"
| "PurePath"
| "PurePosixPath"
| "PureWindowsPath"
| "WindowsPath"
]
)
}
}
impl TypeChecker for PathlibPathChecker {
fn match_annotation(annotation: &Expr, semantic: &ruff_python_semantic::SemanticModel) -> bool {
if Self::is_pathlib_path_constructor(semantic, annotation) {
return true;
}
let mut found = false;
traverse_union_and_optional(
&mut |inner_expr, _| {
if Self::is_pathlib_path_constructor(semantic, inner_expr) {
found = true;
}
},
semantic,
annotation,
);
found
}
fn match_initializer(
initializer: &Expr,
semantic: &ruff_python_semantic::SemanticModel,
) -> bool {
let Expr::Call(ast::ExprCall { func, .. }) = initializer else {
return false;
};
Self::is_pathlib_path_constructor(semantic, func)
}
}
fn maybe_calling_io_operation(attr: &str) -> bool {
// ".open()" is added to the allow list to let ASYNC 230 handle
// that case.
!matches!(
attr,
"ALLOW_MISSING"
| "altsep"
| "anchor"
| "as_posix"
| "as_uri"
| "basename"
| "commonpath"
| "commonprefix"
| "curdir"
| "defpath"
| "devnull"
| "dirname"
| "drive"
| "expandvars"
| "extsep"
| "genericpath"
| "is_absolute"
| "is_relative_to"
| "is_reserved"
| "isabs"
| "join"
| "joinpath"
| "match"
| "name"
| "normcase"
| "os"
| "open"
| "pardir"
| "parent"
| "parents"
| "parts"
| "pathsep"
| "relative_to"
| "root"
| "samestat"
| "sep"
| "split"
| "splitdrive"
| "splitext"
| "splitroot"
| "stem"
| "suffix"
| "suffixes"
| "supports_unicode_filenames"
| "sys"
| "with_name"
| "with_segments"
| "with_stem"
| "with_suffix"
)
}

View File

@@ -5,7 +5,6 @@ pub(crate) use blocking_http_call::*;
pub(crate) use blocking_http_call_httpx::*;
pub(crate) use blocking_input::*;
pub(crate) use blocking_open_call::*;
pub(crate) use blocking_path_methods::*;
pub(crate) use blocking_process_invocation::*;
pub(crate) use blocking_sleep::*;
pub(crate) use cancel_scope_no_checkpoint::*;
@@ -19,7 +18,6 @@ mod blocking_http_call;
mod blocking_http_call_httpx;
mod blocking_input;
mod blocking_open_call;
mod blocking_path_methods;
mod blocking_process_invocation;
mod blocking_sleep;
mod cancel_scope_no_checkpoint;

View File

@@ -1,111 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC240 Async functions should not use os.path methods, use trio.Path or anyio.path
--> ASYNC240.py:67:5
|
65 | file = "file.txt"
66 |
67 | os.path.abspath(file) # ASYNC240
| ^^^^^^^^^^^^^^^
68 | os.path.exists(file) # ASYNC240
|
ASYNC240 Async functions should not use os.path methods, use trio.Path or anyio.path
--> ASYNC240.py:68:5
|
67 | os.path.abspath(file) # ASYNC240
68 | os.path.exists(file) # ASYNC240
| ^^^^^^^^^^^^^^
69 |
70 | async def pathlib_path_in_foo():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:72:5
|
70 | async def pathlib_path_in_foo():
71 | path = Path("src/my_text.txt")
72 | path.exists() # ASYNC240
| ^^^^^^^^^^^
73 |
74 | async def pathlib_path_in_foo():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:78:5
|
77 | path = pathlib.Path("src/my_text.txt")
78 | path.exists() # ASYNC240
| ^^^^^^^^^^^
79 |
80 | async def inline_path_method_call():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:81:5
|
80 | async def inline_path_method_call():
81 | Path("src/my_text.txt").exists() # ASYNC240
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 | Path("src/my_text.txt").absolute().exists() # ASYNC240
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:82:5
|
80 | async def inline_path_method_call():
81 | Path("src/my_text.txt").exists() # ASYNC240
82 | Path("src/my_text.txt").absolute().exists() # ASYNC240
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
83 |
84 | async def aliased_path_in_foo():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:88:5
|
87 | path = PathAlias("src/my_text.txt")
88 | path.exists() # ASYNC240
| ^^^^^^^^^^^
89 |
90 | global_path = Path("src/my_text.txt")
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:93:5
|
92 | async def global_path_in_foo():
93 | global_path.exists() # ASYNC240
| ^^^^^^^^^^^^^^^^^^
94 |
95 | async def path_as_simple_parameter_type(path: Path):
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:96:5
|
95 | async def path_as_simple_parameter_type(path: Path):
96 | path.exists() # ASYNC240
| ^^^^^^^^^^^
97 |
98 | async def path_as_union_parameter_type(path: Path | None):
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:99:5
|
98 | async def path_as_union_parameter_type(path: Path | None):
99 | path.exists() # ASYNC240
| ^^^^^^^^^^^
100 |
101 | async def path_as_optional_parameter_type(path: Optional[Path]):
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:102:5
|
101 | async def path_as_optional_parameter_type(path: Optional[Path]):
102 | path.exists() # ASYNC240
| ^^^^^^^^^^^
|

View File

@@ -4,7 +4,6 @@ use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange};
use crate::Locator;
use ruff_python_ast::{self as ast, Arguments, Expr};
/// Return `true` if the statement containing the current expression is the last
/// top-level expression in the cell. This assumes that the source is a Jupyter
@@ -28,54 +27,3 @@ pub(super) fn at_last_top_level_expression_in_cell(
.all(|token| token.kind() == SimpleTokenKind::Semi || token.kind().is_trivia())
})
}
/// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to
/// `itertools.cycle` or similar).
pub(crate) fn is_infinite_iterable(arg: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
}) = &arg
else {
return false;
};
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| match qualified_name.segments() {
["itertools", "cycle" | "count"] => true,
["itertools", "repeat"] => {
// Ex) `itertools.repeat(1)`
if keywords.is_empty() && args.len() == 1 {
return true;
}
// Ex) `itertools.repeat(1, None)`
if args.len() == 2 && args[1].is_none_literal_expr() {
return true;
}
// Ex) `itertools.repeat(1, times=None)`
for keyword in keywords {
if keyword.arg.as_ref().is_some_and(|name| name == "times")
&& keyword.value.is_none_literal_expr()
{
return true;
}
}
false
}
_ => false,
})
}
/// Return `true` if any expression in the iterator appears to be an infinite iterator.
pub(crate) fn any_infinite_iterables<'a>(
iter: impl IntoIterator<Item = &'a Expr>,
semantic: &SemanticModel,
) -> bool {
iter.into_iter()
.any(|arg| is_infinite_iterable(arg, semantic))
}

View File

@@ -16,7 +16,6 @@ mod tests {
use crate::settings::LinterSettings;
use crate::test::test_path;
use crate::settings::types::PreviewMode;
use ruff_python_ast::PythonVersion;
#[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))]
@@ -82,35 +81,11 @@ mod tests {
Ok(())
}
#[test_case(Rule::MapWithoutExplicitStrict, Path::new("B912.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
unresolved_target_version: PythonVersion::PY314.into(),
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(
Rule::ClassAsDataStructure,
Path::new("class_as_data_structure.py"),
PythonVersion::PY39
)]
#[test_case(
Rule::MapWithoutExplicitStrict,
Path::new("B912.py"),
PythonVersion::PY313
)]
fn rules_with_target_version(
rule_code: Rule,
path: &Path,

View File

@@ -3,7 +3,7 @@ use ruff_python_ast::ExprCall;
use ruff_python_ast::PythonVersion;
use crate::checkers::ast::Checker;
use crate::rules::flake8_bugbear::helpers::is_infinite_iterable;
use crate::rules::flake8_bugbear::rules::is_infinite_iterable;
use crate::{FixAvailability, Violation};
/// ## What it does

View File

@@ -1,89 +0,0 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
use crate::rules::flake8_bugbear::helpers::any_infinite_iterables;
use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ## What it does
/// Checks for `map` calls without an explicit `strict` parameter when called with two or more iterables.
///
/// This rule applies to Python 3.14 and later, where `map` accepts a `strict` keyword
/// argument. For details, see: [Whats New in Python 3.14](https://docs.python.org/dev/whatsnew/3.14.html).
///
/// ## Why is this bad?
/// By default, if the iterables passed to `map` are of different lengths, the
/// resulting iterator will be silently truncated to the length of the shortest
/// iterable. This can lead to subtle bugs.
///
/// Pass `strict=True` to raise a `ValueError` if the iterables are of
/// non-uniform length. Alternatively, if the iterables are deliberately of
/// different lengths, pass `strict=False` to make the intention explicit.
///
/// ## Example
/// ```python
/// map(f, a, b)
/// ```
///
/// Use instead:
/// ```python
/// map(f, a, b, strict=True)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe for `map` calls that contain
/// `**kwargs`, as adding a `strict` keyword argument to such a call may lead
/// to a duplicate keyword argument error.
///
/// ## References
/// - [Python documentation: `map`](https://docs.python.org/3/library/functions.html#map)
/// - [Whats New in Python 3.14](https://docs.python.org/dev/whatsnew/3.14.html)
#[derive(ViolationMetadata)]
pub(crate) struct MapWithoutExplicitStrict;
impl AlwaysFixableViolation for MapWithoutExplicitStrict {
#[derive_message_formats]
fn message(&self) -> String {
"`map()` without an explicit `strict=` parameter".to_string()
}
fn fix_title(&self) -> String {
"Add explicit value for parameter `strict=`".to_string()
}
}
/// B912
pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCall) {
let semantic = checker.semantic();
if semantic.match_builtin_expr(&call.func, "map")
&& call.arguments.find_keyword("strict").is_none()
&& call.arguments.args.len() >= 3 // function + at least 2 iterables
&& !any_infinite_iterables(call.arguments.args.iter().skip(1), semantic)
{
checker
.report_diagnostic(MapWithoutExplicitStrict, call.range())
.set_fix(Fix::applicable_edit(
add_argument(
"strict=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
// If the function call contains `**kwargs`, mark the fix as unsafe.
if call
.arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none())
{
Applicability::Unsafe
} else {
Applicability::Safe
},
));
}
}

View File

@@ -16,7 +16,6 @@ pub(crate) use getattr_with_constant::*;
pub(crate) use jump_statement_in_finally::*;
pub(crate) use loop_iterator_mutation::*;
pub(crate) use loop_variable_overrides_iterator::*;
pub(crate) use map_without_explicit_strict::*;
pub(crate) use mutable_argument_default::*;
pub(crate) use mutable_contextvar_default::*;
pub(crate) use no_explicit_stacklevel::*;
@@ -57,7 +56,6 @@ mod getattr_with_constant;
mod jump_statement_in_finally;
mod loop_iterator_mutation;
mod loop_variable_overrides_iterator;
mod map_without_explicit_strict;
mod mutable_argument_default;
mod mutable_contextvar_default;
mod no_explicit_stacklevel;

View File

@@ -90,11 +90,7 @@ pub(crate) fn unreliable_callable_check(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[ast::Keyword],
) {
if !keywords.is_empty() {
return;
}
let [obj, attr, ..] = args else {
return;
};
@@ -107,21 +103,7 @@ pub(crate) fn unreliable_callable_check(
let Some(builtins_function) = checker.semantic().resolve_builtin_symbol(func) else {
return;
};
// Validate function arguments based on function name
let valid_args = match builtins_function {
"hasattr" => {
// hasattr should have exactly 2 positional arguments and no keywords
args.len() == 2
}
"getattr" => {
// getattr should have 2 or 3 positional arguments and no keywords
args.len() == 2 || args.len() == 3
}
_ => return,
};
if !valid_args {
if !matches!(builtins_function, "hasattr" | "getattr") {
return;
}

View File

@@ -1,11 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast};
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
use crate::rules::flake8_bugbear::helpers::any_infinite_iterables;
use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ## What it does
@@ -57,7 +57,11 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
if semantic.match_builtin_expr(&call.func, "zip")
&& call.arguments.find_keyword("strict").is_none()
&& !any_infinite_iterables(call.arguments.args.iter(), semantic)
&& !call
.arguments
.args
.iter()
.any(|arg| is_infinite_iterable(arg, semantic))
{
checker
.report_diagnostic(ZipWithoutExplicitStrict, call.range())
@@ -82,3 +86,47 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
));
}
}
/// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to
/// `itertools.cycle` or similar).
pub(crate) fn is_infinite_iterable(arg: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
}) = &arg
else {
return false;
};
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
match qualified_name.segments() {
["itertools", "cycle" | "count"] => true,
["itertools", "repeat"] => {
// Ex) `itertools.repeat(1)`
if keywords.is_empty() && args.len() == 1 {
return true;
}
// Ex) `itertools.repeat(1, None)`
if args.len() == 2 && args[1].is_none_literal_expr() {
return true;
}
// Ex) `iterools.repeat(1, times=None)`
for keyword in keywords {
if keyword.arg.as_ref().is_some_and(|name| name == "times") {
if keyword.value.is_none_literal_expr() {
return true;
}
}
}
false
}
_ => false,
}
})
}

View File

@@ -156,6 +156,4 @@ help: Replace with `callable()`
- assert hasattr(A(), "__call__")
53 + assert callable(A())
54 | assert callable(A()) is False
55 |
56 | # https://github.com/astral-sh/ruff/issues/20440
note: This is an unsafe fix and may change runtime behavior

View File

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

View File

@@ -1,141 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:5:1
|
3 | # Errors
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
|
help: Add explicit value for parameter `strict=`
2 |
3 | # Errors
4 | map(lambda x: x, [1, 2, 3])
- map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
5 + map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=False)
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:6:1
|
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
|
help: Add explicit value for parameter `strict=`
3 | # Errors
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
- map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
6 + map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9], strict=False)
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:7:1
|
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
|
help: Add explicit value for parameter `strict=`
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
- map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
8 + map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
10 |
11 | # Errors (limited iterators).
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:9:1
|
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 |
11 | # Errors (limited iterators).
|
help: Add explicit value for parameter `strict=`
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
- map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
9 + map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True), strict=False)
10 |
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:12:1
|
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
13 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
|
help: Add explicit value for parameter `strict=`
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
10 |
11 | # Errors (limited iterators).
- map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
12 + map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1), strict=False)
13 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
14 |
15 | import builtins
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:13:1
|
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
13 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14 |
15 | import builtins
|
help: Add explicit value for parameter `strict=`
10 |
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
- map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
13 + map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4), strict=False)
14 |
15 | import builtins
16 | # Still an error even though it uses the qualified name
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:17:1
|
15 | import builtins
16 | # Still an error even though it uses the qualified name
17 | builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 |
19 | # OK
|
help: Add explicit value for parameter `strict=`
14 |
15 | import builtins
16 | # Still an error even though it uses the qualified name
- builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
17 + builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=False)
18 |
19 | # OK
20 | map(lambda x: x, [1, 2, 3], strict=True)

View File

@@ -14,7 +14,6 @@ mod tests {
use crate::registry::Rule;
use crate::rules::flake8_builtins;
use crate::settings::LinterSettings;
use crate::settings::types::PreviewMode;
use crate::test::{test_path, test_resource_path};
use ruff_python_ast::PythonVersion;
@@ -64,28 +63,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::BuiltinAttributeShadowing, Path::new("A003.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_builtins").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
flake8_builtins: flake8_builtins::settings::Settings {
strict_checking: true,
..Default::default()
},
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(
Rule::StdlibModuleShadowing,
Path::new("A005/modules/utils/logging.py"),

View File

@@ -6,7 +6,6 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_a003_class_scope_shadowing_expansion_enabled;
use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// ## What it does
@@ -124,26 +123,16 @@ pub(crate) fn builtin_attribute_shadowing(
// def repeat(value: int, times: int) -> list[int]:
// return [value] * times
// ```
// In stable, only consider references whose first non-type parent scope is the class
// scope (e.g., decorators, default args, and attribute initializers).
// In preview, also consider references from within the class scope.
let consider_reference = |reference_scope_id: ScopeId| {
if is_a003_class_scope_shadowing_expansion_enabled(checker.settings()) {
if reference_scope_id == scope_id {
return true;
}
}
checker
.semantic()
.first_non_type_parent_scope_id(reference_scope_id)
== Some(scope_id)
};
for reference in binding
.references
.iter()
.map(|reference_id| checker.semantic().reference(*reference_id))
.filter(|reference| consider_reference(reference.scope_id()))
.filter(|reference| {
checker
.semantic()
.first_non_type_parent_scope_id(reference.scope_id())
== Some(scope_id)
})
{
checker.report_diagnostic(
BuiltinAttributeShadowing {

View File

@@ -1,50 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
---
A003 Python builtin is shadowed by method `str` from line 14
--> A003.py:17:31
|
15 | pass
16 |
17 | def method_usage(self) -> str:
| ^^^
18 | pass
|
A003 Python builtin is shadowed by class attribute `id` from line 3
--> A003.py:20:34
|
18 | pass
19 |
20 | def attribute_usage(self) -> id:
| ^^
21 | pass
|
A003 Python builtin is shadowed by method `property` from line 26
--> A003.py:31:7
|
29 | id = 1
30 |
31 | @[property][0]
| ^^^^^^^^
32 | def f(self, x=[id]):
33 | return x
|
A003 Python builtin is shadowed by class attribute `id` from line 29
--> A003.py:32:20
|
31 | @[property][0]
32 | def f(self, x=[id]):
| ^^
33 | return x
|
A003 Python builtin is shadowed by class attribute `bin` from line 35
--> A003.py:36:12
|
35 | bin = 2
36 | foo = [bin]
| ^^^
|

View File

@@ -124,7 +124,7 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
let needs_trailing_comma = if let [item] = elts.as_slice() {
SimpleTokenizer::new(
checker.locator().contents(),
TextRange::new(item.end(), argument.end()),
TextRange::new(item.end(), call.end()),
)
.all(|token| token.kind != SimpleTokenKind::Comma)
} else {

View File

@@ -247,36 +247,3 @@ help: Rewrite as a tuple literal
28 | tuple([x for x in range(5)])
29 | tuple({x for x in range(10)})
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:46:6
|
44 | )
45 |
46 | t9 = tuple([1],)
| ^^^^^^^^^^^
47 | t10 = tuple([1, 2],)
|
help: Rewrite as a tuple literal
43 | }
44 | )
45 |
- t9 = tuple([1],)
46 + t9 = (1,)
47 | t10 = tuple([1, 2],)
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:47:7
|
46 | t9 = tuple([1],)
47 | t10 = tuple([1, 2],)
| ^^^^^^^^^^^^^^
|
help: Rewrite as a tuple literal
44 | )
45 |
46 | t9 = tuple([1],)
- t10 = tuple([1, 2],)
47 + t10 = (1, 2)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -344,36 +344,3 @@ help: Rewrite as a generator
42 | x for x in [1,2,3]
43 | }
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:46:6
|
44 | )
45 |
46 | t9 = tuple([1],)
| ^^^^^^^^^^^
47 | t10 = tuple([1, 2],)
|
help: Rewrite as a tuple literal
43 | }
44 | )
45 |
- t9 = tuple([1],)
46 + t9 = (1,)
47 | t10 = tuple([1, 2],)
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:47:7
|
46 | t9 = tuple([1],)
47 | t10 = tuple([1, 2],)
| ^^^^^^^^^^^^^^
|
help: Rewrite as a tuple literal
44 | )
45 |
46 | t9 = tuple([1],)
- t10 = tuple([1, 2],)
47 + t10 = (1, 2)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,9 +1,8 @@
use ruff_python_ast::InterpolatedStringElement;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator, StringFlags};
use ruff_python_semantic::analyze::logging;
use ruff_python_stdlib::logging::LoggingLevel;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_f_string_logging_enabled;
@@ -199,7 +198,7 @@ fn check_log_record_attr_clash(checker: &Checker, extra: &Keyword) {
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum LoggingCallType {
enum LoggingCallType {
/// Logging call with a level method, e.g., `logging.info`.
LevelCall(LoggingLevel),
/// Logging call with an integer level as an argument, e.g., `logger.log(level, ...)`.
@@ -216,41 +215,39 @@ impl LoggingCallType {
}
}
pub(crate) fn find_logging_call(
checker: &Checker,
call: &ast::ExprCall,
) -> Option<(LoggingCallType, TextRange)> {
/// Check logging calls for violations.
pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
// Determine the call type (e.g., `info` vs. `exception`) and the range of the attribute.
match call.func.as_ref() {
let (logging_call_type, range) = match call.func.as_ref() {
Expr::Attribute(ast::ExprAttribute { value: _, attr, .. }) => {
let call_type = LoggingCallType::from_attribute(attr.as_str())?;
let Some(call_type) = LoggingCallType::from_attribute(attr.as_str()) else {
return;
};
if !logging::is_logger_candidate(
&call.func,
checker.semantic(),
&checker.settings().logger_objects,
) {
return None;
return;
}
Some((call_type, attr.range()))
(call_type, attr.range())
}
Expr::Name(_) => {
let qualified_name = checker
let Some(qualified_name) = checker
.semantic()
.resolve_qualified_name(call.func.as_ref())?;
let ["logging", attribute] = qualified_name.segments() else {
return None;
.resolve_qualified_name(call.func.as_ref())
else {
return;
};
let call_type = LoggingCallType::from_attribute(attribute)?;
Some((call_type, call.func.range()))
let ["logging", attribute] = qualified_name.segments() else {
return;
};
let Some(call_type) = LoggingCallType::from_attribute(attribute) else {
return;
};
(call_type, call.func.range())
}
_ => None,
}
}
/// Check logging calls for violations.
pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
let Some((logging_call_type, range)) = find_logging_call(checker, call) else {
return;
_ => return,
};
// G001, G002, G003, G004

View File

@@ -1,3 +1,5 @@
use std::cmp::Ordering;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::StringFlags;
use ruff_python_ast::{
@@ -5,8 +7,6 @@ use ruff_python_ast::{
StringLiteralValue, UnaryOp, str::TripleQuotes,
};
use ruff_text_size::{Ranged, TextRange};
use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use crate::checkers::ast::Checker;
use crate::preview::is_maxsplit_without_separator_fix_enabled;
@@ -47,40 +47,14 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
/// ## References
/// - [Python documentation: `str.split`](https://docs.python.org/3/library/stdtypes.html#str.split)
#[derive(ViolationMetadata)]
pub(crate) struct SplitStaticString {
method: Method,
}
#[derive(Copy, Clone, Debug)]
enum Method {
Split,
RSplit,
}
impl Method {
fn is_rsplit(self) -> bool {
matches!(self, Method::RSplit)
}
}
impl Display for Method {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Method::Split => f.write_str("split"),
Method::RSplit => f.write_str("rsplit"),
}
}
}
pub(crate) struct SplitStaticString;
impl Violation for SplitStaticString {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Consider using a list literal instead of `str.{}`",
self.method
)
"Consider using a list literal instead of `str.split`".to_string()
}
fn fix_title(&self) -> Option<String> {
@@ -103,21 +77,26 @@ pub(crate) fn split_static_string(
};
// `split` vs `rsplit`.
let method = if attr == "split" {
Method::Split
let direction = if attr == "split" {
Direction::Left
} else {
Method::RSplit
Direction::Right
};
let sep_arg = arguments.find_argument_value("sep", 0);
let split_replacement = if let Some(sep) = sep_arg {
match sep {
Expr::NoneLiteral(_) => {
split_default(str_value, maxsplit_value, method, checker.settings())
split_default(str_value, maxsplit_value, direction, checker.settings())
}
Expr::StringLiteral(sep_value) => {
let sep_value_str = sep_value.value.to_str();
Some(split_sep(str_value, sep_value_str, maxsplit_value, method))
Some(split_sep(
str_value,
sep_value_str,
maxsplit_value,
direction,
))
}
// Ignore names until type inference is available.
_ => {
@@ -125,10 +104,10 @@ pub(crate) fn split_static_string(
}
}
} else {
split_default(str_value, maxsplit_value, method, checker.settings())
split_default(str_value, maxsplit_value, direction, checker.settings())
};
let mut diagnostic = checker.report_diagnostic(SplitStaticString { method }, call.range());
let mut diagnostic = checker.report_diagnostic(SplitStaticString, call.range());
if let Some(ref replacement_expr) = split_replacement {
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement(checker.generator().expr(replacement_expr), call.range()),
@@ -198,22 +177,68 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
fn split_default(
str_value: &StringLiteralValue,
max_split: i32,
method: Method,
direction: Direction,
settings: &LinterSettings,
) -> Option<Expr> {
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
// > consecutive whitespace are regarded as a single separator, and the result will contain
// > no empty strings at the start or end if the string has leading or trailing whitespace.
// > Consequently, splitting an empty string or a string consisting of just whitespace with
// > a None separator returns [].
// https://docs.python.org/3/library/stdtypes.html#str.split
let string_val = str_value.to_str();
match max_split.cmp(&0) {
Ordering::Greater if !is_maxsplit_without_separator_fix_enabled(settings) => None,
Ordering::Greater | Ordering::Equal => {
Ordering::Greater => {
if !is_maxsplit_without_separator_fix_enabled(settings) {
return None;
}
let Ok(max_split) = usize::try_from(max_split) else {
return None;
};
let list_items = split_whitespace_with_maxsplit(string_val, max_split, method);
let list_items: Vec<&str> = if direction == Direction::Left {
string_val
.trim_start_matches(py_unicode_is_whitespace)
.splitn(max_split + 1, py_unicode_is_whitespace)
.filter(|s| !s.is_empty())
.collect()
} else {
let mut items: Vec<&str> = string_val
.trim_end_matches(py_unicode_is_whitespace)
.rsplitn(max_split + 1, py_unicode_is_whitespace)
.filter(|s| !s.is_empty())
.collect();
items.reverse();
items
};
Some(construct_replacement(
&list_items,
str_value.first_literal_flags(),
))
}
Ordering::Equal => {
// Behavior for maxsplit = 0 when sep is None:
// - If the string is empty or all whitespace, result is [].
// - Otherwise:
// - " x ".split(maxsplit=0) -> ['x ']
// - " x ".rsplit(maxsplit=0) -> [' x']
// - "".split(maxsplit=0) -> []
// - " ".split(maxsplit=0) -> []
let processed_str = if direction == Direction::Left {
string_val.trim_start_matches(py_unicode_is_whitespace)
} else {
string_val.trim_end_matches(py_unicode_is_whitespace)
};
let list_items: &[_] = if processed_str.is_empty() {
&[]
} else {
&[processed_str]
};
Some(construct_replacement(
list_items,
str_value.first_literal_flags(),
))
}
Ordering::Less => {
let list_items: Vec<&str> = string_val
.split(py_unicode_is_whitespace)
@@ -231,22 +256,22 @@ fn split_sep(
str_value: &StringLiteralValue,
sep_value: &str,
max_split: i32,
method: Method,
direction: Direction,
) -> Expr {
let value = str_value.to_str();
let list_items: Vec<&str> = if let Ok(split_n) = usize::try_from(max_split) {
match method {
Method::Split => value.splitn(split_n + 1, sep_value).collect(),
Method::RSplit => {
match direction {
Direction::Left => value.splitn(split_n + 1, sep_value).collect(),
Direction::Right => {
let mut items: Vec<&str> = value.rsplitn(split_n + 1, sep_value).collect();
items.reverse();
items
}
}
} else {
match method {
Method::Split => value.split(sep_value).collect(),
Method::RSplit => {
match direction {
Direction::Left => value.split(sep_value).collect(),
Direction::Right => {
let mut items: Vec<&str> = value.rsplit(sep_value).collect();
items.reverse();
items
@@ -291,6 +316,12 @@ fn get_maxsplit_value(arg: Option<&Expr>) -> Option<i32> {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Direction {
Left,
Right,
}
/// Like [`char::is_whitespace`] but with Python's notion of whitespace.
///
/// <https://github.com/astral-sh/ruff/issues/19845>
@@ -321,107 +352,3 @@ const fn py_unicode_is_whitespace(ch: char) -> bool {
| '\u{3000}'
)
}
struct WhitespaceMaxSplitIterator<'a> {
remaining: &'a str,
max_split: usize,
splits: usize,
method: Method,
}
impl<'a> WhitespaceMaxSplitIterator<'a> {
fn new(s: &'a str, max_split: usize, method: Method) -> Self {
let remaining = match method {
Method::Split => s.trim_start_matches(py_unicode_is_whitespace),
Method::RSplit => s.trim_end_matches(py_unicode_is_whitespace),
};
Self {
remaining,
max_split,
splits: 0,
method,
}
}
}
impl<'a> Iterator for WhitespaceMaxSplitIterator<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining.is_empty() {
return None;
}
if self.splits >= self.max_split {
let result = self.remaining;
self.remaining = "";
return Some(result);
}
self.splits += 1;
match self.method {
Method::Split => match self.remaining.split_once(py_unicode_is_whitespace) {
Some((s, remaining)) => {
self.remaining = remaining.trim_start_matches(py_unicode_is_whitespace);
Some(s)
}
None => Some(std::mem::take(&mut self.remaining)),
},
Method::RSplit => match self.remaining.rsplit_once(py_unicode_is_whitespace) {
Some((remaining, s)) => {
self.remaining = remaining.trim_end_matches(py_unicode_is_whitespace);
Some(s)
}
None => Some(std::mem::take(&mut self.remaining)),
},
}
}
}
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
// > consecutive whitespace are regarded as a single separator, and the result will contain
// > no empty strings at the start or end if the string has leading or trailing whitespace.
// > Consequently, splitting an empty string or a string consisting of just whitespace with
// > a None separator returns [].
// https://docs.python.org/3/library/stdtypes.html#str.split
fn split_whitespace_with_maxsplit(s: &str, max_split: usize, method: Method) -> Vec<&str> {
let mut result: Vec<_> = WhitespaceMaxSplitIterator::new(s, max_split, method).collect();
if method.is_rsplit() {
result.reverse();
}
result
}
#[cfg(test)]
mod tests {
use super::{Method, split_whitespace_with_maxsplit};
use test_case::test_case;
#[test_case(" ", 1, &[])]
#[test_case("a b", 1, &["a", "b"])]
#[test_case("a b", 2, &["a", "b"])]
#[test_case(" a b c d ", 2, &["a", "b", "c d "])]
#[test_case(" a b c ", 1, &["a", "b c "])]
#[test_case(" x ", 0, &["x "])]
#[test_case(" ", 0, &[])]
#[test_case("a\u{3000}b", 1, &["a", "b"])]
fn test_split_whitespace_with_maxsplit(s: &str, max_split: usize, expected: &[&str]) {
let parts = split_whitespace_with_maxsplit(s, max_split, Method::Split);
assert_eq!(parts, expected);
}
#[test_case(" ", 1, &[])]
#[test_case("a b", 1, &["a", "b"])]
#[test_case("a b", 2, &["a", "b"])]
#[test_case(" a b c d ", 2, &[" a b", "c", "d"])]
#[test_case(" a b c ", 1, &[" a b", "c"])]
#[test_case(" x ", 0, &[" x"])]
#[test_case(" ", 0, &[])]
#[test_case("a\u{3000}b", 1, &["a", "b"])]
fn test_rsplit_whitespace_with_maxsplit(s: &str, max_split: usize, expected: &[&str]) {
let parts = split_whitespace_with_maxsplit(s, max_split, Method::RSplit);
assert_eq!(parts, expected);
}
}

View File

@@ -846,7 +846,7 @@ help: Replace with list literal
105 | # https://github.com/astral-sh/ruff/issues/18042
106 | print("a,b".rsplit(","))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:111:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -864,7 +864,7 @@ help: Replace with list literal
113 |
114 | # https://github.com/astral-sh/ruff/issues/18069
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:112:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -1043,7 +1043,7 @@ help: Replace with list literal
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:124:7
|
122 | print(" x ".split(maxsplit=0))
@@ -1063,7 +1063,7 @@ help: Replace with list literal
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:125:7
|
123 | print(" x ".split(sep=None, maxsplit=0))
@@ -1083,7 +1083,7 @@ help: Replace with list literal
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:126:7
|
124 | print("".rsplit(maxsplit=0))
@@ -1103,7 +1103,7 @@ help: Replace with list literal
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:127:7
|
125 | print("".rsplit(sep=None, maxsplit=0))
@@ -1123,7 +1123,7 @@ help: Replace with list literal
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:128:7
|
126 | print(" ".rsplit(maxsplit=0))
@@ -1143,7 +1143,7 @@ help: Replace with list literal
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:129:7
|
127 | print(" ".rsplit(sep=None, maxsplit=0))
@@ -1163,7 +1163,7 @@ help: Replace with list literal
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:130:7
|
128 | print(" x ".rsplit(maxsplit=0))
@@ -1183,7 +1183,7 @@ help: Replace with list literal
132 | print(" x ".rsplit(sep=None, maxsplit=0))
133 |
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:131:7
|
129 | print(" x ".rsplit(maxsplit=0))
@@ -1202,7 +1202,7 @@ help: Replace with list literal
133 |
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:132:7
|
130 | print(" x ".rsplit(sep=None, maxsplit=0))
@@ -1345,7 +1345,7 @@ help: Replace with list literal
169 |
170 | # leading/trailing whitespace should not count towards maxsplit
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:168:7
|
166 | print("S\x1cP\x1dL\x1eI\x1fT".split())
@@ -1372,27 +1372,15 @@ SIM905 Consider using a list literal instead of `str.split`
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
SIM905 Consider using a list literal instead of `str.rsplit`
SIM905 Consider using a list literal instead of `str.split`
--> SIM905.py:172:1
|
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
SIM905 Consider using a list literal instead of `str.split`
--> SIM905.py:173:1
|
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with list literal

View File

@@ -894,7 +894,7 @@ help: Replace with list literal
105 | # https://github.com/astral-sh/ruff/issues/18042
106 | print("a,b".rsplit(","))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:111:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -912,7 +912,7 @@ help: Replace with list literal
113 |
114 | # https://github.com/astral-sh/ruff/issues/18069
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:112:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -1091,7 +1091,7 @@ help: Replace with list literal
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:124:7
|
122 | print(" x ".split(maxsplit=0))
@@ -1111,7 +1111,7 @@ help: Replace with list literal
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:125:7
|
123 | print(" x ".split(sep=None, maxsplit=0))
@@ -1131,7 +1131,7 @@ help: Replace with list literal
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:126:7
|
124 | print("".rsplit(maxsplit=0))
@@ -1151,7 +1151,7 @@ help: Replace with list literal
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:127:7
|
125 | print("".rsplit(sep=None, maxsplit=0))
@@ -1171,7 +1171,7 @@ help: Replace with list literal
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:128:7
|
126 | print(" ".rsplit(maxsplit=0))
@@ -1191,7 +1191,7 @@ help: Replace with list literal
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:129:7
|
127 | print(" ".rsplit(sep=None, maxsplit=0))
@@ -1211,7 +1211,7 @@ help: Replace with list literal
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:130:7
|
128 | print(" x ".rsplit(maxsplit=0))
@@ -1231,7 +1231,7 @@ help: Replace with list literal
132 | print(" x ".rsplit(sep=None, maxsplit=0))
133 |
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:131:7
|
129 | print(" x ".rsplit(maxsplit=0))
@@ -1250,7 +1250,7 @@ help: Replace with list literal
133 |
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:132:7
|
130 | print(" x ".rsplit(sep=None, maxsplit=0))
@@ -1393,7 +1393,7 @@ help: Replace with list literal
169 |
170 | # leading/trailing whitespace should not count towards maxsplit
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:168:7
|
166 | print("S\x1cP\x1dL\x1eI\x1fT".split())
@@ -1420,7 +1420,6 @@ SIM905 [*] Consider using a list literal instead of `str.split`
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
@@ -1429,16 +1428,14 @@ help: Replace with list literal
- " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
171 + ["a", "b", "c d "] # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:172:1
|
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
169 |
@@ -1446,19 +1443,3 @@ help: Replace with list literal
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
- " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
172 + [" a b", "c", "d"] # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:173:1
|
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with list literal
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
- "a b".split(maxsplit=1) # ["a", "b"]
173 + ["a", "b"] # ["a", "b"]

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Arguments, Expr, ExprCall};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_python_semantic::{SemanticModel, analyze::typing};
use ruff_text_size::Ranged;
@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation};
pub(crate) fn is_keyword_only_argument_non_default(arguments: &Arguments, name: &str) -> bool {
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
arguments
.find_keyword(name)
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
@@ -24,7 +24,10 @@ pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool {
/// Check if the given segments represent a pathlib Path subclass or `PackagePath` with preview mode support.
/// In stable mode, only checks for `Path` and `PurePath`. In preview mode, also checks for
/// `PosixPath`, `PurePosixPath`, `WindowsPath`, `PureWindowsPath`, and `PackagePath`.
pub(crate) fn is_pure_path_subclass_with_preview(checker: &Checker, segments: &[&str]) -> bool {
pub(crate) fn is_pure_path_subclass_with_preview(
checker: &crate::checkers::ast::Checker,
segments: &[&str],
) -> bool {
let is_core_pathlib = matches!(segments, ["pathlib", "Path" | "PurePath"]);
if is_core_pathlib {
@@ -190,7 +193,7 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
}
pub(crate) fn has_unknown_keywords_or_starred_expr(
arguments: &Arguments,
arguments: &ast::Arguments,
allowed: &[&str],
) -> bool {
if arguments.args.iter().any(Expr::is_starred_expr) {
@@ -204,7 +207,11 @@ pub(crate) fn has_unknown_keywords_or_starred_expr(
}
/// Returns `true` if argument `name` is set to a non-default `None` value.
pub(crate) fn is_argument_non_default(arguments: &Arguments, name: &str, position: usize) -> bool {
pub(crate) fn is_argument_non_default(
arguments: &ast::Arguments,
name: &str,
position: usize,
) -> bool {
arguments
.find_argument_value(name, position)
.is_some_and(|expr| !expr.is_none_literal_expr())

View File

@@ -1,16 +1,11 @@
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ArgOrKeyword, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_chmod_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
has_unknown_keywords_or_starred_expr, is_file_descriptor, is_keyword_only_argument_non_default,
is_pathlib_path_call,
check_os_pathlib_two_arg_calls, is_file_descriptor, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.chmod`.
@@ -78,80 +73,22 @@ pub(crate) fn os_chmod(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// 0 1 2 3
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
// ```
let path_arg = call.arguments.find_argument_value("path", 0);
if path_arg.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
if call
.arguments
.find_argument_value("path", 0)
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|| is_keyword_only_argument_non_default(&call.arguments, "dir_fd")
{
return;
}
let range = call.range();
let mut diagnostic = checker.report_diagnostic(OsChmod, call.func.range());
if !is_fix_os_chmod_enabled(checker.settings()) {
return;
}
if call.arguments.len() < 2 {
return;
}
if has_unknown_keywords_or_starred_expr(
&call.arguments,
&["path", "mode", "dir_fd", "follow_symlinks"],
) {
return;
}
let (Some(path_arg), Some(_)) = (path_arg, call.arguments.find_argument_value("mode", 1))
else {
return;
};
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let locator = checker.locator();
let path_code = locator.slice(path_arg.range());
let args = |arg: ArgOrKeyword| match arg {
ArgOrKeyword::Arg(expr) if expr.range() != path_arg.range() => {
Some(locator.slice(expr.range()))
}
ArgOrKeyword::Keyword(kw)
if matches!(kw.arg.as_deref(), Some("mode" | "follow_symlinks")) =>
{
Some(locator.slice(kw.range()))
}
_ => None,
};
let chmod_args = itertools::join(
call.arguments.arguments_source_order().filter_map(args),
", ",
);
let replacement = if is_pathlib_path_call(checker, path_arg) {
format!("{path_code}.chmod({chmod_args})")
} else {
format!("{binding}({path_code}).chmod({chmod_args})")
};
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
check_os_pathlib_two_arg_calls(
checker,
call,
"chmod",
"path",
"mode",
is_fix_os_chmod_enabled(checker.settings()),
OsChmod,
);
}

View File

@@ -1,8 +1,6 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_samefile_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
};
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_two_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -67,16 +65,13 @@ pub(crate) fn os_path_samefile(checker: &Checker, call: &ExprCall, segments: &[&
return;
}
let fix_enabled = is_fix_os_path_samefile_enabled(checker.settings())
&& !has_unknown_keywords_or_starred_expr(&call.arguments, &["f1", "f2"]);
check_os_pathlib_two_arg_calls(
checker,
call,
"samefile",
"f1",
"f2",
fix_enabled,
is_fix_os_path_samefile_enabled(checker.settings()),
OsPathSamefile,
);
}

View File

@@ -1,8 +1,7 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_rename_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
is_keyword_only_argument_non_default,
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -80,11 +79,13 @@ pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
return;
}
let fix_enabled = is_fix_os_rename_enabled(checker.settings())
&& !has_unknown_keywords_or_starred_expr(
&call.arguments,
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
);
check_os_pathlib_two_arg_calls(checker, call, "rename", "src", "dst", fix_enabled, OsRename);
check_os_pathlib_two_arg_calls(
checker,
call,
"rename",
"src",
"dst",
is_fix_os_rename_enabled(checker.settings()),
OsRename,
);
}

View File

@@ -1,8 +1,7 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_replace_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
is_keyword_only_argument_non_default,
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -83,19 +82,13 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
return;
}
let fix_enabled = is_fix_os_replace_enabled(checker.settings())
&& !has_unknown_keywords_or_starred_expr(
&call.arguments,
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
);
check_os_pathlib_two_arg_calls(
checker,
call,
"replace",
"src",
"dst",
fix_enabled,
is_fix_os_replace_enabled(checker.settings()),
OsReplace,
);
}

View File

@@ -104,6 +104,14 @@ pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str])
return;
};
let target_is_directory_arg = call.arguments.find_argument_value("target_is_directory", 2);
if let Some(expr) = &target_is_directory_arg {
if expr.as_boolean_literal_expr().is_none() {
return;
}
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
@@ -121,9 +129,7 @@ pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str])
let src_code = locator.slice(src.range());
let dst_code = locator.slice(dst.range());
let target_is_directory = call
.arguments
.find_argument_value("target_is_directory", 2)
let target_is_directory = target_is_directory_arg
.and_then(|expr| {
let code = locator.slice(expr.range());
expr.as_boolean_literal_expr()

View File

@@ -500,72 +500,5 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
126 |
127 | os.makedirs("name", unknown_kwarg=True)
| ^^^^^^^^^^^
128 |
129 | # https://github.com/astral-sh/ruff/issues/20134
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:130:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
| ^^^^^^^^
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
help: Replace with `Path(...).chmod(...)`
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:131:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
| ^^^^^^^^
132 |
133 | # Only diagnostic
|
help: Replace with `Path(...).chmod(...)`
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:134:1
|
133 | # Only diagnostic
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).chmod(...)`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> full_name.py:136:1
|
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).rename(...)`
PTH105 `os.replace()` should be replaced by `Path.replace()`
--> full_name.py:137:1
|
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).replace(...)`
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> full_name.py:139:1
|
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^^^^^^^
|
help: Replace with `Path(...).samefile()`

View File

@@ -931,7 +931,6 @@ help: Replace with `Path(...).mkdir(parents=True)`
126 + pathlib.Path("name").mkdir(mode=0o777, exist_ok=False, parents=True)
127 |
128 | os.makedirs("name", unknown_kwarg=True)
129 |
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:127:1
@@ -940,102 +939,5 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
126 |
127 | os.makedirs("name", unknown_kwarg=True)
| ^^^^^^^^^^^
128 |
129 | # https://github.com/astral-sh/ruff/issues/20134
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH101 [*] `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:130:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
| ^^^^^^^^
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
help: Replace with `Path(...).chmod(...)`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
128 | os.makedirs("name", unknown_kwarg=True)
129 |
130 | # https://github.com/astral-sh/ruff/issues/20134
- os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
131 + pathlib.Path("pth1_link").chmod(mode=0o600, follow_symlinks= False)
132 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
133 |
134 | # Only diagnostic
PTH101 [*] `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:131:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
| ^^^^^^^^
132 |
133 | # Only diagnostic
|
help: Replace with `Path(...).chmod(...)`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
129 |
130 | # https://github.com/astral-sh/ruff/issues/20134
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
- os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
132 + pathlib.Path("pth1_link").chmod(mode=0o600, follow_symlinks=True)
133 |
134 | # Only diagnostic
135 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:134:1
|
133 | # Only diagnostic
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).chmod(...)`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> full_name.py:136:1
|
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).rename(...)`
PTH105 `os.replace()` should be replaced by `Path.replace()`
--> full_name.py:137:1
|
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).replace(...)`
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> full_name.py:139:1
|
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^^^^^^^
|
help: Replace with `Path(...).samefile()`

View File

@@ -2,7 +2,7 @@ use ast::FStringFlags;
use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Arguments, Expr, StringFlags};
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -72,42 +72,24 @@ fn is_static_length(elts: &[Expr]) -> bool {
fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option<Expr> {
// If all elements are string constants, join them into a single string.
if joinees.iter().all(Expr::is_string_literal_expr) {
let mut flags: Option<ast::StringLiteralFlags> = None;
let content = joinees
.iter()
.filter_map(|expr| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
if flags.is_none() {
// Take the flags from the first Expr
flags = Some(value.first_literal_flags());
}
Some(value.to_str())
} else {
None
}
})
.join(joiner);
let mut flags = flags?;
// If the result is a raw string and contains a newline, use triple quotes.
if flags.prefix().is_raw() && content.contains(['\n', '\r']) {
flags = flags.with_triple_quotes(ruff_python_ast::str::TripleQuotes::Yes);
// Prefer a delimiter that doesn't occur in the content; if both occur, bail.
if content.contains(flags.quote_str()) {
flags = flags.with_quote_style(flags.quote_style().opposite());
if content.contains(flags.quote_str()) {
// Both "'''" and "\"\"\"" are present in content; avoid emitting
// an invalid raw triple-quoted literal (or escaping). Bail on the fix.
return None;
}
}
}
let mut flags = None;
let node = ast::StringLiteral {
value: content.into_boxed_str(),
flags,
value: joinees
.iter()
.filter_map(|expr| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
if flags.is_none() {
// take the flags from the first Expr
flags = Some(value.first_literal_flags());
}
Some(value.to_str())
} else {
None
}
})
.join(joiner)
.into_boxed_str(),
flags: flags?,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};

View File

@@ -125,39 +125,18 @@ help: Replace with `f"{secrets.token_urlsafe()}a{secrets.token_hex()}"`
13 | nok2 = a.join(["1", "2", "3"]) # Not OK (not a static joiner)
note: This is an unsafe fix and may change runtime behavior
FLY002 [*] Consider f-string instead of string join
--> FLY002.py:20:8
|
18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
19 | # https://github.com/astral-sh/ruff/issues/19887
20 | nok8 = '\n'.join([r'line1','line2'])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
|
help: Replace with f-string
17 | nok6 = "a".join(x for x in "feefoofum") # Not OK (generator)
18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
19 | # https://github.com/astral-sh/ruff/issues/19887
- nok8 = '\n'.join([r'line1','line2'])
20 + nok8 = r'''line1
21 + line2'''
22 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
23 |
24 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
note: This is an unsafe fix and may change runtime behavior
FLY002 [*] Consider `f"{url}{filename}"` instead of string join
--> FLY002.py:25:11
--> FLY002.py:23:11
|
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
24 | def create_file_public_url(url, filename):
25 | return''.join([url, filename])
21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
22 | def create_file_public_url(url, filename):
23 | return''.join([url, filename])
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with `f"{url}{filename}"`
22 |
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
24 | def create_file_public_url(url, filename):
20 |
21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
22 | def create_file_public_url(url, filename):
- return''.join([url, filename])
25 + return f"{url}{filename}"
23 + return f"{url}{filename}"
note: This is an unsafe fix and may change runtime behavior

View File

@@ -269,7 +269,7 @@ pub(crate) fn indentation(
range: TextRange,
context: &LintContext,
) {
if !indent_level.is_multiple_of(indent_size) {
if indent_level % indent_size != 0 {
if logical_line.is_comment_only() {
context.report_diagnostic_if_enabled(
IndentationWithInvalidMultipleComment {

View File

@@ -920,42 +920,6 @@ mod tests {
flakes("__annotations__", &[]);
}
#[test]
fn module_warningregistry() {
// Using __warningregistry__ should not be considered undefined.
flakes("__warningregistry__", &[]);
}
#[test]
fn module_annotate_py314_available() {
// __annotate__ is available starting in Python 3.14.
let diagnostics = crate::test::test_snippet(
"__annotate__",
&crate::settings::LinterSettings {
unresolved_target_version: ruff_python_ast::PythonVersion::PY314.into(),
..crate::settings::LinterSettings::for_rules(vec![
crate::codes::Rule::UndefinedName,
])
},
);
assert!(diagnostics.is_empty());
}
#[test]
fn module_annotate_pre_py314_undefined() {
// __annotate__ is not available before Python 3.14.
let diagnostics = crate::test::test_snippet(
"__annotate__",
&crate::settings::LinterSettings {
unresolved_target_version: ruff_python_ast::PythonVersion::PY313.into(),
..crate::settings::LinterSettings::for_rules(vec![
crate::codes::Rule::UndefinedName,
])
},
);
assert_eq!(diagnostics.len(), 1);
}
#[test]
fn magic_globals_file() {
// Use of the C{__file__} magic global should not emit an undefined name

View File

@@ -139,11 +139,7 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
return;
};
// The `super(__class__, self)` and `super(ParentClass, self)` patterns are redundant in Python 3
// when the first argument refers to the implicit `__class__` cell or to the enclosing class.
// Avoid triggering if a local variable shadows either name.
if !(((first_arg_id == "__class__") || (first_arg_id == parent_name.as_str()))
&& !checker.semantic().current_scope().has(first_arg_id)
if !((first_arg_id == "__class__" || first_arg_id == parent_name.as_str())
&& second_arg_id == parent_arg.name().as_str())
{
return;

View File

@@ -6,7 +6,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
use crate::{Edit, Fix, FixAvailability, Violation};
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for membership tests against single-item containers.
@@ -27,16 +27,13 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// ```
///
/// ## Fix safety
/// The fix is always marked as unsafe.
///
/// When the right-hand side is a string, this fix can change the behavior of your program.
/// This is because `c in "a"` is true both when `c` is `"a"` and when `c` is the empty string.
/// When the right-hand side is a string, the fix is marked as unsafe.
/// This is because `c in "a"` is true both when `c` is `"a"` and when `c` is the empty string,
/// so the fix can change the behavior of your program in these cases.
///
/// Additionally, converting `in`/`not in` against a single-item container to `==`/`!=` can
/// change runtime behavior: `in` may consider identity (e.g., `NaN`) and always
/// yields a `bool`.
///
/// Comments within the replacement range will also be removed.
/// Additionally, if there are comments within the fix's range,
/// it will also be marked as unsafe.
///
/// ## References
/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons)
@@ -103,8 +100,14 @@ pub(crate) fn single_item_membership_test(
expr.range(),
);
// All supported cases can change runtime behavior; mark as unsafe.
let fix = Fix::unsafe_edit(edit);
let applicability =
if right.is_string_literal_expr() || checker.comment_ranges().intersects(expr.range()) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let fix = Fix::applicable_edit(edit, applicability);
checker
.report_diagnostic(SingleItemMembershipTest { membership_test }, expr.range())

View File

@@ -293,19 +293,7 @@ fn handle_non_finite_float_special_case(
return None;
};
let normalized = as_non_finite_float_string_literal(float_arg)?;
let replacement_text = format!(
r#"{constructor_name}("{}")"#,
// `Decimal.from_float(float(" -nan")) == Decimal("nan")`
if normalized == "-nan" {
// Here we do not attempt to remove just the '-' character.
// It may have been encoded (e.g. as '\N{hyphen-minus}')
// in the original source slice, and the added complexity
// does not make sense for this edge case.
"nan"
} else {
normalized
}
);
let replacement_text = format!(r#"{constructor_name}("{normalized}")"#);
Some(Edit::range_replacement(replacement_text, call.range()))
}

View File

@@ -363,7 +363,7 @@ help: Replace with `Decimal` constructor
21 | _ = Decimal.from_float(float("-Infinity"))
22 | _ = Decimal.from_float(float("nan"))
- _ = Decimal.from_float(float("-NaN"))
23 + _ = Decimal("nan")
23 + _ = Decimal("-nan")
24 | _ = Decimal.from_float(float(" \n+nan \t"))
25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 | _ = Decimal.from_float(float(" -inF\n \t"))
@@ -655,7 +655,7 @@ help: Replace with `Decimal` constructor
62 |
63 | # Cases with non-finite floats - should produce safe fixes
- _ = Decimal.from_float(float("-nan"))
64 + _ = Decimal("nan")
64 + _ = Decimal("-nan")
65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
@@ -673,7 +673,7 @@ help: Replace with `Decimal` constructor
63 | # Cases with non-finite floats - should produce safe fixes
64 | _ = Decimal.from_float(float("-nan"))
- _ = Decimal.from_float(float("\x2dnan"))
65 + _ = Decimal("nan")
65 + _ = Decimal("-nan")
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
FURB164 [*] Verbose method `from_float` in `Decimal` construction
@@ -689,4 +689,4 @@ help: Replace with `Decimal` constructor
64 | _ = Decimal.from_float(float("-nan"))
65 | _ = Decimal.from_float(float("\x2dnan"))
- _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
66 + _ = Decimal("nan")
66 + _ = Decimal("-nan")

View File

@@ -18,7 +18,6 @@ help: Convert to equality test
4 | print("Single-element tuple")
5 |
6 | if 1 in [1]:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:6:4
@@ -38,7 +37,6 @@ help: Convert to equality test
7 | print("Single-element list")
8 |
9 | if 1 in {1}:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:9:4
@@ -58,7 +56,6 @@ help: Convert to equality test
10 | print("Single-element set")
11 |
12 | if "a" in "a":
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:12:4
@@ -98,7 +95,6 @@ help: Convert to inequality test
16 | print("Check `not in` membership test")
17 |
18 | if not 1 in (1,):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:18:8
@@ -118,7 +114,6 @@ help: Convert to equality test
19 | print("Check the negated membership test")
20 |
21 | # Non-errors.
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:52:5
@@ -349,122 +344,4 @@ help: Convert to inequality test
- ] and \
113 + if foo != bar and \
114 | 0 < 1: ...
115 |
116 | # https://github.com/astral-sh/ruff/issues/20255
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:125:4
|
124 | # NaN behavior differences
125 | if math.nan in [math.nan]:
| ^^^^^^^^^^^^^^^^^^^^^^
126 | print("This is True")
|
help: Convert to equality test
122 | import math
123 |
124 | # NaN behavior differences
- if math.nan in [math.nan]:
125 + if math.nan == math.nan:
126 | print("This is True")
127 |
128 | if math.nan in (math.nan,):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:128:4
|
126 | print("This is True")
127 |
128 | if math.nan in (math.nan,):
| ^^^^^^^^^^^^^^^^^^^^^^^
129 | print("This is True")
|
help: Convert to equality test
125 | if math.nan in [math.nan]:
126 | print("This is True")
127 |
- if math.nan in (math.nan,):
128 + if math.nan == math.nan:
129 | print("This is True")
130 |
131 | if math.nan in {math.nan}:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:131:4
|
129 | print("This is True")
130 |
131 | if math.nan in {math.nan}:
| ^^^^^^^^^^^^^^^^^^^^^^
132 | print("This is True")
|
help: Convert to equality test
128 | if math.nan in (math.nan,):
129 | print("This is True")
130 |
- if math.nan in {math.nan}:
131 + if math.nan == math.nan:
132 | print("This is True")
133 |
134 | # Potential type differences with custom __eq__ methods
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:140:4
|
139 | obj = CustomEq()
140 | if obj in [CustomEq()]:
| ^^^^^^^^^^^^^^^^^^^
141 | pass
|
help: Convert to equality test
137 | return "custom"
138 |
139 | obj = CustomEq()
- if obj in [CustomEq()]:
140 + if obj == CustomEq():
141 | pass
142 |
143 | if obj in (CustomEq(),):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:143:4
|
141 | pass
142 |
143 | if obj in (CustomEq(),):
| ^^^^^^^^^^^^^^^^^^^^
144 | pass
|
help: Convert to equality test
140 | if obj in [CustomEq()]:
141 | pass
142 |
- if obj in (CustomEq(),):
143 + if obj == CustomEq():
144 | pass
145 |
146 | if obj in {CustomEq()}:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:146:4
|
144 | pass
145 |
146 | if obj in {CustomEq()}:
| ^^^^^^^^^^^^^^^^^^^
147 | pass
|
help: Convert to equality test
143 | if obj in (CustomEq(),):
144 | pass
145 |
- if obj in {CustomEq()}:
146 + if obj == CustomEq():
147 | pass
note: This is an unsafe fix and may change runtime behavior

View File

@@ -18,7 +18,6 @@ help: Convert to equality test
4 | print("Single-element set")
5 |
6 | if 1 in set((1,)):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:6:4
@@ -38,7 +37,6 @@ help: Convert to equality test
7 | print("Single-element set")
8 |
9 | if 1 in set({1}):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:9:4
@@ -58,7 +56,6 @@ help: Convert to equality test
10 | print("Single-element set")
11 |
12 | if 1 in frozenset([1]):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:12:4
@@ -78,7 +75,6 @@ help: Convert to equality test
13 | print("Single-element set")
14 |
15 | if 1 in frozenset((1,)):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:15:4
@@ -98,7 +94,6 @@ help: Convert to equality test
16 | print("Single-element set")
17 |
18 | if 1 in frozenset({1}):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:18:4
@@ -118,7 +113,6 @@ help: Convert to equality test
19 | print("Single-element set")
20 |
21 | if 1 in set(set([1])):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:21:4
@@ -137,42 +131,4 @@ help: Convert to equality test
21 + if 1 == 1:
22 | print('Recursive solution')
23 |
24 |
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:59:4
|
58 | # set() and frozenset() with NaN
59 | if math.nan in set([math.nan]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
60 | print("This should be marked unsafe")
|
help: Convert to equality test
56 | import math
57 |
58 | # set() and frozenset() with NaN
- if math.nan in set([math.nan]):
59 + if math.nan == math.nan:
60 | print("This should be marked unsafe")
61 |
62 | if math.nan in frozenset([math.nan]):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:62:4
|
60 | print("This should be marked unsafe")
61 |
62 | if math.nan in frozenset([math.nan]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 | print("This should be marked unsafe")
|
help: Convert to equality test
59 | if math.nan in set([math.nan]):
60 | print("This should be marked unsafe")
61 |
- if math.nan in frozenset([math.nan]):
62 + if math.nan == math.nan:
63 | print("This should be marked unsafe")
note: This is an unsafe fix and may change runtime behavior
24 |

View File

@@ -112,7 +112,6 @@ mod tests {
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_warns.py"))]
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_deprecated_call.py"))]
#[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))]
#[test_case(Rule::LoggingEagerConversion, Path::new("RUF065.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]

View File

@@ -1,131 +0,0 @@
use std::str::FromStr;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_literal::cformat::{CFormatPart, CFormatString, CFormatType};
use ruff_python_literal::format::FormatConversion;
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::flake8_logging_format::rules::{LoggingCallType, find_logging_call};
/// ## What it does
/// Checks for eager string conversion of arguments to `logging` calls.
///
/// ## Why is this bad?
/// Arguments to `logging` calls will be formatted as strings automatically, so it
/// is unnecessary and less efficient to eagerly format the arguments before passing
/// them in.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`lint.logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`lint.logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
///
/// logging.basicConfig(format="%(message)s", level=logging.INFO)
///
/// user = "Maria"
///
/// logging.info("%s - Something happened", str(user))
/// ```
///
/// Use instead:
/// ```python
/// import logging
///
/// logging.basicConfig(format="%(message)s", level=logging.INFO)
///
/// user = "Maria"
///
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `lint.logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
#[derive(ViolationMetadata)]
pub(crate) struct LoggingEagerConversion {
pub(crate) format_conversion: FormatConversion,
}
impl Violation for LoggingEagerConversion {
#[derive_message_formats]
fn message(&self) -> String {
let LoggingEagerConversion { format_conversion } = self;
let (format_str, call_arg) = match format_conversion {
FormatConversion::Str => ("%s", "str()"),
FormatConversion::Repr => ("%r", "repr()"),
FormatConversion::Ascii => ("%a", "ascii()"),
FormatConversion::Bytes => ("%b", "bytes()"),
};
format!("Unnecessary `{call_arg}` conversion when formatting with `{format_str}`")
}
}
/// RUF065
pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall) {
let Some((logging_call_type, _range)) = find_logging_call(checker, call) else {
return;
};
let msg_pos = match logging_call_type {
LoggingCallType::LevelCall(_) => 0,
LoggingCallType::LogCall => 1,
};
// Extract a format string from the logging statement msg argument
let Some(Expr::StringLiteral(string_literal)) =
call.arguments.find_argument_value("msg", msg_pos)
else {
return;
};
let Ok(format_string) = CFormatString::from_str(string_literal.value.to_str()) else {
return;
};
// Iterate over % placeholders in format string and zip with logging statement arguments
for (spec, arg) in format_string
.iter()
.filter_map(|(_, part)| {
if let CFormatPart::Spec(spec) = part {
Some(spec)
} else {
None
}
})
.zip(call.arguments.args.iter().skip(msg_pos + 1))
{
// Check if the argument is a call to eagerly format a value
if let Expr::Call(ast::ExprCall { func, .. }) = arg {
let CFormatType::String(format_conversion) = spec.format_type else {
continue;
};
// Check for use of %s with str() or %r with repr()
if checker.semantic().match_builtin_expr(func.as_ref(), "str")
&& matches!(format_conversion, FormatConversion::Str)
|| checker.semantic().match_builtin_expr(func.as_ref(), "repr")
&& matches!(format_conversion, FormatConversion::Repr)
{
checker
.report_diagnostic(LoggingEagerConversion { format_conversion }, arg.range());
}
}
}
}

View File

@@ -23,7 +23,6 @@ pub(crate) use invalid_index_type::*;
pub(crate) use invalid_pyproject_toml::*;
pub(crate) use invalid_rule_code::*;
pub(crate) use legacy_form_pytest_raises::*;
pub(crate) use logging_eager_conversion::*;
pub(crate) use map_int_version_parsing::*;
pub(crate) use missing_fstring_syntax::*;
pub(crate) use mutable_class_default::*;
@@ -87,7 +86,6 @@ mod invalid_index_type;
mod invalid_pyproject_toml;
mod invalid_rule_code;
mod legacy_form_pytest_raises;
mod logging_eager_conversion;
mod map_int_version_parsing;
mod missing_fstring_syntax;
mod mutable_class_default;

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