Compare commits
2 Commits
david/gene
...
brent/ruf0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77c51f62f4 | ||
|
|
43942c7617 |
30
.github/workflows/ci.yaml
vendored
30
.github/workflows/ci.yaml
vendored
@@ -259,10 +259,6 @@ jobs:
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
if: ${{ needs.determine_changes.outputs.ty == 'true' }}
|
||||
env:
|
||||
@@ -321,10 +317,6 @@ jobs:
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -348,10 +340,6 @@ jobs:
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -453,7 +441,9 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@837578dfb436769f1e6669b2e23ffea9d9d2da8f # v1.15.4
|
||||
uses: cargo-bins/cargo-binstall@2bb61346d075e720d4c3da92f23b6d612d5a7543 # v1.15.3
|
||||
with:
|
||||
tool: cargo-fuzz@0.11.2
|
||||
- name: "Install cargo-fuzz"
|
||||
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
|
||||
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
|
||||
@@ -473,7 +463,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
@@ -674,7 +664,7 @@ jobs:
|
||||
branch: ${{ github.event.pull_request.base.ref }}
|
||||
workflow: "ci.yaml"
|
||||
check_artifacts: true
|
||||
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: Fuzz
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
@@ -704,7 +694,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@837578dfb436769f1e6669b2e23ffea9d9d2da8f # v1.15.4
|
||||
- uses: cargo-bins/cargo-binstall@2bb61346d075e720d4c3da92f23b6d612d5a7543 # v1.15.3
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
@@ -744,7 +734,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
@@ -787,7 +777,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
@@ -919,7 +909,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
@@ -952,7 +942,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
|
||||
4
.github/workflows/mypy_primer.yaml
vendored
4
.github/workflows/mypy_primer.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/publish-pypi.yml
vendored
2
.github/workflows/publish-pypi.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
6
.github/workflows/sync_typeshed.yaml
vendored
6
.github/workflows/sync_typeshed.yaml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
git config --global user.email '<>'
|
||||
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: Sync typeshed stubs
|
||||
run: |
|
||||
rm -rf "ruff/${VENDORED_TYPESHED}"
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
|
||||
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,33 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.12
|
||||
|
||||
### Preview features
|
||||
|
||||
- Show fixes by default ([#19919](https://github.com/astral-sh/ruff/pull/19919))
|
||||
- \[`airflow`\] Convert `DatasetOrTimeSchedule(datasets=...)` to `AssetOrTimeSchedule(assets=...)` (`AIR311`) ([#20202](https://github.com/astral-sh/ruff/pull/20202))
|
||||
- \[`airflow`\] Improve the `AIR002` error message ([#20173](https://github.com/astral-sh/ruff/pull/20173))
|
||||
- \[`airflow`\] Move `airflow.operators.postgres_operator.Mapping` from `AIR302` to `AIR301` ([#20172](https://github.com/astral-sh/ruff/pull/20172))
|
||||
- \[`flake8-async`\] Implement `blocking-input` rule (`ASYNC250`) ([#20122](https://github.com/astral-sh/ruff/pull/20122))
|
||||
- \[`flake8-use-pathlib`\] Make `PTH119` and `PTH120` fixes unsafe because they can change behavior ([#20118](https://github.com/astral-sh/ruff/pull/20118))
|
||||
- \[`pylint`\] Add U+061C to `PLE2502` ([#20106](https://github.com/astral-sh/ruff/pull/20106))
|
||||
- \[`ruff`\] Fix false negative for empty f-strings in `deque` calls (`RUF037`) ([#20109](https://github.com/astral-sh/ruff/pull/20109))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Less confidently mark f-strings as empty when inferring truthiness ([#20152](https://github.com/astral-sh/ruff/pull/20152))
|
||||
- \[`fastapi`\] Fix false positive for paths with spaces around parameters (`FAST003`) ([#20077](https://github.com/astral-sh/ruff/pull/20077))
|
||||
- \[`flake8-comprehensions`\] Skip `C417` when lambda contains `yield`/`yield from` ([#20201](https://github.com/astral-sh/ruff/pull/20201))
|
||||
- \[`perflint`\] Handle tuples in dictionary comprehensions (`PERF403`) ([#19934](https://github.com/astral-sh/ruff/pull/19934))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pycodestyle`\] Preserve return type annotation for `ParamSpec` (`E731`) ([#20108](https://github.com/astral-sh/ruff/pull/20108))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add fix safety sections to docs ([#17490](https://github.com/astral-sh/ruff/pull/17490),[#17499](https://github.com/astral-sh/ruff/pull/17499))
|
||||
|
||||
## 0.12.11
|
||||
|
||||
### Preview features
|
||||
|
||||
135
Cargo.lock
generated
135
Cargo.lock
generated
@@ -257,9 +257,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.4"
|
||||
version = "2.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
@@ -408,9 +408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.47"
|
||||
version = "4.5.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
|
||||
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -418,9 +418,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.47"
|
||||
version = "4.5.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
|
||||
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -461,9 +461,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.47"
|
||||
version = "4.5.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
|
||||
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -603,7 +603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -612,7 +612,7 @@ version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1035,7 +1035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1241,7 +1241,7 @@ version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -1521,7 +1521,7 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
@@ -1537,9 +1537,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.43.2"
|
||||
version = "1.43.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0"
|
||||
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
|
||||
dependencies = [
|
||||
"console 0.15.11",
|
||||
"globset",
|
||||
@@ -1617,7 +1617,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1681,7 +1681,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1728,9 +1728,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.78"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -1770,9 +1770,9 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
version = "1.8.4"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "052ef5d9fc958a51aeebdf3713573b36c6fd6eed0bf0e60e204d2c0f8cf19b9f"
|
||||
checksum = "ae28ddc5b90c3e3146a21d051ca095cbc8d932ad8714cf65ddf71a9abb35684c"
|
||||
dependencies = [
|
||||
"annotate-snippets",
|
||||
"libcst_derive",
|
||||
@@ -1785,9 +1785,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcst_derive"
|
||||
version = "1.8.4"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a91a751afee92cbdd59d4bc6754c7672712eec2d30a308f23de4e3287b2929cb"
|
||||
checksum = "dc2de5c2f62bcf8a4f7290b1854388b262c4b68f1db1a3ee3ef6d4c1319b00a3"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -1809,7 +1809,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
@@ -1850,9 +1850,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "lsp-server"
|
||||
@@ -2014,7 +2014,7 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -2026,7 +2026,7 @@ version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -2054,7 +2054,7 @@ version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
@@ -2659,7 +2659,7 @@ version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2721,13 +2721,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.12"
|
||||
version = "0.12.11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
"assert_fs",
|
||||
"bincode 2.0.1",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"cachedir",
|
||||
"clap",
|
||||
"clap_complete_command",
|
||||
@@ -2977,11 +2977,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.12"
|
||||
version = "0.12.11"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"clap",
|
||||
"colored 3.0.0",
|
||||
"fern",
|
||||
@@ -3086,7 +3086,7 @@ name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"compact_str",
|
||||
"get-size2",
|
||||
"is-macro",
|
||||
@@ -3174,7 +3174,7 @@ dependencies = [
|
||||
name = "ruff_python_literal"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"itertools 0.14.0",
|
||||
"ruff_python_ast",
|
||||
"unic-ucd-category",
|
||||
@@ -3185,7 +3185,7 @@ name = "ruff_python_parser"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"bstr",
|
||||
"compact_str",
|
||||
"get-size2",
|
||||
@@ -3210,7 +3210,7 @@ dependencies = [
|
||||
name = "ruff_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"ruff_cache",
|
||||
@@ -3231,7 +3231,7 @@ dependencies = [
|
||||
name = "ruff_python_stdlib"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -3315,7 +3315,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.12"
|
||||
version = "0.12.11"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3408,11 +3408,11 @@ version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3805,7 +3805,7 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4218,7 +4218,7 @@ dependencies = [
|
||||
name = "ty_ide"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"camino",
|
||||
"get-size2",
|
||||
"insta",
|
||||
@@ -4283,7 +4283,7 @@ name = "ty_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"bitvec",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
@@ -4336,7 +4336,7 @@ name = "ty_server"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"crossbeam",
|
||||
"dunce",
|
||||
"insta",
|
||||
@@ -4379,7 +4379,7 @@ name = "ty_test"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
"insta",
|
||||
@@ -4738,22 +4738,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.101"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.101"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@@ -4765,9 +4764,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.51"
|
||||
version = "0.4.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
|
||||
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -4778,9 +4777,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.101"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -4788,9 +4787,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.101"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4801,18 +4800,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.101"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.51"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80cc7f8a4114fdaa0c58383caf973fc126cf004eba25c9dc639bccd3880d55ad"
|
||||
checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"minicov",
|
||||
@@ -4823,9 +4822,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.51"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5ada2ab788d46d4bda04c9d567702a79c8ced14f51f221646a16ed39d0e6a5d"
|
||||
checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4834,9 +4833,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.78"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
|
||||
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -4878,7 +4877,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5117,7 +5116,7 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"bitflags 2.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -115,7 +115,7 @@ jiff = { version = "0.2.0" }
|
||||
js-sys = { version = "0.3.69" }
|
||||
jod-thread = { version = "1.0.0" }
|
||||
libc = { version = "0.2.153" }
|
||||
libcst = { version = "1.8.4", default-features = false }
|
||||
libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
lsp-server = { version = "0.7.6" }
|
||||
lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [
|
||||
@@ -251,14 +251,6 @@ rest_pat_in_fully_bound_structs = "warn"
|
||||
redundant_clone = "warn"
|
||||
debug_assert_with_mut_call = "warn"
|
||||
unused_peekable = "warn"
|
||||
# This lint sometimes flags code whose `if` and `else`
|
||||
# bodies could be flipped when a `!` operator is removed.
|
||||
# While perhaps sometimes a good idea, it is also often
|
||||
# not a good idea due to other factors impacting
|
||||
# readability. For example, if flipping the bodies results
|
||||
# in the `if` being an order of magnitude bigger than the
|
||||
# `else`, then some might consider that harder to read.
|
||||
if_not_else = "allow"
|
||||
|
||||
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
|
||||
large_stack_arrays = "allow"
|
||||
|
||||
@@ -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.12.12/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.12/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.12.11/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.11/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.12.12
|
||||
rev: v0.12.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.12.12"
|
||||
version = "0.12.11"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::io::Write;
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
@@ -1489,8 +1489,6 @@ fn deprecated_direct() {
|
||||
|
||||
#[test]
|
||||
fn deprecated_multiple_direct() {
|
||||
// Multiple deprecated rules selected by exact code should be included
|
||||
// but a warning should be displayed
|
||||
let mut cmd = RuffCheck::default()
|
||||
.args(["--select", "RUF920", "--select", "RUF921"])
|
||||
.build();
|
||||
@@ -1518,10 +1516,16 @@ fn deprecated_indirect() {
|
||||
// since it is not a "direct" selection
|
||||
let mut cmd = RuffCheck::default().args(["--select", "RUF92"]).build();
|
||||
assert_cmd_snapshot!(cmd, @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
RUF920 Hey this is a deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
RUF921 Hey this is another deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
@@ -2151,10 +2155,16 @@ extend-safe-fixes = ["RUF9"]
|
||||
RUF903 Hey this is a stable test rule with a display only fix.
|
||||
--> -:1:1
|
||||
|
||||
RUF920 Hey this is a deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
RUF921 Hey this is another deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
RUF950 Hey this is a test rule that was redirected from another.
|
||||
--> -:1:1
|
||||
|
||||
Found 5 errors.
|
||||
Found 7 errors.
|
||||
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -5780,6 +5780,28 @@ match 42: # invalid-syntax
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_annotations_preview_warning() {
|
||||
assert_cmd_snapshot!(
|
||||
Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--config", "lint.future-annotations = true"])
|
||||
.args(["--select", "F"])
|
||||
.arg("--no-preview")
|
||||
.arg("-")
|
||||
.pass_stdin("1"),
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
warning: The `lint.future-annotations` setting will have no effect because `preview` is disabled
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn up045_nested_optional_flatten_all() {
|
||||
let contents = "\
|
||||
|
||||
@@ -55,10 +55,6 @@ either a redundant alias or, if already present in the file, an `__all__` entry.
|
||||
to remove third-party and standard library imports -- the fix is unsafe because the module's
|
||||
interface changes.
|
||||
|
||||
See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)
|
||||
for more details on how Ruff
|
||||
determines whether an import is first or third-party.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
@@ -87,6 +83,11 @@ else:
|
||||
print("numpy is not installed")
|
||||
```
|
||||
|
||||
## Preview
|
||||
When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
|
||||
the criterion for determining whether an import is first-party
|
||||
is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
|
||||
|
||||
## Options
|
||||
- `lint.ignore-init-module-imports`
|
||||
- `lint.pyflakes.allowed-unused-imports`
|
||||
|
||||
@@ -95,7 +95,7 @@ exit_code: 1
|
||||
"rules": [
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
|
||||
@@ -454,26 +454,24 @@ impl Diagnostic {
|
||||
|
||||
/// Computes the start source location for the message.
|
||||
///
|
||||
/// Returns None if the diagnostic has no primary span, if its file is not a `SourceFile`,
|
||||
/// or if the span has no range.
|
||||
pub fn ruff_start_location(&self) -> Option<LineColumn> {
|
||||
Some(
|
||||
self.ruff_source_file()?
|
||||
.to_source_code()
|
||||
.line_column(self.range()?.start()),
|
||||
)
|
||||
/// Panics if the diagnostic has no primary span, if its file is not a `SourceFile`, or if the
|
||||
/// span has no range.
|
||||
pub fn expect_ruff_start_location(&self) -> LineColumn {
|
||||
self.expect_primary_span()
|
||||
.expect_ruff_file()
|
||||
.to_source_code()
|
||||
.line_column(self.expect_range().start())
|
||||
}
|
||||
|
||||
/// Computes the end source location for the message.
|
||||
///
|
||||
/// Returns None if the diagnostic has no primary span, if its file is not a `SourceFile`,
|
||||
/// or if the span has no range.
|
||||
pub fn ruff_end_location(&self) -> Option<LineColumn> {
|
||||
Some(
|
||||
self.ruff_source_file()?
|
||||
.to_source_code()
|
||||
.line_column(self.range()?.end()),
|
||||
)
|
||||
/// Panics if the diagnostic has no primary span, if its file is not a `SourceFile`, or if the
|
||||
/// span has no range.
|
||||
pub fn expect_ruff_end_location(&self) -> LineColumn {
|
||||
self.expect_primary_span()
|
||||
.expect_ruff_file()
|
||||
.to_source_code()
|
||||
.line_column(self.expect_range().end())
|
||||
}
|
||||
|
||||
/// Returns the [`SourceFile`] which the message belongs to.
|
||||
@@ -503,18 +501,13 @@ impl Diagnostic {
|
||||
|
||||
/// Returns the ordering of diagnostics based on the start of their ranges, if they have any.
|
||||
///
|
||||
/// Panics if either diagnostic has no primary span, or if its file is not a `SourceFile`.
|
||||
/// Panics if either diagnostic has no primary span, if the span has no range, or if its file is
|
||||
/// not a `SourceFile`.
|
||||
pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering {
|
||||
let a = (
|
||||
self.expect_ruff_source_file(),
|
||||
self.range().map(|r| r.start()),
|
||||
);
|
||||
let b = (
|
||||
(self.expect_ruff_source_file(), self.expect_range().start()).cmp(&(
|
||||
other.expect_ruff_source_file(),
|
||||
other.range().map(|r| r.start()),
|
||||
);
|
||||
|
||||
a.cmp(&b)
|
||||
other.expect_range().start(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,19 +81,14 @@ impl IndentStyle {
|
||||
pub const fn is_space(&self) -> bool {
|
||||
matches!(self, IndentStyle::Space)
|
||||
}
|
||||
|
||||
/// Returns the string representation of the indent style.
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
IndentStyle::Tab => "tab",
|
||||
IndentStyle::Space => "space",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IndentStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
match self {
|
||||
IndentStyle::Tab => std::write!(f, "tab"),
|
||||
IndentStyle::Space => std::write!(f, "space"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,16 +139,4 @@ impl LineEnding {
|
||||
LineEnding::CarriageReturn => "\r",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string used to configure this line ending.
|
||||
///
|
||||
/// See [`LineEnding::as_str`] for the actual string representation of the line ending.
|
||||
#[inline]
|
||||
pub const fn as_setting_str(&self) -> &'static str {
|
||||
match self {
|
||||
LineEnding::LineFeed => "lf",
|
||||
LineEnding::CarriageReturnLineFeed => "crlf",
|
||||
LineEnding::CarriageReturn => "cr",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.12"
|
||||
version = "0.12.11"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -141,133 +141,3 @@ class ExampleWithKeywords:
|
||||
|
||||
def method3(self):
|
||||
super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/19357
|
||||
# Must be detected
|
||||
class ParentD:
|
||||
def f(self):
|
||||
print("D")
|
||||
|
||||
class ChildD1(ParentD):
|
||||
def f(self):
|
||||
if False: __class__ # Python injects __class__ into scope
|
||||
builtins.super(ChildD1, self).f()
|
||||
|
||||
class ChildD2(ParentD):
|
||||
def f(self):
|
||||
if False: super # Python injects __class__ into scope
|
||||
builtins.super(ChildD2, self).f()
|
||||
|
||||
class ChildD3(ParentD):
|
||||
def f(self):
|
||||
builtins.super(ChildD3, self).f()
|
||||
super # Python injects __class__ into scope
|
||||
|
||||
import builtins as builtins_alias
|
||||
class ChildD4(ParentD):
|
||||
def f(self):
|
||||
builtins_alias.super(ChildD4, self).f()
|
||||
super # Python injects __class__ into scope
|
||||
|
||||
class ChildD5(ParentD):
|
||||
def f(self):
|
||||
super = 1
|
||||
super # Python injects __class__ into scope
|
||||
builtins.super(ChildD5, self).f()
|
||||
|
||||
class ChildD6(ParentD):
|
||||
def f(self):
|
||||
super: "Any"
|
||||
__class__ # Python injects __class__ into scope
|
||||
builtins.super(ChildD6, self).f()
|
||||
|
||||
class ChildD7(ParentD):
|
||||
def f(self):
|
||||
def x():
|
||||
__class__ # Python injects __class__ into scope
|
||||
builtins.super(ChildD7, self).f()
|
||||
|
||||
class ChildD8(ParentD):
|
||||
def f(self):
|
||||
def x():
|
||||
super = 1
|
||||
super # Python injects __class__ into scope
|
||||
builtins.super(ChildD8, self).f()
|
||||
|
||||
class ChildD9(ParentD):
|
||||
def f(self):
|
||||
def x():
|
||||
__class__ = 1
|
||||
__class__ # Python injects __class__ into scope
|
||||
builtins.super(ChildD9, self).f()
|
||||
|
||||
class ChildD10(ParentD):
|
||||
def f(self):
|
||||
def x():
|
||||
__class__ = 1
|
||||
super # Python injects __class__ into scope
|
||||
builtins.super(ChildD10, self).f()
|
||||
|
||||
|
||||
# Must be ignored
|
||||
class ParentI:
|
||||
def f(self):
|
||||
print("I")
|
||||
|
||||
class ChildI1(ParentI):
|
||||
def f(self):
|
||||
builtins.super(ChildI1, self).f() # no __class__ in the local scope
|
||||
|
||||
|
||||
class ChildI2(ParentI):
|
||||
def b(self):
|
||||
x = __class__
|
||||
if False: super
|
||||
|
||||
def f(self):
|
||||
self.b()
|
||||
builtins.super(ChildI2, self).f() # no __class__ in the local scope
|
||||
|
||||
class ChildI3(ParentI):
|
||||
def f(self):
|
||||
if False: super
|
||||
def x(_):
|
||||
builtins.super(ChildI3, self).f() # no __class__ in the local scope
|
||||
x(None)
|
||||
|
||||
class ChildI4(ParentI):
|
||||
def f(self):
|
||||
super: "str"
|
||||
builtins.super(ChildI4, self).f() # no __class__ in the local scope
|
||||
|
||||
class ChildI5(ParentI):
|
||||
def f(self):
|
||||
super = 1
|
||||
__class__ = 3
|
||||
builtins.super(ChildI5, self).f() # no __class__ in the local scope
|
||||
|
||||
class ChildI6(ParentI):
|
||||
def f(self):
|
||||
__class__ = None
|
||||
__class__
|
||||
builtins.super(ChildI6, self).f() # no __class__ in the local scope
|
||||
|
||||
class ChildI7(ParentI):
|
||||
def f(self):
|
||||
__class__ = None
|
||||
super
|
||||
builtins.super(ChildI7, self).f()
|
||||
|
||||
class ChildI8(ParentI):
|
||||
def f(self):
|
||||
__class__: "Any"
|
||||
super
|
||||
builtins.super(ChildI8, self).f()
|
||||
|
||||
class ChildI9(ParentI):
|
||||
def f(self):
|
||||
class A:
|
||||
def foo(self):
|
||||
if False: super
|
||||
if False: __class__
|
||||
builtins.super(ChildI9, self).f()
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
from collections.abc import Generator, AsyncGenerator
|
||||
|
||||
|
||||
def func() -> Generator[int, None, None]:
|
||||
yield 42
|
||||
|
||||
|
||||
def func() -> Generator[int, None]:
|
||||
yield 42
|
||||
|
||||
|
||||
def func() -> Generator[int]:
|
||||
yield 42
|
||||
|
||||
|
||||
def func() -> Generator[int, int, int]:
|
||||
foo = yield 42
|
||||
return foo
|
||||
|
||||
|
||||
def func() -> Generator[int, int, None]:
|
||||
_ = yield 42
|
||||
return None
|
||||
|
||||
|
||||
def func() -> Generator[int, None, int]:
|
||||
yield 42
|
||||
return 42
|
||||
|
||||
|
||||
async def func() -> AsyncGenerator[int, None]:
|
||||
yield 42
|
||||
|
||||
|
||||
async def func() -> AsyncGenerator[int]:
|
||||
yield 42
|
||||
|
||||
|
||||
async def func() -> AsyncGenerator[int, int]:
|
||||
foo = yield 42
|
||||
return foo
|
||||
|
||||
|
||||
from typing import Generator, AsyncGenerator
|
||||
|
||||
|
||||
def func() -> Generator[str, None, None]:
|
||||
yield "hello"
|
||||
|
||||
|
||||
async def func() -> AsyncGenerator[str, None]:
|
||||
yield "hello"
|
||||
|
||||
|
||||
async def func() -> AsyncGenerator[ # type: ignore
|
||||
str,
|
||||
None
|
||||
]:
|
||||
yield "hello"
|
||||
@@ -42,7 +42,3 @@ b"a" in bytes("a", "utf-8")
|
||||
1 in set(set([1]))
|
||||
'' in {""}
|
||||
frozenset() in {frozenset()}
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/20238
|
||||
"b" in f"" "" # Error
|
||||
"b" in f"" "x" # OK
|
||||
|
||||
@@ -8,7 +8,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::{
|
||||
is_optional_as_none_in_union_enabled, is_unnecessary_default_type_args_stubs_enabled,
|
||||
is_assert_raises_exception_call_enabled, is_optional_as_none_in_union_enabled,
|
||||
};
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::{
|
||||
@@ -142,10 +142,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
}
|
||||
|
||||
if checker.is_rule_enabled(Rule::UnnecessaryDefaultTypeArgs) {
|
||||
if checker.target_version() >= PythonVersion::PY313
|
||||
|| is_unnecessary_default_type_args_stubs_enabled(checker.settings())
|
||||
&& checker.semantic().in_stub_file()
|
||||
{
|
||||
if checker.target_version() >= PythonVersion::PY313 {
|
||||
pyupgrade::rules::unnecessary_default_type_args(checker, expr);
|
||||
}
|
||||
}
|
||||
@@ -1295,7 +1292,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::NonOctalPermissions) {
|
||||
ruff::rules::non_octal_permissions(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::AssertRaisesException) {
|
||||
if checker.is_rule_enabled(Rule::AssertRaisesException)
|
||||
&& is_assert_raises_exception_call_enabled(checker.settings())
|
||||
{
|
||||
flake8_bugbear::rules::assert_raises_exception_call(checker, call);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Async, "109") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncFunctionWithTimeout),
|
||||
(Flake8Async, "110") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncBusyWait),
|
||||
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncZeroSleep),
|
||||
(Flake8Async, "116") => (RuleGroup::Stable, rules::flake8_async::rules::LongSleepNotForever),
|
||||
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::LongSleepNotForever),
|
||||
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
|
||||
(Flake8Async, "212") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingHttpCallHttpxInAsyncFunction),
|
||||
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
|
||||
@@ -563,7 +563,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pyupgrade, "035") => (RuleGroup::Stable, rules::pyupgrade::rules::DeprecatedImport),
|
||||
(Pyupgrade, "036") => (RuleGroup::Stable, rules::pyupgrade::rules::OutdatedVersionBlock),
|
||||
(Pyupgrade, "037") => (RuleGroup::Stable, rules::pyupgrade::rules::QuotedAnnotation),
|
||||
(Pyupgrade, "038") => (RuleGroup::Removed, rules::pyupgrade::rules::NonPEP604Isinstance),
|
||||
(Pyupgrade, "038") => (RuleGroup::Deprecated, rules::pyupgrade::rules::NonPEP604Isinstance),
|
||||
(Pyupgrade, "039") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryClassParentheses),
|
||||
(Pyupgrade, "040") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695TypeAlias),
|
||||
(Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias),
|
||||
@@ -574,7 +574,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pyupgrade, "046") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695GenericClass),
|
||||
(Pyupgrade, "047") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695GenericFunction),
|
||||
(Pyupgrade, "049") => (RuleGroup::Stable, rules::pyupgrade::rules::PrivateTypeParameter),
|
||||
(Pyupgrade, "050") => (RuleGroup::Stable, rules::pyupgrade::rules::UselessClassMetaclassType),
|
||||
(Pyupgrade, "050") => (RuleGroup::Preview, rules::pyupgrade::rules::UselessClassMetaclassType),
|
||||
|
||||
// pydocstyle
|
||||
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),
|
||||
@@ -773,7 +773,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(PandasVet, "013") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfDotStack),
|
||||
(PandasVet, "015") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasUseOfPdMerge),
|
||||
(PandasVet, "101") => (RuleGroup::Stable, rules::pandas_vet::rules::PandasNuniqueConstantSeriesCheck),
|
||||
(PandasVet, "901") => (RuleGroup::Removed, rules::pandas_vet::rules::PandasDfVariableName),
|
||||
(PandasVet, "901") => (RuleGroup::Deprecated, rules::pandas_vet::rules::PandasDfVariableName),
|
||||
|
||||
// flake8-errmsg
|
||||
(Flake8ErrMsg, "101") => (RuleGroup::Stable, rules::flake8_errmsg::rules::RawStringInException),
|
||||
@@ -830,8 +830,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Pyi, "056") => (RuleGroup::Stable, rules::flake8_pyi::rules::UnsupportedMethodCallOnAll),
|
||||
(Flake8Pyi, "058") => (RuleGroup::Stable, rules::flake8_pyi::rules::GeneratorReturnFromIterMethod),
|
||||
(Flake8Pyi, "057") => (RuleGroup::Stable, rules::flake8_pyi::rules::ByteStringUsage),
|
||||
(Flake8Pyi, "059") => (RuleGroup::Stable, rules::flake8_pyi::rules::GenericNotLastBaseClass),
|
||||
(Flake8Pyi, "061") => (RuleGroup::Stable, rules::flake8_pyi::rules::RedundantNoneLiteral),
|
||||
(Flake8Pyi, "059") => (RuleGroup::Preview, rules::flake8_pyi::rules::GenericNotLastBaseClass),
|
||||
(Flake8Pyi, "061") => (RuleGroup::Preview, rules::flake8_pyi::rules::RedundantNoneLiteral),
|
||||
(Flake8Pyi, "062") => (RuleGroup::Stable, rules::flake8_pyi::rules::DuplicateLiteralMember),
|
||||
(Flake8Pyi, "063") => (RuleGroup::Stable, rules::flake8_pyi::rules::Pep484StylePositionalOnlyParameter),
|
||||
(Flake8Pyi, "064") => (RuleGroup::Stable, rules::flake8_pyi::rules::RedundantFinalLiteral),
|
||||
@@ -956,7 +956,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8UsePathlib, "207") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::Glob),
|
||||
(Flake8UsePathlib, "208") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsListdir),
|
||||
(Flake8UsePathlib, "210") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::InvalidPathlibWithSuffix),
|
||||
(Flake8UsePathlib, "211") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsSymlink),
|
||||
(Flake8UsePathlib, "211") => (RuleGroup::Preview, rules::flake8_use_pathlib::rules::OsSymlink),
|
||||
|
||||
// flake8-logging-format
|
||||
(Flake8LoggingFormat, "001") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingStringFormat),
|
||||
@@ -1032,7 +1032,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern),
|
||||
(Ruff, "040") => (RuleGroup::Stable, rules::ruff::rules::InvalidAssertMessageLiteralArgument),
|
||||
(Ruff, "041") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryNestedLiteral),
|
||||
(Ruff, "043") => (RuleGroup::Stable, rules::ruff::rules::PytestRaisesAmbiguousPattern),
|
||||
(Ruff, "043") => (RuleGroup::Preview, rules::ruff::rules::PytestRaisesAmbiguousPattern),
|
||||
(Ruff, "045") => (RuleGroup::Preview, rules::ruff::rules::ImplicitClassVarInDataclass),
|
||||
(Ruff, "046") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryCastToInt),
|
||||
(Ruff, "047") => (RuleGroup::Preview, rules::ruff::rules::NeedlessElse),
|
||||
@@ -1046,8 +1046,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
|
||||
(Ruff, "057") => (RuleGroup::Stable, rules::ruff::rules::UnnecessaryRound),
|
||||
(Ruff, "058") => (RuleGroup::Stable, rules::ruff::rules::StarmapZip),
|
||||
(Ruff, "059") => (RuleGroup::Stable, rules::ruff::rules::UnusedUnpackedVariable),
|
||||
(Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection),
|
||||
(Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable),
|
||||
(Ruff, "060") => (RuleGroup::Stable, rules::ruff::rules::InEmptyCollection),
|
||||
(Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises),
|
||||
(Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict),
|
||||
(Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::NonOctalPermissions),
|
||||
@@ -1106,11 +1106,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
|
||||
// airflow
|
||||
(Airflow, "001") => (RuleGroup::Stable, rules::airflow::rules::AirflowVariableNameTaskIdMismatch),
|
||||
(Airflow, "002") => (RuleGroup::Stable, rules::airflow::rules::AirflowDagNoScheduleArgument),
|
||||
(Airflow, "301") => (RuleGroup::Stable, rules::airflow::rules::Airflow3Removal),
|
||||
(Airflow, "302") => (RuleGroup::Stable, rules::airflow::rules::Airflow3MovedToProvider),
|
||||
(Airflow, "311") => (RuleGroup::Stable, rules::airflow::rules::Airflow3SuggestedUpdate),
|
||||
(Airflow, "312") => (RuleGroup::Stable, rules::airflow::rules::Airflow3SuggestedToMoveToProvider),
|
||||
(Airflow, "002") => (RuleGroup::Preview, rules::airflow::rules::AirflowDagNoScheduleArgument),
|
||||
(Airflow, "301") => (RuleGroup::Preview, rules::airflow::rules::Airflow3Removal),
|
||||
(Airflow, "302") => (RuleGroup::Preview, rules::airflow::rules::Airflow3MovedToProvider),
|
||||
(Airflow, "311") => (RuleGroup::Preview, rules::airflow::rules::Airflow3SuggestedUpdate),
|
||||
(Airflow, "312") => (RuleGroup::Preview, rules::airflow::rules::Airflow3SuggestedToMoveToProvider),
|
||||
|
||||
// perflint
|
||||
(Perflint, "101") => (RuleGroup::Stable, rules::perflint::rules::UnnecessaryListCast),
|
||||
@@ -1137,7 +1137,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "105") => (RuleGroup::Stable, rules::refurb::rules::PrintEmptyString),
|
||||
(Refurb, "110") => (RuleGroup::Preview, rules::refurb::rules::IfExpInsteadOfOrOperator),
|
||||
(Refurb, "113") => (RuleGroup::Preview, rules::refurb::rules::RepeatedAppend),
|
||||
(Refurb, "116") => (RuleGroup::Stable, rules::refurb::rules::FStringNumberFormat),
|
||||
(Refurb, "116") => (RuleGroup::Preview, rules::refurb::rules::FStringNumberFormat),
|
||||
(Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator),
|
||||
(Refurb, "122") => (RuleGroup::Stable, rules::refurb::rules::ForLoopWrites),
|
||||
(Refurb, "129") => (RuleGroup::Stable, rules::refurb::rules::ReadlinesInFor),
|
||||
|
||||
@@ -19,7 +19,7 @@ impl Emitter for GithubEmitter {
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for diagnostic in diagnostics {
|
||||
let source_location = diagnostic.ruff_start_location().unwrap_or_default();
|
||||
let source_location = diagnostic.expect_ruff_start_location();
|
||||
let filename = diagnostic.expect_ruff_filename();
|
||||
let location = if context.is_notebook(&filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
@@ -29,7 +29,7 @@ impl Emitter for GithubEmitter {
|
||||
source_location
|
||||
};
|
||||
|
||||
let end_location = diagnostic.ruff_end_location().unwrap_or_default();
|
||||
let end_location = diagnostic.expect_ruff_end_location();
|
||||
|
||||
write!(
|
||||
writer,
|
||||
|
||||
@@ -105,7 +105,7 @@ fn group_diagnostics_by_filename(
|
||||
.or_insert_with(Vec::new)
|
||||
.push(MessageWithLocation {
|
||||
message: diagnostic,
|
||||
start_location: diagnostic.ruff_start_location().unwrap_or_default(),
|
||||
start_location: diagnostic.expect_ruff_start_location(),
|
||||
});
|
||||
}
|
||||
grouped_messages
|
||||
|
||||
@@ -158,8 +158,8 @@ 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 start_location = message.expect_ruff_start_location();
|
||||
let end_location = message.expect_ruff_end_location();
|
||||
let path = normalize_path(&*message.expect_ruff_filename());
|
||||
Ok(Self {
|
||||
code: RuleCode::from(message),
|
||||
@@ -178,8 +178,8 @@ impl<'a> SarifResult<'a> {
|
||||
#[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 start_location = message.expect_ruff_start_location();
|
||||
let end_location = message.expect_ruff_end_location();
|
||||
let path = normalize_path(&*message.expect_ruff_filename());
|
||||
Ok(Self {
|
||||
code: RuleCode::from(message),
|
||||
|
||||
@@ -81,7 +81,7 @@ expression: value
|
||||
"rules": [
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
@@ -119,7 +119,7 @@ expression: value
|
||||
},
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Fix safety\n\nThis rule's fix is marked as unsafe because removing an unused variable assignment may\ndelete comments that are attached to the assignment.\n\n## See also\n\nThis rule does not apply to bindings in unpacked assignments (e.g. `x, y = 1, 2`). See\n[`unused-unpacked-variable`][RUF059] for this case.\n\n## Options\n- `lint.dummy-variable-rgx`\n\n[RUF059]: https://docs.astral.sh/ruff/rules/unused-unpacked-variable/\n"
|
||||
"text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Fix safety\n\nThis rule's fix is marked as unsafe because removing an unused variable assignment may\ndelete comments that are attached to the assignment.\n\n## Options\n- `lint.dummy-variable-rgx`\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "Local variable `{name}` is assigned to but never used"
|
||||
|
||||
@@ -11,6 +11,11 @@ pub(crate) const fn is_py314_support_enabled(settings: &LinterSettings) -> bool
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/16565
|
||||
pub(crate) const fn is_full_path_match_source_strategy_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// Rule-specific behavior
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/15541
|
||||
@@ -195,11 +200,35 @@ pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) ->
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/18208
|
||||
pub(crate) const fn is_multiple_with_statements_fix_safe_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/18400
|
||||
pub(crate) const fn is_ignore_init_files_in_useless_alias_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/18572
|
||||
pub(crate) const fn is_optional_as_none_in_union_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/18547
|
||||
pub(crate) const fn is_invalid_async_mock_access_check_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/18867
|
||||
pub(crate) const fn is_raise_exception_byte_string_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/18683
|
||||
pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
|
||||
settings: &LinterSettings,
|
||||
@@ -207,14 +236,27 @@ pub(crate) const fn is_safe_super_call_with_parameters_fix_enabled(
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19063
|
||||
pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19100
|
||||
pub(crate) const fn is_add_future_annotations_imports_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19390
|
||||
pub(crate) const fn is_trailing_comma_type_params_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19851
|
||||
pub(crate) const fn is_maxsplit_without_separator_fix_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/20027
|
||||
pub(crate) const fn is_unnecessary_default_type_args_stubs_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
// https://github.com/astral-sh/ruff/pull/20106
|
||||
pub(crate) const fn is_bidi_forbid_arabic_letter_mark_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -214,8 +214,10 @@ impl RuleSelector {
|
||||
RuleGroup::Preview => {
|
||||
preview_enabled && (self.is_exact() || !preview_require_explicit)
|
||||
}
|
||||
// Deprecated rules are excluded by default unless explicitly selected
|
||||
RuleGroup::Deprecated => !preview_enabled && self.is_exact(),
|
||||
// Deprecated rules are excluded in preview mode and with 'All' option unless explicitly selected
|
||||
RuleGroup::Deprecated => {
|
||||
(!preview_enabled || self.is_exact()) && !matches!(self, RuleSelector::All)
|
||||
}
|
||||
// Removed rules are included if explicitly selected but will error downstream
|
||||
RuleGroup::Removed => self.is_exact(),
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ use ruff_text_size::TextRange;
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of Airflow functions and values that have been moved to its providers
|
||||
/// (e.g., `apache-airflow-providers-fab`).
|
||||
/// Checks for uses of Airflow functions and values that have been moved to it providers.
|
||||
/// (e.g., apache-airflow-providers-fab)
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Airflow 3.0 moved various deprecated functions, members, and other
|
||||
/// values to its providers. The user needs to install the corresponding provider and replace
|
||||
/// the original usage with the one in the provider.
|
||||
/// the original usage with the one in the provider
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
|
||||
@@ -23,7 +23,7 @@ use ruff_text_size::TextRange;
|
||||
/// ## Why is this bad?
|
||||
/// Airflow 3.0 removed various deprecated functions, members, and other
|
||||
/// values. Some have more modern replacements. Others are considered too niche
|
||||
/// and not worth continued maintenance in Airflow.
|
||||
/// and not worth to be maintained in Airflow.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
|
||||
@@ -17,9 +17,9 @@ use ruff_text_size::TextRange;
|
||||
/// ## Why is this bad?
|
||||
/// Airflow 3.0 removed various deprecated functions, members, and other
|
||||
/// values. Some have more modern replacements. Others are considered too niche
|
||||
/// and not worth continued maintenance in Airflow.
|
||||
/// and not worth to be maintained in Airflow.
|
||||
/// Even though these symbols still work fine on Airflow 3.0, they are expected to be removed in a future version.
|
||||
/// Where available, users should replace the removed functionality with the new alternatives.
|
||||
/// The user is suggested to replace the original usage with the new ones.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
|
||||
@@ -79,6 +79,7 @@ impl Violation for FastApiUnusedPathParameter {
|
||||
function_name,
|
||||
is_positional,
|
||||
} = self;
|
||||
#[expect(clippy::if_not_else)]
|
||||
if !is_positional {
|
||||
format!(
|
||||
"Parameter `{arg_name}` appears in route path, but not in `{function_name}` signature"
|
||||
|
||||
@@ -16,6 +16,8 @@ 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"))]
|
||||
@@ -175,4 +177,23 @@ mod tests {
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::AssertRaisesException, Path::new("B017_0.py"))]
|
||||
#[test_case(Rule::AssertRaisesException, Path::new("B017_1.py"))]
|
||||
fn rules_preview(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,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B017 Do not assert blind exception: `Exception`
|
||||
--> B017_1.py:20:9
|
||||
|
|
||||
18 | class Foobar(unittest.TestCase):
|
||||
19 | def call_form_raises(self) -> None:
|
||||
20 | self.assertRaises(Exception, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
21 | self.assertRaises(BaseException, something_else)
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `BaseException`
|
||||
--> B017_1.py:21:9
|
||||
|
|
||||
19 | def call_form_raises(self) -> None:
|
||||
20 | self.assertRaises(Exception, something_else)
|
||||
21 | self.assertRaises(BaseException, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `Exception`
|
||||
--> B017_1.py:25:5
|
||||
|
|
||||
24 | def test_pytest_call_form() -> None:
|
||||
25 | pytest.raises(Exception, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
26 | pytest.raises(BaseException, something_else)
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `BaseException`
|
||||
--> B017_1.py:26:5
|
||||
|
|
||||
24 | def test_pytest_call_form() -> None:
|
||||
25 | pytest.raises(Exception, something_else)
|
||||
26 | pytest.raises(BaseException, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
27 |
|
||||
28 | pytest.raises(Exception, something_else, match="hello")
|
||||
|
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B017 Do not assert blind exception: `Exception`
|
||||
--> B017_0.py:23:14
|
||||
|
|
||||
21 | class Foobar(unittest.TestCase):
|
||||
22 | def evil_raises(self) -> None:
|
||||
23 | with self.assertRaises(Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
24 | raise Exception("Evil I say!")
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `BaseException`
|
||||
--> B017_0.py:27:14
|
||||
|
|
||||
26 | def also_evil_raises(self) -> None:
|
||||
27 | with self.assertRaises(BaseException):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
28 | raise Exception("Evil I say!")
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `Exception`
|
||||
--> B017_0.py:45:10
|
||||
|
|
||||
44 | def test_pytest_raises():
|
||||
45 | with pytest.raises(Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
46 | raise ValueError("Hello")
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `Exception`
|
||||
--> B017_0.py:48:10
|
||||
|
|
||||
46 | raise ValueError("Hello")
|
||||
47 |
|
||||
48 | with pytest.raises(Exception), pytest.raises(ValueError):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
49 | raise ValueError("Hello")
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `Exception`
|
||||
--> B017_0.py:57:36
|
||||
|
|
||||
55 | raise ValueError("This is also fine")
|
||||
56 |
|
||||
57 | with contextlib.nullcontext(), pytest.raises(Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
58 | raise ValueError("Multiple context managers")
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `Exception`
|
||||
--> B017_0.py:62:10
|
||||
|
|
||||
61 | def test_pytest_raises_keyword():
|
||||
62 | with pytest.raises(expected_exception=Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
63 | raise ValueError("Should be flagged")
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `Exception`
|
||||
--> B017_0.py:68:18
|
||||
|
|
||||
66 | class TestKwargs(unittest.TestCase):
|
||||
67 | def test_method(self):
|
||||
68 | with self.assertRaises(exception=Exception):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
69 | raise ValueError("Should be flagged")
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `BaseException`
|
||||
--> B017_0.py:71:18
|
||||
|
|
||||
69 | raise ValueError("Should be flagged")
|
||||
70 |
|
||||
71 | with self.assertRaises(exception=BaseException):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
72 | raise ValueError("Should be flagged")
|
||||
|
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B017 Do not assert blind exception: `Exception`
|
||||
--> B017_1.py:20:9
|
||||
|
|
||||
18 | class Foobar(unittest.TestCase):
|
||||
19 | def call_form_raises(self) -> None:
|
||||
20 | self.assertRaises(Exception, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
21 | self.assertRaises(BaseException, something_else)
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `BaseException`
|
||||
--> B017_1.py:21:9
|
||||
|
|
||||
19 | def call_form_raises(self) -> None:
|
||||
20 | self.assertRaises(Exception, something_else)
|
||||
21 | self.assertRaises(BaseException, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `Exception`
|
||||
--> B017_1.py:25:5
|
||||
|
|
||||
24 | def test_pytest_call_form() -> None:
|
||||
25 | pytest.raises(Exception, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
26 | pytest.raises(BaseException, something_else)
|
||||
|
|
||||
|
||||
B017 Do not assert blind exception: `BaseException`
|
||||
--> B017_1.py:26:5
|
||||
|
|
||||
24 | def test_pytest_call_form() -> None:
|
||||
25 | pytest.raises(Exception, something_else)
|
||||
26 | pytest.raises(BaseException, something_else)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
27 |
|
||||
28 | pytest.raises(Exception, something_else, match="hello")
|
||||
|
|
||||
@@ -10,7 +10,7 @@ mod tests {
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_diagnostics, settings};
|
||||
use crate::{assert_diagnostics, assert_diagnostics_diff, settings};
|
||||
|
||||
#[test_case(Path::new("COM81.py"))]
|
||||
#[test_case(Path::new("COM81_syntax_error.py"))]
|
||||
@@ -27,4 +27,28 @@ mod tests {
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("COM81.py"))]
|
||||
#[test_case(Path::new("COM81_syntax_error.py"))]
|
||||
fn preview_rules(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("preview_diff__{}", path.to_string_lossy());
|
||||
let rules = vec![
|
||||
Rule::MissingTrailingComma,
|
||||
Rule::TrailingCommaOnBareTuple,
|
||||
Rule::ProhibitedTrailingComma,
|
||||
];
|
||||
let settings_before = settings::LinterSettings::for_rules(rules.clone());
|
||||
let settings_after = settings::LinterSettings {
|
||||
preview: crate::settings::types::PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rules(rules)
|
||||
};
|
||||
|
||||
assert_diagnostics_diff!(
|
||||
snapshot,
|
||||
Path::new("flake8_commas").join(path).as_path(),
|
||||
&settings_before,
|
||||
&settings_after
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::checkers::ast::LintContext;
|
||||
use crate::preview::is_trailing_comma_type_params_enabled;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::{AlwaysFixableViolation, Violation};
|
||||
use crate::{Edit, Fix};
|
||||
|
||||
@@ -296,7 +298,7 @@ pub(crate) fn trailing_commas(
|
||||
}
|
||||
|
||||
// Update the comma context stack.
|
||||
let context = update_context(token, prev, prev_prev, &mut stack);
|
||||
let context = update_context(token, prev, prev_prev, &mut stack, lint_context.settings());
|
||||
|
||||
check_token(token, prev, prev_prev, context, locator, lint_context);
|
||||
|
||||
@@ -415,6 +417,7 @@ fn update_context(
|
||||
prev: SimpleToken,
|
||||
prev_prev: SimpleToken,
|
||||
stack: &mut Vec<Context>,
|
||||
settings: &LinterSettings,
|
||||
) -> Context {
|
||||
let new_context = match token.ty {
|
||||
TokenType::OpeningBracket => match (prev.ty, prev_prev.ty) {
|
||||
@@ -424,11 +427,19 @@ fn update_context(
|
||||
}
|
||||
_ => Context::new(ContextType::Tuple),
|
||||
},
|
||||
TokenType::OpeningSquareBracket => match (prev.ty, prev_prev.ty) {
|
||||
(TokenType::Named, TokenType::Def | TokenType::Class | TokenType::Type) => {
|
||||
Context::new(ContextType::TypeParameters)
|
||||
TokenType::OpeningSquareBracket if is_trailing_comma_type_params_enabled(settings) => {
|
||||
match (prev.ty, prev_prev.ty) {
|
||||
(TokenType::Named, TokenType::Def | TokenType::Class | TokenType::Type) => {
|
||||
Context::new(ContextType::TypeParameters)
|
||||
}
|
||||
(TokenType::ClosingBracket | TokenType::Named | TokenType::String, _) => {
|
||||
Context::new(ContextType::Subscript)
|
||||
}
|
||||
_ => Context::new(ContextType::List),
|
||||
}
|
||||
(TokenType::ClosingBracket | TokenType::Named | TokenType::String, _) => {
|
||||
}
|
||||
TokenType::OpeningSquareBracket => match prev.ty {
|
||||
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
|
||||
Context::new(ContextType::Subscript)
|
||||
}
|
||||
_ => Context::new(ContextType::List),
|
||||
|
||||
@@ -939,111 +939,3 @@ help: Add trailing comma
|
||||
644 | )
|
||||
645 |
|
||||
646 | assert False, f"<- This is not a trailing comma"
|
||||
|
||||
COM812 [*] Trailing comma missing
|
||||
--> COM81.py:655:6
|
||||
|
|
||||
654 | type X[
|
||||
655 | T
|
||||
| ^
|
||||
656 | ] = T
|
||||
657 | def f[
|
||||
|
|
||||
help: Add trailing comma
|
||||
652 | }"""
|
||||
653 |
|
||||
654 | type X[
|
||||
- T
|
||||
655 + T,
|
||||
656 | ] = T
|
||||
657 | def f[
|
||||
658 | T
|
||||
|
||||
COM812 [*] Trailing comma missing
|
||||
--> COM81.py:658:6
|
||||
|
|
||||
656 | ] = T
|
||||
657 | def f[
|
||||
658 | T
|
||||
| ^
|
||||
659 | ](): pass
|
||||
660 | class C[
|
||||
|
|
||||
help: Add trailing comma
|
||||
655 | T
|
||||
656 | ] = T
|
||||
657 | def f[
|
||||
- T
|
||||
658 + T,
|
||||
659 | ](): pass
|
||||
660 | class C[
|
||||
661 | T
|
||||
|
||||
COM812 [*] Trailing comma missing
|
||||
--> COM81.py:661:6
|
||||
|
|
||||
659 | ](): pass
|
||||
660 | class C[
|
||||
661 | T
|
||||
| ^
|
||||
662 | ]: pass
|
||||
|
|
||||
help: Add trailing comma
|
||||
658 | T
|
||||
659 | ](): pass
|
||||
660 | class C[
|
||||
- T
|
||||
661 + T,
|
||||
662 | ]: pass
|
||||
663 |
|
||||
664 | type X[T,] = T
|
||||
|
||||
COM819 [*] Trailing comma prohibited
|
||||
--> COM81.py:664:9
|
||||
|
|
||||
662 | ]: pass
|
||||
663 |
|
||||
664 | type X[T,] = T
|
||||
| ^
|
||||
665 | def f[T,](): pass
|
||||
666 | class C[T,]: pass
|
||||
|
|
||||
help: Remove trailing comma
|
||||
661 | T
|
||||
662 | ]: pass
|
||||
663 |
|
||||
- type X[T,] = T
|
||||
664 + type X[T] = T
|
||||
665 | def f[T,](): pass
|
||||
666 | class C[T,]: pass
|
||||
|
||||
COM819 [*] Trailing comma prohibited
|
||||
--> COM81.py:665:8
|
||||
|
|
||||
664 | type X[T,] = T
|
||||
665 | def f[T,](): pass
|
||||
| ^
|
||||
666 | class C[T,]: pass
|
||||
|
|
||||
help: Remove trailing comma
|
||||
662 | ]: pass
|
||||
663 |
|
||||
664 | type X[T,] = T
|
||||
- def f[T,](): pass
|
||||
665 + def f[T](): pass
|
||||
666 | class C[T,]: pass
|
||||
|
||||
COM819 [*] Trailing comma prohibited
|
||||
--> COM81.py:666:10
|
||||
|
|
||||
664 | type X[T,] = T
|
||||
665 | def f[T,](): pass
|
||||
666 | class C[T,]: pass
|
||||
| ^
|
||||
|
|
||||
help: Remove trailing comma
|
||||
663 |
|
||||
664 | type X[T,] = T
|
||||
665 | def f[T,](): pass
|
||||
- class C[T,]: pass
|
||||
666 + class C[T]: pass
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 6
|
||||
|
||||
--- Added ---
|
||||
COM812 [*] Trailing comma missing
|
||||
--> COM81.py:655:6
|
||||
|
|
||||
654 | type X[
|
||||
655 | T
|
||||
| ^
|
||||
656 | ] = T
|
||||
657 | def f[
|
||||
|
|
||||
help: Add trailing comma
|
||||
652 | }"""
|
||||
653 |
|
||||
654 | type X[
|
||||
- T
|
||||
655 + T,
|
||||
656 | ] = T
|
||||
657 | def f[
|
||||
658 | T
|
||||
|
||||
|
||||
COM812 [*] Trailing comma missing
|
||||
--> COM81.py:658:6
|
||||
|
|
||||
656 | ] = T
|
||||
657 | def f[
|
||||
658 | T
|
||||
| ^
|
||||
659 | ](): pass
|
||||
660 | class C[
|
||||
|
|
||||
help: Add trailing comma
|
||||
655 | T
|
||||
656 | ] = T
|
||||
657 | def f[
|
||||
- T
|
||||
658 + T,
|
||||
659 | ](): pass
|
||||
660 | class C[
|
||||
661 | T
|
||||
|
||||
|
||||
COM812 [*] Trailing comma missing
|
||||
--> COM81.py:661:6
|
||||
|
|
||||
659 | ](): pass
|
||||
660 | class C[
|
||||
661 | T
|
||||
| ^
|
||||
662 | ]: pass
|
||||
|
|
||||
help: Add trailing comma
|
||||
658 | T
|
||||
659 | ](): pass
|
||||
660 | class C[
|
||||
- T
|
||||
661 + T,
|
||||
662 | ]: pass
|
||||
663 |
|
||||
664 | type X[T,] = T
|
||||
|
||||
|
||||
COM819 [*] Trailing comma prohibited
|
||||
--> COM81.py:664:9
|
||||
|
|
||||
662 | ]: pass
|
||||
663 |
|
||||
664 | type X[T,] = T
|
||||
| ^
|
||||
665 | def f[T,](): pass
|
||||
666 | class C[T,]: pass
|
||||
|
|
||||
help: Remove trailing comma
|
||||
661 | T
|
||||
662 | ]: pass
|
||||
663 |
|
||||
- type X[T,] = T
|
||||
664 + type X[T] = T
|
||||
665 | def f[T,](): pass
|
||||
666 | class C[T,]: pass
|
||||
|
||||
|
||||
COM819 [*] Trailing comma prohibited
|
||||
--> COM81.py:665:8
|
||||
|
|
||||
664 | type X[T,] = T
|
||||
665 | def f[T,](): pass
|
||||
| ^
|
||||
666 | class C[T,]: pass
|
||||
|
|
||||
help: Remove trailing comma
|
||||
662 | ]: pass
|
||||
663 |
|
||||
664 | type X[T,] = T
|
||||
- def f[T,](): pass
|
||||
665 + def f[T](): pass
|
||||
666 | class C[T,]: pass
|
||||
|
||||
|
||||
COM819 [*] Trailing comma prohibited
|
||||
--> COM81.py:666:10
|
||||
|
|
||||
664 | type X[T,] = T
|
||||
665 | def f[T,](): pass
|
||||
666 | class C[T,]: pass
|
||||
| ^
|
||||
|
|
||||
help: Remove trailing comma
|
||||
663 |
|
||||
664 | type X[T,] = T
|
||||
665 | def f[T,](): pass
|
||||
- class C[T,]: pass
|
||||
666 + class C[T]: pass
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
@@ -9,6 +9,7 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_diagnostics, settings};
|
||||
|
||||
@@ -46,14 +47,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_exception() -> Result<()> {
|
||||
fn preview_string_exception() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_errmsg/EM101_byte_string.py"),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(Rule::RawStringInException)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(diagnostics);
|
||||
assert_diagnostics!("preview", diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,16 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Locator;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_raise_exception_byte_string_enabled;
|
||||
use crate::registry::Rule;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of string literals in exception constructors.
|
||||
///
|
||||
/// In [preview], this rule checks for byte string literals in
|
||||
/// exception constructors.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python includes the `raise` in the default traceback (and formatters
|
||||
/// like Rich and IPython do too).
|
||||
@@ -47,6 +51,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// raise RuntimeError(msg)
|
||||
/// RuntimeError: 'Some value' is incorrect
|
||||
/// ```
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct RawStringInException;
|
||||
|
||||
@@ -212,7 +218,9 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
|
||||
// Check for byte string literals.
|
||||
Expr::BytesLiteral(ast::ExprBytesLiteral { value: bytes, .. }) => {
|
||||
if checker.settings().rules.enabled(Rule::RawStringInException) {
|
||||
if bytes.len() >= checker.settings().flake8_errmsg.max_string_length {
|
||||
if bytes.len() >= checker.settings().flake8_errmsg.max_string_length
|
||||
&& is_raise_exception_byte_string_enabled(checker.settings())
|
||||
{
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(RawStringInException, first.range());
|
||||
if let Some(indentation) = whitespace::indentation(checker.source(), stmt) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::{self as ast, ParameterWithDefault};
|
||||
use ruff_python_semantic::analyze::function_type;
|
||||
|
||||
use crate::Violation;
|
||||
@@ -85,9 +85,16 @@ pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast
|
||||
function_type::FunctionType::Method | function_type::FunctionType::ClassMethod
|
||||
));
|
||||
|
||||
if let Some(param) = function_def.parameters.args.get(skip) {
|
||||
if param.uses_pep_484_positional_only_convention() {
|
||||
checker.report_diagnostic(Pep484StylePositionalOnlyParameter, param.identifier());
|
||||
if let Some(arg) = function_def.parameters.args.get(skip) {
|
||||
if is_old_style_positional_only(arg) {
|
||||
checker.report_diagnostic(Pep484StylePositionalOnlyParameter, arg.identifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`ParameterWithDefault`] is an old-style positional-only parameter (i.e.,
|
||||
/// its name starts with `__` and does not end with `__`).
|
||||
fn is_old_style_positional_only(param: &ParameterWithDefault) -> bool {
|
||||
let arg_name = param.name();
|
||||
arg_name.starts_with("__") && !arg_name.ends_with("__")
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))]
|
||||
#[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
|
||||
@@ -10,6 +10,7 @@ use super::fix_with;
|
||||
use crate::Fix;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::fits;
|
||||
use crate::preview::is_multiple_with_statements_fix_safe_enabled;
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -44,8 +45,15 @@ use crate::{FixAvailability, Violation};
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
///
|
||||
/// This fix is marked as always unsafe unless [preview] mode is enabled, in which case it is always
|
||||
/// marked as safe. Note that the fix is unavailable if it would remove comments (in either case).
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: The `with` statement](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct MultipleWithStatements;
|
||||
|
||||
@@ -187,7 +195,11 @@ pub(crate) fn multiple_with_statements(
|
||||
checker.settings().tab_size,
|
||||
)
|
||||
}) {
|
||||
Ok(Some(Fix::safe_edit(edit)))
|
||||
if is_multiple_with_statements_fix_safe_enabled(checker.settings()) {
|
||||
Ok(Some(Fix::safe_edit(edit)))
|
||||
} else {
|
||||
Ok(Some(Fix::unsafe_edit(edit)))
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ help: Combine `with` statements
|
||||
4 |
|
||||
5 | # SIM117
|
||||
6 | with A():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:7:1
|
||||
@@ -45,6 +46,7 @@ help: Combine `with` statements
|
||||
10 |
|
||||
11 | # SIM117
|
||||
12 | with A() as a:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:13:1
|
||||
@@ -82,6 +84,7 @@ help: Combine `with` statements
|
||||
22 |
|
||||
23 | # OK
|
||||
24 | with A() as a:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:47:1
|
||||
@@ -104,6 +107,7 @@ help: Combine `with` statements
|
||||
49 |
|
||||
50 | while True:
|
||||
51 | # SIM117
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:53:5
|
||||
@@ -140,6 +144,7 @@ help: Combine `with` statements
|
||||
64 | "this for some reason")
|
||||
65 |
|
||||
66 | # SIM117
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:68:1
|
||||
@@ -166,6 +171,7 @@ help: Combine `with` statements
|
||||
73 |
|
||||
74 | # SIM117
|
||||
75 | with A() as a:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:76:1
|
||||
@@ -197,6 +203,7 @@ help: Combine `with` statements
|
||||
81 |
|
||||
82 | # SIM117
|
||||
83 | with (
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:84:1
|
||||
@@ -230,6 +237,7 @@ help: Combine `with` statements
|
||||
90 |
|
||||
91 | # SIM117 (auto-fixable)
|
||||
92 | with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:95:1
|
||||
@@ -252,6 +260,7 @@ help: Combine `with` statements
|
||||
97 |
|
||||
98 | # SIM117 (not auto-fixable too long)
|
||||
99 | with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ890") as a:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:100:1
|
||||
@@ -310,6 +319,7 @@ help: Combine `with` statements
|
||||
136 |
|
||||
137 | # Allow cascading for some statements.
|
||||
138 | import anyio
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:163:1
|
||||
@@ -329,3 +339,4 @@ help: Combine `with` statements
|
||||
- pass
|
||||
163 + async with asyncio.timeout(1), A(), B():
|
||||
164 + pass
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:2:1
|
||||
|
|
||||
1 | # SIM117
|
||||
2 | / with A() as a:
|
||||
3 | | with B() as b:
|
||||
| |__________________^
|
||||
4 | print("hello")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
1 | # SIM117
|
||||
- with A() as a:
|
||||
- with B() as b:
|
||||
- print("hello")
|
||||
2 + with A() as a, B() as b:
|
||||
3 + print("hello")
|
||||
4 |
|
||||
5 | # SIM117
|
||||
6 | with A():
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:7:1
|
||||
|
|
||||
6 | # SIM117
|
||||
7 | / with A():
|
||||
8 | | with B():
|
||||
| |_____________^
|
||||
9 | with C():
|
||||
10 | print("hello")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
4 | print("hello")
|
||||
5 |
|
||||
6 | # SIM117
|
||||
- with A():
|
||||
- with B():
|
||||
- with C():
|
||||
- print("hello")
|
||||
7 + with A(), B():
|
||||
8 + with C():
|
||||
9 + print("hello")
|
||||
10 |
|
||||
11 | # SIM117
|
||||
12 | with A() as a:
|
||||
|
||||
SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:13:1
|
||||
|
|
||||
12 | # SIM117
|
||||
13 | / with A() as a:
|
||||
14 | | # Unfixable due to placement of this comment.
|
||||
15 | | with B() as b:
|
||||
| |__________________^
|
||||
16 | print("hello")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:19:1
|
||||
|
|
||||
18 | # SIM117
|
||||
19 | / with A() as a:
|
||||
20 | | with B() as b:
|
||||
| |__________________^
|
||||
21 | # Fixable due to placement of this comment.
|
||||
22 | print("hello")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
16 | print("hello")
|
||||
17 |
|
||||
18 | # SIM117
|
||||
- with A() as a:
|
||||
- with B() as b:
|
||||
- # Fixable due to placement of this comment.
|
||||
- print("hello")
|
||||
19 + with A() as a, B() as b:
|
||||
20 + # Fixable due to placement of this comment.
|
||||
21 + print("hello")
|
||||
22 |
|
||||
23 | # OK
|
||||
24 | with A() as a:
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:47:1
|
||||
|
|
||||
46 | # SIM117
|
||||
47 | / async with A() as a:
|
||||
48 | | async with B() as b:
|
||||
| |________________________^
|
||||
49 | print("hello")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
44 | print("hello")
|
||||
45 |
|
||||
46 | # SIM117
|
||||
- async with A() as a:
|
||||
- async with B() as b:
|
||||
- print("hello")
|
||||
47 + async with A() as a, B() as b:
|
||||
48 + print("hello")
|
||||
49 |
|
||||
50 | while True:
|
||||
51 | # SIM117
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:53:5
|
||||
|
|
||||
51 | while True:
|
||||
52 | # SIM117
|
||||
53 | / with A() as a:
|
||||
54 | | with B() as b:
|
||||
| |______________________^
|
||||
55 | """this
|
||||
56 | is valid"""
|
||||
|
|
||||
help: Combine `with` statements
|
||||
50 |
|
||||
51 | while True:
|
||||
52 | # SIM117
|
||||
- with A() as a:
|
||||
- with B() as b:
|
||||
- """this
|
||||
53 + with A() as a, B() as b:
|
||||
54 + """this
|
||||
55 | is valid"""
|
||||
56 |
|
||||
- """the indentation on
|
||||
57 + """the indentation on
|
||||
58 | this line is significant"""
|
||||
59 |
|
||||
- "this is" \
|
||||
60 + "this is" \
|
||||
61 | "allowed too"
|
||||
62 |
|
||||
- ("so is"
|
||||
63 + ("so is"
|
||||
64 | "this for some reason")
|
||||
65 |
|
||||
66 | # SIM117
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:68:1
|
||||
|
|
||||
67 | # SIM117
|
||||
68 | / with (
|
||||
69 | | A() as a,
|
||||
70 | | B() as b,
|
||||
71 | | ):
|
||||
72 | | with C() as c:
|
||||
| |__________________^
|
||||
73 | print("hello")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
67 | # SIM117
|
||||
68 | with (
|
||||
69 | A() as a,
|
||||
- B() as b,
|
||||
70 + B() as b,C() as c
|
||||
71 | ):
|
||||
- with C() as c:
|
||||
- print("hello")
|
||||
72 + print("hello")
|
||||
73 |
|
||||
74 | # SIM117
|
||||
75 | with A() as a:
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:76:1
|
||||
|
|
||||
75 | # SIM117
|
||||
76 | / with A() as a:
|
||||
77 | | with (
|
||||
78 | | B() as b,
|
||||
79 | | C() as c,
|
||||
80 | | ):
|
||||
| |______^
|
||||
81 | print("hello")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
73 | print("hello")
|
||||
74 |
|
||||
75 | # SIM117
|
||||
- with A() as a:
|
||||
- with (
|
||||
- B() as b,
|
||||
- C() as c,
|
||||
- ):
|
||||
- print("hello")
|
||||
76 + with (
|
||||
77 + A() as a, B() as b,
|
||||
78 + C() as c,
|
||||
79 + ):
|
||||
80 + print("hello")
|
||||
81 |
|
||||
82 | # SIM117
|
||||
83 | with (
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:84:1
|
||||
|
|
||||
83 | # SIM117
|
||||
84 | / with (
|
||||
85 | | A() as a,
|
||||
86 | | B() as b,
|
||||
87 | | ):
|
||||
88 | | with (
|
||||
89 | | C() as c,
|
||||
90 | | D() as d,
|
||||
91 | | ):
|
||||
| |______^
|
||||
92 | print("hello")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
83 | # SIM117
|
||||
84 | with (
|
||||
85 | A() as a,
|
||||
- B() as b,
|
||||
86 + B() as b,C() as c,
|
||||
87 + D() as d,
|
||||
88 | ):
|
||||
- with (
|
||||
- C() as c,
|
||||
- D() as d,
|
||||
- ):
|
||||
- print("hello")
|
||||
89 + print("hello")
|
||||
90 |
|
||||
91 | # SIM117 (auto-fixable)
|
||||
92 | with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a:
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:95:1
|
||||
|
|
||||
94 | # SIM117 (auto-fixable)
|
||||
95 | / with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a:
|
||||
96 | | with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b:
|
||||
| |__________________________________________________^
|
||||
97 | print("hello")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
92 | print("hello")
|
||||
93 |
|
||||
94 | # SIM117 (auto-fixable)
|
||||
- with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a:
|
||||
- with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b:
|
||||
- print("hello")
|
||||
95 + with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as a, B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b:
|
||||
96 + print("hello")
|
||||
97 |
|
||||
98 | # SIM117 (not auto-fixable too long)
|
||||
99 | with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ890") as a:
|
||||
|
||||
SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:100:1
|
||||
|
|
||||
99 | # SIM117 (not auto-fixable too long)
|
||||
100 | / with A("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ890") as a:
|
||||
101 | | with B("01ß9💣2ℝ8901ß9💣2ℝ8901ß9💣2ℝ89") as b:
|
||||
| |__________________________________________________^
|
||||
102 | print("hello")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
|
||||
SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:106:5
|
||||
|
|
||||
104 | # From issue #3025.
|
||||
105 | async def main():
|
||||
106 | / async with A() as a: # SIM117.
|
||||
107 | | async with B() as b:
|
||||
| |____________________________^
|
||||
108 | print("async-inside!")
|
||||
|
|
||||
help: Combine `with` statements
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:126:1
|
||||
|
|
||||
125 | # SIM117
|
||||
126 | / with A() as a:
|
||||
127 | | with B() as b:
|
||||
| |__________________^
|
||||
128 | type ListOrSet[T] = list[T] | set[T]
|
||||
|
|
||||
help: Combine `with` statements
|
||||
123 | f(b2, c2, d2)
|
||||
124 |
|
||||
125 | # SIM117
|
||||
- with A() as a:
|
||||
- with B() as b:
|
||||
- type ListOrSet[T] = list[T] | set[T]
|
||||
126 + with A() as a, B() as b:
|
||||
127 + type ListOrSet[T] = list[T] | set[T]
|
||||
128 |
|
||||
- class ClassA[T: str]:
|
||||
- def method1(self) -> T:
|
||||
- ...
|
||||
129 + class ClassA[T: str]:
|
||||
130 + def method1(self) -> T:
|
||||
131 + ...
|
||||
132 |
|
||||
- f" something { my_dict["key"] } something else "
|
||||
133 + f" something { my_dict["key"] } something else "
|
||||
134 |
|
||||
- f"foo {f"bar {x}"} baz"
|
||||
135 + f"foo {f"bar {x}"} baz"
|
||||
136 |
|
||||
137 | # Allow cascading for some statements.
|
||||
138 | import anyio
|
||||
|
||||
SIM117 [*] Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
--> SIM117.py:163:1
|
||||
|
|
||||
162 | # Do not suppress combination, if a context manager is already combined with another.
|
||||
163 | / async with asyncio.timeout(1), A():
|
||||
164 | | async with B():
|
||||
| |___________________^
|
||||
165 | pass
|
||||
|
|
||||
help: Combine `with` statements
|
||||
160 | pass
|
||||
161 |
|
||||
162 | # Do not suppress combination, if a context manager is already combined with another.
|
||||
- async with asyncio.timeout(1), A():
|
||||
- async with B():
|
||||
- pass
|
||||
163 + async with asyncio.timeout(1), A(), B():
|
||||
164 + pass
|
||||
@@ -66,7 +66,7 @@ impl TypingReference {
|
||||
}
|
||||
|
||||
// prefer `from __future__ import annotations` to quoting
|
||||
if settings.future_annotations
|
||||
if settings.future_annotations()
|
||||
&& !reference.in_typing_only_annotation()
|
||||
&& reference.in_runtime_evaluated_annotation()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ mod tests {
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::{Linter, Rule};
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::{test_path, test_snippet};
|
||||
use crate::{assert_diagnostics, settings};
|
||||
|
||||
@@ -85,6 +86,7 @@ mod tests {
|
||||
Path::new("flake8_type_checking").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
future_annotations: true,
|
||||
preview: PreviewMode::Enabled,
|
||||
// also enable quoting annotations to check the interaction. the future import
|
||||
// should take precedence.
|
||||
flake8_type_checking: super::settings::Settings {
|
||||
|
||||
@@ -11,10 +11,12 @@ use crate::checkers::ast::{Checker, DiagnosticGuard};
|
||||
use crate::codes::Rule;
|
||||
use crate::fix;
|
||||
use crate::importer::ImportedMembers;
|
||||
use crate::preview::is_full_path_match_source_strategy_enabled;
|
||||
use crate::rules::flake8_type_checking::helpers::{
|
||||
TypingReference, filter_contained, quote_annotation,
|
||||
};
|
||||
use crate::rules::flake8_type_checking::imports::ImportBinding;
|
||||
use crate::rules::isort::categorize::MatchSourceStrategy;
|
||||
use crate::rules::isort::{ImportSection, ImportType, categorize};
|
||||
use crate::{Fix, FixAvailability, Violation};
|
||||
|
||||
@@ -38,13 +40,6 @@ use crate::{Fix, FixAvailability, Violation};
|
||||
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`] settings to mark them
|
||||
/// as such.
|
||||
///
|
||||
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
|
||||
/// annotations` will be added if doing so would enable an import to be
|
||||
/// moved into an `if TYPE_CHECKING:` block. This takes precedence over the
|
||||
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if
|
||||
/// both settings are enabled.
|
||||
///
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from __future__ import annotations
|
||||
@@ -70,6 +65,18 @@ use crate::{Fix, FixAvailability, Violation};
|
||||
/// return len(sized)
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ## Preview
|
||||
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
|
||||
/// the criterion for determining whether an import is first-party
|
||||
/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-third-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
|
||||
///
|
||||
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
|
||||
/// annotations` will be added if doing so would enable an import to be moved into an `if
|
||||
/// TYPE_CHECKING:` block. This takes precedence over the
|
||||
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if both settings are
|
||||
/// enabled.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-type-checking.quote-annotations`
|
||||
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
|
||||
@@ -121,12 +128,6 @@ impl Violation for TypingOnlyFirstPartyImport {
|
||||
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`] settings to mark them
|
||||
/// as such.
|
||||
///
|
||||
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
|
||||
/// annotations` will be added if doing so would enable an import to be
|
||||
/// moved into an `if TYPE_CHECKING:` block. This takes precedence over the
|
||||
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if
|
||||
/// both settings are enabled.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from __future__ import annotations
|
||||
@@ -152,6 +153,17 @@ impl Violation for TypingOnlyFirstPartyImport {
|
||||
/// return len(df)
|
||||
/// ```
|
||||
///
|
||||
/// ## Preview
|
||||
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
|
||||
/// the criterion for determining whether an import is first-party
|
||||
/// is stricter, which could affect whether this lint is triggered vs [`TC001`](https://docs.astral.sh/ruff/rules/typing-only-first-party-import/). See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
|
||||
///
|
||||
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
|
||||
/// annotations` will be added if doing so would enable an import to be moved into an `if
|
||||
/// TYPE_CHECKING:` block. This takes precedence over the
|
||||
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if both settings are
|
||||
/// enabled.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-type-checking.quote-annotations`
|
||||
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
|
||||
@@ -203,12 +215,6 @@ impl Violation for TypingOnlyThirdPartyImport {
|
||||
/// [`lint.flake8-type-checking.runtime-evaluated-decorators`] settings to mark them
|
||||
/// as such.
|
||||
///
|
||||
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
|
||||
/// annotations` will be added if doing so would enable an import to be
|
||||
/// moved into an `if TYPE_CHECKING:` block. This takes precedence over the
|
||||
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if
|
||||
/// both settings are enabled.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from __future__ import annotations
|
||||
@@ -234,6 +240,15 @@ impl Violation for TypingOnlyThirdPartyImport {
|
||||
/// return str(path)
|
||||
/// ```
|
||||
///
|
||||
/// ## Preview
|
||||
///
|
||||
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled, if
|
||||
/// [`lint.future-annotations`] is set to `true`, `from __future__ import
|
||||
/// annotations` will be added if doing so would enable an import to be moved into an `if
|
||||
/// TYPE_CHECKING:` block. This takes precedence over the
|
||||
/// [`lint.flake8-type-checking.quote-annotations`] setting described above if both settings are
|
||||
/// enabled.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-type-checking.quote-annotations`
|
||||
/// - `lint.flake8-type-checking.runtime-evaluated-base-classes`
|
||||
@@ -282,7 +297,7 @@ pub(crate) fn typing_only_runtime_import(
|
||||
|
||||
// If we can't add a `__future__` import and in un-strict mode, don't flag typing-only
|
||||
// imports that are implicitly loaded by way of a valid runtime import.
|
||||
if !checker.settings().future_annotations
|
||||
if !checker.settings().future_annotations()
|
||||
&& !checker.settings().flake8_type_checking.strict
|
||||
&& runtime_imports
|
||||
.iter()
|
||||
@@ -332,6 +347,13 @@ pub(crate) fn typing_only_runtime_import(
|
||||
let source_name = import.source_name().join(".");
|
||||
|
||||
// Categorize the import, using coarse-grained categorization.
|
||||
let match_source_strategy =
|
||||
if is_full_path_match_source_strategy_enabled(checker.settings()) {
|
||||
MatchSourceStrategy::FullPath
|
||||
} else {
|
||||
MatchSourceStrategy::Root
|
||||
};
|
||||
|
||||
let import_type = match categorize(
|
||||
&source_name,
|
||||
qualified_name.is_unresolved_import(),
|
||||
@@ -343,6 +365,7 @@ pub(crate) fn typing_only_runtime_import(
|
||||
checker.settings().isort.no_sections,
|
||||
&checker.settings().isort.section_order,
|
||||
&checker.settings().isort.default_section,
|
||||
match_source_strategy,
|
||||
) {
|
||||
ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => {
|
||||
ImportType::FirstParty
|
||||
|
||||
@@ -129,6 +129,7 @@ mod tests {
|
||||
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]
|
||||
#[test_case(Rule::OsPathGetmtime, Path::new("PTH204.py"))]
|
||||
#[test_case(Rule::OsPathGetctime, Path::new("PTH205.py"))]
|
||||
#[test_case(Rule::OsSymlink, Path::new("PTH211.py"))]
|
||||
fn preview_flake8_use_pathlib(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
|
||||
---
|
||||
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
|
||||
--> PTH211.py:5:1
|
||||
|
|
||||
5 | os.symlink("usr/bin/python", "tmp/python")
|
||||
| ^^^^^^^^^^
|
||||
6 | os.symlink(b"usr/bin/python", b"tmp/python")
|
||||
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
||||
|
|
||||
help: Replace with `Path(...).symlink_to(...)`
|
||||
2 | from pathlib import Path
|
||||
3 |
|
||||
4 |
|
||||
- os.symlink("usr/bin/python", "tmp/python")
|
||||
5 + Path("tmp/python").symlink_to("usr/bin/python")
|
||||
6 | os.symlink(b"usr/bin/python", b"tmp/python")
|
||||
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
||||
8 |
|
||||
|
||||
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
|
||||
--> PTH211.py:6:1
|
||||
|
|
||||
5 | os.symlink("usr/bin/python", "tmp/python")
|
||||
6 | os.symlink(b"usr/bin/python", b"tmp/python")
|
||||
| ^^^^^^^^^^
|
||||
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
||||
|
|
||||
help: Replace with `Path(...).symlink_to(...)`
|
||||
3 |
|
||||
4 |
|
||||
5 | os.symlink("usr/bin/python", "tmp/python")
|
||||
- os.symlink(b"usr/bin/python", b"tmp/python")
|
||||
6 + Path(b"tmp/python").symlink_to(b"usr/bin/python")
|
||||
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
||||
8 |
|
||||
9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
|
||||
|
||||
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
|
||||
--> PTH211.py:9:1
|
||||
|
|
||||
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
||||
8 |
|
||||
9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
|
||||
| ^^^^^^^^^^
|
||||
10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
|
||||
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
||||
|
|
||||
help: Replace with `Path(...).symlink_to(...)`
|
||||
6 | os.symlink(b"usr/bin/python", b"tmp/python")
|
||||
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
||||
8 |
|
||||
- os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
|
||||
9 + Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True)
|
||||
10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
|
||||
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
||||
12 |
|
||||
|
||||
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
|
||||
--> PTH211.py:10:1
|
||||
|
|
||||
9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
|
||||
10 | os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
|
||||
| ^^^^^^^^^^
|
||||
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
||||
|
|
||||
help: Replace with `Path(...).symlink_to(...)`
|
||||
7 | Path("tmp/python").symlink_to("usr/bin/python") # Ok
|
||||
8 |
|
||||
9 | os.symlink("usr/bin/python", "tmp/python", target_is_directory=True)
|
||||
- os.symlink(b"usr/bin/python", b"tmp/python", target_is_directory=True)
|
||||
10 + Path(b"tmp/python").symlink_to(b"usr/bin/python", target_is_directory=True)
|
||||
11 | Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
||||
12 |
|
||||
13 | fd = os.open(".", os.O_RDONLY)
|
||||
|
||||
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
|
||||
--> PTH211.py:17:1
|
||||
|
|
||||
15 | os.close(fd)
|
||||
16 |
|
||||
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
|
||||
| ^^^^^^^^^^
|
||||
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
|
||||
|
|
||||
help: Replace with `Path(...).symlink_to(...)`
|
||||
|
||||
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
|
||||
--> PTH211.py:18:1
|
||||
|
|
||||
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
|
||||
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
|
||||
| ^^^^^^^^^^
|
||||
19 |
|
||||
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
|
||||
|
|
||||
help: Replace with `Path(...).symlink_to(...)`
|
||||
15 | os.close(fd)
|
||||
16 |
|
||||
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
|
||||
- os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
|
||||
18 + Path("tmp/python").symlink_to("usr/bin/python")
|
||||
19 |
|
||||
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
|
||||
21 |
|
||||
|
||||
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
|
||||
--> PTH211.py:20:1
|
||||
|
|
||||
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
|
||||
19 |
|
||||
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
|
||||
| ^^^^^^^^^^
|
||||
21 |
|
||||
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
|
||||
|
|
||||
help: Replace with `Path(...).symlink_to(...)`
|
||||
17 | os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
|
||||
18 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
|
||||
19 |
|
||||
- os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
|
||||
20 + Path("tmp/python").symlink_to("usr/bin/python")
|
||||
21 |
|
||||
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
|
||||
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
|
||||
|
||||
PTH211 [*] `os.symlink` should be replaced by `Path.symlink_to`
|
||||
--> PTH211.py:22:1
|
||||
|
|
||||
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
|
||||
21 |
|
||||
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
|
||||
| ^^^^^^^^^^
|
||||
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
|
||||
|
|
||||
help: Replace with `Path(...).symlink_to(...)`
|
||||
19 |
|
||||
20 | os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
|
||||
21 |
|
||||
- os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
|
||||
22 + Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True)
|
||||
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
|
||||
|
||||
PTH211 `os.symlink` should be replaced by `Path.symlink_to`
|
||||
--> PTH211.py:23:1
|
||||
|
|
||||
22 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
|
||||
23 | os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
|
||||
| ^^^^^^^^^^
|
||||
|
|
||||
help: Replace with `Path(...).symlink_to(...)`
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::iter;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -100,6 +101,7 @@ pub(crate) fn categorize<'a>(
|
||||
no_sections: bool,
|
||||
section_order: &'a [ImportSection],
|
||||
default_section: &'a ImportSection,
|
||||
match_source_strategy: MatchSourceStrategy,
|
||||
) -> &'a ImportSection {
|
||||
let module_base = module_name.split('.').next().unwrap();
|
||||
let (mut import_type, mut reason) = {
|
||||
@@ -127,7 +129,7 @@ pub(crate) fn categorize<'a>(
|
||||
&ImportSection::Known(ImportType::FirstParty),
|
||||
Reason::SamePackage,
|
||||
)
|
||||
} else if let Some(src) = match_sources(src, module_name) {
|
||||
} else if let Some(src) = match_sources(src, module_name, match_source_strategy) {
|
||||
(
|
||||
&ImportSection::Known(ImportType::FirstParty),
|
||||
Reason::SourceMatch(src),
|
||||
@@ -159,29 +161,61 @@ fn same_package(package: Option<PackageRoot<'_>>, module_base: &str) -> bool {
|
||||
/// Returns the source path with respect to which the module `name`
|
||||
/// should be considered first party, or `None` if no path is found.
|
||||
///
|
||||
/// The [`MatchSourceStrategy`] is the criterion used to decide whether
|
||||
/// the module path matches a given source directory.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// - The module named `foo` will match `[SRC]` if `[SRC]/foo` is a directory
|
||||
/// - The module named `foo` will match `[SRC]` if `[SRC]/foo` is a directory,
|
||||
/// no matter the strategy.
|
||||
///
|
||||
/// - The module named `foo.baz` will match `[SRC]` only if `[SRC]/foo/baz`
|
||||
/// is a directory, or `[SRC]/foo/baz.py` exists,
|
||||
/// or `[SRC]/foo/baz.pyi` exists.
|
||||
fn match_sources<'a>(paths: &'a [PathBuf], name: &str) -> Option<&'a Path> {
|
||||
let relative_path: PathBuf = name.split('.').collect();
|
||||
relative_path.components().next()?;
|
||||
for root in paths {
|
||||
let candidate = root.join(&relative_path);
|
||||
if candidate.is_dir() {
|
||||
return Some(root);
|
||||
/// - With `match_source_strategy == MatchSourceStrategy::Root`, the module
|
||||
/// named `foo.baz` will match `[SRC]` if `[SRC]/foo` is a
|
||||
/// directory or `[SRC]/foo.py` exists.
|
||||
///
|
||||
/// - With `match_source_stratgy == MatchSourceStrategy::FullPath`, the module
|
||||
/// named `foo.baz` will match `[SRC]` only if `[SRC]/foo/baz` is a directory,
|
||||
/// or `[SRC]/foo/baz.py` exists or `[SRC]/foo/baz.pyi` exists.
|
||||
fn match_sources<'a>(
|
||||
paths: &'a [PathBuf],
|
||||
name: &str,
|
||||
match_source_strategy: MatchSourceStrategy,
|
||||
) -> Option<&'a Path> {
|
||||
match match_source_strategy {
|
||||
MatchSourceStrategy::Root => {
|
||||
let base = name.split('.').next()?;
|
||||
for path in paths {
|
||||
if let Ok(metadata) = fs::metadata(path.join(base)) {
|
||||
if metadata.is_dir() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
if let Ok(metadata) = fs::metadata(path.join(format!("{base}.py"))) {
|
||||
if metadata.is_file() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
if ["py", "pyi"]
|
||||
.into_iter()
|
||||
.any(|extension| candidate.with_extension(extension).is_file())
|
||||
{
|
||||
return Some(root);
|
||||
MatchSourceStrategy::FullPath => {
|
||||
let relative_path: PathBuf = name.split('.').collect();
|
||||
relative_path.components().next()?;
|
||||
for root in paths {
|
||||
let candidate = root.join(&relative_path);
|
||||
if candidate.is_dir() {
|
||||
return Some(root);
|
||||
}
|
||||
if ["py", "pyi"]
|
||||
.into_iter()
|
||||
.any(|extension| candidate.with_extension(extension).is_file())
|
||||
{
|
||||
return Some(root);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
@@ -195,6 +229,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
no_sections: bool,
|
||||
section_order: &'a [ImportSection],
|
||||
default_section: &'a ImportSection,
|
||||
match_source_strategy: MatchSourceStrategy,
|
||||
) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> {
|
||||
let mut block_by_type: BTreeMap<&ImportSection, ImportBlock> = BTreeMap::default();
|
||||
// Categorize `Stmt::Import`.
|
||||
@@ -210,6 +245,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
no_sections,
|
||||
section_order,
|
||||
default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
block_by_type
|
||||
.entry(import_type)
|
||||
@@ -230,6 +266,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
no_sections,
|
||||
section_order,
|
||||
default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
block_by_type
|
||||
.entry(classification)
|
||||
@@ -250,6 +287,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
no_sections,
|
||||
section_order,
|
||||
default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
block_by_type
|
||||
.entry(classification)
|
||||
@@ -270,6 +308,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
no_sections,
|
||||
section_order,
|
||||
default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
block_by_type
|
||||
.entry(classification)
|
||||
@@ -424,9 +463,25 @@ impl fmt::Display for KnownModules {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rule to determine whether a module path matches
|
||||
/// a relative path from a source directory.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum MatchSourceStrategy {
|
||||
/// Matches if first term in module path is found in file system
|
||||
///
|
||||
/// # Example
|
||||
/// Module is `foo.bar.baz` and `[SRC]/foo` exists
|
||||
Root,
|
||||
/// Matches only if full module path is reflected in file system
|
||||
///
|
||||
/// # Example
|
||||
/// Module is `foo.bar.baz` and `[SRC]/foo/bar/baz` exists
|
||||
FullPath,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::rules::isort::categorize::match_sources;
|
||||
use crate::rules::isort::categorize::{MatchSourceStrategy, match_sources};
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -467,17 +522,49 @@ mod tests {
|
||||
|
||||
let paths = vec![project_dir.clone()];
|
||||
|
||||
// Test with Root strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage"),
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1"),
|
||||
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(match_sources(&paths, "mypackage.nonexistent",), None);
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.nonexistent", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "nonexistent", MatchSourceStrategy::Root),
|
||||
None
|
||||
);
|
||||
|
||||
// Test with FullPath strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::FullPath),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Differs in behavior from [`MatchSourceStrategy::Root`]
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"mypackage.nonexistent",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests a src-based Python package layout:
|
||||
@@ -501,12 +588,39 @@ mod tests {
|
||||
|
||||
let paths = vec![src_dir.clone()];
|
||||
|
||||
// Test with Root strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1"),
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
|
||||
Some(src_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(match_sources(&paths, "mypackage.nonexistent"), None);
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::Root),
|
||||
Some(src_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.nonexistent", MatchSourceStrategy::Root),
|
||||
Some(src_dir.as_path())
|
||||
);
|
||||
|
||||
// Test with FullPath strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath),
|
||||
Some(src_dir.as_path())
|
||||
);
|
||||
|
||||
// Differs in behavior from [`MatchSourceStrategy::Root`]
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"mypackage.nonexistent",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests a nested package layout:
|
||||
@@ -533,13 +647,35 @@ mod tests {
|
||||
|
||||
let paths = vec![project_dir.clone()];
|
||||
|
||||
// Test with Root strategy
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.subpackage.module2"),
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.subpackage.nonexistent"),
|
||||
match_sources(&paths, "mypackage.subpackage", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Test with FullPath strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"mypackage.subpackage.module2",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Differs in behavior from [`MatchSourceStrategy::Root`]
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"mypackage.subpackage.nonexistent",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -563,17 +699,52 @@ mod tests {
|
||||
create_file(project_dir.join("namespace/package1/module1.py"));
|
||||
|
||||
let paths = vec![project_dir.clone()];
|
||||
// Test with Root strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "namespace.package1"),
|
||||
match_sources(&paths, "namespace", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "namespace.package1.module1"),
|
||||
match_sources(&paths, "namespace.package1", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(match_sources(&paths, "namespace.package2.module1"), None);
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"namespace.package2.module1",
|
||||
MatchSourceStrategy::Root
|
||||
),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Test with FullPath strategy
|
||||
|
||||
assert_eq!(
|
||||
match_sources(&paths, "namespace.package1", MatchSourceStrategy::FullPath),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"namespace.package1.module1",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Differs in behavior from [`MatchSourceStrategy::Root`]
|
||||
assert_eq!(
|
||||
match_sources(
|
||||
&paths,
|
||||
"namespace.package2.module1",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests a package with type stubs (.pyi files):
|
||||
@@ -593,11 +764,12 @@ mod tests {
|
||||
create_file(project_dir.join("mypackage/__init__.py"));
|
||||
create_file(project_dir.join("mypackage/module1.pyi")); // Only create .pyi file, not .py
|
||||
|
||||
// Test with FullPath strategy
|
||||
let paths = vec![project_dir.clone()];
|
||||
|
||||
// Module "mypackage.module1" should match project_dir using .pyi file
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.module1"),
|
||||
match_sources(&paths, "mypackage.module1", MatchSourceStrategy::FullPath),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
}
|
||||
@@ -624,17 +796,30 @@ mod tests {
|
||||
create_file(project_dir.join("mypackage/feature/__init__.py"));
|
||||
create_file(project_dir.join("mypackage/feature/submodule.py"));
|
||||
|
||||
// Test with Root strategy
|
||||
let paths = vec![project_dir.clone()];
|
||||
|
||||
// Module "mypackage.feature" should match project_dir (matches the file first)
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.feature", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Test with FullPath strategy
|
||||
|
||||
// Module "mypackage.feature" should match project_dir
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.feature"),
|
||||
match_sources(&paths, "mypackage.feature", MatchSourceStrategy::FullPath),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
|
||||
// Module "mypackage.feature.submodule" should match project_dir
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage.feature.submodule"),
|
||||
match_sources(
|
||||
&paths,
|
||||
"mypackage.feature.submodule",
|
||||
MatchSourceStrategy::FullPath
|
||||
),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
}
|
||||
@@ -672,13 +857,13 @@ mod tests {
|
||||
|
||||
// Module "package1" should match project1_dir
|
||||
assert_eq!(
|
||||
match_sources(&paths, "package1"),
|
||||
match_sources(&paths, "package1", MatchSourceStrategy::Root),
|
||||
Some(project1_dir.as_path())
|
||||
);
|
||||
|
||||
// Module "package2" should match project2_dir
|
||||
assert_eq!(
|
||||
match_sources(&paths, "package2"),
|
||||
match_sources(&paths, "package2", MatchSourceStrategy::Root),
|
||||
Some(project2_dir.as_path())
|
||||
);
|
||||
|
||||
@@ -687,7 +872,7 @@ mod tests {
|
||||
|
||||
// Module "package1" should still match project1_dir
|
||||
assert_eq!(
|
||||
match_sources(&paths_reversed, "package1"),
|
||||
match_sources(&paths_reversed, "package1", MatchSourceStrategy::Root),
|
||||
Some(project1_dir.as_path())
|
||||
);
|
||||
}
|
||||
@@ -700,7 +885,8 @@ mod tests {
|
||||
///
|
||||
/// In theory this should never happen since we expect
|
||||
/// module names to have been normalized by the time we
|
||||
/// call `match_sources`.
|
||||
/// call `match_sources`. But it is worth noting that the
|
||||
/// behavior is different depending on the [`MatchSourceStrategy`]
|
||||
#[test]
|
||||
fn test_empty_module_name() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
@@ -708,9 +894,16 @@ mod tests {
|
||||
|
||||
create_dir(project_dir.join("mypackage"));
|
||||
|
||||
let paths = vec![project_dir];
|
||||
let paths = vec![project_dir.clone()];
|
||||
|
||||
assert_eq!(match_sources(&paths, ""), None);
|
||||
assert_eq!(
|
||||
match_sources(&paths, "", MatchSourceStrategy::Root),
|
||||
Some(project_dir.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
match_sources(&paths, "", MatchSourceStrategy::FullPath),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests behavior with an empty list of source paths
|
||||
@@ -718,6 +911,14 @@ mod tests {
|
||||
fn test_empty_paths() {
|
||||
let paths: Vec<PathBuf> = vec![];
|
||||
|
||||
assert_eq!(match_sources(&paths, "mypackage"), None);
|
||||
// Empty paths should return None
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::Root),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
match_sources(&paths, "mypackage", MatchSourceStrategy::FullPath),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use std::path::PathBuf;
|
||||
use annotate::annotate_imports;
|
||||
use block::{Block, Trailer};
|
||||
pub(crate) use categorize::categorize;
|
||||
use categorize::categorize_imports;
|
||||
pub use categorize::{ImportSection, ImportType};
|
||||
use categorize::{MatchSourceStrategy, categorize_imports};
|
||||
use comments::Comment;
|
||||
use normalize::normalize_imports;
|
||||
use order::order_imports;
|
||||
@@ -76,6 +76,7 @@ pub(crate) fn format_imports(
|
||||
source_type: PySourceType,
|
||||
target_version: PythonVersion,
|
||||
settings: &Settings,
|
||||
match_source_strategy: MatchSourceStrategy,
|
||||
tokens: &Tokens,
|
||||
) -> String {
|
||||
let trailer = &block.trailer;
|
||||
@@ -103,6 +104,7 @@ pub(crate) fn format_imports(
|
||||
package,
|
||||
target_version,
|
||||
settings,
|
||||
match_source_strategy,
|
||||
);
|
||||
|
||||
if !block_output.is_empty() && !output.is_empty() {
|
||||
@@ -159,6 +161,7 @@ fn format_import_block(
|
||||
package: Option<PackageRoot<'_>>,
|
||||
target_version: PythonVersion,
|
||||
settings: &Settings,
|
||||
match_source_strategy: MatchSourceStrategy,
|
||||
) -> String {
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum LineInsertion {
|
||||
@@ -179,6 +182,7 @@ fn format_import_block(
|
||||
settings.no_sections,
|
||||
&settings.section_order,
|
||||
&settings.default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
@@ -14,7 +14,9 @@ use crate::Locator;
|
||||
use crate::checkers::ast::LintContext;
|
||||
use crate::line_width::LineWidthBuilder;
|
||||
use crate::package::PackageRoot;
|
||||
use crate::preview::is_full_path_match_source_strategy_enabled;
|
||||
use crate::rules::isort::block::Block;
|
||||
use crate::rules::isort::categorize::MatchSourceStrategy;
|
||||
use crate::rules::isort::{comments, format_imports};
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
@@ -38,6 +40,12 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// import pandas
|
||||
/// ```
|
||||
///
|
||||
/// ## Preview
|
||||
/// When [`preview`](https://docs.astral.sh/ruff/preview/) mode is enabled, Ruff applies a stricter criterion
|
||||
/// for determining whether an import should be classified as first-party.
|
||||
/// Specifically, for an import of the form `import foo.bar.baz`, Ruff will
|
||||
/// check that `foo/bar`, relative to a [user-specified `src`](https://docs.astral.sh/ruff/settings/#src) directory, contains either
|
||||
/// the directory `baz` or else a file with the name `baz.py` or `baz.pyi`.
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnsortedImports;
|
||||
|
||||
@@ -121,6 +129,12 @@ pub(crate) fn organize_imports(
|
||||
trailing_lines_end(block.imports.last().unwrap(), locator.contents())
|
||||
};
|
||||
|
||||
let match_source_strategy = if is_full_path_match_source_strategy_enabled(settings) {
|
||||
MatchSourceStrategy::FullPath
|
||||
} else {
|
||||
MatchSourceStrategy::Root
|
||||
};
|
||||
|
||||
// Generate the sorted import block.
|
||||
let expected = format_imports(
|
||||
block,
|
||||
@@ -134,6 +148,7 @@ pub(crate) fn organize_imports(
|
||||
source_type,
|
||||
target_version,
|
||||
&settings.isort,
|
||||
match_source_strategy,
|
||||
tokens,
|
||||
);
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::{Violation, checkers::ast::Checker};
|
||||
|
||||
/// ## Removed
|
||||
/// ## Deprecated
|
||||
///
|
||||
/// This rule has been removed as it's highly opinionated and overly strict in most cases.
|
||||
/// This rule has been deprecated as it's highly opinionated and overly strict in most cases.
|
||||
///
|
||||
/// ## What it does
|
||||
/// Checks for assignments to the variable `df`.
|
||||
|
||||
@@ -141,6 +141,7 @@ impl Violation for InvalidFirstArgumentNameForClassMethod {
|
||||
#[derive_message_formats]
|
||||
// The first string below is what shows up in the documentation
|
||||
// in the rule table, and it is the more common case.
|
||||
#[expect(clippy::if_not_else)]
|
||||
fn message(&self) -> String {
|
||||
if !self.is_new {
|
||||
"First argument of a class method should be named `cls`".to_string()
|
||||
|
||||
@@ -18,8 +18,8 @@ use crate::rules::pep8_naming::helpers;
|
||||
/// > (Let’s hope that these variables are meant for use inside one module
|
||||
/// > only.) The conventions are about the same as those for functions.
|
||||
/// >
|
||||
/// > Modules that are designed for use via `from M import *` should use the
|
||||
/// > `__all__` mechanism to prevent exporting globals, or use the older
|
||||
/// > Modules that are designed for use via from M import * should use the
|
||||
/// > __all__ mechanism to prevent exporting globals, or use the older
|
||||
/// > convention of prefixing such globals with an underscore (which you might
|
||||
/// > want to do to indicate these globals are “module non-public”).
|
||||
/// >
|
||||
|
||||
@@ -15,8 +15,11 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix;
|
||||
use crate::preview::is_dunder_init_fix_unused_import_enabled;
|
||||
use crate::preview::{
|
||||
is_dunder_init_fix_unused_import_enabled, is_full_path_match_source_strategy_enabled,
|
||||
};
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::isort::categorize::MatchSourceStrategy;
|
||||
use crate::rules::{isort, isort::ImportSection, isort::ImportType};
|
||||
use crate::{Applicability, Fix, FixAvailability, Violation};
|
||||
|
||||
@@ -60,10 +63,6 @@ use crate::{Applicability, Fix, FixAvailability, Violation};
|
||||
/// to remove third-party and standard library imports -- the fix is unsafe because the module's
|
||||
/// interface changes.
|
||||
///
|
||||
/// See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)
|
||||
/// for more details on how Ruff
|
||||
/// determines whether an import is first or third-party.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
@@ -92,6 +91,11 @@ use crate::{Applicability, Fix, FixAvailability, Violation};
|
||||
/// print("numpy is not installed")
|
||||
/// ```
|
||||
///
|
||||
/// ## Preview
|
||||
/// When [preview](https://docs.astral.sh/ruff/preview/) is enabled,
|
||||
/// the criterion for determining whether an import is first-party
|
||||
/// is stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.ignore-init-module-imports`
|
||||
/// - `lint.pyflakes.allowed-unused-imports`
|
||||
@@ -227,6 +231,11 @@ enum UnusedImportContext {
|
||||
|
||||
fn is_first_party(import: &AnyImport, checker: &Checker) -> bool {
|
||||
let source_name = import.source_name().join(".");
|
||||
let match_source_strategy = if is_full_path_match_source_strategy_enabled(checker.settings()) {
|
||||
MatchSourceStrategy::FullPath
|
||||
} else {
|
||||
MatchSourceStrategy::Root
|
||||
};
|
||||
let category = isort::categorize(
|
||||
&source_name,
|
||||
import.qualified_name().is_unresolved_import(),
|
||||
@@ -238,6 +247,7 @@ fn is_first_party(import: &AnyImport, checker: &Checker) -> bool {
|
||||
checker.settings().isort.no_sections,
|
||||
&checker.settings().isort.section_order,
|
||||
&checker.settings().isort.default_section,
|
||||
match_source_strategy,
|
||||
);
|
||||
matches! {
|
||||
category,
|
||||
|
||||
@@ -43,15 +43,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// This rule's fix is marked as unsafe because removing an unused variable assignment may
|
||||
/// delete comments that are attached to the assignment.
|
||||
///
|
||||
/// ## See also
|
||||
///
|
||||
/// This rule does not apply to bindings in unpacked assignments (e.g. `x, y = 1, 2`). See
|
||||
/// [`unused-unpacked-variable`][RUF059] for this case.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.dummy-variable-rgx`
|
||||
///
|
||||
/// [RUF059]: https://docs.astral.sh/ruff/rules/unused-unpacked-variable/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnusedVariable {
|
||||
pub name: String,
|
||||
|
||||
@@ -10,6 +10,7 @@ mod tests {
|
||||
|
||||
use crate::registry::Rule;
|
||||
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_diagnostics, settings};
|
||||
|
||||
@@ -29,4 +30,22 @@ mod tests {
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::InvalidMockAccess, Path::new("PGH005_0.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("pygrep_hooks").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_invalid_async_mock_access_check_enabled;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Reason {
|
||||
@@ -61,16 +62,18 @@ pub(crate) fn uncalled_mock_method(checker: &Checker, expr: &Expr) {
|
||||
| "assert_has_calls"
|
||||
| "assert_not_called"
|
||||
);
|
||||
let is_uncalled_async_mock_method = matches!(
|
||||
attr.as_str(),
|
||||
"assert_awaited"
|
||||
| "assert_awaited_once"
|
||||
| "assert_awaited_with"
|
||||
| "assert_awaited_once_with"
|
||||
| "assert_any_await"
|
||||
| "assert_has_awaits"
|
||||
| "assert_not_awaited"
|
||||
);
|
||||
let is_uncalled_async_mock_method =
|
||||
is_invalid_async_mock_access_check_enabled(checker.settings())
|
||||
&& matches!(
|
||||
attr.as_str(),
|
||||
"assert_awaited"
|
||||
| "assert_awaited_once"
|
||||
| "assert_awaited_with"
|
||||
| "assert_awaited_once_with"
|
||||
| "assert_any_await"
|
||||
| "assert_has_awaits"
|
||||
| "assert_not_awaited"
|
||||
);
|
||||
if is_uncalled_mock_method || is_uncalled_async_mock_method {
|
||||
checker.report_diagnostic(
|
||||
InvalidMockAccess {
|
||||
@@ -101,16 +104,18 @@ pub(crate) fn non_existent_mock_method(checker: &Checker, test: &Expr) {
|
||||
| "has_calls"
|
||||
| "not_called"
|
||||
);
|
||||
let is_missing_async_mock_method = matches!(
|
||||
attr.as_str(),
|
||||
"awaited"
|
||||
| "awaited_once"
|
||||
| "awaited_with"
|
||||
| "awaited_once_with"
|
||||
| "any_await"
|
||||
| "has_awaits"
|
||||
| "not_awaited"
|
||||
);
|
||||
let is_missing_async_mock_method =
|
||||
is_invalid_async_mock_access_check_enabled(checker.settings())
|
||||
&& matches!(
|
||||
attr.as_str(),
|
||||
"awaited"
|
||||
| "awaited_once"
|
||||
| "awaited_with"
|
||||
| "awaited_once_with"
|
||||
| "any_await"
|
||||
| "has_awaits"
|
||||
| "not_awaited"
|
||||
);
|
||||
if is_missing_mock_method || is_missing_async_mock_method {
|
||||
checker.report_diagnostic(
|
||||
InvalidMockAccess {
|
||||
|
||||
@@ -98,112 +98,3 @@ PGH005 Mock method should be called: `assert_called_once_with`
|
||||
13 |
|
||||
14 | # OK
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `not_awaited`
|
||||
--> PGH005_0.py:26:8
|
||||
|
|
||||
24 | # =================
|
||||
25 | # Errors
|
||||
26 | assert my_mock.not_awaited()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
27 | assert my_mock.awaited_once_with()
|
||||
28 | assert my_mock.not_awaited
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `awaited_once_with`
|
||||
--> PGH005_0.py:27:8
|
||||
|
|
||||
25 | # Errors
|
||||
26 | assert my_mock.not_awaited()
|
||||
27 | assert my_mock.awaited_once_with()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
28 | assert my_mock.not_awaited
|
||||
29 | assert my_mock.awaited_once_with
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `not_awaited`
|
||||
--> PGH005_0.py:28:8
|
||||
|
|
||||
26 | assert my_mock.not_awaited()
|
||||
27 | assert my_mock.awaited_once_with()
|
||||
28 | assert my_mock.not_awaited
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
29 | assert my_mock.awaited_once_with
|
||||
30 | my_mock.assert_not_awaited
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `awaited_once_with`
|
||||
--> PGH005_0.py:29:8
|
||||
|
|
||||
27 | assert my_mock.awaited_once_with()
|
||||
28 | assert my_mock.not_awaited
|
||||
29 | assert my_mock.awaited_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
30 | my_mock.assert_not_awaited
|
||||
31 | my_mock.assert_awaited
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_not_awaited`
|
||||
--> PGH005_0.py:30:1
|
||||
|
|
||||
28 | assert my_mock.not_awaited
|
||||
29 | assert my_mock.awaited_once_with
|
||||
30 | my_mock.assert_not_awaited
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
31 | my_mock.assert_awaited
|
||||
32 | my_mock.assert_awaited_once_with
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_awaited`
|
||||
--> PGH005_0.py:31:1
|
||||
|
|
||||
29 | assert my_mock.awaited_once_with
|
||||
30 | my_mock.assert_not_awaited
|
||||
31 | my_mock.assert_awaited
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
32 | my_mock.assert_awaited_once_with
|
||||
33 | my_mock.assert_awaited_once_with
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_awaited_once_with`
|
||||
--> PGH005_0.py:32:1
|
||||
|
|
||||
30 | my_mock.assert_not_awaited
|
||||
31 | my_mock.assert_awaited
|
||||
32 | my_mock.assert_awaited_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
33 | my_mock.assert_awaited_once_with
|
||||
34 | MyMock.assert_awaited_once_with
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_awaited_once_with`
|
||||
--> PGH005_0.py:33:1
|
||||
|
|
||||
31 | my_mock.assert_awaited
|
||||
32 | my_mock.assert_awaited_once_with
|
||||
33 | my_mock.assert_awaited_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
34 | MyMock.assert_awaited_once_with
|
||||
35 | assert my_mock.awaited
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_awaited_once_with`
|
||||
--> PGH005_0.py:34:1
|
||||
|
|
||||
32 | my_mock.assert_awaited_once_with
|
||||
33 | my_mock.assert_awaited_once_with
|
||||
34 | MyMock.assert_awaited_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
35 | assert my_mock.awaited
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `awaited`
|
||||
--> PGH005_0.py:35:8
|
||||
|
|
||||
33 | my_mock.assert_awaited_once_with
|
||||
34 | MyMock.assert_awaited_once_with
|
||||
35 | assert my_mock.awaited
|
||||
| ^^^^^^^^^^^^^^^
|
||||
36 |
|
||||
37 | # OK
|
||||
|
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pygrep_hooks/mod.rs
|
||||
---
|
||||
PGH005 Non-existent mock method: `not_called`
|
||||
--> PGH005_0.py:4:8
|
||||
|
|
||||
2 | # ============
|
||||
3 | # Errors
|
||||
4 | assert my_mock.not_called()
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
5 | assert my_mock.called_once_with()
|
||||
6 | assert my_mock.not_called
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `called_once_with`
|
||||
--> PGH005_0.py:5:8
|
||||
|
|
||||
3 | # Errors
|
||||
4 | assert my_mock.not_called()
|
||||
5 | assert my_mock.called_once_with()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
6 | assert my_mock.not_called
|
||||
7 | assert my_mock.called_once_with
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `not_called`
|
||||
--> PGH005_0.py:6:8
|
||||
|
|
||||
4 | assert my_mock.not_called()
|
||||
5 | assert my_mock.called_once_with()
|
||||
6 | assert my_mock.not_called
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
7 | assert my_mock.called_once_with
|
||||
8 | my_mock.assert_not_called
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `called_once_with`
|
||||
--> PGH005_0.py:7:8
|
||||
|
|
||||
5 | assert my_mock.called_once_with()
|
||||
6 | assert my_mock.not_called
|
||||
7 | assert my_mock.called_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
8 | my_mock.assert_not_called
|
||||
9 | my_mock.assert_called
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_not_called`
|
||||
--> PGH005_0.py:8:1
|
||||
|
|
||||
6 | assert my_mock.not_called
|
||||
7 | assert my_mock.called_once_with
|
||||
8 | my_mock.assert_not_called
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
9 | my_mock.assert_called
|
||||
10 | my_mock.assert_called_once_with
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_called`
|
||||
--> PGH005_0.py:9:1
|
||||
|
|
||||
7 | assert my_mock.called_once_with
|
||||
8 | my_mock.assert_not_called
|
||||
9 | my_mock.assert_called
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
10 | my_mock.assert_called_once_with
|
||||
11 | my_mock.assert_called_once_with
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_called_once_with`
|
||||
--> PGH005_0.py:10:1
|
||||
|
|
||||
8 | my_mock.assert_not_called
|
||||
9 | my_mock.assert_called
|
||||
10 | my_mock.assert_called_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
11 | my_mock.assert_called_once_with
|
||||
12 | MyMock.assert_called_once_with
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_called_once_with`
|
||||
--> PGH005_0.py:11:1
|
||||
|
|
||||
9 | my_mock.assert_called
|
||||
10 | my_mock.assert_called_once_with
|
||||
11 | my_mock.assert_called_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
12 | MyMock.assert_called_once_with
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_called_once_with`
|
||||
--> PGH005_0.py:12:1
|
||||
|
|
||||
10 | my_mock.assert_called_once_with
|
||||
11 | my_mock.assert_called_once_with
|
||||
12 | MyMock.assert_called_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
13 |
|
||||
14 | # OK
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `not_awaited`
|
||||
--> PGH005_0.py:26:8
|
||||
|
|
||||
24 | # =================
|
||||
25 | # Errors
|
||||
26 | assert my_mock.not_awaited()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
27 | assert my_mock.awaited_once_with()
|
||||
28 | assert my_mock.not_awaited
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `awaited_once_with`
|
||||
--> PGH005_0.py:27:8
|
||||
|
|
||||
25 | # Errors
|
||||
26 | assert my_mock.not_awaited()
|
||||
27 | assert my_mock.awaited_once_with()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
28 | assert my_mock.not_awaited
|
||||
29 | assert my_mock.awaited_once_with
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `not_awaited`
|
||||
--> PGH005_0.py:28:8
|
||||
|
|
||||
26 | assert my_mock.not_awaited()
|
||||
27 | assert my_mock.awaited_once_with()
|
||||
28 | assert my_mock.not_awaited
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
29 | assert my_mock.awaited_once_with
|
||||
30 | my_mock.assert_not_awaited
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `awaited_once_with`
|
||||
--> PGH005_0.py:29:8
|
||||
|
|
||||
27 | assert my_mock.awaited_once_with()
|
||||
28 | assert my_mock.not_awaited
|
||||
29 | assert my_mock.awaited_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
30 | my_mock.assert_not_awaited
|
||||
31 | my_mock.assert_awaited
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_not_awaited`
|
||||
--> PGH005_0.py:30:1
|
||||
|
|
||||
28 | assert my_mock.not_awaited
|
||||
29 | assert my_mock.awaited_once_with
|
||||
30 | my_mock.assert_not_awaited
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
31 | my_mock.assert_awaited
|
||||
32 | my_mock.assert_awaited_once_with
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_awaited`
|
||||
--> PGH005_0.py:31:1
|
||||
|
|
||||
29 | assert my_mock.awaited_once_with
|
||||
30 | my_mock.assert_not_awaited
|
||||
31 | my_mock.assert_awaited
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
32 | my_mock.assert_awaited_once_with
|
||||
33 | my_mock.assert_awaited_once_with
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_awaited_once_with`
|
||||
--> PGH005_0.py:32:1
|
||||
|
|
||||
30 | my_mock.assert_not_awaited
|
||||
31 | my_mock.assert_awaited
|
||||
32 | my_mock.assert_awaited_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
33 | my_mock.assert_awaited_once_with
|
||||
34 | MyMock.assert_awaited_once_with
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_awaited_once_with`
|
||||
--> PGH005_0.py:33:1
|
||||
|
|
||||
31 | my_mock.assert_awaited
|
||||
32 | my_mock.assert_awaited_once_with
|
||||
33 | my_mock.assert_awaited_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
34 | MyMock.assert_awaited_once_with
|
||||
35 | assert my_mock.awaited
|
||||
|
|
||||
|
||||
PGH005 Mock method should be called: `assert_awaited_once_with`
|
||||
--> PGH005_0.py:34:1
|
||||
|
|
||||
32 | my_mock.assert_awaited_once_with
|
||||
33 | my_mock.assert_awaited_once_with
|
||||
34 | MyMock.assert_awaited_once_with
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
35 | assert my_mock.awaited
|
||||
|
|
||||
|
||||
PGH005 Non-existent mock method: `awaited`
|
||||
--> PGH005_0.py:35:8
|
||||
|
|
||||
33 | my_mock.assert_awaited_once_with
|
||||
34 | MyMock.assert_awaited_once_with
|
||||
35 | assert my_mock.awaited
|
||||
| ^^^^^^^^^^^^^^^
|
||||
36 |
|
||||
37 | # OK
|
||||
|
|
||||
@@ -252,6 +252,30 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::BidirectionalUnicode, Path::new("bidirectional_unicode.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("pylint").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
pylint: pylint::settings::Settings {
|
||||
allow_dunder_method_names: FxHashSet::from_iter([
|
||||
"__special_custom_magic__".to_string()
|
||||
]),
|
||||
..pylint::settings::Settings::default()
|
||||
},
|
||||
preview: PreviewMode::Enabled,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continue_in_finally() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
@@ -420,6 +444,19 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_useless_import_alias() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pylint/import_aliasing_2/__init__.py"),
|
||||
&LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..LinterSettings::for_rule(Rule::UselessImportAlias)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_outside_top_level_with_banned() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_source_file::Line;
|
||||
|
||||
use crate::{Violation, checkers::ast::LintContext};
|
||||
use crate::{
|
||||
Violation, checkers::ast::LintContext, preview::is_bidi_forbid_arabic_letter_mark_enabled,
|
||||
};
|
||||
|
||||
const BIDI_UNICODE: [char; 11] = [
|
||||
const BIDI_UNICODE: [char; 10] = [
|
||||
'\u{202A}', //{LEFT-TO-RIGHT EMBEDDING}
|
||||
'\u{202B}', //{RIGHT-TO-LEFT EMBEDDING}
|
||||
'\u{202C}', //{POP DIRECTIONAL FORMATTING}
|
||||
@@ -17,7 +19,6 @@ const BIDI_UNICODE: [char; 11] = [
|
||||
// https://peps.python.org/pep-0672/
|
||||
// so the list above might not be complete
|
||||
'\u{200F}', //{RIGHT-TO-LEFT MARK}
|
||||
'\u{061C}', //{ARABIC LETTER MARK}
|
||||
// We don't use
|
||||
// "\u200E" # \n{LEFT-TO-RIGHT MARK}
|
||||
// as this is the default for latin files and can't be used
|
||||
@@ -61,7 +62,12 @@ impl Violation for BidirectionalUnicode {
|
||||
|
||||
/// PLE2502
|
||||
pub(crate) fn bidirectional_unicode(line: &Line, context: &LintContext) {
|
||||
if line.contains(BIDI_UNICODE) {
|
||||
if line.contains(BIDI_UNICODE)
|
||||
|| (is_bidi_forbid_arabic_letter_mark_enabled(context.settings())
|
||||
&& line.contains(
|
||||
'\u{061C}', //{ARABIC LETTER MARK}
|
||||
))
|
||||
{
|
||||
context.report_diagnostic(BidirectionalUnicode, line.full_range());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_ignore_init_files_in_useless_alias_enabled;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for import aliases that do not rename the original package.
|
||||
/// This rule does not apply in `__init__.py` files.
|
||||
///
|
||||
/// In [preview] this rule does not apply in `__init__.py` files.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The import alias is redundant and should be removed to avoid confusion.
|
||||
@@ -33,6 +35,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// ```python
|
||||
/// import numpy
|
||||
/// ```
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UselessImportAlias {
|
||||
required_import_conflict: bool,
|
||||
@@ -43,6 +47,7 @@ impl Violation for UselessImportAlias {
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
#[expect(clippy::if_not_else)]
|
||||
if !self.required_import_conflict {
|
||||
"Import alias does not rename original package".to_string()
|
||||
} else {
|
||||
@@ -69,7 +74,9 @@ pub(crate) fn useless_import_alias(checker: &Checker, alias: &Alias) {
|
||||
}
|
||||
|
||||
// A re-export in __init__.py is probably intentional.
|
||||
if checker.path().ends_with("__init__.py") {
|
||||
if checker.path().ends_with("__init__.py")
|
||||
&& is_ignore_init_files_in_useless_alias_enabled(checker.settings())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
|
||||
PLC0414 [*] Import alias does not rename original package
|
||||
--> __init__.py:1:8
|
||||
|
|
||||
1 | import collections as collections
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
2 | from collections import OrderedDict as OrderedDict
|
||||
3 | from . import foo as foo
|
||||
|
|
||||
help: Remove import alias
|
||||
- import collections as collections
|
||||
1 + import collections
|
||||
2 | from collections import OrderedDict as OrderedDict
|
||||
3 | from . import foo as foo
|
||||
4 | from .foo import bar as bar
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -21,16 +21,6 @@ PLE2502 Contains control characters that can permit obfuscated code
|
||||
7 | # E2502
|
||||
|
|
||||
|
||||
PLE2502 Contains control characters that can permit obfuscated code
|
||||
--> bidirectional_unicode.py:8:1
|
||||
|
|
||||
7 | # E2502
|
||||
8 | another = "x" * 50 # "x" is assigned
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
9 |
|
||||
10 | # E2502
|
||||
|
|
||||
|
||||
PLE2502 Contains control characters that can permit obfuscated code
|
||||
--> bidirectional_unicode.py:11:1
|
||||
|
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
PLE2502 Contains control characters that can permit obfuscated code
|
||||
--> bidirectional_unicode.py:2:1
|
||||
|
|
||||
1 | # E2502
|
||||
2 | print("שלום")
|
||||
| ^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | # E2502
|
||||
|
|
||||
|
||||
PLE2502 Contains control characters that can permit obfuscated code
|
||||
--> bidirectional_unicode.py:5:1
|
||||
|
|
||||
4 | # E2502
|
||||
5 | example = "x" * 100 # "x" is assigned
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
6 |
|
||||
7 | # E2502
|
||||
|
|
||||
|
||||
PLE2502 Contains control characters that can permit obfuscated code
|
||||
--> bidirectional_unicode.py:8:1
|
||||
|
|
||||
7 | # E2502
|
||||
8 | another = "x" * 50 # "x" is assigned
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
9 |
|
||||
10 | # E2502
|
||||
|
|
||||
|
||||
PLE2502 Contains control characters that can permit obfuscated code
|
||||
--> bidirectional_unicode.py:11:1
|
||||
|
|
||||
10 | # E2502
|
||||
11 | if access_level != "none": # Check if admin ' and access_level != 'user
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
12 | print("You are an admin.")
|
||||
|
|
||||
|
||||
PLE2502 Contains control characters that can permit obfuscated code
|
||||
--> bidirectional_unicode.py:17:1
|
||||
|
|
||||
15 | # E2502
|
||||
16 | def subtract_funds(account: str, amount: int):
|
||||
17 | """Subtract funds from bank account then """
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
18 | return
|
||||
19 | bank[account] -= amount
|
||||
|
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
|
||||
@@ -147,6 +147,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
future_annotations: true,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
@@ -356,19 +357,4 @@ mod tests {
|
||||
2 | from pipes import quote, Template
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnecessary_default_type_args_stubs_py312_preview() -> Result<()> {
|
||||
let snapshot = format!("{}__preview", "UP043.pyi");
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade/UP043.pyi"),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
unresolved_target_version: PythonVersion::PY312.into(),
|
||||
..settings::LinterSettings::for_rule(Rule::UnnecessaryDefaultTypeArgs)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Are
|
||||
stack.push(&mut string.left);
|
||||
stack.push(&mut string.right);
|
||||
}
|
||||
libcst_native::String::Formatted(_) | libcst_native::String::Templated(_) => {}
|
||||
libcst_native::String::Formatted(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ impl AlwaysFixableViolation for QuotedAnnotation {
|
||||
|
||||
/// UP037
|
||||
pub(crate) fn quoted_annotation(checker: &Checker, annotation: &str, range: TextRange) {
|
||||
let add_future_import = checker.settings().future_annotations
|
||||
let add_future_import = checker.settings().future_annotations()
|
||||
&& checker.semantic().in_runtime_evaluated_annotation();
|
||||
|
||||
if !(checker.semantic().in_typing_only_annotation() || add_future_import) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -96,22 +94,14 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
|
||||
};
|
||||
|
||||
// Find the enclosing function definition (if any).
|
||||
let Some(
|
||||
func_stmt @ Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||
parameters: parent_parameters,
|
||||
..
|
||||
}),
|
||||
) = parents.find(|stmt| stmt.is_function_def_stmt())
|
||||
let Some(Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||
parameters: parent_parameters,
|
||||
..
|
||||
})) = parents.find(|stmt| stmt.is_function_def_stmt())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if is_builtins_super(checker.semantic(), call)
|
||||
&& !has_local_dunder_class_var_ref(checker.semantic(), func_stmt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the name of the first argument to the enclosing function.
|
||||
let Some(parent_arg) = parent_parameters.args.first() else {
|
||||
return;
|
||||
@@ -203,67 +193,3 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
|
||||
fn is_super_call_with_arguments(call: &ast::ExprCall, checker: &Checker) -> bool {
|
||||
checker.semantic().match_builtin_expr(&call.func, "super") && !call.arguments.is_empty()
|
||||
}
|
||||
|
||||
/// Returns `true` if the function contains load references to `__class__` or `super` without
|
||||
/// local binding.
|
||||
///
|
||||
/// This indicates that the function relies on the implicit `__class__` cell variable created by
|
||||
/// Python when `super()` is called without arguments, making it unsafe to remove `super()` parameters.
|
||||
fn has_local_dunder_class_var_ref(semantic: &SemanticModel, func_stmt: &Stmt) -> bool {
|
||||
if semantic.current_scope().has("__class__") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut finder = ClassCellReferenceFinder::new();
|
||||
finder.visit_stmt(func_stmt);
|
||||
|
||||
finder.found()
|
||||
}
|
||||
|
||||
/// Returns `true` if the call is to the built-in `builtins.super` function.
|
||||
fn is_builtins_super(semantic: &SemanticModel, call: &ast::ExprCall) -> bool {
|
||||
semantic
|
||||
.resolve_qualified_name(&call.func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["builtins", "super"]))
|
||||
}
|
||||
|
||||
/// A [`Visitor`] that searches for implicit reference to `__class__` cell,
|
||||
/// excluding nested class definitions.
|
||||
#[derive(Debug)]
|
||||
struct ClassCellReferenceFinder {
|
||||
has_class_cell: bool,
|
||||
}
|
||||
|
||||
impl ClassCellReferenceFinder {
|
||||
pub(crate) fn new() -> Self {
|
||||
ClassCellReferenceFinder {
|
||||
has_class_cell: false,
|
||||
}
|
||||
}
|
||||
pub(crate) fn found(&self) -> bool {
|
||||
self.has_class_cell
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for ClassCellReferenceFinder {
|
||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||
match stmt {
|
||||
Stmt::ClassDef(_) => {}
|
||||
_ => {
|
||||
if !self.has_class_cell {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||
if expr.as_name_expr().is_some_and(|name| {
|
||||
matches!(name.id.as_str(), "super" | "__class__") && name.ctx.is_load()
|
||||
}) {
|
||||
self.has_class_cell = true;
|
||||
return;
|
||||
}
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary default type arguments for `Generator` and
|
||||
/// `AsyncGenerator` on Python 3.13+.
|
||||
/// In [preview], this rule will also apply to stub files.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python 3.13 introduced the ability for type parameters to specify default
|
||||
@@ -60,8 +59,6 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
|
||||
/// - [Annotating generators and coroutines](https://docs.python.org/3/library/typing.html#annotating-generators-and-coroutines)
|
||||
/// - [Python documentation: `typing.Generator`](https://docs.python.org/3/library/typing.html#typing.Generator)
|
||||
/// - [Python documentation: `typing.AsyncGenerator`](https://docs.python.org/3/library/typing.html#typing.AsyncGenerator)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnnecessaryDefaultTypeArgs;
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ impl CallKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Removed
|
||||
/// This rule was removed as using [PEP 604] syntax in `isinstance` and `issubclass` calls
|
||||
/// ## Deprecation
|
||||
/// This rule was deprecated as using [PEP 604] syntax in `isinstance` and `issubclass` calls
|
||||
/// isn't recommended practice, and it incorrectly suggests that other typing syntaxes like [PEP 695]
|
||||
/// would be supported by `isinstance` and `issubclass`. Using the [PEP 604] syntax
|
||||
/// is also slightly slower.
|
||||
|
||||
@@ -146,6 +146,25 @@ help: Remove `super()` parameters
|
||||
95 | # see: https://github.com/astral-sh/ruff/issues/18684
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:107:23
|
||||
|
|
||||
105 | class C:
|
||||
106 | def f(self):
|
||||
107 | builtins.super(C, self)
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
104 |
|
||||
105 | class C:
|
||||
106 | def f(self):
|
||||
- builtins.super(C, self)
|
||||
107 + builtins.super()
|
||||
108 |
|
||||
109 |
|
||||
110 | # see: https://github.com/astral-sh/ruff/issues/18533
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:113:14
|
||||
|
|
||||
@@ -275,8 +294,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
142 | def method3(self):
|
||||
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
144 |
|
||||
145 | # See: https://github.com/astral-sh/ruff/issues/19357
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
|
||||
@@ -284,213 +301,4 @@ help: Remove `super()` parameters
|
||||
142 | def method3(self):
|
||||
- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
|
||||
143 + super().some_method() # Should be fixed - no keywords
|
||||
144 |
|
||||
145 | # See: https://github.com/astral-sh/ruff/issues/19357
|
||||
146 | # Must be detected
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:154:23
|
||||
|
|
||||
152 | def f(self):
|
||||
153 | if False: __class__ # Python injects __class__ into scope
|
||||
154 | builtins.super(ChildD1, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
155 |
|
||||
156 | class ChildD2(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
151 | class ChildD1(ParentD):
|
||||
152 | def f(self):
|
||||
153 | if False: __class__ # Python injects __class__ into scope
|
||||
- builtins.super(ChildD1, self).f()
|
||||
154 + builtins.super().f()
|
||||
155 |
|
||||
156 | class ChildD2(ParentD):
|
||||
157 | def f(self):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:159:23
|
||||
|
|
||||
157 | def f(self):
|
||||
158 | if False: super # Python injects __class__ into scope
|
||||
159 | builtins.super(ChildD2, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
160 |
|
||||
161 | class ChildD3(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
156 | class ChildD2(ParentD):
|
||||
157 | def f(self):
|
||||
158 | if False: super # Python injects __class__ into scope
|
||||
- builtins.super(ChildD2, self).f()
|
||||
159 + builtins.super().f()
|
||||
160 |
|
||||
161 | class ChildD3(ParentD):
|
||||
162 | def f(self):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:163:23
|
||||
|
|
||||
161 | class ChildD3(ParentD):
|
||||
162 | def f(self):
|
||||
163 | builtins.super(ChildD3, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
164 | super # Python injects __class__ into scope
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
160 |
|
||||
161 | class ChildD3(ParentD):
|
||||
162 | def f(self):
|
||||
- builtins.super(ChildD3, self).f()
|
||||
163 + builtins.super().f()
|
||||
164 | super # Python injects __class__ into scope
|
||||
165 |
|
||||
166 | import builtins as builtins_alias
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:169:29
|
||||
|
|
||||
167 | class ChildD4(ParentD):
|
||||
168 | def f(self):
|
||||
169 | builtins_alias.super(ChildD4, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
170 | super # Python injects __class__ into scope
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
166 | import builtins as builtins_alias
|
||||
167 | class ChildD4(ParentD):
|
||||
168 | def f(self):
|
||||
- builtins_alias.super(ChildD4, self).f()
|
||||
169 + builtins_alias.super().f()
|
||||
170 | super # Python injects __class__ into scope
|
||||
171 |
|
||||
172 | class ChildD5(ParentD):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:176:23
|
||||
|
|
||||
174 | super = 1
|
||||
175 | super # Python injects __class__ into scope
|
||||
176 | builtins.super(ChildD5, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
177 |
|
||||
178 | class ChildD6(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
173 | def f(self):
|
||||
174 | super = 1
|
||||
175 | super # Python injects __class__ into scope
|
||||
- builtins.super(ChildD5, self).f()
|
||||
176 + builtins.super().f()
|
||||
177 |
|
||||
178 | class ChildD6(ParentD):
|
||||
179 | def f(self):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:182:23
|
||||
|
|
||||
180 | super: "Any"
|
||||
181 | __class__ # Python injects __class__ into scope
|
||||
182 | builtins.super(ChildD6, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
183 |
|
||||
184 | class ChildD7(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
179 | def f(self):
|
||||
180 | super: "Any"
|
||||
181 | __class__ # Python injects __class__ into scope
|
||||
- builtins.super(ChildD6, self).f()
|
||||
182 + builtins.super().f()
|
||||
183 |
|
||||
184 | class ChildD7(ParentD):
|
||||
185 | def f(self):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:188:23
|
||||
|
|
||||
186 | def x():
|
||||
187 | __class__ # Python injects __class__ into scope
|
||||
188 | builtins.super(ChildD7, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
189 |
|
||||
190 | class ChildD8(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
185 | def f(self):
|
||||
186 | def x():
|
||||
187 | __class__ # Python injects __class__ into scope
|
||||
- builtins.super(ChildD7, self).f()
|
||||
188 + builtins.super().f()
|
||||
189 |
|
||||
190 | class ChildD8(ParentD):
|
||||
191 | def f(self):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:195:23
|
||||
|
|
||||
193 | super = 1
|
||||
194 | super # Python injects __class__ into scope
|
||||
195 | builtins.super(ChildD8, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
196 |
|
||||
197 | class ChildD9(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
192 | def x():
|
||||
193 | super = 1
|
||||
194 | super # Python injects __class__ into scope
|
||||
- builtins.super(ChildD8, self).f()
|
||||
195 + builtins.super().f()
|
||||
196 |
|
||||
197 | class ChildD9(ParentD):
|
||||
198 | def f(self):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:202:23
|
||||
|
|
||||
200 | __class__ = 1
|
||||
201 | __class__ # Python injects __class__ into scope
|
||||
202 | builtins.super(ChildD9, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
203 |
|
||||
204 | class ChildD10(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
199 | def x():
|
||||
200 | __class__ = 1
|
||||
201 | __class__ # Python injects __class__ into scope
|
||||
- builtins.super(ChildD9, self).f()
|
||||
202 + builtins.super().f()
|
||||
203 |
|
||||
204 | class ChildD10(ParentD):
|
||||
205 | def f(self):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:209:23
|
||||
|
|
||||
207 | __class__ = 1
|
||||
208 | super # Python injects __class__ into scope
|
||||
209 | builtins.super(ChildD10, self).f()
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
206 | def x():
|
||||
207 | __class__ = 1
|
||||
208 | super # Python injects __class__ into scope
|
||||
- builtins.super(ChildD10, self).f()
|
||||
209 + builtins.super().f()
|
||||
210 |
|
||||
211 |
|
||||
212 | # Must be ignored
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -139,6 +139,24 @@ help: Remove `super()` parameters
|
||||
94 |
|
||||
95 | # see: https://github.com/astral-sh/ruff/issues/18684
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:107:23
|
||||
|
|
||||
105 | class C:
|
||||
106 | def f(self):
|
||||
107 | builtins.super(C, self)
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
104 |
|
||||
105 | class C:
|
||||
106 | def f(self):
|
||||
- builtins.super(C, self)
|
||||
107 + builtins.super()
|
||||
108 |
|
||||
109 |
|
||||
110 | # see: https://github.com/astral-sh/ruff/issues/18533
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:113:14
|
||||
|
|
||||
@@ -268,8 +286,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
142 | def method3(self):
|
||||
143 | super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
144 |
|
||||
145 | # See: https://github.com/astral-sh/ruff/issues/19357
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
140 | super(ExampleWithKeywords, self, **{"kwarg": "value"}).some_method() # Should emit diagnostic but NOT be fixed
|
||||
@@ -277,202 +293,3 @@ help: Remove `super()` parameters
|
||||
142 | def method3(self):
|
||||
- super(ExampleWithKeywords, self).some_method() # Should be fixed - no keywords
|
||||
143 + super().some_method() # Should be fixed - no keywords
|
||||
144 |
|
||||
145 | # See: https://github.com/astral-sh/ruff/issues/19357
|
||||
146 | # Must be detected
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:154:23
|
||||
|
|
||||
152 | def f(self):
|
||||
153 | if False: __class__ # Python injects __class__ into scope
|
||||
154 | builtins.super(ChildD1, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
155 |
|
||||
156 | class ChildD2(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
151 | class ChildD1(ParentD):
|
||||
152 | def f(self):
|
||||
153 | if False: __class__ # Python injects __class__ into scope
|
||||
- builtins.super(ChildD1, self).f()
|
||||
154 + builtins.super().f()
|
||||
155 |
|
||||
156 | class ChildD2(ParentD):
|
||||
157 | def f(self):
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:159:23
|
||||
|
|
||||
157 | def f(self):
|
||||
158 | if False: super # Python injects __class__ into scope
|
||||
159 | builtins.super(ChildD2, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
160 |
|
||||
161 | class ChildD3(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
156 | class ChildD2(ParentD):
|
||||
157 | def f(self):
|
||||
158 | if False: super # Python injects __class__ into scope
|
||||
- builtins.super(ChildD2, self).f()
|
||||
159 + builtins.super().f()
|
||||
160 |
|
||||
161 | class ChildD3(ParentD):
|
||||
162 | def f(self):
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:163:23
|
||||
|
|
||||
161 | class ChildD3(ParentD):
|
||||
162 | def f(self):
|
||||
163 | builtins.super(ChildD3, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
164 | super # Python injects __class__ into scope
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
160 |
|
||||
161 | class ChildD3(ParentD):
|
||||
162 | def f(self):
|
||||
- builtins.super(ChildD3, self).f()
|
||||
163 + builtins.super().f()
|
||||
164 | super # Python injects __class__ into scope
|
||||
165 |
|
||||
166 | import builtins as builtins_alias
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:169:29
|
||||
|
|
||||
167 | class ChildD4(ParentD):
|
||||
168 | def f(self):
|
||||
169 | builtins_alias.super(ChildD4, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
170 | super # Python injects __class__ into scope
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
166 | import builtins as builtins_alias
|
||||
167 | class ChildD4(ParentD):
|
||||
168 | def f(self):
|
||||
- builtins_alias.super(ChildD4, self).f()
|
||||
169 + builtins_alias.super().f()
|
||||
170 | super # Python injects __class__ into scope
|
||||
171 |
|
||||
172 | class ChildD5(ParentD):
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:176:23
|
||||
|
|
||||
174 | super = 1
|
||||
175 | super # Python injects __class__ into scope
|
||||
176 | builtins.super(ChildD5, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
177 |
|
||||
178 | class ChildD6(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
173 | def f(self):
|
||||
174 | super = 1
|
||||
175 | super # Python injects __class__ into scope
|
||||
- builtins.super(ChildD5, self).f()
|
||||
176 + builtins.super().f()
|
||||
177 |
|
||||
178 | class ChildD6(ParentD):
|
||||
179 | def f(self):
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:182:23
|
||||
|
|
||||
180 | super: "Any"
|
||||
181 | __class__ # Python injects __class__ into scope
|
||||
182 | builtins.super(ChildD6, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
183 |
|
||||
184 | class ChildD7(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
179 | def f(self):
|
||||
180 | super: "Any"
|
||||
181 | __class__ # Python injects __class__ into scope
|
||||
- builtins.super(ChildD6, self).f()
|
||||
182 + builtins.super().f()
|
||||
183 |
|
||||
184 | class ChildD7(ParentD):
|
||||
185 | def f(self):
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:188:23
|
||||
|
|
||||
186 | def x():
|
||||
187 | __class__ # Python injects __class__ into scope
|
||||
188 | builtins.super(ChildD7, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
189 |
|
||||
190 | class ChildD8(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
185 | def f(self):
|
||||
186 | def x():
|
||||
187 | __class__ # Python injects __class__ into scope
|
||||
- builtins.super(ChildD7, self).f()
|
||||
188 + builtins.super().f()
|
||||
189 |
|
||||
190 | class ChildD8(ParentD):
|
||||
191 | def f(self):
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:195:23
|
||||
|
|
||||
193 | super = 1
|
||||
194 | super # Python injects __class__ into scope
|
||||
195 | builtins.super(ChildD8, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
196 |
|
||||
197 | class ChildD9(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
192 | def x():
|
||||
193 | super = 1
|
||||
194 | super # Python injects __class__ into scope
|
||||
- builtins.super(ChildD8, self).f()
|
||||
195 + builtins.super().f()
|
||||
196 |
|
||||
197 | class ChildD9(ParentD):
|
||||
198 | def f(self):
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:202:23
|
||||
|
|
||||
200 | __class__ = 1
|
||||
201 | __class__ # Python injects __class__ into scope
|
||||
202 | builtins.super(ChildD9, self).f()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
203 |
|
||||
204 | class ChildD10(ParentD):
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
199 | def x():
|
||||
200 | __class__ = 1
|
||||
201 | __class__ # Python injects __class__ into scope
|
||||
- builtins.super(ChildD9, self).f()
|
||||
202 + builtins.super().f()
|
||||
203 |
|
||||
204 | class ChildD10(ParentD):
|
||||
205 | def f(self):
|
||||
|
||||
UP008 [*] Use `super()` instead of `super(__class__, self)`
|
||||
--> UP008.py:209:23
|
||||
|
|
||||
207 | __class__ = 1
|
||||
208 | super # Python injects __class__ into scope
|
||||
209 | builtins.super(ChildD10, self).f()
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Remove `super()` parameters
|
||||
206 | def x():
|
||||
207 | __class__ = 1
|
||||
208 | super # Python injects __class__ into scope
|
||||
- builtins.super(ChildD10, self).f()
|
||||
209 + builtins.super().f()
|
||||
210 |
|
||||
211 |
|
||||
212 | # Must be ignored
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP043 [*] Unnecessary default type arguments
|
||||
--> UP043.pyi:4:15
|
||||
|
|
||||
4 | def func() -> Generator[int, None, None]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
5 | yield 42
|
||||
|
|
||||
help: Remove default type arguments
|
||||
1 | from collections.abc import Generator, AsyncGenerator
|
||||
2 |
|
||||
3 |
|
||||
- def func() -> Generator[int, None, None]:
|
||||
4 + def func() -> Generator[int]:
|
||||
5 | yield 42
|
||||
6 |
|
||||
7 |
|
||||
|
||||
UP043 [*] Unnecessary default type arguments
|
||||
--> UP043.pyi:8:15
|
||||
|
|
||||
8 | def func() -> Generator[int, None]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
9 | yield 42
|
||||
|
|
||||
help: Remove default type arguments
|
||||
5 | yield 42
|
||||
6 |
|
||||
7 |
|
||||
- def func() -> Generator[int, None]:
|
||||
8 + def func() -> Generator[int]:
|
||||
9 | yield 42
|
||||
10 |
|
||||
11 |
|
||||
|
||||
UP043 [*] Unnecessary default type arguments
|
||||
--> UP043.pyi:21:15
|
||||
|
|
||||
21 | def func() -> Generator[int, int, None]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
22 | _ = yield 42
|
||||
23 | return None
|
||||
|
|
||||
help: Remove default type arguments
|
||||
18 | return foo
|
||||
19 |
|
||||
20 |
|
||||
- def func() -> Generator[int, int, None]:
|
||||
21 + def func() -> Generator[int, int]:
|
||||
22 | _ = yield 42
|
||||
23 | return None
|
||||
24 |
|
||||
|
||||
UP043 [*] Unnecessary default type arguments
|
||||
--> UP043.pyi:31:21
|
||||
|
|
||||
31 | async def func() -> AsyncGenerator[int, None]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
32 | yield 42
|
||||
|
|
||||
help: Remove default type arguments
|
||||
28 | return 42
|
||||
29 |
|
||||
30 |
|
||||
- async def func() -> AsyncGenerator[int, None]:
|
||||
31 + async def func() -> AsyncGenerator[int]:
|
||||
32 | yield 42
|
||||
33 |
|
||||
34 |
|
||||
|
||||
UP043 [*] Unnecessary default type arguments
|
||||
--> UP043.pyi:47:15
|
||||
|
|
||||
47 | def func() -> Generator[str, None, None]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
48 | yield "hello"
|
||||
|
|
||||
help: Remove default type arguments
|
||||
44 | from typing import Generator, AsyncGenerator
|
||||
45 |
|
||||
46 |
|
||||
- def func() -> Generator[str, None, None]:
|
||||
47 + def func() -> Generator[str]:
|
||||
48 | yield "hello"
|
||||
49 |
|
||||
50 |
|
||||
|
||||
UP043 [*] Unnecessary default type arguments
|
||||
--> UP043.pyi:51:21
|
||||
|
|
||||
51 | async def func() -> AsyncGenerator[str, None]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
52 | yield "hello"
|
||||
|
|
||||
help: Remove default type arguments
|
||||
48 | yield "hello"
|
||||
49 |
|
||||
50 |
|
||||
- async def func() -> AsyncGenerator[str, None]:
|
||||
51 + async def func() -> AsyncGenerator[str]:
|
||||
52 | yield "hello"
|
||||
53 |
|
||||
54 |
|
||||
|
||||
UP043 [*] Unnecessary default type arguments
|
||||
--> UP043.pyi:55:21
|
||||
|
|
||||
55 | async def func() -> AsyncGenerator[ # type: ignore
|
||||
| _____________________^
|
||||
56 | | str,
|
||||
57 | | None
|
||||
58 | | ]:
|
||||
| |_^
|
||||
59 | yield "hello"
|
||||
|
|
||||
help: Remove default type arguments
|
||||
52 | yield "hello"
|
||||
53 |
|
||||
54 |
|
||||
- async def func() -> AsyncGenerator[ # type: ignore
|
||||
- str,
|
||||
- None
|
||||
- ]:
|
||||
55 + async def func() -> AsyncGenerator[str]:
|
||||
56 | yield "hello"
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -85,7 +85,6 @@ mod tests {
|
||||
#[test_case(Rule::InvalidAssertMessageLiteralArgument, Path::new("RUF040.py"))]
|
||||
#[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.py"))]
|
||||
#[test_case(Rule::UnnecessaryNestedLiteral, Path::new("RUF041.pyi"))]
|
||||
#[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))]
|
||||
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))]
|
||||
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046_CR.py"))]
|
||||
#[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046_LF.py"))]
|
||||
@@ -536,6 +535,7 @@ mod tests {
|
||||
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))]
|
||||
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_2.py"))]
|
||||
#[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_3.py"))]
|
||||
#[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))]
|
||||
#[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))]
|
||||
#[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
@@ -649,6 +649,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("ruff").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
future_annotations: true,
|
||||
unresolved_target_version: PythonVersion::PY39.into(),
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
|
||||
@@ -19,10 +19,6 @@ use crate::rules::ruff::typing::type_hint_explicitly_allows_none;
|
||||
/// Checks for the use of implicit `Optional` in type annotations when the
|
||||
/// default parameter value is `None`.
|
||||
///
|
||||
/// If [`lint.future-annotations`] is set to `true`, `from __future__ import
|
||||
/// annotations` will be added if doing so would allow using the `|` operator on
|
||||
/// a Python version before 3.10.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Implicit `Optional` is prohibited by [PEP 484]. It is confusing and
|
||||
/// inconsistent with the rest of the type system.
|
||||
@@ -77,6 +73,12 @@ use crate::rules::ruff::typing::type_hint_explicitly_allows_none;
|
||||
/// - `target-version`
|
||||
/// - `lint.future-annotations`
|
||||
///
|
||||
/// ## Preview
|
||||
///
|
||||
/// When [preview] is enabled, if [`lint.future-annotations`] is set to `true`,
|
||||
/// `from __future__ import annotations` will be added if doing so would allow using the `|`
|
||||
/// operator on a Python version before 3.10.
|
||||
///
|
||||
/// ## Fix safety
|
||||
///
|
||||
/// This fix is always marked as unsafe because it can change the behavior of code that relies on
|
||||
@@ -215,7 +217,7 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
|
||||
};
|
||||
|
||||
let conversion_type = if checker.target_version() >= PythonVersion::PY310
|
||||
|| checker.settings().future_annotations
|
||||
|| checker.settings().future_annotations()
|
||||
{
|
||||
ConversionType::BinOpOr
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{self as ast, CmpOp, Expr, helpers::is_empty_f_string};
|
||||
use ruff_python_ast::{self as ast, CmpOp, Expr};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -7,10 +7,10 @@ use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for membership tests on empty collections (such as `list`, `tuple`, `set` or `dict`).
|
||||
/// Checks for membership tests on empty collections (such as `list`, `tuple`, `set`, or `dict`).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// If the collection is always empty, the check is unnecessary, and can be removed.
|
||||
/// If the collection is always empty, the check is unnecessary and can be removed.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
@@ -75,7 +75,10 @@ fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
Expr::Dict(ast::ExprDict { items, .. }) => items.is_empty(),
|
||||
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.is_empty(),
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),
|
||||
Expr::FString(s) => is_empty_f_string(s),
|
||||
Expr::FString(s) => s
|
||||
.value
|
||||
.elements()
|
||||
.all(|elt| elt.as_literal().is_some_and(|elt| elt.is_empty())),
|
||||
Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
arguments,
|
||||
|
||||
@@ -27,8 +27,7 @@ use crate::rules::flake8_pytest_style::rules::is_pytest_raises;
|
||||
/// do_thing_that_raises()
|
||||
/// ```
|
||||
///
|
||||
/// If the pattern is intended to be a regular expression, use a raw string to signal this
|
||||
/// intention:
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
@@ -38,7 +37,7 @@ use crate::rules::flake8_pytest_style::rules::is_pytest_raises;
|
||||
/// do_thing_that_raises()
|
||||
/// ```
|
||||
///
|
||||
/// Alternatively, escape any regex metacharacters with `re.escape`:
|
||||
/// Alternatively:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
@@ -49,7 +48,7 @@ use crate::rules::flake8_pytest_style::rules::is_pytest_raises;
|
||||
/// do_thing_that_raises()
|
||||
/// ```
|
||||
///
|
||||
/// or directly with backslashes:
|
||||
/// or:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
|
||||
@@ -36,15 +36,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// return x
|
||||
/// ```
|
||||
///
|
||||
/// ## See also
|
||||
///
|
||||
/// This rule applies only to unpacked assignments. For regular assignments, see
|
||||
/// [`unused-variable`][F841].
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.dummy-variable-rgx`
|
||||
///
|
||||
/// [F841]: https://docs.astral.sh/ruff/rules/unused-variable/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnusedUnpackedVariable {
|
||||
pub name: String,
|
||||
|
||||
@@ -251,12 +251,3 @@ RUF060 Unnecessary membership test on empty collection
|
||||
25 |
|
||||
26 | # OK
|
||||
|
|
||||
|
||||
RUF060 Unnecessary membership test on empty collection
|
||||
--> RUF060.py:47:1
|
||||
|
|
||||
46 | # https://github.com/astral-sh/ruff/issues/20238
|
||||
47 | "b" in f"" "" # Error
|
||||
| ^^^^^^^^^^^^^
|
||||
48 | "b" in f"" "x" # OK
|
||||
|
|
||||
|
||||
@@ -475,6 +475,11 @@ impl LinterSettings {
|
||||
.is_match(path)
|
||||
.map_or(self.unresolved_target_version, TargetVersion::from)
|
||||
}
|
||||
|
||||
pub fn future_annotations(&self) -> bool {
|
||||
// TODO(brent) we can just access the field directly once this is stabilized.
|
||||
self.future_annotations && crate::preview::is_add_future_annotations_imports_enabled(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LinterSettings {
|
||||
|
||||
@@ -3219,6 +3219,7 @@ impl<'a> IntoIterator for &'a Box<Parameters> {
|
||||
/// Used by `Arguments` original type.
|
||||
///
|
||||
/// NOTE: This type is different from original Python AST.
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
|
||||
pub struct ParameterWithDefault {
|
||||
@@ -3240,14 +3241,6 @@ impl ParameterWithDefault {
|
||||
pub fn annotation(&self) -> Option<&Expr> {
|
||||
self.parameter.annotation()
|
||||
}
|
||||
|
||||
/// Return `true` if the parameter name uses the pre-PEP-570 convention
|
||||
/// (specified in PEP 484) to indicate to a type checker that it should be treated
|
||||
/// as positional-only.
|
||||
pub fn uses_pep_484_positional_only_convention(&self) -> bool {
|
||||
let name = self.name();
|
||||
name.starts_with("__") && !name.ends_with("__")
|
||||
}
|
||||
}
|
||||
|
||||
/// An AST node used to represent the arguments passed to a function call or class definition.
|
||||
|
||||
@@ -252,20 +252,15 @@ impl QuoteStyle {
|
||||
pub const fn is_preserve(self) -> bool {
|
||||
matches!(self, QuoteStyle::Preserve)
|
||||
}
|
||||
|
||||
/// Returns the string representation of the quote style.
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
QuoteStyle::Single => "single",
|
||||
QuoteStyle::Double => "double",
|
||||
QuoteStyle::Preserve => "preserve",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for QuoteStyle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
match self {
|
||||
Self::Single => write!(f, "single"),
|
||||
Self::Double => write!(f, "double"),
|
||||
Self::Preserve => write!(f, "preserve"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,10 +302,10 @@ impl MagicTrailingComma {
|
||||
|
||||
impl fmt::Display for MagicTrailingComma {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
MagicTrailingComma::Respect => "respect",
|
||||
MagicTrailingComma::Ignore => "ignore",
|
||||
})
|
||||
match self {
|
||||
Self::Respect => write!(f, "respect"),
|
||||
Self::Ignore => write!(f, "ignore"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,5 @@ insta = { workspace = true }
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
[features]
|
||||
test-uv = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,52 +1,18 @@
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use ruff_formatter::{FormatOptions, PrintedRange};
|
||||
use ruff_formatter::PrintedRange;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_formatter::{FormatModuleError, PyFormatOptions, format_module_source};
|
||||
use ruff_source_file::LineIndex;
|
||||
use ruff_python_formatter::{FormatModuleError, format_module_source};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::edit::TextDocument;
|
||||
|
||||
/// The backend to use for formatting.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum FormatBackend {
|
||||
/// Use the built-in Ruff formatter.
|
||||
///
|
||||
/// The formatter version will match the LSP version.
|
||||
#[default]
|
||||
Internal,
|
||||
/// Use uv for formatting.
|
||||
///
|
||||
/// The formatter version may differ from the LSP version.
|
||||
Uv,
|
||||
}
|
||||
|
||||
pub(crate) fn format(
|
||||
document: &TextDocument,
|
||||
source_type: PySourceType,
|
||||
formatter_settings: &FormatterSettings,
|
||||
path: &Path,
|
||||
backend: FormatBackend,
|
||||
) -> crate::Result<Option<String>> {
|
||||
match backend {
|
||||
FormatBackend::Uv => format_external(document, source_type, formatter_settings, path),
|
||||
FormatBackend::Internal => format_internal(document, source_type, formatter_settings, path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format using the built-in Ruff formatter.
|
||||
fn format_internal(
|
||||
document: &TextDocument,
|
||||
source_type: PySourceType,
|
||||
formatter_settings: &FormatterSettings,
|
||||
path: &Path,
|
||||
) -> crate::Result<Option<String>> {
|
||||
let format_options =
|
||||
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
||||
@@ -69,44 +35,12 @@ fn format_internal(
|
||||
}
|
||||
}
|
||||
|
||||
/// Format using an external uv command.
|
||||
fn format_external(
|
||||
document: &TextDocument,
|
||||
source_type: PySourceType,
|
||||
formatter_settings: &FormatterSettings,
|
||||
path: &Path,
|
||||
) -> crate::Result<Option<String>> {
|
||||
let format_options =
|
||||
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
||||
let uv_command = UvFormatCommand::from(format_options);
|
||||
uv_command.format_document(document.contents(), path)
|
||||
}
|
||||
|
||||
pub(crate) fn format_range(
|
||||
document: &TextDocument,
|
||||
source_type: PySourceType,
|
||||
formatter_settings: &FormatterSettings,
|
||||
range: TextRange,
|
||||
path: &Path,
|
||||
backend: FormatBackend,
|
||||
) -> crate::Result<Option<PrintedRange>> {
|
||||
match backend {
|
||||
FormatBackend::Uv => {
|
||||
format_range_external(document, source_type, formatter_settings, range, path)
|
||||
}
|
||||
FormatBackend::Internal => {
|
||||
format_range_internal(document, source_type, formatter_settings, range, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format range using the built-in Ruff formatter
|
||||
fn format_range_internal(
|
||||
document: &TextDocument,
|
||||
source_type: PySourceType,
|
||||
formatter_settings: &FormatterSettings,
|
||||
range: TextRange,
|
||||
path: &Path,
|
||||
) -> crate::Result<Option<PrintedRange>> {
|
||||
let format_options =
|
||||
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
||||
@@ -129,198 +63,6 @@ fn format_range_internal(
|
||||
}
|
||||
}
|
||||
|
||||
/// Format range using an external command, i.e., `uv`.
|
||||
fn format_range_external(
|
||||
document: &TextDocument,
|
||||
source_type: PySourceType,
|
||||
formatter_settings: &FormatterSettings,
|
||||
range: TextRange,
|
||||
path: &Path,
|
||||
) -> crate::Result<Option<PrintedRange>> {
|
||||
let format_options =
|
||||
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
||||
let uv_command = UvFormatCommand::from(format_options);
|
||||
|
||||
// Format the range using uv and convert the result to `PrintedRange`
|
||||
match uv_command.format_range(document.contents(), range, path, document.index())? {
|
||||
Some(formatted) => Ok(Some(PrintedRange::new(formatted, range))),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for uv format commands
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UvFormatCommand {
|
||||
options: PyFormatOptions,
|
||||
}
|
||||
|
||||
impl From<PyFormatOptions> for UvFormatCommand {
|
||||
fn from(options: PyFormatOptions) -> Self {
|
||||
Self { options }
|
||||
}
|
||||
}
|
||||
|
||||
impl UvFormatCommand {
|
||||
/// Build the command with all necessary arguments
|
||||
fn build_command(
|
||||
&self,
|
||||
path: &Path,
|
||||
range_with_index: Option<(TextRange, &LineIndex, &str)>,
|
||||
) -> Command {
|
||||
let mut command = Command::new("uv");
|
||||
command.arg("format");
|
||||
command.arg("--");
|
||||
|
||||
let target_version = format!(
|
||||
"py{}{}",
|
||||
self.options.target_version().major,
|
||||
self.options.target_version().minor
|
||||
);
|
||||
|
||||
// Add only the formatting options that the CLI supports
|
||||
command.arg("--target-version");
|
||||
command.arg(&target_version);
|
||||
|
||||
command.arg("--line-length");
|
||||
command.arg(self.options.line_width().to_string());
|
||||
|
||||
if self.options.preview().is_enabled() {
|
||||
command.arg("--preview");
|
||||
}
|
||||
|
||||
// Pass other formatting options via --config
|
||||
command.arg("--config");
|
||||
command.arg(format!(
|
||||
"format.indent-style = '{}'",
|
||||
self.options.indent_style()
|
||||
));
|
||||
|
||||
command.arg("--config");
|
||||
command.arg(format!("indent-width = {}", self.options.indent_width()));
|
||||
|
||||
command.arg("--config");
|
||||
command.arg(format!(
|
||||
"format.quote-style = '{}'",
|
||||
self.options.quote_style()
|
||||
));
|
||||
|
||||
command.arg("--config");
|
||||
command.arg(format!(
|
||||
"format.line-ending = '{}'",
|
||||
self.options.line_ending().as_setting_str()
|
||||
));
|
||||
|
||||
command.arg("--config");
|
||||
command.arg(format!(
|
||||
"format.skip-magic-trailing-comma = {}",
|
||||
match self.options.magic_trailing_comma() {
|
||||
ruff_python_formatter::MagicTrailingComma::Respect => "false",
|
||||
ruff_python_formatter::MagicTrailingComma::Ignore => "true",
|
||||
}
|
||||
));
|
||||
|
||||
if let Some((range, line_index, source)) = range_with_index {
|
||||
// The CLI expects line:column format
|
||||
let start_pos = line_index.line_column(range.start(), source);
|
||||
let end_pos = line_index.line_column(range.end(), source);
|
||||
let range_str = format!(
|
||||
"{}:{}-{}:{}",
|
||||
start_pos.line.get(),
|
||||
start_pos.column.get(),
|
||||
end_pos.line.get(),
|
||||
end_pos.column.get()
|
||||
);
|
||||
command.arg("--range");
|
||||
command.arg(&range_str);
|
||||
}
|
||||
|
||||
command.arg("--stdin-filename");
|
||||
command.arg(path.to_string_lossy().as_ref());
|
||||
|
||||
command.stdin(Stdio::piped());
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
|
||||
command
|
||||
}
|
||||
|
||||
/// Execute the format command on the given source.
|
||||
pub(crate) fn format(
|
||||
&self,
|
||||
source: &str,
|
||||
path: &Path,
|
||||
range_with_index: Option<(TextRange, &LineIndex)>,
|
||||
) -> crate::Result<Option<String>> {
|
||||
let mut command =
|
||||
self.build_command(path, range_with_index.map(|(r, idx)| (r, idx, source)));
|
||||
let mut child = match command.spawn() {
|
||||
Ok(child) => child,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
anyhow::bail!("uv was not found; is it installed and on the PATH?")
|
||||
}
|
||||
Err(err) => return Err(err).context("Failed to spawn uv"),
|
||||
};
|
||||
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.context("Failed to get stdin from format subprocess")?;
|
||||
stdin
|
||||
.write_all(source.as_bytes())
|
||||
.context("Failed to write to stdin")?;
|
||||
drop(stdin);
|
||||
|
||||
let result = child
|
||||
.wait_with_output()
|
||||
.context("Failed to get output from format subprocess")?;
|
||||
|
||||
if !result.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||
// We don't propagate format errors due to invalid syntax
|
||||
if stderr.contains("Failed to parse") {
|
||||
tracing::warn!("Unable to format document: {}", stderr);
|
||||
return Ok(None);
|
||||
}
|
||||
// Special-case for when `uv format` is not available
|
||||
if stderr.contains("unrecognized subcommand 'format'") {
|
||||
anyhow::bail!(
|
||||
"The installed version of uv does not support `uv format`; upgrade to a newer version"
|
||||
);
|
||||
}
|
||||
anyhow::bail!("Failed to format document: {}", stderr);
|
||||
}
|
||||
|
||||
let formatted = String::from_utf8(result.stdout)
|
||||
.context("Failed to parse stdout from format subprocess as utf-8")?;
|
||||
|
||||
if formatted == source {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(formatted))
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the entire document.
|
||||
pub(crate) fn format_document(
|
||||
&self,
|
||||
source: &str,
|
||||
path: &Path,
|
||||
) -> crate::Result<Option<String>> {
|
||||
self.format(source, path, None)
|
||||
}
|
||||
|
||||
/// Format a specific range.
|
||||
pub(crate) fn format_range(
|
||||
&self,
|
||||
source: &str,
|
||||
range: TextRange,
|
||||
path: &Path,
|
||||
line_index: &LineIndex,
|
||||
) -> crate::Result<Option<String>> {
|
||||
self.format(source, path, Some((range, line_index)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
@@ -332,7 +74,7 @@ mod tests {
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::TextDocument;
|
||||
use crate::format::{FormatBackend, format, format_range};
|
||||
use crate::format::{format, format_range};
|
||||
|
||||
#[test]
|
||||
fn format_per_file_version() {
|
||||
@@ -356,7 +98,6 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
|
||||
..Default::default()
|
||||
},
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Internal,
|
||||
)
|
||||
.expect("Expected no errors when formatting")
|
||||
.expect("Expected formatting changes");
|
||||
@@ -379,7 +120,6 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
|
||||
..Default::default()
|
||||
},
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Internal,
|
||||
)
|
||||
.expect("Expected no errors when formatting")
|
||||
.expect("Expected formatting changes");
|
||||
@@ -428,7 +168,6 @@ sys.exit(
|
||||
},
|
||||
range,
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Internal,
|
||||
)
|
||||
.expect("Expected no errors when formatting")
|
||||
.expect("Expected formatting changes");
|
||||
@@ -452,7 +191,6 @@ sys.exit(
|
||||
},
|
||||
range,
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Internal,
|
||||
)
|
||||
.expect("Expected no errors when formatting")
|
||||
.expect("Expected formatting changes");
|
||||
@@ -466,279 +204,4 @@ sys.exit(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-uv")]
|
||||
mod uv_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_uv_format_document() {
|
||||
let document = TextDocument::new(
|
||||
r#"
|
||||
def hello( x,y ,z ):
|
||||
return x+y +z
|
||||
|
||||
|
||||
def world( ):
|
||||
pass
|
||||
"#
|
||||
.to_string(),
|
||||
0,
|
||||
);
|
||||
|
||||
let result = format(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&FormatterSettings::default(),
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Uv,
|
||||
)
|
||||
.expect("Expected no errors when formatting with uv")
|
||||
.expect("Expected formatting changes");
|
||||
|
||||
// uv should format this to a consistent style
|
||||
assert_snapshot!(result, @r#"
|
||||
def hello(x, y, z):
|
||||
return x + y + z
|
||||
|
||||
|
||||
def world():
|
||||
pass
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uv_format_range() -> anyhow::Result<()> {
|
||||
let document = TextDocument::new(
|
||||
r#"
|
||||
def messy_function( a, b,c ):
|
||||
return a+b+c
|
||||
|
||||
def another_function(x,y,z):
|
||||
result=x+y+z
|
||||
return result
|
||||
"#
|
||||
.to_string(),
|
||||
0,
|
||||
);
|
||||
|
||||
// Find the range of the second function
|
||||
let start = document.contents().find("def another_function").unwrap();
|
||||
let end = document.contents().find("return result").unwrap() + "return result".len();
|
||||
let range = TextRange::new(TextSize::try_from(start)?, TextSize::try_from(end)?);
|
||||
|
||||
let result = format_range(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&FormatterSettings::default(),
|
||||
range,
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Uv,
|
||||
)
|
||||
.expect("Expected no errors when formatting range with uv")
|
||||
.expect("Expected formatting changes");
|
||||
|
||||
assert_snapshot!(result.as_code(), @r#"
|
||||
def messy_function( a, b,c ):
|
||||
return a+b+c
|
||||
|
||||
def another_function(x, y, z):
|
||||
result = x + y + z
|
||||
return result
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uv_format_with_line_length() {
|
||||
use ruff_formatter::LineWidth;
|
||||
|
||||
let document = TextDocument::new(
|
||||
r#"
|
||||
def hello(very_long_parameter_name_1, very_long_parameter_name_2, very_long_parameter_name_3):
|
||||
return very_long_parameter_name_1 + very_long_parameter_name_2 + very_long_parameter_name_3
|
||||
"#
|
||||
.to_string(),
|
||||
0,
|
||||
);
|
||||
|
||||
// Test with shorter line length
|
||||
let formatter_settings = FormatterSettings {
|
||||
line_width: LineWidth::try_from(60).unwrap(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = format(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&formatter_settings,
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Uv,
|
||||
)
|
||||
.expect("Expected no errors when formatting with uv")
|
||||
.expect("Expected formatting changes");
|
||||
|
||||
// With line length 60, the function should be wrapped
|
||||
assert_snapshot!(result, @r#"
|
||||
def hello(
|
||||
very_long_parameter_name_1,
|
||||
very_long_parameter_name_2,
|
||||
very_long_parameter_name_3,
|
||||
):
|
||||
return (
|
||||
very_long_parameter_name_1
|
||||
+ very_long_parameter_name_2
|
||||
+ very_long_parameter_name_3
|
||||
)
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uv_format_with_indent_style() {
|
||||
use ruff_formatter::IndentStyle;
|
||||
|
||||
let document = TextDocument::new(
|
||||
r#"
|
||||
def hello():
|
||||
if True:
|
||||
print("Hello")
|
||||
if False:
|
||||
print("World")
|
||||
"#
|
||||
.to_string(),
|
||||
0,
|
||||
);
|
||||
|
||||
// Test with tabs instead of spaces
|
||||
let formatter_settings = FormatterSettings {
|
||||
indent_style: IndentStyle::Tab,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = format(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&formatter_settings,
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Uv,
|
||||
)
|
||||
.expect("Expected no errors when formatting with uv")
|
||||
.expect("Expected formatting changes");
|
||||
|
||||
// Should have formatting changes (spaces to tabs)
|
||||
assert_snapshot!(result, @r#"
|
||||
def hello():
|
||||
if True:
|
||||
print("Hello")
|
||||
if False:
|
||||
print("World")
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uv_format_syntax_error() {
|
||||
let document = TextDocument::new(
|
||||
r#"
|
||||
def broken(:
|
||||
pass
|
||||
"#
|
||||
.to_string(),
|
||||
0,
|
||||
);
|
||||
|
||||
// uv should return None for syntax errors (as indicated by the TODO comment)
|
||||
let result = format(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&FormatterSettings::default(),
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Uv,
|
||||
)
|
||||
.expect("Expected no errors from format function");
|
||||
|
||||
// Should return None since the syntax is invalid
|
||||
assert_eq!(result, None, "Expected None for syntax error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uv_format_with_quote_style() {
|
||||
use ruff_python_formatter::QuoteStyle;
|
||||
|
||||
let document = TextDocument::new(
|
||||
r#"
|
||||
x = "hello"
|
||||
y = 'world'
|
||||
z = '''multi
|
||||
line'''
|
||||
"#
|
||||
.to_string(),
|
||||
0,
|
||||
);
|
||||
|
||||
// Test with single quotes
|
||||
let formatter_settings = FormatterSettings {
|
||||
quote_style: QuoteStyle::Single,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = format(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&formatter_settings,
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Uv,
|
||||
)
|
||||
.expect("Expected no errors when formatting with uv")
|
||||
.expect("Expected formatting changes");
|
||||
|
||||
assert_snapshot!(result, @r#"
|
||||
x = 'hello'
|
||||
y = 'world'
|
||||
z = """multi
|
||||
line"""
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uv_format_with_magic_trailing_comma() {
|
||||
use ruff_python_formatter::MagicTrailingComma;
|
||||
|
||||
let document = TextDocument::new(
|
||||
r#"
|
||||
foo = [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
]
|
||||
|
||||
bar = [1, 2, 3,]
|
||||
"#
|
||||
.to_string(),
|
||||
0,
|
||||
);
|
||||
|
||||
// Test with ignore magic trailing comma
|
||||
let formatter_settings = FormatterSettings {
|
||||
magic_trailing_comma: MagicTrailingComma::Ignore,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = format(
|
||||
&document,
|
||||
PySourceType::Python,
|
||||
&formatter_settings,
|
||||
Path::new("test.py"),
|
||||
FormatBackend::Uv,
|
||||
)
|
||||
.expect("Expected no errors when formatting with uv")
|
||||
.expect("Expected formatting changes");
|
||||
|
||||
assert_snapshot!(result, @r#"
|
||||
foo = [1, 2, 3]
|
||||
|
||||
bar = [1, 2, 3]
|
||||
"#);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,6 @@ impl super::BackgroundDocumentRequestHandler for Format {
|
||||
pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes> {
|
||||
let mut fixes = Fixes::default();
|
||||
let query = snapshot.query();
|
||||
let backend = snapshot
|
||||
.client_settings()
|
||||
.editor_settings()
|
||||
.format_backend();
|
||||
|
||||
match snapshot.query() {
|
||||
DocumentQuery::Notebook { notebook, .. } => {
|
||||
@@ -45,7 +41,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes>
|
||||
.map(|url| (url.clone(), notebook.cell_document_by_uri(url).unwrap()))
|
||||
{
|
||||
if let Some(changes) =
|
||||
format_text_document(text_document, query, snapshot.encoding(), true, backend)?
|
||||
format_text_document(text_document, query, snapshot.encoding(), true)?
|
||||
{
|
||||
fixes.insert(url, changes);
|
||||
}
|
||||
@@ -53,7 +49,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes>
|
||||
}
|
||||
DocumentQuery::Text { document, .. } => {
|
||||
if let Some(changes) =
|
||||
format_text_document(document, query, snapshot.encoding(), false, backend)?
|
||||
format_text_document(document, query, snapshot.encoding(), false)?
|
||||
{
|
||||
fixes.insert(snapshot.query().make_key().into_url(), changes);
|
||||
}
|
||||
@@ -72,16 +68,11 @@ pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result<super::Form
|
||||
.context("Failed to get text document for the format request")
|
||||
.unwrap();
|
||||
let query = snapshot.query();
|
||||
let backend = snapshot
|
||||
.client_settings()
|
||||
.editor_settings()
|
||||
.format_backend();
|
||||
format_text_document(
|
||||
text_document,
|
||||
query,
|
||||
snapshot.encoding(),
|
||||
query.as_notebook().is_some(),
|
||||
backend,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,7 +81,6 @@ fn format_text_document(
|
||||
query: &DocumentQuery,
|
||||
encoding: PositionEncoding,
|
||||
is_notebook: bool,
|
||||
backend: crate::format::FormatBackend,
|
||||
) -> Result<super::FormatResponse> {
|
||||
let settings = query.settings();
|
||||
let file_path = query.virtual_file_path();
|
||||
@@ -111,7 +101,6 @@ fn format_text_document(
|
||||
query.source_type(),
|
||||
&settings.formatter,
|
||||
&file_path,
|
||||
backend,
|
||||
)
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
let Some(mut formatted) = formatted else {
|
||||
|
||||
@@ -36,11 +36,7 @@ fn format_document_range(
|
||||
.context("Failed to get text document for the format range request")
|
||||
.unwrap();
|
||||
let query = snapshot.query();
|
||||
let backend = snapshot
|
||||
.client_settings()
|
||||
.editor_settings()
|
||||
.format_backend();
|
||||
format_text_document_range(text_document, range, query, snapshot.encoding(), backend)
|
||||
format_text_document_range(text_document, range, query, snapshot.encoding())
|
||||
}
|
||||
|
||||
/// Formats the specified [`Range`] in the [`TextDocument`].
|
||||
@@ -49,7 +45,6 @@ fn format_text_document_range(
|
||||
range: Range,
|
||||
query: &DocumentQuery,
|
||||
encoding: PositionEncoding,
|
||||
backend: crate::format::FormatBackend,
|
||||
) -> Result<super::FormatResponse> {
|
||||
let settings = query.settings();
|
||||
let file_path = query.virtual_file_path();
|
||||
@@ -73,7 +68,6 @@ fn format_text_document_range(
|
||||
&settings.formatter,
|
||||
range,
|
||||
&file_path,
|
||||
backend,
|
||||
)
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user