Compare commits

..

2 Commits

Author SHA1 Message Date
Brent Westbrook
77c51f62f4 Stabilize in-empty-collection (RUF060)
Tests look good, just two very small comma changes to the docs
2025-09-04 09:32:41 -04:00
Brent Westbrook
43942c7617 TODO drop this empty commit 2025-09-04 09:12:30 -04:00
196 changed files with 3614 additions and 5916 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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:

View File

@@ -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-*

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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
View File

@@ -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]]

View File

@@ -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"

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.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

View File

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

View File

@@ -1,4 +1,5 @@
use std::io::Write;
use std::process::ExitCode;
use clap::Parser;

View File

@@ -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 -----

View File

@@ -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 = "\

View File

@@ -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`

View File

@@ -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"

View File

@@ -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(),
))
}
}

View File

@@ -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"),
}
}
}

View File

@@ -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",
}
}
}

View File

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

View File

@@ -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()

View File

@@ -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"

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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),

View File

@@ -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,

View File

@@ -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

View File

@@ -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),

View File

@@ -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"

View File

@@ -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()
}

View File

@@ -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(),
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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(())
}
}

View File

@@ -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")
|

View File

@@ -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")
|

View File

@@ -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")
|

View File

@@ -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(())
}
}

View File

@@ -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),

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(())
}
}

View File

@@ -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) {

View File

@@ -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("__")
}

View File

@@ -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!(

View File

@@ -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)
}

View File

@@ -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💣28901ß9💣28901ß9💣289") 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💣28901ß9💣28901ß9💣2890") 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

View File

@@ -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💣28901ß9💣28901ß9💣289") 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💣28901ß9💣28901ß9💣289") as a:
96 | | with B("01ß9💣28901ß9💣28901ß9💣289") as b:
| |__________________________________________________^
97 | print("hello")
|
help: Combine `with` statements
92 | print("hello")
93 |
94 | # SIM117 (auto-fixable)
- with A("01ß9💣28901ß9💣28901ß9💣289") as a:
- with B("01ß9💣28901ß9💣28901ß9💣289") as b:
- print("hello")
95 + with A("01ß9💣28901ß9💣28901ß9💣289") as a, B("01ß9💣28901ß9💣28901ß9💣289") as b:
96 + print("hello")
97 |
98 | # SIM117 (not auto-fixable too long)
99 | with A("01ß9💣28901ß9💣28901ß9💣2890") 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💣28901ß9💣28901ß9💣2890") as a:
101 | | with B("01ß9💣28901ß9💣28901ß9💣289") 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

View File

@@ -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()
{

View File

@@ -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 {

View File

@@ -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

View File

@@ -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__{}_{}",

View File

@@ -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(...)`

View File

@@ -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
);
}
}

View File

@@ -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();

View File

@@ -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,
);

View File

@@ -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`.

View File

@@ -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()

View File

@@ -18,8 +18,8 @@ use crate::rules::pep8_naming::helpers;
/// > (Lets 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”).
/// >

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(())
}
}

View File

@@ -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 {

View File

@@ -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
|

View File

@@ -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
|

View File

@@ -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(

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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
|

View File

@@ -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
|

View File

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

View File

@@ -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(())
}
}

View File

@@ -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(_) => {}
}
}
}

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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
|

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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"),
}
}
}

View File

@@ -50,8 +50,5 @@ insta = { workspace = true }
[target.'cfg(target_vendor = "apple")'.dependencies]
libc = { workspace = true }
[features]
test-uv = []
[lints]
workspace = true

View File

@@ -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]
"#);
}
}
}

View File

@@ -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 {

View File

@@ -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