Compare commits
63 Commits
gankra/imp
...
david/full
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
048182635a | ||
|
|
09f570af92 | ||
|
|
722f1a7d7a | ||
|
|
dbc5983503 | ||
|
|
46decd4feb | ||
|
|
bf38e69870 | ||
|
|
4ed8c65d29 | ||
|
|
2c916562ba | ||
|
|
edb920b4d5 | ||
|
|
346842f003 | ||
|
|
742f8a4ee6 | ||
|
|
fd5c48c539 | ||
|
|
ef4df34652 | ||
|
|
036f3616a1 | ||
|
|
68ae9c8a15 | ||
|
|
094bf70a60 | ||
|
|
32d00cd569 | ||
|
|
00a9e65d00 | ||
|
|
0c7cfd2a8d | ||
|
|
61bb2a8245 | ||
|
|
f1aacd0f2c | ||
|
|
3033d1e5a5 | ||
|
|
d96d40ef42 | ||
|
|
79224cc53d | ||
|
|
fe01a5e032 | ||
|
|
740425d39d | ||
|
|
4fddd373aa | ||
|
|
0b1e12f086 | ||
|
|
d12324f06e | ||
|
|
2c6c3e78f6 | ||
|
|
3ffe56d19d | ||
|
|
eb354608d2 | ||
|
|
12086dfa69 | ||
|
|
5f294f9f2e | ||
|
|
44fc87f491 | ||
|
|
43cda2dfe9 | ||
|
|
bd5b3e4f6e | ||
|
|
3bf4dae452 | ||
|
|
8eeca023d6 | ||
|
|
c94ddb590f | ||
|
|
bae8ddfb8a | ||
|
|
c0fb235a70 | ||
|
|
b5a3503a58 | ||
|
|
d45209f425 | ||
|
|
5d1cd85662 | ||
|
|
902b0b4ce9 | ||
|
|
6f2b60708e | ||
|
|
bc89d0394c | ||
|
|
706be0a6e7 | ||
|
|
7b40428b6a | ||
|
|
b9b5755368 | ||
|
|
b4b5d67a4a | ||
|
|
0b60584b7e | ||
|
|
821b2f8b2e | ||
|
|
1758f26d94 | ||
|
|
2502ff7638 | ||
|
|
144373fb3c | ||
|
|
91995aa516 | ||
|
|
1ebbe73a1d | ||
|
|
48ada2d359 | ||
|
|
50bd3943da | ||
|
|
5707958dad | ||
|
|
c4d359306b |
45
.github/workflows/ci.yaml
vendored
@@ -88,7 +88,6 @@ jobs:
|
||||
':!crates/ruff_python_formatter/**' \
|
||||
':!crates/ruff_formatter/**' \
|
||||
':!crates/ruff_dev/**' \
|
||||
':!crates/ruff_db/**' \
|
||||
':scripts/*' \
|
||||
':python/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
@@ -907,10 +906,13 @@ jobs:
|
||||
run: npm run fmt:check
|
||||
working-directory: playground
|
||||
|
||||
benchmarks-instrumented:
|
||||
benchmarks-instrumented-ruff:
|
||||
name: "benchmarks instrumented (ruff)"
|
||||
runs-on: ubuntu-24.04
|
||||
needs: determine_changes
|
||||
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') }}
|
||||
if: |
|
||||
github.ref == 'refs/heads/main' ||
|
||||
(needs.determine_changes.outputs.formatter == 'true' || needs.determine_changes.outputs.linter == 'true')
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
@@ -930,7 +932,42 @@ jobs:
|
||||
tool: cargo-codspeed
|
||||
|
||||
- name: "Build benchmarks"
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
|
||||
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
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@fc0f612798710b0dd69bb7528bc9b361dc60bd43"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--verbose \
|
||||
|
||||
5
.vscode/settings.json
vendored
@@ -3,4 +3,7 @@
|
||||
"--all-features"
|
||||
],
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
}
|
||||
"search.exclude": {
|
||||
"**/*.snap": true
|
||||
}
|
||||
}
|
||||
|
||||
60
CHANGELOG.md
@@ -1,5 +1,65 @@
|
||||
# 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
@@ -23,12 +23,6 @@ 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"
|
||||
@@ -104,9 +98,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-svg"
|
||||
version = "0.1.10"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc03a770ef506fe1396c0e476120ac0e6523cf14b74218dd5f18cd6833326fa9"
|
||||
checksum = "26b9ec8c976eada1b0f9747a3d7cc4eae3bef10613e443746e7487f26c872fde"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-lossy",
|
||||
@@ -128,9 +122,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.99"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
@@ -284,9 +278,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "boxcar"
|
||||
version = "0.2.13"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26c4925bc979b677330a8c7fe7a8c94af2dbb4a2d37b4a20a80d884400f46baa"
|
||||
checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
@@ -346,10 +340,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.31"
|
||||
version = "1.2.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
||||
checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
@@ -357,9 +352,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.1"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
@@ -369,14 +364,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"windows-link 0.1.3",
|
||||
"windows-link 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -408,9 +402,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.47"
|
||||
version = "4.5.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
|
||||
checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -418,9 +412,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.47"
|
||||
version = "4.5.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
|
||||
checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -431,9 +425,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.55"
|
||||
version = "4.5.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
|
||||
checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
@@ -650,15 +644,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
|
||||
checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width 0.2.1",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -837,9 +831,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
@@ -847,9 +841,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
@@ -861,9 +855,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
@@ -956,7 +950,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1037,12 +1031,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.13"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1053,12 +1047,11 @@ checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6"
|
||||
|
||||
[[package]]
|
||||
name = "escargot"
|
||||
version = "0.5.14"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83f351750780493fc33fa0ce8ba3c7d61f9736cfa3b3bb9ee2342643ffe40211"
|
||||
checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -1101,6 +1094,12 @@ 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"
|
||||
@@ -1168,9 +1167,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size-derive2"
|
||||
version = "0.6.2"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75a17a226478b2e8294ded60782c03efe54476aa8cd1371d0e5ad9d1071e74e0"
|
||||
checksum = "e3814abc7da8ab18d2fd820f5b540b5e39b6af0a32de1bdd7c47576693074843"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"quote",
|
||||
@@ -1179,21 +1178,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size2"
|
||||
version = "0.6.2"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5697765925a05c9d401dd04a93dfd662d336cc25fdcc3301220385a1ffcfdde5"
|
||||
checksum = "5dfe2cec5b5ce8fb94dcdb16a1708baa4d0609cc3ce305ca5d3f6f2ffb59baed"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"get-size-derive2",
|
||||
"hashbrown 0.15.5",
|
||||
"hashbrown 0.16.0",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.23"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
|
||||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||
dependencies = [
|
||||
"unicode-width 0.2.1",
|
||||
]
|
||||
@@ -1219,7 +1218,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasi 0.14.7+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -1280,6 +1279,15 @@ 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"
|
||||
@@ -1321,9 +1329,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
@@ -1493,13 +1501,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.1"
|
||||
version = "2.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.5",
|
||||
"hashbrown 0.16.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1508,7 +1517,7 @@ version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
|
||||
dependencies = [
|
||||
"console 0.16.0",
|
||||
"console 0.16.1",
|
||||
"portable-atomic",
|
||||
"unicode-width 0.2.1",
|
||||
"unit-prefix",
|
||||
@@ -1588,9 +1597,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.20"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83"
|
||||
checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
@@ -1719,9 +1728,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.33"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"libc",
|
||||
@@ -1735,9 +1744,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.78"
|
||||
version = "0.3.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
|
||||
checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -1812,9 +1821,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"libc",
|
||||
@@ -1835,9 +1844,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -2133,12 +2142,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordermap"
|
||||
version = "0.5.10"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dcd63f1ae4b091e314a26627c467dd8810d674ba798abc0e566679955776c63"
|
||||
checksum = "b100f7dd605611822d30e182214d3c02fdefce2d801d23993f6b6ba6ca1392af"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2289,9 +2299,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.8.1"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
|
||||
checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.16",
|
||||
@@ -2300,9 +2310,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.8.1"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
|
||||
checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
@@ -2310,9 +2320,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.8.1"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
|
||||
checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
@@ -2323,9 +2333,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.8.1"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
|
||||
checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"sha2",
|
||||
@@ -2398,9 +2408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
|
||||
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
@@ -2453,9 +2463,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
|
||||
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
@@ -2705,15 +2715,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-lite"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
|
||||
checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
@@ -2728,7 +2738,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.13.0"
|
||||
version = "0.13.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2984,7 +2994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.13.0"
|
||||
version = "0.13.1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -2994,7 +3004,7 @@ dependencies = [
|
||||
"fern",
|
||||
"glob",
|
||||
"globset",
|
||||
"hashbrown 0.15.5",
|
||||
"hashbrown 0.16.0",
|
||||
"imperative",
|
||||
"insta",
|
||||
"is-macro",
|
||||
@@ -3338,7 +3348,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.13.0"
|
||||
version = "0.13.1"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3427,22 +3437,22 @@ checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.8"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -3537,9 +3547,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.223"
|
||||
version = "1.0.226"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac"
|
||||
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
@@ -3558,18 +3568,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.223"
|
||||
version = "1.0.226"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9"
|
||||
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.223"
|
||||
version = "1.0.226"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56"
|
||||
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3613,11 +3623,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
|
||||
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3631,9 +3641,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.14.0"
|
||||
version = "3.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
|
||||
checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -3642,9 +3652,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.14.0"
|
||||
version = "3.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
|
||||
checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
@@ -3830,7 +3840,7 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3844,12 +3854,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
|
||||
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4009,9 +4019,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.9.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
|
||||
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
@@ -4024,14 +4034,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.5"
|
||||
version = "0.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
|
||||
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.7.0",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
@@ -4039,44 +4049,39 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
|
||||
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
version = "0.23.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
|
||||
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
|
||||
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
@@ -4286,6 +4291,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"ty_combine",
|
||||
"ty_python_semantic",
|
||||
"ty_static",
|
||||
"ty_vendored",
|
||||
]
|
||||
|
||||
@@ -4303,7 +4309,7 @@ dependencies = [
|
||||
"drop_bomb",
|
||||
"get-size2",
|
||||
"glob",
|
||||
"hashbrown 0.15.5",
|
||||
"hashbrown 0.16.0",
|
||||
"indexmap",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
@@ -4513,9 +4519,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-id"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
|
||||
checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
@@ -4740,18 +4746,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
version = "0.14.7+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
"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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.101"
|
||||
version = "0.2.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
|
||||
checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -4762,9 +4777,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.101"
|
||||
version = "0.2.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
|
||||
checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@@ -4776,9 +4791,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.51"
|
||||
version = "0.4.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
|
||||
checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -4789,9 +4804,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.101"
|
||||
version = "0.2.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
|
||||
checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -4799,9 +4814,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.101"
|
||||
version = "0.2.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
|
||||
checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4812,18 +4827,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.101"
|
||||
version = "0.2.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
|
||||
checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.51"
|
||||
version = "0.3.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80cc7f8a4114fdaa0c58383caf973fc126cf004eba25c9dc639bccd3880d55ad"
|
||||
checksum = "aee0a0f5343de9221a0d233b04520ed8dc2e6728dce180b1dcd9288ec9d9fa3c"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"minicov",
|
||||
@@ -4834,9 +4849,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.51"
|
||||
version = "0.3.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5ada2ab788d46d4bda04c9d567702a79c8ced14f51f221646a16ed39d0e6a5d"
|
||||
checksum = "a369369e4360c2884c3168d22bded735c43cccae97bbc147586d4b480edd138d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4845,9 +4860,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.78"
|
||||
version = "0.3.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
|
||||
checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -4885,22 +4900,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
version = "0.62.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link 0.1.3",
|
||||
"windows-link 0.2.0",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
@@ -4941,20 +4956,20 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-link 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-link 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5124,9 +5139,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.12"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
|
||||
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -5138,13 +5153,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
]
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
@@ -5193,18 +5205,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.26"
|
||||
version = "0.8.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
|
||||
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.26"
|
||||
version = "0.8.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5299,9 +5311,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.15+zstd.1.5.7"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
|
||||
@@ -5,7 +5,7 @@ resolver = "2"
|
||||
[workspace.package]
|
||||
# Please update rustfmt.toml when bumping the Rust edition
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
rust-version = "1.88"
|
||||
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.6.2", features = [
|
||||
get-size2 = { version = "0.7.0", features = [
|
||||
"derive",
|
||||
"smallvec",
|
||||
"hashbrown",
|
||||
@@ -95,7 +95,7 @@ get-size2 = { version = "0.6.2", features = [
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
globwalk = { version = "0.9.1" }
|
||||
hashbrown = { version = "0.15.0", default-features = false, features = [
|
||||
hashbrown = { version = "0.16.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"]
|
||||
ignored = ["getrandom", "ruff_options_metadata", "uuid", "get-size2"]
|
||||
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
@@ -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.0/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.13.0/install.ps1 | iex"
|
||||
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"
|
||||
```
|
||||
|
||||
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.0
|
||||
rev: v0.13.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.13.0"
|
||||
version = "0.13.1"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -500,6 +500,35 @@ 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()?;
|
||||
|
||||
@@ -271,6 +271,50 @@ 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()?;
|
||||
|
||||
@@ -22,6 +22,30 @@ 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": [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="182px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="164px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="1356px" height="128px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="869px" height="236px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="911px" height="236px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="768px" height="290px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #000000 }
|
||||
.fg-bright-blue { fill: #5555FF }
|
||||
.fg-bright-red { fill: #FF5555 }
|
||||
.container {
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="146px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="1196px" height="128px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="182px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -1,7 +1,7 @@
|
||||
<svg width="1196px" height="164px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.bg { fill: #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 |
@@ -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,
|
||||
},
|
||||
500,
|
||||
600,
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -58,10 +58,11 @@ impl<'a> FullRenderer<'a> {
|
||||
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
|
||||
}
|
||||
|
||||
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}")?;
|
||||
}
|
||||
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}")?;
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
|
||||
@@ -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(&self) -> usize {
|
||||
self.0.capacity()
|
||||
fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
|
||||
(self.0.capacity(), tracker)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,8 +92,8 @@ impl ToOwned for VendoredPath {
|
||||
pub struct VendoredPathBuf(Utf8PathBuf);
|
||||
|
||||
impl get_size2::GetSize for VendoredPathBuf {
|
||||
fn get_heap_size(&self) -> usize {
|
||||
self.0.capacity()
|
||||
fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
|
||||
(self.0.capacity(), tracker)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.13.0"
|
||||
version = "0.13.1"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
104
crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC240.py
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -52,3 +52,21 @@ 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
|
||||
|
||||
33
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B912.py
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
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())
|
||||
@@ -19,3 +19,18 @@ 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]
|
||||
|
||||
@@ -42,3 +42,6 @@ tuple(
|
||||
x for x in [1,2,3]
|
||||
}
|
||||
)
|
||||
|
||||
t9 = tuple([1],)
|
||||
t10 = tuple([1, 2],)
|
||||
|
||||
@@ -170,3 +170,4 @@ 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"]
|
||||
|
||||
@@ -125,3 +125,15 @@ 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)
|
||||
@@ -16,7 +16,9 @@ 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):
|
||||
|
||||
@@ -271,3 +271,35 @@ 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
|
||||
|
||||
@@ -117,3 +117,31 @@ 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
|
||||
|
||||
@@ -51,3 +51,13 @@ 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")
|
||||
|
||||
39
crates/ruff_linter/resources/test/fixtures/ruff/RUF065.py
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
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!"))
|
||||
@@ -669,6 +669,9 @@ 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,
|
||||
@@ -717,7 +720,9 @@ 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);
|
||||
flake8_bugbear::rules::unreliable_callable_check(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::StripWithMultiCharacters) {
|
||||
flake8_bugbear::rules::strip_with_multi_characters(checker, expr, func, args);
|
||||
@@ -741,6 +746,11 @@ 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);
|
||||
}
|
||||
@@ -1279,6 +1289,9 @@ 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);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ use ruff_python_semantic::{
|
||||
Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel,
|
||||
SemanticModelFlags, StarImport, SubmoduleImport,
|
||||
};
|
||||
use ruff_python_stdlib::builtins::{MAGIC_GLOBALS, python_builtins};
|
||||
use ruff_python_stdlib::builtins::{python_builtins, python_magic_globals};
|
||||
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 MAGIC_GLOBALS {
|
||||
for builtin in python_magic_globals(target_version.minor) {
|
||||
bind_builtin(builtin);
|
||||
}
|
||||
for builtin in &settings.builtins {
|
||||
|
||||
@@ -341,6 +341,7 @@ 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),
|
||||
|
||||
@@ -394,6 +395,7 @@ 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),
|
||||
@@ -1051,6 +1053,8 @@ 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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,17 +2,24 @@ 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;
|
||||
use ruff_source_file::{OneIndexed, SourceFile};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
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 {
|
||||
@@ -29,7 +36,7 @@ impl Emitter for SarifEmitter {
|
||||
|
||||
let unique_rules: HashSet<_> = results
|
||||
.iter()
|
||||
.filter_map(|result| result.code.as_secondary_code())
|
||||
.filter_map(|result| result.rule_id.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));
|
||||
@@ -134,6 +141,15 @@ 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() {
|
||||
@@ -143,12 +159,83 @@ impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 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")]
|
||||
struct SarifResult<'a> {
|
||||
code: RuleCode<'a>,
|
||||
rule_id: RuleCode<'a>,
|
||||
level: String,
|
||||
message: 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 {
|
||||
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,
|
||||
@@ -156,70 +243,107 @@ struct SarifResult<'a> {
|
||||
}
|
||||
|
||||
impl<'a> SarifResult<'a> {
|
||||
#[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 {
|
||||
code: RuleCode::from(message),
|
||||
level: "error".to_string(),
|
||||
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(),
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
})
|
||||
}
|
||||
}
|
||||
fn fix(diagnostic: &'a Diagnostic, uri: &str) -> Option<SarifFix> {
|
||||
let fix = diagnostic.fix()?;
|
||||
|
||||
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,
|
||||
}
|
||||
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(),
|
||||
}),
|
||||
}
|
||||
}],
|
||||
"ruleId": self.code.as_str(),
|
||||
})
|
||||
.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)?;
|
||||
|
||||
Ok(Self {
|
||||
rule_id: RuleCode::from(diagnostic),
|
||||
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,
|
||||
},
|
||||
}],
|
||||
})
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +380,7 @@ 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]",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,30 @@ 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": [
|
||||
{
|
||||
@@ -30,6 +54,30 @@ 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": [
|
||||
{
|
||||
|
||||
@@ -228,3 +228,10 @@ 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()
|
||||
}
|
||||
|
||||
@@ -487,7 +487,6 @@ impl<'a> Iterator for PathParamIterator<'a> {
|
||||
let param_name_end = param_content.find(':').unwrap_or(param_content.len());
|
||||
let param_name = ¶m_content[..param_name_end];
|
||||
|
||||
#[expect(clippy::range_plus_one)]
|
||||
return Some((param_name, start..end + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ 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<()> {
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
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"
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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::*;
|
||||
@@ -18,6 +19,7 @@ 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;
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
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
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -27,3 +28,54 @@ 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))
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ 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"))]
|
||||
@@ -81,11 +82,35 @@ 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,
|
||||
|
||||
@@ -3,7 +3,7 @@ use ruff_python_ast::ExprCall;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_bugbear::rules::is_infinite_iterable;
|
||||
use crate::rules::flake8_bugbear::helpers::is_infinite_iterable;
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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: [What’s 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)
|
||||
/// - [What’s 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
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ 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::*;
|
||||
@@ -56,6 +57,7 @@ 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;
|
||||
|
||||
@@ -90,7 +90,11 @@ 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;
|
||||
};
|
||||
@@ -103,7 +107,21 @@ pub(crate) fn unreliable_callable_check(
|
||||
let Some(builtins_function) = checker.semantic().resolve_builtin_symbol(func) else {
|
||||
return;
|
||||
};
|
||||
if !matches!(builtins_function, "hasattr" | "getattr") {
|
||||
|
||||
// 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 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
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
|
||||
@@ -57,11 +57,7 @@ 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()
|
||||
&& !call
|
||||
.arguments
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| is_infinite_iterable(arg, semantic))
|
||||
&& !any_infinite_iterables(call.arguments.args.iter(), semantic)
|
||||
{
|
||||
checker
|
||||
.report_diagnostic(ZipWithoutExplicitStrict, call.range())
|
||||
@@ -86,47 +82,3 @@ 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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -156,4 +156,6 @@ 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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
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)
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
|
||||
@@ -63,6 +64,28 @@ 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"),
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -123,16 +124,26 @@ 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| {
|
||||
checker
|
||||
.semantic()
|
||||
.first_non_type_parent_scope_id(reference.scope_id())
|
||||
== Some(scope_id)
|
||||
})
|
||||
.filter(|reference| consider_reference(reference.scope_id()))
|
||||
{
|
||||
checker.report_diagnostic(
|
||||
BuiltinAttributeShadowing {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
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]
|
||||
| ^^^
|
||||
|
|
||||
@@ -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(), call.end()),
|
||||
TextRange::new(item.end(), argument.end()),
|
||||
)
|
||||
.all(|token| token.kind != SimpleTokenKind::Comma)
|
||||
} else {
|
||||
|
||||
@@ -247,3 +247,36 @@ 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
|
||||
|
||||
@@ -344,3 +344,36 @@ 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
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_f_string_logging_enabled;
|
||||
@@ -198,7 +199,7 @@ fn check_log_record_attr_clash(checker: &Checker, extra: &Keyword) {
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum LoggingCallType {
|
||||
pub(crate) 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, ...)`.
|
||||
@@ -215,39 +216,41 @@ impl LoggingCallType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check logging calls for violations.
|
||||
pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
|
||||
pub(crate) fn find_logging_call(
|
||||
checker: &Checker,
|
||||
call: &ast::ExprCall,
|
||||
) -> Option<(LoggingCallType, TextRange)> {
|
||||
// Determine the call type (e.g., `info` vs. `exception`) and the range of the attribute.
|
||||
let (logging_call_type, range) = match call.func.as_ref() {
|
||||
match call.func.as_ref() {
|
||||
Expr::Attribute(ast::ExprAttribute { value: _, attr, .. }) => {
|
||||
let Some(call_type) = LoggingCallType::from_attribute(attr.as_str()) else {
|
||||
return;
|
||||
};
|
||||
let call_type = LoggingCallType::from_attribute(attr.as_str())?;
|
||||
if !logging::is_logger_candidate(
|
||||
&call.func,
|
||||
checker.semantic(),
|
||||
&checker.settings().logger_objects,
|
||||
) {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
(call_type, attr.range())
|
||||
Some((call_type, attr.range()))
|
||||
}
|
||||
Expr::Name(_) => {
|
||||
let Some(qualified_name) = checker
|
||||
let qualified_name = checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(call.func.as_ref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
.resolve_qualified_name(call.func.as_ref())?;
|
||||
let ["logging", attribute] = qualified_name.segments() else {
|
||||
return;
|
||||
return None;
|
||||
};
|
||||
let Some(call_type) = LoggingCallType::from_attribute(attribute) else {
|
||||
return;
|
||||
};
|
||||
(call_type, call.func.range())
|
||||
let call_type = LoggingCallType::from_attribute(attribute)?;
|
||||
Some((call_type, call.func.range()))
|
||||
}
|
||||
_ => return,
|
||||
_ => 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;
|
||||
};
|
||||
|
||||
// G001, G002, G003, G004
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::StringFlags;
|
||||
use ruff_python_ast::{
|
||||
@@ -7,6 +5,8 @@ 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,14 +47,40 @@ 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;
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Violation for SplitStaticString {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Consider using a list literal instead of `str.split`".to_string()
|
||||
format!(
|
||||
"Consider using a list literal instead of `str.{}`",
|
||||
self.method
|
||||
)
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
@@ -77,26 +103,21 @@ pub(crate) fn split_static_string(
|
||||
};
|
||||
|
||||
// `split` vs `rsplit`.
|
||||
let direction = if attr == "split" {
|
||||
Direction::Left
|
||||
let method = if attr == "split" {
|
||||
Method::Split
|
||||
} else {
|
||||
Direction::Right
|
||||
Method::RSplit
|
||||
};
|
||||
|
||||
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, direction, checker.settings())
|
||||
split_default(str_value, maxsplit_value, method, 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,
|
||||
direction,
|
||||
))
|
||||
Some(split_sep(str_value, sep_value_str, maxsplit_value, method))
|
||||
}
|
||||
// Ignore names until type inference is available.
|
||||
_ => {
|
||||
@@ -104,10 +125,10 @@ pub(crate) fn split_static_string(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
split_default(str_value, maxsplit_value, direction, checker.settings())
|
||||
split_default(str_value, maxsplit_value, method, checker.settings())
|
||||
};
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(SplitStaticString, call.range());
|
||||
let mut diagnostic = checker.report_diagnostic(SplitStaticString { method }, 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()),
|
||||
@@ -177,68 +198,22 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
|
||||
fn split_default(
|
||||
str_value: &StringLiteralValue,
|
||||
max_split: i32,
|
||||
direction: Direction,
|
||||
method: Method,
|
||||
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) {
|
||||
return None;
|
||||
}
|
||||
Ordering::Greater if !is_maxsplit_without_separator_fix_enabled(settings) => None,
|
||||
Ordering::Greater | Ordering::Equal => {
|
||||
let Ok(max_split) = usize::try_from(max_split) else {
|
||||
return None;
|
||||
};
|
||||
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
|
||||
};
|
||||
let list_items = split_whitespace_with_maxsplit(string_val, max_split, method);
|
||||
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)
|
||||
@@ -256,22 +231,22 @@ fn split_sep(
|
||||
str_value: &StringLiteralValue,
|
||||
sep_value: &str,
|
||||
max_split: i32,
|
||||
direction: Direction,
|
||||
method: Method,
|
||||
) -> Expr {
|
||||
let value = str_value.to_str();
|
||||
let list_items: Vec<&str> = if let Ok(split_n) = usize::try_from(max_split) {
|
||||
match direction {
|
||||
Direction::Left => value.splitn(split_n + 1, sep_value).collect(),
|
||||
Direction::Right => {
|
||||
match method {
|
||||
Method::Split => value.splitn(split_n + 1, sep_value).collect(),
|
||||
Method::RSplit => {
|
||||
let mut items: Vec<&str> = value.rsplitn(split_n + 1, sep_value).collect();
|
||||
items.reverse();
|
||||
items
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match direction {
|
||||
Direction::Left => value.split(sep_value).collect(),
|
||||
Direction::Right => {
|
||||
match method {
|
||||
Method::Split => value.split(sep_value).collect(),
|
||||
Method::RSplit => {
|
||||
let mut items: Vec<&str> = value.rsplit(sep_value).collect();
|
||||
items.reverse();
|
||||
items
|
||||
@@ -316,12 +291,6 @@ 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>
|
||||
@@ -352,3 +321,107 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> SIM905.py:168:7
|
||||
|
|
||||
166 | print("S\x1cP\x1dL\x1eI\x1fT".split())
|
||||
@@ -1372,15 +1372,27 @@ 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.split`
|
||||
SIM905 Consider using a list literal instead of `str.rsplit`
|
||||
--> 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
|
||||
|
||||
@@ -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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> SIM905.py:168:7
|
||||
|
|
||||
166 | print("S\x1cP\x1dL\x1eI\x1fT".split())
|
||||
@@ -1420,6 +1420,7 @@ 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))
|
||||
@@ -1428,14 +1429,16 @@ 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.split`
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
--> 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 |
|
||||
@@ -1443,3 +1446,19 @@ 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"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall};
|
||||
use ruff_python_ast::{self as ast, Arguments, 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: &ast::Arguments, name: &str) -> bool {
|
||||
pub(crate) fn is_keyword_only_argument_non_default(arguments: &Arguments, name: &str) -> bool {
|
||||
arguments
|
||||
.find_keyword(name)
|
||||
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
|
||||
@@ -24,10 +24,7 @@ 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: &crate::checkers::ast::Checker,
|
||||
segments: &[&str],
|
||||
) -> bool {
|
||||
pub(crate) fn is_pure_path_subclass_with_preview(checker: &Checker, segments: &[&str]) -> bool {
|
||||
let is_core_pathlib = matches!(segments, ["pathlib", "Path" | "PurePath"]);
|
||||
|
||||
if is_core_pathlib {
|
||||
@@ -193,7 +190,7 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
|
||||
}
|
||||
|
||||
pub(crate) fn has_unknown_keywords_or_starred_expr(
|
||||
arguments: &ast::Arguments,
|
||||
arguments: &Arguments,
|
||||
allowed: &[&str],
|
||||
) -> bool {
|
||||
if arguments.args.iter().any(Expr::is_starred_expr) {
|
||||
@@ -207,11 +204,7 @@ 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: &ast::Arguments,
|
||||
name: &str,
|
||||
position: usize,
|
||||
) -> bool {
|
||||
pub(crate) fn is_argument_non_default(arguments: &Arguments, name: &str, position: usize) -> bool {
|
||||
arguments
|
||||
.find_argument_value(name, position)
|
||||
.is_some_and(|expr| !expr.is_none_literal_expr())
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
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::{
|
||||
check_os_pathlib_two_arg_calls, is_file_descriptor, is_keyword_only_argument_non_default,
|
||||
has_unknown_keywords_or_starred_expr, is_file_descriptor, is_keyword_only_argument_non_default,
|
||||
is_pathlib_path_call,
|
||||
};
|
||||
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`.
|
||||
@@ -73,22 +78,80 @@ 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)
|
||||
// ```
|
||||
if call
|
||||
.arguments
|
||||
.find_argument_value("path", 0)
|
||||
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
let path_arg = call.arguments.find_argument_value("path", 0);
|
||||
|
||||
if path_arg.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"chmod",
|
||||
"path",
|
||||
"mode",
|
||||
is_fix_os_chmod_enabled(checker.settings()),
|
||||
OsChmod,
|
||||
);
|
||||
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,
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
@@ -65,13 +67,16 @@ 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",
|
||||
is_fix_os_path_samefile_enabled(checker.settings()),
|
||||
fix_enabled,
|
||||
OsPathSamefile,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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, is_keyword_only_argument_non_default,
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
@@ -79,13 +80,11 @@ pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
return;
|
||||
}
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"rename",
|
||||
"src",
|
||||
"dst",
|
||||
is_fix_os_rename_enabled(checker.settings()),
|
||||
OsRename,
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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, is_keyword_only_argument_non_default,
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
@@ -82,13 +83,19 @@ 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",
|
||||
is_fix_os_replace_enabled(checker.settings()),
|
||||
fix_enabled,
|
||||
OsReplace,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,14 +104,6 @@ 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"),
|
||||
@@ -129,7 +121,9 @@ 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 = target_is_directory_arg
|
||||
let target_is_directory = call
|
||||
.arguments
|
||||
.find_argument_value("target_is_directory", 2)
|
||||
.and_then(|expr| {
|
||||
let code = locator.slice(expr.range());
|
||||
expr.as_boolean_literal_expr()
|
||||
|
||||
@@ -500,5 +500,72 @@ 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()`
|
||||
|
||||
@@ -931,6 +931,7 @@ 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
|
||||
@@ -939,5 +940,102 @@ 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()`
|
||||
|
||||
@@ -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};
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, StringFlags};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -72,24 +72,42 @@ 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 = None;
|
||||
let node = ast::StringLiteral {
|
||||
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
|
||||
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());
|
||||
}
|
||||
})
|
||||
.join(joiner)
|
||||
.into_boxed_str(),
|
||||
flags: 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 node = ast::StringLiteral {
|
||||
value: content.into_boxed_str(),
|
||||
flags,
|
||||
range: TextRange::default(),
|
||||
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
|
||||
};
|
||||
|
||||
@@ -125,18 +125,39 @@ 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"{url}{filename}"` instead of string join
|
||||
--> FLY002.py:23:11
|
||||
FLY002 [*] Consider f-string instead of string join
|
||||
--> FLY002.py:20:8
|
||||
|
|
||||
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])
|
||||
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
|
||||
|
|
||||
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])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Replace with `f"{url}{filename}"`
|
||||
20 |
|
||||
21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
|
||||
22 | def create_file_public_url(url, filename):
|
||||
22 |
|
||||
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
|
||||
24 | def create_file_public_url(url, filename):
|
||||
- return''.join([url, filename])
|
||||
23 + return f"{url}{filename}"
|
||||
25 + return f"{url}{filename}"
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -269,7 +269,7 @@ pub(crate) fn indentation(
|
||||
range: TextRange,
|
||||
context: &LintContext,
|
||||
) {
|
||||
if indent_level % indent_size != 0 {
|
||||
if !indent_level.is_multiple_of(indent_size) {
|
||||
if logical_line.is_comment_only() {
|
||||
context.report_diagnostic_if_enabled(
|
||||
IndentationWithInvalidMultipleComment {
|
||||
|
||||
@@ -920,6 +920,42 @@ 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
|
||||
|
||||
@@ -139,7 +139,11 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
|
||||
return;
|
||||
};
|
||||
|
||||
if !((first_arg_id == "__class__" || first_arg_id == parent_name.as_str())
|
||||
// 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)
|
||||
&& second_arg_id == parent_arg.name().as_str())
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::pad;
|
||||
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for membership tests against single-item containers.
|
||||
@@ -27,13 +27,16 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// The fix is always marked as unsafe.
|
||||
///
|
||||
/// 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.
|
||||
/// 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.
|
||||
///
|
||||
/// Additionally, if there are comments within the fix's range,
|
||||
/// it will also be marked as unsafe.
|
||||
/// 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.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons)
|
||||
@@ -100,14 +103,8 @@ pub(crate) fn single_item_membership_test(
|
||||
expr.range(),
|
||||
);
|
||||
|
||||
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);
|
||||
// All supported cases can change runtime behavior; mark as unsafe.
|
||||
let fix = Fix::unsafe_edit(edit);
|
||||
|
||||
checker
|
||||
.report_diagnostic(SingleItemMembershipTest { membership_test }, expr.range())
|
||||
|
||||
@@ -293,7 +293,19 @@ 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}("{normalized}")"#);
|
||||
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
|
||||
}
|
||||
);
|
||||
Some(Edit::range_replacement(replacement_text, call.range()))
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -18,6 +18,7 @@ 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
|
||||
@@ -37,6 +38,7 @@ 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
|
||||
@@ -56,6 +58,7 @@ 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
|
||||
@@ -95,6 +98,7 @@ 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
|
||||
@@ -114,6 +118,7 @@ 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
|
||||
@@ -344,4 +349,122 @@ 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
|
||||
|
||||
@@ -18,6 +18,7 @@ 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
|
||||
@@ -37,6 +38,7 @@ 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
|
||||
@@ -56,6 +58,7 @@ 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
|
||||
@@ -75,6 +78,7 @@ 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
|
||||
@@ -94,6 +98,7 @@ 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
|
||||
@@ -113,6 +118,7 @@ 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
|
||||
@@ -131,4 +137,42 @@ help: Convert to equality test
|
||||
21 + if 1 == 1:
|
||||
22 | print('Recursive solution')
|
||||
23 |
|
||||
24 |
|
||||
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
|
||||
|
||||
@@ -112,6 +112,7 @@ 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"))]
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ 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::*;
|
||||
@@ -86,6 +87,7 @@ 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;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
assertion_line: 124
|
||||
---
|
||||
RUF065 Unnecessary `str()` conversion when formatting with `%s`
|
||||
--> RUF065.py:4:26
|
||||
|
|
||||
3 | # %s + str()
|
||||
4 | logging.info("Hello %s", str("World!"))
|
||||
| ^^^^^^^^^^^^^
|
||||
5 | logging.log(logging.INFO, "Hello %s", str("World!"))
|
||||
|
|
||||
|
||||
RUF065 Unnecessary `str()` conversion when formatting with `%s`
|
||||
--> RUF065.py:5:39
|
||||
|
|
||||
3 | # %s + str()
|
||||
4 | logging.info("Hello %s", str("World!"))
|
||||
5 | logging.log(logging.INFO, "Hello %s", str("World!"))
|
||||
| ^^^^^^^^^^^^^
|
||||
6 |
|
||||
7 | # %s + repr()
|
||||
|
|
||||
|
||||
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
|
||||
--> RUF065.py:16:26
|
||||
|
|
||||
15 | # %r + repr()
|
||||
16 | logging.info("Hello %r", repr("World!"))
|
||||
| ^^^^^^^^^^^^^^
|
||||
17 | logging.log(logging.INFO, "Hello %r", repr("World!"))
|
||||
|
|
||||
|
||||
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
|
||||
--> RUF065.py:17:39
|
||||
|
|
||||
15 | # %r + repr()
|
||||
16 | logging.info("Hello %r", repr("World!"))
|
||||
17 | logging.log(logging.INFO, "Hello %r", repr("World!"))
|
||||
| ^^^^^^^^^^^^^^
|
||||
18 |
|
||||
19 | from logging import info, log
|
||||
|
|
||||
|
||||
RUF065 Unnecessary `str()` conversion when formatting with `%s`
|
||||
--> RUF065.py:22:18
|
||||
|
|
||||
21 | # %s + str()
|
||||
22 | info("Hello %s", str("World!"))
|
||||
| ^^^^^^^^^^^^^
|
||||
23 | log(logging.INFO, "Hello %s", str("World!"))
|
||||
|
|
||||
|
||||
RUF065 Unnecessary `str()` conversion when formatting with `%s`
|
||||
--> RUF065.py:23:31
|
||||
|
|
||||
21 | # %s + str()
|
||||
22 | info("Hello %s", str("World!"))
|
||||
23 | log(logging.INFO, "Hello %s", str("World!"))
|
||||
| ^^^^^^^^^^^^^
|
||||
24 |
|
||||
25 | # %s + repr()
|
||||
|
|
||||
|
||||
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
|
||||
--> RUF065.py:34:18
|
||||
|
|
||||
33 | # %r + repr()
|
||||
34 | info("Hello %r", repr("World!"))
|
||||
| ^^^^^^^^^^^^^^
|
||||
35 | log(logging.INFO, "Hello %r", repr("World!"))
|
||||
|
|
||||
|
||||
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
|
||||
--> RUF065.py:35:31
|
||||
|
|
||||
33 | # %r + repr()
|
||||
34 | info("Hello %r", repr("World!"))
|
||||
35 | log(logging.INFO, "Hello %r", repr("World!"))
|
||||
| ^^^^^^^^^^^^^^
|
||||
36 |
|
||||
37 | def str(s): return f"str = {s}"
|
||||
|
|
||||