Compare commits

..

1 Commits

Author SHA1 Message Date
Douglas Creager
e4be76e812 lazy/eager bounds/etc should not cause assignability failures 2025-11-17 21:54:37 -05:00
409 changed files with 7610 additions and 223216 deletions

View File

@@ -261,15 +261,15 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with:
enable-cache: "true"
- name: ty mdtests (GitHub annotations)
@@ -284,10 +284,6 @@ jobs:
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
- name: Dogfood ty on py-fuzzer
run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
- name: Dogfood ty on the scripts directory
run: uv run --project=./scripts cargo run -p ty check --project=./scripts
- name: Dogfood ty on ty_benchmark
run: uv run --project=./scripts/ty_benchmark cargo run -p ty check --project=./scripts/ty_benchmark
# Check for broken links in the documentation.
- run: cargo doc --all --no-deps
env:
@@ -323,11 +319,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with:
enable-cache: "true"
- name: "Run tests"
@@ -356,11 +352,11 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with:
enable-cache: "true"
- name: "Run tests"
@@ -466,7 +462,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
shared-key: ruff-linux-debug
@@ -501,7 +497,7 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -536,7 +532,7 @@ jobs:
ref: ${{ github.event.pull_request.base.ref }}
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with:
python-version: ${{ env.PYTHON_VERSION }}
activate-environment: true
@@ -642,7 +638,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -701,7 +697,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -752,7 +748,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -796,7 +792,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with:
python-version: 3.13
activate-environment: true
@@ -951,13 +947,13 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with:
tool: cargo-codspeed
@@ -991,13 +987,13 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with:
tool: cargo-codspeed
@@ -1031,13 +1027,13 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with:
tool: cargo-codspeed

View File

@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"

View File

@@ -43,7 +43,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
@@ -55,7 +55,6 @@ jobs:
- name: Run mypy_primer
env:
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
CLICOLOR_FORCE: "1"
DIFF_FILE: mypy_primer.diff
run: |
cd ruff
@@ -81,7 +80,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
pattern: wheels-*

View File

@@ -60,7 +60,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive
@@ -123,7 +123,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive
@@ -174,7 +174,7 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive
@@ -250,7 +250,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
submodules: recursive

View File

@@ -77,7 +77,7 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: Sync typeshed stubs
run: |
rm -rf "ruff/${VENDORED_TYPESHED}"
@@ -131,7 +131,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -170,7 +170,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -207,12 +207,12 @@ jobs:
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
if: ${{ success() }}
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with:
tool: cargo-nextest
- name: "Install cargo insta"
if: ${{ success() }}
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
with:
tool: cargo-insta
- name: Update snapshots

View File

@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
@@ -67,7 +67,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
ecosystem-analyzer \
--repository ruff \

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
@@ -52,7 +52,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
ecosystem-analyzer \
--verbose \

View File

@@ -5,6 +5,5 @@
"rust-analyzer.check.command": "clippy",
"search.exclude": {
"**/*.snap": true
},
"ty.diagnosticMode": "openFilesOnly"
}
}

View File

@@ -1,83 +1,5 @@
# Changelog
## 0.14.7
Released on 2025-11-28.
### Preview features
- \[`flake8-bandit`\] Handle string literal bindings in suspicious-url-open-usage (`S310`) ([#21469](https://github.com/astral-sh/ruff/pull/21469))
- \[`pylint`\] Fix `PLR1708` false positives on nested functions ([#21177](https://github.com/astral-sh/ruff/pull/21177))
- \[`pylint`\] Fix suppression for empty dict without tuple key annotation (`PLE1141`) ([#21290](https://github.com/astral-sh/ruff/pull/21290))
- \[`ruff`\] Add rule `RUF066` to detect unnecessary class properties ([#21535](https://github.com/astral-sh/ruff/pull/21535))
- \[`ruff`\] Catch more dummy variable uses (`RUF052`) ([#19799](https://github.com/astral-sh/ruff/pull/19799))
### Bug fixes
- [server] Set severity for non-rule diagnostics ([#21559](https://github.com/astral-sh/ruff/pull/21559))
- \[`flake8-implicit-str-concat`\] Avoid invalid fix in (`ISC003`) ([#21517](https://github.com/astral-sh/ruff/pull/21517))
- \[`parser`\] Fix panic when parsing IPython escape command expressions ([#21480](https://github.com/astral-sh/ruff/pull/21480))
### CLI
- Show partial fixability indicator in statistics output ([#21513](https://github.com/astral-sh/ruff/pull/21513))
### Contributors
- [@mikeleppane](https://github.com/mikeleppane)
- [@senekor](https://github.com/senekor)
- [@ShaharNaveh](https://github.com/ShaharNaveh)
- [@JumboBear](https://github.com/JumboBear)
- [@prakhar1144](https://github.com/prakhar1144)
- [@tsvikas](https://github.com/tsvikas)
- [@danparizher](https://github.com/danparizher)
- [@chirizxc](https://github.com/chirizxc)
- [@AlexWaygood](https://github.com/AlexWaygood)
- [@MichaReiser](https://github.com/MichaReiser)
## 0.14.6
Released on 2025-11-21.
### Preview features
- \[`flake8-bandit`\] Support new PySNMP API paths (`S508`, `S509`) ([#21374](https://github.com/astral-sh/ruff/pull/21374))
### Bug fixes
- Adjust own-line comment placement between branches ([#21185](https://github.com/astral-sh/ruff/pull/21185))
- Avoid syntax error when formatting attribute expressions with outer parentheses, parenthesized value, and trailing comment on value ([#20418](https://github.com/astral-sh/ruff/pull/20418))
- Fix panic when formatting comments in unary expressions ([#21501](https://github.com/astral-sh/ruff/pull/21501))
- Respect `fmt: skip` for compound statements on a single line ([#20633](https://github.com/astral-sh/ruff/pull/20633))
- \[`refurb`\] Fix `FURB103` autofix ([#21454](https://github.com/astral-sh/ruff/pull/21454))
- \[`ruff`\] Fix false positive for complex conversion specifiers in `logging-eager-conversion` (`RUF065`) ([#21464](https://github.com/astral-sh/ruff/pull/21464))
### Rule changes
- \[`ruff`\] Avoid false positive on `ClassVar` reassignment (`RUF012`) ([#21478](https://github.com/astral-sh/ruff/pull/21478))
### CLI
- Render hyperlinks for lint errors ([#21514](https://github.com/astral-sh/ruff/pull/21514))
- Add a `ruff analyze` option to skip over imports in `TYPE_CHECKING` blocks ([#21472](https://github.com/astral-sh/ruff/pull/21472))
### Documentation
- Limit `eglot-format` hook to eglot-managed Python buffers ([#21459](https://github.com/astral-sh/ruff/pull/21459))
- Mention `force-exclude` in "Configuration > Python file discovery" ([#21500](https://github.com/astral-sh/ruff/pull/21500))
### Contributors
- [@ntBre](https://github.com/ntBre)
- [@dylwil3](https://github.com/dylwil3)
- [@gauthsvenkat](https://github.com/gauthsvenkat)
- [@MichaReiser](https://github.com/MichaReiser)
- [@thamer](https://github.com/thamer)
- [@Ruchir28](https://github.com/Ruchir28)
- [@thejcannon](https://github.com/thejcannon)
- [@danparizher](https://github.com/danparizher)
- [@chirizxc](https://github.com/chirizxc)
## 0.14.5
Released on 2025-11-13.

60
Cargo.lock generated
View File

@@ -442,9 +442,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.53"
version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
dependencies = [
"clap_builder",
"clap_derive",
@@ -452,9 +452,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.53"
version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
dependencies = [
"anstream",
"anstyle",
@@ -642,7 +642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -651,7 +651,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1016,7 +1016,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -1255,8 +1255,7 @@ checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af"
dependencies = [
"compact_str",
"get-size-derive2",
"hashbrown 0.16.1",
"indexmap",
"hashbrown 0.16.0",
"smallvec",
]
@@ -1353,9 +1352,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.16.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
dependencies = [
"equivalent",
]
@@ -1564,12 +1563,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.12.1"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"hashbrown 0.16.0",
"serde",
"serde_core",
]
@@ -1699,7 +1698,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2859,7 +2858,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.7"
version = "0.14.5"
dependencies = [
"anyhow",
"argfile",
@@ -3005,7 +3004,6 @@ dependencies = [
"serde",
"serde_json",
"similar",
"supports-hyperlinks",
"tempfile",
"thiserror 2.0.17",
"tracing",
@@ -3117,7 +3115,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.7"
version = "0.14.5"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3127,7 +3125,7 @@ dependencies = [
"fern",
"glob",
"globset",
"hashbrown 0.16.1",
"hashbrown 0.16.0",
"imperative",
"insta",
"is-macro",
@@ -3472,7 +3470,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.7"
version = "0.14.5"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3588,7 +3586,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
dependencies = [
"boxcar",
"compact_str",
@@ -3612,12 +3610,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
dependencies = [
"proc-macro2",
"quote",
@@ -3927,17 +3925,11 @@ dependencies = [
"syn",
]
[[package]]
name = "supports-hyperlinks"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
[[package]]
name = "syn"
version = "2.0.111"
version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
@@ -4462,7 +4454,7 @@ dependencies = [
"drop_bomb",
"get-size2",
"glob",
"hashbrown 0.16.1",
"hashbrown 0.16.0",
"indexmap",
"indoc",
"insta",
@@ -4474,7 +4466,6 @@ dependencies = [
"quickcheck_macros",
"ruff_annotate_snippets",
"ruff_db",
"ruff_diagnostics",
"ruff_index",
"ruff_macros",
"ruff_memory_usage",
@@ -4520,7 +4511,6 @@ dependencies = [
"lsp-types",
"regex",
"ruff_db",
"ruff_diagnostics",
"ruff_macros",
"ruff_notebook",
"ruff_python_ast",
@@ -4561,7 +4551,6 @@ dependencies = [
"path-slash",
"regex",
"ruff_db",
"ruff_diagnostics",
"ruff_index",
"ruff_notebook",
"ruff_python_ast",
@@ -4603,7 +4592,6 @@ dependencies = [
"js-sys",
"log",
"ruff_db",
"ruff_diagnostics",
"ruff_notebook",
"ruff_python_formatter",
"ruff_source_file",

View File

@@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "17bc55d699565e5a1cb1bd42363b905af2f9f3e7", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a885bb4c4c192741b8a17418fef81a71e33d111e", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
@@ -173,7 +173,6 @@ snapbox = { version = "0.6.0", features = [
static_assertions = "1.1.0"
strum = { version = "0.27.0", features = ["strum_macros"] }
strum_macros = { version = "0.27.0" }
supports-hyperlinks = { version = "3.1.0" }
syn = { version = "2.0.55" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }

View File

@@ -147,8 +147,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.14.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.7/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.5/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -181,7 +181,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.14.7
rev: v0.14.5
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

@@ -34,21 +34,9 @@ struct ExpandedStatistics<'a> {
code: Option<&'a SecondaryCode>,
name: &'static str,
count: usize,
#[serde(rename = "fixable")]
all_fixable: bool,
fixable_count: usize,
fixable: bool,
}
impl ExpandedStatistics<'_> {
fn any_fixable(&self) -> bool {
self.fixable_count > 0
}
}
/// Accumulator type for grouping diagnostics by code.
/// Format: (`code`, `representative_diagnostic`, `total_count`, `fixable_count`)
type DiagnosticGroup<'a> = (Option<&'a SecondaryCode>, &'a Diagnostic, usize, usize);
pub(crate) struct Printer {
format: OutputFormat,
log_level: LogLevel,
@@ -145,7 +133,7 @@ impl Printer {
if fixables.applicable > 0 {
writeln!(
writer,
"{fix_prefix} {} fixable with the `--fix` option.",
"{fix_prefix} {} fixable with the --fix option.",
fixables.applicable
)?;
}
@@ -268,41 +256,35 @@ impl Printer {
diagnostics: &Diagnostics,
writer: &mut dyn Write,
) -> Result<()> {
let required_applicability = self.unsafe_fixes.required_applicability();
let statistics: Vec<ExpandedStatistics> = diagnostics
.inner
.iter()
.sorted_by_key(|diagnostic| diagnostic.secondary_code())
.fold(vec![], |mut acc: Vec<DiagnosticGroup>, diagnostic| {
let is_fixable = diagnostic
.fix()
.is_some_and(|fix| fix.applies(required_applicability));
let code = diagnostic.secondary_code();
if let Some((prev_code, _prev_message, count, fixable_count)) = acc.last_mut() {
if *prev_code == code {
*count += 1;
if is_fixable {
*fixable_count += 1;
.map(|message| (message.secondary_code(), message))
.sorted_by_key(|(code, message)| (*code, message.fixable()))
.fold(
vec![],
|mut acc: Vec<((Option<&SecondaryCode>, &Diagnostic), usize)>, (code, message)| {
if let Some(((prev_code, _prev_message), count)) = acc.last_mut() {
if *prev_code == code {
*count += 1;
return acc;
}
return acc;
}
}
acc.push((code, diagnostic, 1, usize::from(is_fixable)));
acc
})
.iter()
.map(
|&(code, message, count, fixable_count)| ExpandedStatistics {
code,
name: message.name(),
count,
// Backward compatibility: `fixable` is true only when all violations are fixable.
// See: https://github.com/astral-sh/ruff/pull/21513
all_fixable: fixable_count == count,
fixable_count,
acc.push(((code, message), 1));
acc
},
)
.iter()
.map(|&((code, message), count)| ExpandedStatistics {
code,
name: message.name(),
count,
fixable: if let Some(fix) = message.fix() {
fix.applies(self.unsafe_fixes.required_applicability())
} else {
false
},
})
.sorted_by_key(|statistic| Reverse(statistic.count))
.collect();
@@ -326,14 +308,13 @@ impl Printer {
.map(|statistic| statistic.code.map_or(0, |s| s.len()))
.max()
.unwrap();
let any_fixable = statistics.iter().any(ExpandedStatistics::any_fixable);
let any_fixable = statistics.iter().any(|statistic| statistic.fixable);
let all_fixable = format!("[{}] ", "*".cyan());
let partially_fixable = format!("[{}] ", "-".cyan());
let fixable = format!("[{}] ", "*".cyan());
let unfixable = "[ ] ";
// By default, we mimic Flake8's `--statistics` format.
for statistic in &statistics {
for statistic in statistics {
writeln!(
writer,
"{:>count_width$}\t{:<code_width$}\t{}{}",
@@ -345,10 +326,8 @@ impl Printer {
.red()
.bold(),
if any_fixable {
if statistic.all_fixable {
&all_fixable
} else if statistic.any_fixable() {
&partially_fixable
if statistic.fixable {
&fixable
} else {
unfixable
}

View File

@@ -1043,7 +1043,7 @@ def mvce(keys, values):
----- stdout -----
1 C416 [*] unnecessary-comprehension
Found 1 error.
[*] 1 fixable with the `--fix` option.
[*] 1 fixable with the --fix option.
----- stderr -----
");
@@ -1073,8 +1073,7 @@ def mvce(keys, values):
"code": "C416",
"name": "unnecessary-comprehension",
"count": 1,
"fixable": false,
"fixable_count": 0
"fixable": false
}
]
@@ -1107,8 +1106,7 @@ def mvce(keys, values):
"code": "C416",
"name": "unnecessary-comprehension",
"count": 1,
"fixable": true,
"fixable_count": 1
"fixable": true
}
]
@@ -1116,54 +1114,6 @@ def mvce(keys, values):
"#);
}
#[test]
fn show_statistics_json_partial_fix() {
let mut cmd = RuffCheck::default()
.args([
"--select",
"UP035",
"--statistics",
"--output-format",
"json",
])
.build();
assert_cmd_snapshot!(cmd
.pass_stdin("from typing import List, AsyncGenerator"), @r#"
success: false
exit_code: 1
----- stdout -----
[
{
"code": "UP035",
"name": "deprecated-import",
"count": 2,
"fixable": false,
"fixable_count": 1
}
]
----- stderr -----
"#);
}
#[test]
fn show_statistics_partial_fix() {
let mut cmd = RuffCheck::default()
.args(["--select", "UP035", "--statistics"])
.build();
assert_cmd_snapshot!(cmd
.pass_stdin("from typing import List, AsyncGenerator"), @r"
success: false
exit_code: 1
----- stdout -----
2 UP035 [-] deprecated-import
Found 2 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----
");
}
#[test]
fn show_statistics_syntax_errors() {
let mut cmd = RuffCheck::default()
@@ -1860,7 +1810,7 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
--> -:1:1
Found 2 errors.
[*] 1 fixable with the `--fix` option.
[*] 1 fixable with the --fix option.
----- stderr -----
");
@@ -1903,7 +1853,7 @@ fn check_shows_unsafe_fixes_with_opt_in() {
--> -:1:1
Found 2 errors.
[*] 2 fixable with the `--fix` option.
[*] 2 fixable with the --fix option.
----- stderr -----
");

View File

@@ -31,7 +31,7 @@
//! styling.
//!
//! The above snippet has been built out of the following structure:
use crate::{Id, snippet};
use crate::snippet;
use std::cmp::{Reverse, max, min};
use std::collections::HashMap;
use std::fmt::Display;
@@ -189,7 +189,6 @@ impl DisplaySet<'_> {
}
Ok(())
}
fn format_annotation(
&self,
line_offset: usize,
@@ -200,13 +199,11 @@ impl DisplaySet<'_> {
) -> fmt::Result {
let hide_severity = annotation.annotation_type.is_none();
let color = get_annotation_style(&annotation.annotation_type, stylesheet);
let formatted_len = if let Some(id) = &annotation.id {
let id_len = id.id.len();
if hide_severity {
id_len
id.len()
} else {
2 + id_len + annotation_type_len(&annotation.annotation_type)
2 + id.len() + annotation_type_len(&annotation.annotation_type)
}
} else {
annotation_type_len(&annotation.annotation_type)
@@ -259,20 +256,9 @@ impl DisplaySet<'_> {
let annotation_type = annotation_type_str(&annotation.annotation_type);
if let Some(id) = annotation.id {
if hide_severity {
buffer.append(
line_offset,
&format!("{id} ", id = fmt_with_hyperlink(id.id, id.url, stylesheet)),
*stylesheet.error(),
);
buffer.append(line_offset, &format!("{id} "), *stylesheet.error());
} else {
buffer.append(
line_offset,
&format!(
"{annotation_type}[{id}]",
id = fmt_with_hyperlink(id.id, id.url, stylesheet)
),
*color,
);
buffer.append(line_offset, &format!("{annotation_type}[{id}]"), *color);
}
} else {
buffer.append(line_offset, annotation_type, *color);
@@ -721,7 +707,7 @@ impl DisplaySet<'_> {
let style =
get_annotation_style(&annotation.annotation_type, stylesheet);
let mut formatted_len = if let Some(id) = &annotation.annotation.id {
2 + id.id.len()
2 + id.len()
+ annotation_type_len(&annotation.annotation.annotation_type)
} else {
annotation_type_len(&annotation.annotation.annotation_type)
@@ -738,10 +724,7 @@ impl DisplaySet<'_> {
} else if formatted_len != 0 {
formatted_len += 2;
let id = match &annotation.annotation.id {
Some(id) => format!(
"[{id}]",
id = fmt_with_hyperlink(&id.id, id.url, stylesheet)
),
Some(id) => format!("[{id}]"),
None => String::new(),
};
buffer.puts(
@@ -844,7 +827,7 @@ impl DisplaySet<'_> {
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Annotation<'a> {
pub(crate) annotation_type: DisplayAnnotationType,
pub(crate) id: Option<Id<'a>>,
pub(crate) id: Option<&'a str>,
pub(crate) label: Vec<DisplayTextFragment<'a>>,
pub(crate) is_fixable: bool,
}
@@ -1157,7 +1140,7 @@ fn format_message<'m>(
fn format_title<'a>(
level: crate::Level,
id: Option<Id<'a>>,
id: Option<&'a str>,
label: &'a str,
is_fixable: bool,
) -> DisplayLine<'a> {
@@ -1175,7 +1158,7 @@ fn format_title<'a>(
fn format_footer<'a>(
level: crate::Level,
id: Option<Id<'a>>,
id: Option<&'a str>,
label: &'a str,
) -> Vec<DisplayLine<'a>> {
let mut result = vec![];
@@ -1723,7 +1706,6 @@ fn format_body<'m>(
annotation: Annotation {
annotation_type,
id: None,
label: format_label(annotation.label, None),
is_fixable: false,
},
@@ -1905,40 +1887,3 @@ fn char_width(c: char) -> Option<usize> {
unicode_width::UnicodeWidthChar::width(c)
}
}
pub(super) fn fmt_with_hyperlink<'a, T>(
content: T,
url: Option<&'a str>,
stylesheet: &Stylesheet,
) -> impl std::fmt::Display + 'a
where
T: std::fmt::Display + 'a,
{
struct FmtHyperlink<'a, T> {
content: T,
url: Option<&'a str>,
}
impl<T> std::fmt::Display for FmtHyperlink<'_, T>
where
T: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(url) = self.url {
write!(f, "\x1B]8;;{url}\x1B\\")?;
}
self.content.fmt(f)?;
if self.url.is_some() {
f.write_str("\x1B]8;;\x1B\\")?;
}
Ok(())
}
}
let url = if stylesheet.hyperlink { url } else { None };
FmtHyperlink { content, url }
}

View File

@@ -76,7 +76,6 @@ impl Renderer {
}
.effects(Effects::BOLD),
none: Style::new(),
hyperlink: true,
},
..Self::plain()
}
@@ -155,11 +154,6 @@ impl Renderer {
self
}
pub const fn hyperlink(mut self, hyperlink: bool) -> Self {
self.stylesheet.hyperlink = hyperlink;
self
}
/// Set the string used for when a long line is cut.
///
/// The default is `...` (three `U+002E` characters).

View File

@@ -10,7 +10,6 @@ pub(crate) struct Stylesheet {
pub(crate) line_no: Style,
pub(crate) emphasis: Style,
pub(crate) none: Style,
pub(crate) hyperlink: bool,
}
impl Default for Stylesheet {
@@ -30,7 +29,6 @@ impl Stylesheet {
line_no: Style::new(),
emphasis: Style::new(),
none: Style::new(),
hyperlink: false,
}
}
}

View File

@@ -12,19 +12,13 @@
use std::ops::Range;
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub(crate) struct Id<'a> {
pub(crate) id: &'a str,
pub(crate) url: Option<&'a str>,
}
/// Primary structure provided for formatting
///
/// See [`Level::title`] to create a [`Message`]
#[derive(Debug)]
pub struct Message<'a> {
pub(crate) level: Level,
pub(crate) id: Option<Id<'a>>,
pub(crate) id: Option<&'a str>,
pub(crate) title: &'a str,
pub(crate) snippets: Vec<Snippet<'a>>,
pub(crate) footer: Vec<Message<'a>>,
@@ -34,12 +28,7 @@ pub struct Message<'a> {
impl<'a> Message<'a> {
pub fn id(mut self, id: &'a str) -> Self {
self.id = Some(Id { id, url: None });
self
}
pub fn id_with_url(mut self, id: &'a str, url: Option<&'a str>) -> Self {
self.id = Some(Id { id, url });
self.id = Some(id);
self
}

View File

@@ -59,6 +59,8 @@ divan = { workspace = true, optional = true }
anyhow = { workspace = true }
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true }
criterion = { workspace = true, default-features = false, optional = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
@@ -86,7 +88,3 @@ mimalloc = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dev-dependencies]
tikv-jemallocator = { workspace = true }
[dev-dependencies]
rustc-hash = { workspace = true }
rayon = { workspace = true }

View File

@@ -667,7 +667,7 @@ fn attrs(criterion: &mut Criterion) {
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
120,
110,
);
bench_project(&benchmark, criterion);

View File

@@ -120,7 +120,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY310,
},
1070,
600,
);
static FREQTRADE: Benchmark = Benchmark::new(
@@ -143,7 +143,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
600,
525,
);
static PANDAS: Benchmark = Benchmark::new(
@@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
4000,
3000,
);
static PYDANTIC: Benchmark = Benchmark::new(
@@ -181,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY39,
},
7000,
5000,
);
static SYMPY: Benchmark = Benchmark::new(
@@ -223,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
950,
900,
);
#[track_caller]

View File

@@ -42,7 +42,6 @@ schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
similar = { workspace = true }
supports-hyperlinks = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, optional = true }

View File

@@ -64,8 +64,6 @@ impl Diagnostic {
id,
severity,
message: message.into_diagnostic_message(),
custom_concise_message: None,
documentation_url: None,
annotations: vec![],
subs: vec![],
fix: None,
@@ -215,10 +213,6 @@ impl Diagnostic {
/// cases, just converting it to a string (or printing it) will do what
/// you want.
pub fn concise_message(&self) -> ConciseMessage<'_> {
if let Some(custom_message) = &self.inner.custom_concise_message {
return ConciseMessage::Custom(custom_message.as_str());
}
let main = self.inner.message.as_str();
let annotation = self
.primary_annotation()
@@ -232,15 +226,6 @@ impl Diagnostic {
}
}
/// Set a custom message for the concise formatting of this diagnostic.
///
/// This overrides the default behavior of generating a concise message
/// from the main diagnostic message and the primary annotation.
pub fn set_concise_message(&mut self, message: impl IntoDiagnosticMessage) {
Arc::make_mut(&mut self.inner).custom_concise_message =
Some(message.into_diagnostic_message());
}
/// Returns the severity of this diagnostic.
///
/// Note that this may be different than the severity of sub-diagnostics.
@@ -371,14 +356,6 @@ impl Diagnostic {
.is_some_and(|fix| fix.applies(config.fix_applicability))
}
pub fn documentation_url(&self) -> Option<&str> {
self.inner.documentation_url.as_deref()
}
pub fn set_documentation_url(&mut self, url: Option<String>) {
Arc::make_mut(&mut self.inner).documentation_url = url;
}
/// Returns the offset of the parent statement for this diagnostic if it exists.
///
/// This is primarily used for checking noqa/secondary code suppressions.
@@ -452,6 +429,28 @@ impl Diagnostic {
.map(|sub| sub.inner.message.as_str())
}
/// Returns the URL for the rule documentation, if it exists.
pub fn to_ruff_url(&self) -> Option<String> {
match self.id() {
DiagnosticId::Panic
| DiagnosticId::Io
| DiagnosticId::InvalidSyntax
| DiagnosticId::RevealedType
| DiagnosticId::UnknownRule
| DiagnosticId::InvalidGlob
| DiagnosticId::EmptyInclude
| DiagnosticId::UnnecessaryOverridesSection
| DiagnosticId::UselessOverridesSection
| DiagnosticId::DeprecatedSetting
| DiagnosticId::Unformatted
| DiagnosticId::InvalidCliOption
| DiagnosticId::InternalError => None,
DiagnosticId::Lint(lint_name) => {
Some(format!("{}/rules/{lint_name}", env!("CARGO_PKG_HOMEPAGE")))
}
}
}
/// Returns the filename for the message.
///
/// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`.
@@ -531,10 +530,8 @@ impl Diagnostic {
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
struct DiagnosticInner {
id: DiagnosticId,
documentation_url: Option<String>,
severity: Severity,
message: DiagnosticMessage,
custom_concise_message: Option<DiagnosticMessage>,
annotations: Vec<Annotation>,
subs: Vec<SubDiagnostic>,
fix: Option<Fix>,
@@ -1523,8 +1520,6 @@ pub enum ConciseMessage<'a> {
/// This indicates that the diagnostic is probably using the old
/// model.
Empty,
/// A custom concise message has been provided.
Custom(&'a str),
}
impl std::fmt::Display for ConciseMessage<'_> {
@@ -1540,9 +1535,6 @@ impl std::fmt::Display for ConciseMessage<'_> {
write!(f, "{main}: {annotation}")
}
ConciseMessage::Empty => Ok(()),
ConciseMessage::Custom(message) => {
write!(f, "{message}")
}
}
}
}

View File

@@ -205,7 +205,6 @@ impl<'a> Resolved<'a> {
struct ResolvedDiagnostic<'a> {
level: AnnotateLevel,
id: Option<String>,
documentation_url: Option<String>,
message: String,
annotations: Vec<ResolvedAnnotation<'a>>,
is_fixable: bool,
@@ -241,12 +240,12 @@ impl<'a> ResolvedDiagnostic<'a> {
// `DisplaySet::format_annotation` for both cases, but this is a small hack to improve
// the formatting of syntax errors for now. This should also be kept consistent with the
// concise formatting.
diag.secondary_code().map_or_else(
Some(diag.secondary_code().map_or_else(
|| format!("{id}:", id = diag.inner.id),
|code| code.to_string(),
)
))
} else {
diag.inner.id.to_string()
Some(diag.inner.id.to_string())
};
let level = if config.hide_severity {
@@ -257,8 +256,7 @@ impl<'a> ResolvedDiagnostic<'a> {
ResolvedDiagnostic {
level,
id: Some(id),
documentation_url: diag.documentation_url().map(ToString::to_string),
id,
message: diag.inner.message.as_str().to_string(),
annotations,
is_fixable: config.show_fix_status && diag.has_applicable_fix(config),
@@ -289,7 +287,6 @@ impl<'a> ResolvedDiagnostic<'a> {
ResolvedDiagnostic {
level: diag.inner.severity.to_annotate(),
id: None,
documentation_url: None,
message: diag.inner.message.as_str().to_string(),
annotations,
is_fixable: false,
@@ -388,7 +385,6 @@ impl<'a> ResolvedDiagnostic<'a> {
RenderableDiagnostic {
level: self.level,
id: self.id.as_deref(),
documentation_url: self.documentation_url.as_deref(),
message: &self.message,
snippets_by_input,
is_fixable: self.is_fixable,
@@ -489,7 +485,6 @@ struct RenderableDiagnostic<'r> {
/// An ID is always present for top-level diagnostics and always absent for
/// sub-diagnostics.
id: Option<&'r str>,
documentation_url: Option<&'r str>,
/// The message emitted with the diagnostic, before any snippets are
/// rendered.
message: &'r str,
@@ -524,7 +519,7 @@ impl RenderableDiagnostic<'_> {
.is_fixable(self.is_fixable)
.lineno_offset(self.header_offset);
if let Some(id) = self.id {
message = message.id_with_url(id, self.documentation_url);
message = message.id(id);
}
message.snippets(snippets)
}
@@ -2881,12 +2876,6 @@ watermelon
self.diag.help(message);
self
}
/// Set the documentation URL for the diagnostic.
pub(super) fn documentation_url(mut self, url: impl Into<String>) -> DiagnosticBuilder<'e> {
self.diag.set_documentation_url(Some(url.into()));
self
}
}
/// A helper builder for tersely populating a `SubDiagnostic`.
@@ -3001,7 +2990,6 @@ def fibonacci(n):
TextSize::from(10),
))))
.noqa_offset(TextSize::from(7))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-variable",
@@ -3016,13 +3004,11 @@ def fibonacci(n):
TextSize::from(99),
)))
.noqa_offset(TextSize::from(94))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
.build(),
env.builder("undefined-name", Severity::Error, "Undefined name `a`")
.primary("undef.py", "1:3", "1:4", "")
.secondary_code("F821")
.noqa_offset(TextSize::from(3))
.documentation_url("https://docs.astral.sh/ruff/rules/undefined-name")
.build(),
];
@@ -3137,7 +3123,6 @@ if call(foo
TextSize::from(19),
))))
.noqa_offset(TextSize::from(16))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-import",
@@ -3152,7 +3137,6 @@ if call(foo
TextSize::from(40),
))))
.noqa_offset(TextSize::from(35))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-variable",
@@ -3167,7 +3151,6 @@ if call(foo
TextSize::from(104),
))))
.noqa_offset(TextSize::from(98))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
.build(),
];

View File

@@ -1,6 +1,6 @@
use crate::diagnostic::{
Diagnostic, DisplayDiagnosticConfig, Severity,
stylesheet::{DiagnosticStylesheet, fmt_styled, fmt_with_hyperlink},
stylesheet::{DiagnosticStylesheet, fmt_styled},
};
use super::FileResolver;
@@ -62,29 +62,18 @@ impl<'a> ConciseRenderer<'a> {
}
write!(f, "{sep} ")?;
}
if self.config.hide_severity {
if let Some(code) = diag.secondary_code() {
write!(
f,
"{code} ",
code = fmt_styled(
fmt_with_hyperlink(&code, diag.documentation_url(), &stylesheet),
stylesheet.secondary_code
)
code = fmt_styled(code, stylesheet.secondary_code)
)?;
} else {
write!(
f,
"{id}: ",
id = fmt_styled(
fmt_with_hyperlink(
&diag.inner.id,
diag.documentation_url(),
&stylesheet
),
stylesheet.secondary_code
)
id = fmt_styled(diag.inner.id.as_str(), stylesheet.secondary_code)
)?;
}
if self.config.show_fix_status {
@@ -104,10 +93,7 @@ impl<'a> ConciseRenderer<'a> {
f,
"{severity}[{id}] ",
severity = fmt_styled(severity, severity_style),
id = fmt_styled(
fmt_with_hyperlink(&diag.id(), diag.documentation_url(), &stylesheet),
stylesheet.emphasis
)
id = fmt_styled(diag.id(), stylesheet.emphasis)
)?;
}

View File

@@ -49,8 +49,7 @@ impl<'a> FullRenderer<'a> {
.help(stylesheet.help)
.line_no(stylesheet.line_no)
.emphasis(stylesheet.emphasis)
.none(stylesheet.none)
.hyperlink(stylesheet.hyperlink);
.none(stylesheet.none);
for diag in diagnostics {
let resolved = Resolved::new(self.resolver, diag, self.config);
@@ -704,7 +703,52 @@ print()
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
error[unused-import][*]: `math` imported but unused
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^
3 |
4 | print('hello world')
|
help: Remove unused import: `math`
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
error[unused-variable][*]: Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| ^
|
help: Remove assignment to unused variable `x`
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior
");
}
#[test]
@@ -724,7 +768,31 @@ print()
}
*fix = Fix::unsafe_edits(edits.remove(0), edits);
insta::assert_snapshot!(env.render(&diagnostic));
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior
");
}
/// Carriage return (`\r`) is a valid line-ending in Python, so we should normalize this to a

View File

@@ -100,7 +100,7 @@ pub(super) fn diagnostic_to_json<'a>(
if config.preview {
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
url: diagnostic.documentation_url(),
url: diagnostic.to_ruff_url(),
message: diagnostic.body(),
fix,
cell: notebook_cell_index,
@@ -112,7 +112,7 @@ pub(super) fn diagnostic_to_json<'a>(
} else {
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
url: diagnostic.documentation_url(),
url: diagnostic.to_ruff_url(),
message: diagnostic.body(),
fix,
cell: notebook_cell_index,
@@ -228,7 +228,7 @@ pub(crate) struct JsonDiagnostic<'a> {
location: Option<JsonLocation>,
message: &'a str,
noqa_row: Option<OneIndexed>,
url: Option<&'a str>,
url: Option<String>,
}
#[derive(Serialize)]
@@ -294,10 +294,7 @@ mod tests {
env.format(DiagnosticFormat::Json);
env.preview(false);
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
let diag = env.err().build();
insta::assert_snapshot!(
env.render(&diag),
@@ -331,10 +328,7 @@ mod tests {
env.format(DiagnosticFormat::Json);
env.preview(true);
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
let diag = env.err().build();
insta::assert_snapshot!(
env.render(&diag),

View File

@@ -82,7 +82,7 @@ fn diagnostic_to_rdjson<'a>(
value: diagnostic
.secondary_code()
.map_or_else(|| diagnostic.name(), |code| code.as_str()),
url: diagnostic.documentation_url(),
url: diagnostic.to_ruff_url(),
},
suggestions: rdjson_suggestions(
edits,
@@ -182,7 +182,7 @@ impl RdjsonRange {
#[derive(Serialize)]
struct RdjsonCode<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<&'a str>,
url: Option<String>,
value: &'a str,
}
@@ -217,10 +217,7 @@ mod tests {
env.format(DiagnosticFormat::Rdjson);
env.preview(false);
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
let diag = env.err().build();
insta::assert_snapshot!(env.render(&diag));
}
@@ -231,10 +228,7 @@ mod tests {
env.format(DiagnosticFormat::Rdjson);
env.preview(true);
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
let diag = env.err().build();
insta::assert_snapshot!(env.render(&diag));
}

View File

@@ -1,48 +0,0 @@
---
source: crates/ruff_db/src/diagnostic/render/full.rs
expression: env.render_diagnostics(&diagnostics)
---
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
error[unused-import][*]: `math` imported but unused
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^
3 |
4 | print('hello world')
|
help: Remove unused import: `math`
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
error[unused-variable][*]: Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| ^
|
help: Remove assignment to unused variable `x`
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,27 +0,0 @@
---
source: crates/ruff_db/src/diagnostic/render/full.rs
expression: env.render(&diagnostic)
---
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -31,43 +31,6 @@ where
FmtStyled { content, style }
}
pub(super) fn fmt_with_hyperlink<'a, T>(
content: T,
url: Option<&'a str>,
stylesheet: &DiagnosticStylesheet,
) -> impl std::fmt::Display + 'a
where
T: std::fmt::Display + 'a,
{
struct FmtHyperlink<'a, T> {
content: T,
url: Option<&'a str>,
}
impl<T> std::fmt::Display for FmtHyperlink<'_, T>
where
T: std::fmt::Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(url) = self.url {
write!(f, "\x1B]8;;{url}\x1B\\")?;
}
self.content.fmt(f)?;
if self.url.is_some() {
f.write_str("\x1B]8;;\x1B\\")?;
}
Ok(())
}
}
let url = if stylesheet.hyperlink { url } else { None };
FmtHyperlink { content, url }
}
#[derive(Clone, Debug)]
pub struct DiagnosticStylesheet {
pub(crate) error: Style,
@@ -84,7 +47,6 @@ pub struct DiagnosticStylesheet {
pub(crate) deletion: Style,
pub(crate) insertion_line_no: Style,
pub(crate) deletion_line_no: Style,
pub(crate) hyperlink: bool,
}
impl Default for DiagnosticStylesheet {
@@ -97,8 +59,6 @@ impl DiagnosticStylesheet {
/// Default terminal styling
pub fn styled() -> Self {
let bright_blue = AnsiColor::BrightBlue.on_default();
let hyperlink = supports_hyperlinks::supports_hyperlinks();
Self {
error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD),
warning: AnsiColor::Yellow.on_default().effects(Effects::BOLD),
@@ -114,7 +74,6 @@ impl DiagnosticStylesheet {
deletion: AnsiColor::Red.on_default(),
insertion_line_no: AnsiColor::Green.on_default().effects(Effects::BOLD),
deletion_line_no: AnsiColor::Red.on_default().effects(Effects::BOLD),
hyperlink,
}
}
@@ -134,7 +93,6 @@ impl DiagnosticStylesheet {
deletion: Style::new(),
insertion_line_no: Style::new(),
deletion_line_no: Style::new(),
hyperlink: false,
}
}
}

View File

@@ -1,5 +1,5 @@
use ruff_db::files::FilePath;
use ty_python_semantic::{ModuleName, resolve_module_old, resolve_real_module_old};
use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
use crate::ModuleDb;
use crate::collector::CollectedImport;
@@ -70,13 +70,13 @@ impl<'a> Resolver<'a> {
/// Resolves a module name to a module.
pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_module_old(self.db, module_name)?;
let module = resolve_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
/// Resolves a module name to a module (stubs not allowed).
fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_real_module_old(self.db, module_name)?;
let module = resolve_real_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
}

View File

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

View File

@@ -45,22 +45,3 @@ urllib.request.urlopen(urllib.request.Request(url))
# https://github.com/astral-sh/ruff/issues/15522
map(urllib.request.urlopen, [])
foo = urllib.request.urlopen
# https://github.com/astral-sh/ruff/issues/21462
path = "https://example.com/data.csv"
urllib.request.urlretrieve(path, "data.csv")
url = "https://example.com/api"
urllib.request.Request(url)
# Test resolved f-strings and concatenated string literals
fstring_url = f"https://example.com/data.csv"
urllib.request.urlopen(fstring_url)
urllib.request.Request(fstring_url)
concatenated_url = "https://" + "example.com/data.csv"
urllib.request.urlopen(concatenated_url)
urllib.request.Request(concatenated_url)
nested_concatenated = "http://" + "example.com" + "/data.csv"
urllib.request.urlopen(nested_concatenated)
urllib.request.Request(nested_concatenated)

View File

@@ -4,31 +4,3 @@ CommunityData("public", mpModel=0) # S508
CommunityData("public", mpModel=1) # S508
CommunityData("public", mpModel=2) # OK
# New API paths
import pysnmp.hlapi.asyncio
import pysnmp.hlapi.v1arch
import pysnmp.hlapi.v1arch.asyncio
import pysnmp.hlapi.v1arch.asyncio.auth
import pysnmp.hlapi.v3arch
import pysnmp.hlapi.v3arch.asyncio
import pysnmp.hlapi.v3arch.asyncio.auth
import pysnmp.hlapi.auth
pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v1arch.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v3arch.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.auth.CommunityData("public", mpModel=2) # OK

View File

@@ -5,19 +5,3 @@ insecure = UsmUserData("securityName") # S509
auth_no_priv = UsmUserData("securityName", "authName") # S509
less_insecure = UsmUserData("securityName", "authName", "privName") # OK
# New API paths
import pysnmp.hlapi.asyncio
import pysnmp.hlapi.v3arch.asyncio
import pysnmp.hlapi.v3arch.asyncio.auth
import pysnmp.hlapi.auth
pysnmp.hlapi.asyncio.UsmUserData("user") # S509
pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
pysnmp.hlapi.auth.UsmUserData("user") # S509
pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK
pysnmp.hlapi.v3arch.asyncio.UsmUserData("user", "authkey", "privkey") # OK
pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user", "authkey", "privkey") # OK
pysnmp.hlapi.auth.UsmUserData("user", "authkey", "privkey") # OK

View File

@@ -208,17 +208,3 @@ _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
_ = f"b {t"abc" \
t"def"} g"
# Explicit concatenation with either operand being
# a string literal that wraps across multiple lines (in parentheses)
# reports diagnostic - no autofix.
# See https://github.com/astral-sh/ruff/issues/19757
_ = "abc" + (
"def"
"ghi"
)
_ = (
"abc"
"def"
) + "ghi"

View File

@@ -30,23 +30,3 @@ for a, b in d_tuple:
pass
for a, b in d_tuple_annotated:
pass
# Empty dict cases
empty_dict = {}
empty_dict["x"] = 1
for k, v in empty_dict:
pass
empty_dict_annotated_tuple_keys: dict[tuple[int, str], bool] = {}
for k, v in empty_dict_annotated_tuple_keys:
pass
empty_dict_unannotated = {}
empty_dict_unannotated[("x", "y")] = True
for k, v in empty_dict_unannotated:
pass
empty_dict_annotated_str_keys: dict[str, int] = {}
empty_dict_annotated_str_keys["x"] = 1
for k, v in empty_dict_annotated_str_keys:
pass

View File

@@ -129,26 +129,3 @@ def generator_with_lambda():
yield 1
func = lambda x: x # Just a regular lambda
yield 2
# See: https://github.com/astral-sh/ruff/issues/21162
def foo():
def g():
yield 1
raise StopIteration # Should not trigger
def foo():
def g():
raise StopIteration # Should not trigger
yield 1
# https://github.com/astral-sh/ruff/pull/21177#pullrequestreview-3430209718
def foo():
yield 1
class C:
raise StopIteration # Should trigger
yield C
# https://github.com/astral-sh/ruff/pull/21177#discussion_r2539702728
def foo():
raise StopIteration((yield 1)) # Should trigger

View File

@@ -1,109 +0,0 @@
# Correct usage in loop and comprehension
def process_data():
return 42
def test_correct_dummy_usage():
my_list = [{"foo": 1}, {"foo": 2}]
# Should NOT detect - dummy variable is not used
[process_data() for _ in my_list] # OK: `_` is ignored by rule
# Should NOT detect - dummy variable is not used
[item["foo"] for item in my_list] # OK: not a dummy variable name
# Should NOT detect - dummy variable is not used
[42 for _unused in my_list] # OK: `_unused` is not accessed
# Regular For Loops
def test_for_loops():
my_list = [{"foo": 1}, {"foo": 2}]
# Should detect used dummy variable
for _item in my_list:
print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
# Should detect used dummy variable
for _index, _value in enumerate(my_list):
result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
# List Comprehensions
def test_list_comprehensions():
my_list = [{"foo": 1}, {"foo": 2}]
# Should detect used dummy variable
result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
# Should detect used dummy variable in nested comprehension
nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
# RUF052: Both `_item` and `_sublist` are accessed
# Should detect with conditions
filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
# RUF052: Local dummy variable `_item` is accessed
# Dict Comprehensions
def test_dict_comprehensions():
my_list = [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
# Should detect used dummy variable
result = {_item["key"]: _item["value"] for _item in my_list}
# RUF052: Local dummy variable `_item` is accessed
# Should detect with enumerate
indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
# RUF052: Both `_index` and `_item` are accessed
# Should detect in nested dict comprehension
nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
for _outer, sublist in enumerate([my_list])}
# RUF052: `_outer`, `_inner` are accessed
# Set Comprehensions
def test_set_comprehensions():
my_list = [{"foo": 1}, {"foo": 2}, {"foo": 1}] # Note: duplicate values
# Should detect used dummy variable
unique_values = {_item["foo"] for _item in my_list}
# RUF052: Local dummy variable `_item` is accessed
# Should detect with conditions
filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
# RUF052: Local dummy variable `_item` is accessed
# Should detect with complex expression
processed = {_item["foo"] * 2 for _item in my_list}
# RUF052: Local dummy variable `_item` is accessed
# Generator Expressions
def test_generator_expressions():
my_list = [{"foo": 1}, {"foo": 2}]
# Should detect used dummy variable
gen = (_item["foo"] for _item in my_list)
# RUF052: Local dummy variable `_item` is accessed
# Should detect when passed to function
total = sum(_item["foo"] for _item in my_list)
# RUF052: Local dummy variable `_item` is accessed
# Should detect with multiple generators
pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
# RUF052: Both `_x` and `_y` are accessed
# Should detect in nested generator
nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
# RUF052: `_inner` and `_sublist` are accessed
# Complex Examples with Multiple Comprehension Types
def test_mixed_comprehensions():
data = [{"items": [1, 2, 3]}, {"items": [4, 5, 6]}]
# Should detect in mixed comprehensions
result = [
{_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
for _record in data
]
# RUF052: `_key`, `_val`, and `_record` are all accessed
# Should detect in generator passed to list constructor
gen_list = list(_item["items"][0] for _item in data)
# RUF052: Local dummy variable `_item` is accessed

View File

@@ -16,19 +16,3 @@ logging.warning("%s", str(**{"object": b"\xf0\x9f\x9a\xa8", "encoding": "utf-8"}
# str() with single keyword argument - should be flagged (equivalent to str("!"))
logging.warning("%s", str(object="!"))
# Complex conversion specifiers that make oct() and hex() necessary
# These should NOT be flagged because the behavior differs between %s and %#o/%#x
# https://github.com/astral-sh/ruff/issues/21458
# %06s with oct() - zero-pad flag with width (should NOT be flagged)
logging.warning("%06s", oct(123))
# % s with oct() - blank sign flag (should NOT be flagged)
logging.warning("% s", oct(123))
# %+s with oct() - sign char flag (should NOT be flagged)
logging.warning("%+s", oct(123))
# %.3s with hex() - precision (should NOT be flagged)
logging.warning("%.3s", hex(123))

View File

@@ -1,70 +0,0 @@
import abc
import typing
class User: # Test normal class properties
@property
def name(self): # ERROR: No return
f"{self.first_name} {self.last_name}"
@property
def age(self): # OK: Returning something
return 100
def method(self): # OK: Not a property
x = 1
@property
def nested(self): # ERROR: Property itself doesn't return
def inner():
return 0
@property
def stub(self): ... # OK: A stub; doesn't return anything
class UserMeta(metaclass=abc.ABCMeta): # Test properies inside of an ABC class
@property
@abc.abstractmethod
def abstr_prop1(self): ... # OK: Abstract methods doesn't need to return anything
@property
@abc.abstractmethod
def abstr_prop2(self): # OK: Abstract methods doesn't need to return anything
"""
A cool docstring
"""
@property
def prop1(self): # OK: Returning a value
return 1
@property
def prop2(self): # ERROR: Not returning something (even when we are inside an ABC)
50
def method(self): # OK: Not a property
x = 1
def func(): # OK: Not a property
x = 1
class Proto(typing.Protocol): # Tests for a Protocol class
@property
def prop1(self) -> int: ... # OK: A stub property
class File: # Extra tests for things like yield/yield from/raise
@property
def stream1(self): # OK: Yields something
yield
@property
def stream2(self): # OK: Yields from something
yield from self.stream1
@property
def children(self): # OK: Raises
raise ValueError("File does not have children")

View File

@@ -17,7 +17,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:8:5: F841 [
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
crates/ruff_linter/resources/test/project/project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
Found 7 errors.
[*] 7 potentially fixable with the `--fix` option.
[*] 7 potentially fixable with the --fix option.
```
Running from the project directory itself should exhibit the same behavior:
@@ -32,7 +32,7 @@ examples/docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but n
project/file.py:1:8: F401 [*] `os` imported but unused
project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
Found 7 errors.
[*] 7 potentially fixable with the `--fix` option.
[*] 7 potentially fixable with the --fix option.
```
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
@@ -43,7 +43,7 @@ files:
docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
Found 2 errors.
[*] 2 potentially fixable with the `--fix` option.
[*] 2 potentially fixable with the --fix option.
```
`--config` should force Ruff to use the specified `pyproject.toml` for all files, and resolve
@@ -61,7 +61,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:4:27: F401
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
Found 9 errors.
[*] 9 potentially fixable with the `--fix` option.
[*] 9 potentially fixable with the --fix option.
```
Running from a parent directory should "ignore" the `exclude` (hence, `concepts/file.py` gets
@@ -74,7 +74,7 @@ docs/docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
excluded/script.py:5:5: F841 [*] Local variable `x` is assigned to but never used
Found 4 errors.
[*] 4 potentially fixable with the `--fix` option.
[*] 4 potentially fixable with the --fix option.
```
Passing an excluded directory directly should report errors in the contained files:
@@ -83,7 +83,7 @@ Passing an excluded directory directly should report errors in the contained fil
∴ cargo run -p ruff -- check crates/ruff_linter/resources/test/project/examples/excluded/
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the `--fix` option.
[*] 1 potentially fixable with the --fix option.
```
Unless we `--force-exclude`:

View File

@@ -131,9 +131,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::GeneratorReturnFromIterMethod) {
flake8_pyi::rules::bad_generator_return_type(function_def, checker);
}
if checker.is_rule_enabled(Rule::StopIterationReturn) {
pylint::rules::stop_iteration_return(checker, function_def);
}
if checker.source_type.is_stub() {
if checker.is_rule_enabled(Rule::StrOrReprDefinedInStub) {
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
@@ -347,9 +344,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::InvalidArgumentName) {
pep8_naming::rules::invalid_argument_name_function(checker, function_def);
}
if checker.is_rule_enabled(Rule::PropertyWithoutReturn) {
ruff::rules::property_without_return(checker, function_def);
}
}
Stmt::Return(_) => {
if checker.is_rule_enabled(Rule::ReturnInInit) {
@@ -956,6 +950,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::MisplacedBareRaise) {
pylint::rules::misplaced_bare_raise(checker, raise);
}
if checker.is_rule_enabled(Rule::StopIterationReturn) {
pylint::rules::stop_iteration_return(checker, raise);
}
}
Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => {
if checker.is_rule_enabled(Rule::GlobalStatement) {

View File

@@ -1058,7 +1058,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "063") => rules::ruff::rules::AccessAnnotationsFromClassDict,
(Ruff, "064") => rules::ruff::rules::NonOctalPermissions,
(Ruff, "065") => rules::ruff::rules::LoggingEagerConversion,
(Ruff, "066") => rules::ruff::rules::PropertyWithoutReturn,
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,

View File

@@ -125,7 +125,6 @@ where
}
diagnostic.set_secondary_code(SecondaryCode::new(rule.noqa_code().to_string()));
diagnostic.set_documentation_url(rule.url());
diagnostic
}

View File

@@ -270,19 +270,7 @@ pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &Linter
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21374
pub(crate) const fn is_extended_snmp_api_path_detection_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21395
pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21469
pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}

View File

@@ -10,11 +10,11 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::assert_diagnostics;
use crate::registry::Rule;
use crate::settings::LinterSettings;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_diagnostics, assert_diagnostics_diff};
#[test_case(Rule::Assert, Path::new("S101.py"))]
#[test_case(Rule::BadFilePermissions, Path::new("S103.py"))]
@@ -104,27 +104,20 @@ mod tests {
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
#[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))]
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
assert_diagnostics_diff!(
snapshot,
let diagnostics = test_path(
Path::new("flake8_bandit").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Disabled,
..LinterSettings::for_rule(rule_code)
},
&LinterSettings {
preview: PreviewMode::Enabled,
..LinterSettings::for_rule(rule_code)
}
);
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}

View File

@@ -4,7 +4,6 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_extended_snmp_api_path_detection_enabled;
/// ## What it does
/// Checks for uses of SNMPv1 or SNMPv2.
@@ -48,17 +47,10 @@ pub(crate) fn snmp_insecure_version(checker: &Checker, call: &ast::ExprCall) {
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
if is_extended_snmp_api_path_detection_enabled(checker.settings()) {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", .., "CommunityData"]
)
} else {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "CommunityData"]
)
}
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "CommunityData"]
)
})
{
if let Some(keyword) = call.arguments.find_keyword("mpModel") {

View File

@@ -4,7 +4,6 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_extended_snmp_api_path_detection_enabled;
/// ## What it does
/// Checks for uses of the SNMPv3 protocol without encryption.
@@ -48,17 +47,10 @@ pub(crate) fn snmp_weak_cryptography(checker: &Checker, call: &ast::ExprCall) {
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
if is_extended_snmp_api_path_detection_enabled(checker.settings()) {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", .., "UsmUserData"]
)
} else {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "UsmUserData"]
)
}
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "UsmUserData"]
)
})
{
checker.report_diagnostic(SnmpWeakCryptography, call.func.range());

View File

@@ -4,16 +4,11 @@
use itertools::Either;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Arguments, Decorator, Expr, ExprCall, Operator};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing::find_binding_value;
use ruff_text_size::{Ranged, TextRange};
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::{
is_s310_resolve_string_literal_bindings_enabled, is_suspicious_function_reference_enabled,
};
use crate::settings::LinterSettings;
use crate::preview::is_suspicious_function_reference_enabled;
/// ## What it does
/// Checks for calls to `pickle` functions or modules that wrap them.
@@ -1021,25 +1016,6 @@ fn suspicious_function(
|| has_prefix(chars.skip_while(|c| c.is_whitespace()), "https://")
}
/// Resolves `expr` to its binding and checks if the resolved expression starts with an HTTP or HTTPS prefix.
fn expression_starts_with_http_prefix(
expr: &Expr,
semantic: &SemanticModel,
settings: &LinterSettings,
) -> bool {
let resolved_expression = if is_s310_resolve_string_literal_bindings_enabled(settings)
&& let Some(name_expr) = expr.as_name_expr()
&& let Some(binding_id) = semantic.only_binding(name_expr)
&& let Some(value) = find_binding_value(semantic.binding(binding_id), semantic)
{
value
} else {
expr
};
leading_chars(resolved_expression).is_some_and(has_http_prefix)
}
/// Return the leading characters for an expression, if it's a string literal, f-string, or
/// string concatenation.
fn leading_chars(expr: &Expr) -> Option<impl Iterator<Item = char> + Clone + '_> {
@@ -1163,19 +1139,17 @@ fn suspicious_function(
// URLOpen (`Request`)
["urllib", "request", "Request"] | ["six", "moves", "urllib", "request", "Request"] => {
if let Some(arguments) = arguments {
// If the `url` argument is a string literal (including resolved bindings), allow `http` and `https` schemes.
// If the `url` argument is a string literal or an f-string, allow `http` and `https` schemes.
if arguments.args.iter().all(|arg| !arg.is_starred_expr())
&& arguments
.keywords
.iter()
.all(|keyword| keyword.arg.is_some())
{
if let Some(url_expr) = arguments.find_argument_value("url", 0)
&& expression_starts_with_http_prefix(
url_expr,
checker.semantic(),
checker.settings(),
)
if arguments
.find_argument_value("url", 0)
.and_then(leading_chars)
.is_some_and(has_http_prefix)
{
return;
}
@@ -1212,25 +1186,19 @@ fn suspicious_function(
name.segments() == ["urllib", "request", "Request"]
})
{
if let Some(url_expr) = arguments.find_argument_value("url", 0)
&& expression_starts_with_http_prefix(
url_expr,
checker.semantic(),
checker.settings(),
)
if arguments
.find_argument_value("url", 0)
.and_then(leading_chars)
.is_some_and(has_http_prefix)
{
return;
}
}
}
// If the `url` argument is a string literal (including resolved bindings), allow `http` and `https` schemes.
// If the `url` argument is a string literal, allow `http` and `https` schemes.
Some(expr) => {
if expression_starts_with_http_prefix(
expr,
checker.semantic(),
checker.settings(),
) {
if leading_chars(expr).is_some_and(has_http_prefix) {
return;
}
}

View File

@@ -254,84 +254,3 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
42 | urllib.request.urlopen(urllib.request.Request(url))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:51:1
|
49 | # https://github.com/astral-sh/ruff/issues/21462
50 | path = "https://example.com/data.csv"
51 | urllib.request.urlretrieve(path, "data.csv")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | url = "https://example.com/api"
53 | urllib.request.Request(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:53:1
|
51 | urllib.request.urlretrieve(path, "data.csv")
52 | url = "https://example.com/api"
53 | urllib.request.Request(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
54 |
55 | # Test resolved f-strings and concatenated string literals
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:57:1
|
55 | # Test resolved f-strings and concatenated string literals
56 | fstring_url = f"https://example.com/data.csv"
57 | urllib.request.urlopen(fstring_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 | urllib.request.Request(fstring_url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:58:1
|
56 | fstring_url = f"https://example.com/data.csv"
57 | urllib.request.urlopen(fstring_url)
58 | urllib.request.Request(fstring_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 |
60 | concatenated_url = "https://" + "example.com/data.csv"
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:61:1
|
60 | concatenated_url = "https://" + "example.com/data.csv"
61 | urllib.request.urlopen(concatenated_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 | urllib.request.Request(concatenated_url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:62:1
|
60 | concatenated_url = "https://" + "example.com/data.csv"
61 | urllib.request.urlopen(concatenated_url)
62 | urllib.request.Request(concatenated_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 |
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:65:1
|
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
65 | urllib.request.urlopen(nested_concatenated)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 | urllib.request.Request(nested_concatenated)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:66:1
|
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
65 | urllib.request.urlopen(nested_concatenated)
66 | urllib.request.Request(nested_concatenated)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|

View File

@@ -1,15 +1,15 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
--> S301.py:3:1
|
1 | import pickle
2 |
3 | pickle.loads()
| ^^^^^^^^^^^^^^
|
--- Summary ---
Removed: 0
Added: 2
--- Added ---
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
--> S301.py:7:5
|
@@ -19,7 +19,6 @@ S301 `pickle` and modules that wrap it can be unsafe when used to deserialize un
8 | foo = pickle.load
|
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
--> S301.py:8:7
|

View File

@@ -1,15 +1,24 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
S307 Use of possibly insecure function; consider using `ast.literal_eval`
--> S307.py:3:7
|
1 | import os
2 |
3 | print(eval("1+1")) # S307
| ^^^^^^^^^^^
4 | print(eval("os.getcwd()")) # S307
|
--- Summary ---
Removed: 0
Added: 2
S307 Use of possibly insecure function; consider using `ast.literal_eval`
--> S307.py:4:7
|
3 | print(eval("1+1")) # S307
4 | print(eval("os.getcwd()")) # S307
| ^^^^^^^^^^^^^^^^^^^
|
--- Added ---
S307 Use of possibly insecure function; consider using `ast.literal_eval`
--> S307.py:16:5
|
@@ -19,7 +28,6 @@ S307 Use of possibly insecure function; consider using `ast.literal_eval`
17 | foo = eval
|
S307 Use of possibly insecure function; consider using `ast.literal_eval`
--> S307.py:17:7
|

View File

@@ -1,37 +1,60 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 2
Added: 4
--- Removed ---
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:16:1
|
16 | @mark_safe
| ^^^^^^^^^^
17 | def some_func():
18 | return '<script>alert("evil!")</script>'
|
--> S308.py:6:5
|
4 | def bad_func():
5 | inject = "harmful_input"
6 | mark_safe(inject)
| ^^^^^^^^^^^^^^^^^
7 | mark_safe("I will add" + inject + "to my string")
8 | mark_safe("I will add %s to my string" % inject)
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:36:1
--> S308.py:7:5
|
5 | inject = "harmful_input"
6 | mark_safe(inject)
7 | mark_safe("I will add" + inject + "to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | mark_safe("I will add %s to my string" % inject)
9 | mark_safe("I will add {} to my string".format(inject))
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:8:5
|
36 | @mark_safe
| ^^^^^^^^^^
37 | def some_func():
38 | return '<script>alert("evil!")</script>'
6 | mark_safe(inject)
7 | mark_safe("I will add" + inject + "to my string")
8 | mark_safe("I will add %s to my string" % inject)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9 | mark_safe("I will add {} to my string".format(inject))
10 | mark_safe(f"I will add {inject} to my string")
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:9:5
|
7 | mark_safe("I will add" + inject + "to my string")
8 | mark_safe("I will add %s to my string" % inject)
9 | mark_safe("I will add {} to my string".format(inject))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 | mark_safe(f"I will add {inject} to my string")
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:10:5
|
8 | mark_safe("I will add %s to my string" % inject)
9 | mark_safe("I will add {} to my string".format(inject))
10 | mark_safe(f"I will add {inject} to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 |
12 | def good_func():
|
--- Added ---
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:16:2
|
@@ -41,6 +64,59 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
18 | return '<script>alert("evil!")</script>'
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:26:5
|
24 | def bad_func():
25 | inject = "harmful_input"
26 | mark_safe(inject)
| ^^^^^^^^^^^^^^^^^
27 | mark_safe("I will add" + inject + "to my string")
28 | mark_safe("I will add %s to my string" % inject)
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:27:5
|
25 | inject = "harmful_input"
26 | mark_safe(inject)
27 | mark_safe("I will add" + inject + "to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | mark_safe("I will add %s to my string" % inject)
29 | mark_safe("I will add {} to my string".format(inject))
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:28:5
|
26 | mark_safe(inject)
27 | mark_safe("I will add" + inject + "to my string")
28 | mark_safe("I will add %s to my string" % inject)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29 | mark_safe("I will add {} to my string".format(inject))
30 | mark_safe(f"I will add {inject} to my string")
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:29:5
|
27 | mark_safe("I will add" + inject + "to my string")
28 | mark_safe("I will add %s to my string" % inject)
29 | mark_safe("I will add {} to my string".format(inject))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30 | mark_safe(f"I will add {inject} to my string")
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:30:5
|
28 | mark_safe("I will add %s to my string" % inject)
29 | mark_safe("I will add {} to my string".format(inject))
30 | mark_safe(f"I will add {inject} to my string")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31 |
32 | def good_func():
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:36:2
@@ -51,7 +127,6 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
38 | return '<script>alert("evil!")</script>'
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:42:5
|
@@ -61,7 +136,6 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
43 | foo = mark_safe
|
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
--> S308.py:43:7
|

View File

@@ -1,106 +1,260 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 8
Added: 2
--- Removed ---
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:51:1
|
49 | # https://github.com/astral-sh/ruff/issues/21462
50 | path = "https://example.com/data.csv"
51 | urllib.request.urlretrieve(path, "data.csv")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | url = "https://example.com/api"
53 | urllib.request.Request(url)
|
--> S310.py:6:1
|
4 | urllib.request.urlopen(url=f'http://www.google.com')
5 | urllib.request.urlopen(url='http://' + 'www' + '.google.com')
6 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 | urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
8 | urllib.request.urlopen('http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:53:1
--> S310.py:7:1
|
5 | urllib.request.urlopen(url='http://' + 'www' + '.google.com')
6 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
7 | urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | urllib.request.urlopen('http://www.google.com')
9 | urllib.request.urlopen(f'http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:10:1
|
51 | urllib.request.urlretrieve(path, "data.csv")
52 | url = "https://example.com/api"
53 | urllib.request.Request(url)
8 | urllib.request.urlopen('http://www.google.com')
9 | urllib.request.urlopen(f'http://www.google.com')
10 | urllib.request.urlopen('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 | urllib.request.urlopen(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:11:1
|
9 | urllib.request.urlopen(f'http://www.google.com')
10 | urllib.request.urlopen('file:///foo/bar/baz')
11 | urllib.request.urlopen(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
54 |
55 | # Test resolved f-strings and concatenated string literals
12 |
13 | urllib.request.Request(url='http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:57:1
--> S310.py:16:1
|
55 | # Test resolved f-strings and concatenated string literals
56 | fstring_url = f"https://example.com/data.csv"
57 | urllib.request.urlopen(fstring_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 | urllib.request.Request(fstring_url)
14 | urllib.request.Request(url=f'http://www.google.com')
15 | urllib.request.Request(url='http://' + 'www' + '.google.com')
16 | urllib.request.Request(url='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17 | urllib.request.Request(url=f'http://www.google.com', **kwargs)
18 | urllib.request.Request('http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:58:1
--> S310.py:17:1
|
56 | fstring_url = f"https://example.com/data.csv"
57 | urllib.request.urlopen(fstring_url)
58 | urllib.request.Request(fstring_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59 |
60 | concatenated_url = "https://" + "example.com/data.csv"
15 | urllib.request.Request(url='http://' + 'www' + '.google.com')
16 | urllib.request.Request(url='http://www.google.com', **kwargs)
17 | urllib.request.Request(url=f'http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 | urllib.request.Request('http://www.google.com')
19 | urllib.request.Request(f'http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:61:1
--> S310.py:20:1
|
60 | concatenated_url = "https://" + "example.com/data.csv"
61 | urllib.request.urlopen(concatenated_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 | urllib.request.Request(concatenated_url)
18 | urllib.request.Request('http://www.google.com')
19 | urllib.request.Request(f'http://www.google.com')
20 | urllib.request.Request('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21 | urllib.request.Request(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:62:1
--> S310.py:21:1
|
60 | concatenated_url = "https://" + "example.com/data.csv"
61 | urllib.request.urlopen(concatenated_url)
62 | urllib.request.Request(concatenated_url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 |
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
19 | urllib.request.Request(f'http://www.google.com')
20 | urllib.request.Request('file:///foo/bar/baz')
21 | urllib.request.Request(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
22 |
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:65:1
--> S310.py:23:1
|
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
65 | urllib.request.urlopen(nested_concatenated)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 | urllib.request.Request(nested_concatenated)
21 | urllib.request.Request(url)
22 |
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:66:1
--> S310.py:24:1
|
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
65 | urllib.request.urlopen(nested_concatenated)
66 | urllib.request.Request(nested_concatenated)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:25:1
|
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:26:1
|
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
28 | urllib.request.URLopener().open('http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:27:1
|
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
28 | urllib.request.URLopener().open('http://www.google.com')
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:28:1
|
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
28 | urllib.request.URLopener().open('http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
29 | urllib.request.URLopener().open(f'http://www.google.com')
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:29:1
|
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
28 | urllib.request.URLopener().open('http://www.google.com')
29 | urllib.request.URLopener().open(f'http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:30:1
|
28 | urllib.request.URLopener().open('http://www.google.com')
29 | urllib.request.URLopener().open(f'http://www.google.com')
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
32 | urllib.request.URLopener().open(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:31:1
|
29 | urllib.request.URLopener().open(f'http://www.google.com')
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | urllib.request.URLopener().open(url)
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:32:1
|
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
32 | urllib.request.URLopener().open(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
33 |
34 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:37:1
|
35 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'))
36 | urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
37 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:38:1
|
36 | urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
37 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
38 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:41:1
|
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42 | urllib.request.urlopen(urllib.request.Request(url))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:41:24
|
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42 | urllib.request.urlopen(urllib.request.Request(url))
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:42:1
|
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
42 | urllib.request.urlopen(urllib.request.Request(url))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:42:24
|
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
42 | urllib.request.urlopen(urllib.request.Request(url))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
--- Added ---
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:46:5
|
@@ -110,7 +264,6 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
47 | foo = urllib.request.urlopen
|
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
--> S310.py:47:7
|
@@ -118,6 +271,4 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
46 | map(urllib.request.urlopen, [])
47 | foo = urllib.request.urlopen
| ^^^^^^^^^^^^^^^^^^^^^^
48 |
49 | # https://github.com/astral-sh/ruff/issues/21462
|

View File

@@ -1,15 +1,103 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:10:1
|
9 | # Errors
10 | random.Random()
| ^^^^^^^^^^^^^^^
11 | random.random()
12 | random.randrange()
|
--- Summary ---
Removed: 0
Added: 2
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:11:1
|
9 | # Errors
10 | random.Random()
11 | random.random()
| ^^^^^^^^^^^^^^^
12 | random.randrange()
13 | random.randint()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:12:1
|
10 | random.Random()
11 | random.random()
12 | random.randrange()
| ^^^^^^^^^^^^^^^^^^
13 | random.randint()
14 | random.choice()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:13:1
|
11 | random.random()
12 | random.randrange()
13 | random.randint()
| ^^^^^^^^^^^^^^^^
14 | random.choice()
15 | random.choices()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:14:1
|
12 | random.randrange()
13 | random.randint()
14 | random.choice()
| ^^^^^^^^^^^^^^^
15 | random.choices()
16 | random.uniform()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:15:1
|
13 | random.randint()
14 | random.choice()
15 | random.choices()
| ^^^^^^^^^^^^^^^^
16 | random.uniform()
17 | random.triangular()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:16:1
|
14 | random.choice()
15 | random.choices()
16 | random.uniform()
| ^^^^^^^^^^^^^^^^
17 | random.triangular()
18 | random.randbytes()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:17:1
|
15 | random.choices()
16 | random.uniform()
17 | random.triangular()
| ^^^^^^^^^^^^^^^^^^^
18 | random.randbytes()
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:18:1
|
16 | random.uniform()
17 | random.triangular()
18 | random.randbytes()
| ^^^^^^^^^^^^^^^^^^
19 |
20 | # Unrelated
|
--- Added ---
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:26:5
|
@@ -19,7 +107,6 @@ S311 Standard pseudo-random generators are not suitable for cryptographic purpos
27 | foo = random.randrange
|
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
--> S311.py:27:7
|

View File

@@ -1,15 +1,15 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:3:1
|
1 | from telnetlib import Telnet
2 |
3 | Telnet("localhost", 23)
| ^^^^^^^^^^^^^^^^^^^^^^^
|
--- Summary ---
Removed: 0
Added: 3
--- Added ---
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:7:5
|
@@ -19,7 +19,6 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
8 | foo = Telnet
|
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:8:7
|
@@ -31,7 +30,6 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
10 | import telnetlib
|
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:11:5
|
@@ -41,3 +39,13 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
12 |
13 | from typing import Annotated
|
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
--> S312.py:14:24
|
13 | from typing import Annotated
14 | foo: Annotated[Telnet, telnetlib.Telnet()]
| ^^^^^^^^^^^^^^^^^^
15 |
16 | def _() -> Telnet: ...
|

View File

@@ -1,104 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 8
--- Added ---
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:18:46
|
16 | import pysnmp.hlapi.auth
17 |
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:19:58
|
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:20:53
|
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:21:45
|
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:22:58
|
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:23:53
|
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:24:45
|
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:25:43
|
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
26 |
27 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK
|

View File

@@ -1,56 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 4
--- Added ---
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:15:1
|
13 | import pysnmp.hlapi.auth
14 |
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:16:1
|
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:17:1
|
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:18:1
|
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
19 |
20 | pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK
|

View File

@@ -25,11 +25,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// keyword-only argument, to force callers to be explicit when providing
/// the argument.
///
/// This rule exempts methods decorated with [`@typing.override`][override],
/// since changing the signature of a subclass method that overrides a
/// superclass method may cause type checkers to complain about a violation of
/// the Liskov Substitution Principle.
///
/// ## Example
/// ```python
/// from math import ceil, floor
@@ -94,8 +89,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.127")]
pub(crate) struct BooleanDefaultValuePositionalArgument;

View File

@@ -28,7 +28,7 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// the argument.
///
/// Dunder methods that define operators are exempt from this rule, as are
/// setters and [`@override`][override] definitions.
/// setters and `@override` definitions.
///
/// ## Example
///
@@ -93,8 +93,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.127")]
pub(crate) struct BooleanTypeHintPositionalArgument;

View File

@@ -17,8 +17,6 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// non-obvious errors, as readers may mistake the argument for the
/// builtin and vice versa.
///
/// Function definitions decorated with [`@override`][override] or
/// [`@overload`][overload] are exempt from this rule by default.
/// Builtins can be marked as exceptions to this rule via the
/// [`lint.flake8-builtins.ignorelist`] configuration option.
///
@@ -50,9 +48,6 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// ## References
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
/// [overload]: https://docs.python.org/3/library/typing.html#typing.overload
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.48")]
pub(crate) struct BuiltinArgumentShadowing {

View File

@@ -1,12 +1,12 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_trivia::is_python_whitespace;
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::AlwaysFixableViolation;
use crate::checkers::ast::Checker;
use crate::{Edit, Fix, FixAvailability, Violation};
use crate::{Edit, Fix};
/// ## What it does
/// Checks for string literals that are explicitly concatenated (using the
@@ -36,16 +36,14 @@ use crate::{Edit, Fix, FixAvailability, Violation};
#[violation_metadata(stable_since = "v0.0.201")]
pub(crate) struct ExplicitStringConcatenation;
impl Violation for ExplicitStringConcatenation {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
impl AlwaysFixableViolation for ExplicitStringConcatenation {
#[derive_message_formats]
fn message(&self) -> String {
"Explicitly concatenated string should be implicitly concatenated".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Remove redundant '+' operator to implicitly concatenate".to_string())
fn fix_title(&self) -> String {
"Remove redundant '+' operator to implicitly concatenate".to_string()
}
}
@@ -84,27 +82,9 @@ pub(crate) fn explicit(checker: &Checker, expr: &Expr) {
.locator()
.contains_line_break(TextRange::new(left.end(), right.start()))
{
let mut diagnostic =
checker.report_diagnostic(ExplicitStringConcatenation, expr.range());
let is_parenthesized = |expr: &Expr| {
parenthesized_range(
expr.into(),
bin_op.into(),
checker.comment_ranges(),
checker.source(),
)
.is_some()
};
// If either `left` or `right` is parenthesized, generating
// a fix would be too involved. Just report the diagnostic.
// Currently, attempting `generate_fix` would result in
// an invalid code. See: #19757
if is_parenthesized(left) || is_parenthesized(right) {
return;
}
diagnostic.set_fix(generate_fix(checker, bin_op));
checker
.report_diagnostic(ExplicitStringConcatenation, expr.range())
.set_fix(generate_fix(checker, bin_op));
}
}
}

View File

@@ -357,33 +357,3 @@ help: Remove redundant '+' operator to implicitly concatenate
203 | )
204 |
205 | # nested examples with both t and f-strings
ISC003 Explicitly concatenated string should be implicitly concatenated
--> ISC.py:216:5
|
214 | # reports diagnostic - no autofix.
215 | # See https://github.com/astral-sh/ruff/issues/19757
216 | _ = "abc" + (
| _____^
217 | | "def"
218 | | "ghi"
219 | | )
| |_^
220 |
221 | _ = (
|
help: Remove redundant '+' operator to implicitly concatenate
ISC003 Explicitly concatenated string should be implicitly concatenated
--> ISC.py:221:5
|
219 | )
220 |
221 | _ = (
| _____^
222 | | "abc"
223 | | "def"
224 | | ) + "ghi"
| |_________^
|
help: Remove redundant '+' operator to implicitly concatenate

View File

@@ -89,24 +89,3 @@ ISC002 Implicitly concatenated string literals over multiple lines
209 | | t"def"} g"
| |__________^
|
ISC002 Implicitly concatenated string literals over multiple lines
--> ISC.py:217:5
|
215 | # See https://github.com/astral-sh/ruff/issues/19757
216 | _ = "abc" + (
217 | / "def"
218 | | "ghi"
| |_________^
219 | )
|
ISC002 Implicitly concatenated string literals over multiple lines
--> ISC.py:222:5
|
221 | _ = (
222 | / "abc"
223 | | "def"
| |_________^
224 | ) + "ghi"
|

View File

@@ -1,6 +1,8 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::whitespace::trailing_comment_start_offset;
use ruff_python_ast::{Expr, ExprStringLiteral, Stmt, StmtExpr};
use ruff_python_semantic::{ScopeKind, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -99,7 +101,7 @@ pub(crate) fn unnecessary_placeholder(checker: &Checker, body: &[Stmt]) {
// Ellipses are significant in protocol methods and abstract methods.
// Specifically, Pyright uses the presence of an ellipsis to indicate that
// a method is a stub, rather than a default implementation.
if checker.semantic().in_protocol_or_abstract_method() {
if in_protocol_or_abstract_method(checker.semantic()) {
return;
}
Placeholder::Ellipsis
@@ -161,3 +163,21 @@ impl std::fmt::Display for Placeholder {
}
}
}
/// Return `true` if the [`SemanticModel`] is in a `typing.Protocol` subclass or an abstract
/// method.
fn in_protocol_or_abstract_method(semantic: &SemanticModel) -> bool {
semantic.current_scopes().any(|scope| match scope.kind {
ScopeKind::Class(class_def) => class_def
.bases()
.iter()
.any(|base| semantic.match_typing_expr(map_subscript(base), "Protocol")),
ScopeKind::Function(function_def) => {
ruff_python_semantic::analyze::visibility::is_abstract(
&function_def.decorator_list,
semantic,
)
}
_ => false,
})
}

View File

@@ -60,16 +60,6 @@ impl Violation for UnusedFunctionArgument {
/// prefixed with an underscore, or some other value that adheres to the
/// [`lint.dummy-variable-rgx`] pattern.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Removing a parameter from a subclass method (or changing a parameter's
/// name) may cause type checkers to complain about a violation of the Liskov
/// Substitution Principle if it means that the method now incompatibly
/// overrides a method defined on a superclass. Explicitly decorating an
/// overriding method with `@override` signals to Ruff that the method is
/// intended to override a superclass method and that a type checker will
/// enforce that it does so; Ruff therefore knows that it should not enforce
/// rules about unused arguments on such methods.
///
/// ## Example
/// ```python
/// class Class:
@@ -86,8 +76,6 @@ impl Violation for UnusedFunctionArgument {
///
/// ## Options
/// - `lint.dummy-variable-rgx`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.168")]
pub(crate) struct UnusedMethodArgument {
@@ -113,16 +101,6 @@ impl Violation for UnusedMethodArgument {
/// prefixed with an underscore, or some other value that adheres to the
/// [`lint.dummy-variable-rgx`] pattern.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Removing a parameter from a subclass method (or changing a parameter's
/// name) may cause type checkers to complain about a violation of the Liskov
/// Substitution Principle if it means that the method now incompatibly
/// overrides a method defined on a superclass. Explicitly decorating an
/// overriding method with `@override` signals to Ruff that the method is
/// intended to override a superclass method and that a type checker will
/// enforce that it does so; Ruff therefore knows that it should not enforce
/// rules about unused arguments on such methods.
///
/// ## Example
/// ```python
/// class Class:
@@ -141,8 +119,6 @@ impl Violation for UnusedMethodArgument {
///
/// ## Options
/// - `lint.dummy-variable-rgx`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.168")]
pub(crate) struct UnusedClassMethodArgument {
@@ -168,16 +144,6 @@ impl Violation for UnusedClassMethodArgument {
/// prefixed with an underscore, or some other value that adheres to the
/// [`lint.dummy-variable-rgx`] pattern.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Removing a parameter from a subclass method (or changing a parameter's
/// name) may cause type checkers to complain about a violation of the Liskov
/// Substitution Principle if it means that the method now incompatibly
/// overrides a method defined on a superclass. Explicitly decorating an
/// overriding method with `@override` signals to Ruff that the method is
/// intended to override a superclass method, and that a type checker will
/// enforce that it does so; Ruff therefore knows that it should not enforce
/// rules about unused arguments on such methods.
///
/// ## Example
/// ```python
/// class Class:
@@ -196,8 +162,6 @@ impl Violation for UnusedClassMethodArgument {
///
/// ## Options
/// - `lint.dummy-variable-rgx`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.168")]
pub(crate) struct UnusedStaticMethodArgument {

View File

@@ -23,7 +23,7 @@ use crate::checkers::ast::Checker;
/// > mixedCase is allowed only in contexts where thats already the
/// > prevailing style (e.g. threading.py), to retain backwards compatibility.
///
/// Methods decorated with [`@typing.override`][override] are ignored.
/// Methods decorated with `@typing.override` are ignored.
///
/// ## Example
/// ```python
@@ -43,8 +43,6 @@ use crate::checkers::ast::Checker;
///
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments
/// [preview]: https://docs.astral.sh/ruff/preview/
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.77")]
pub(crate) struct InvalidArgumentName {

View File

@@ -24,11 +24,6 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
/// to ignore all functions starting with `test_` from this rule, set the
/// [`lint.pep8-naming.extend-ignore-names`] option to `["test_*"]`.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Explicitly decorating a method with `@override` signals to Ruff that the method is intended
/// to override a superclass method, and that a type checker will enforce that it does so. Ruff
/// therefore knows that it should not enforce naming conventions on such methods.
///
/// ## Example
/// ```python
/// def myFunction():
@@ -46,7 +41,6 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
/// - `lint.pep8-naming.extend-ignore-names`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-variable-names
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.77")]
pub(crate) struct InvalidFunctionName {

View File

@@ -169,12 +169,7 @@ impl Violation for UndocumentedPublicClass {
/// If the codebase adheres to a standard format for method docstrings, follow
/// that format for consistency.
///
/// This rule exempts methods decorated with [`@typing.override`][override],
/// since it is a common practice to document a method on a superclass but not
/// on an overriding method in a subclass.
///
/// ## Example
///
/// ```python
/// class Cat(Animal):
/// def greet(self, happy: bool = True):
@@ -185,7 +180,6 @@ impl Violation for UndocumentedPublicClass {
/// ```
///
/// Use instead (in the NumPy docstring format):
///
/// ```python
/// class Cat(Animal):
/// def greet(self, happy: bool = True):
@@ -208,7 +202,6 @@ impl Violation for UndocumentedPublicClass {
/// ```
///
/// Or (in the Google docstring format):
///
/// ```python
/// class Cat(Animal):
/// def greet(self, happy: bool = True):
@@ -234,8 +227,6 @@ impl Violation for UndocumentedPublicClass {
/// - [PEP 287 reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.70")]
pub(crate) struct UndocumentedPublicMethod;

View File

@@ -21,7 +21,7 @@ use crate::rules::pylint::helpers::is_known_dunder_method;
///
/// This rule will detect all methods starting and ending with at least
/// one underscore (e.g., `_str_`), but ignores known dunder methods (like
/// `__init__`), as well as methods that are marked with [`@override`][override].
/// `__init__`), as well as methods that are marked with `@override`.
///
/// Additional dunder methods names can be allowed via the
/// [`lint.pylint.allow-dunder-method-names`] setting.
@@ -42,8 +42,6 @@ use crate::rules::pylint::helpers::is_known_dunder_method;
///
/// ## Options
/// - `lint.pylint.allow-dunder-method-names`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "v0.0.285")]
pub(crate) struct BadDunderMethodName {

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_ast::{Expr, Stmt};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::analyze::typing::is_dict;
@@ -108,77 +108,15 @@ fn is_dict_key_tuple_with_two_elements(binding: &Binding, semantic: &SemanticMod
return false;
};
let (value, annotation) = match statement {
Stmt::Assign(assign_stmt) => (assign_stmt.value.as_ref(), None),
Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value),
annotation,
..
}) => (value.as_ref(), Some(annotation.as_ref())),
_ => return false,
};
let Expr::Dict(dict_expr) = value else {
let Stmt::Assign(assign_stmt) = statement else {
return false;
};
// Check if dict is empty
let is_empty = dict_expr.is_empty();
let Expr::Dict(dict_expr) = &*assign_stmt.value else {
return false;
};
if is_empty {
// For empty dicts, check type annotation
return annotation
.is_some_and(|annotation| is_annotation_dict_with_tuple_keys(annotation, semantic));
}
// For non-empty dicts, check if all keys are 2-tuples
dict_expr
.iter_keys()
.all(|key| matches!(key, Some(Expr::Tuple(tuple)) if tuple.len() == 2))
}
/// Returns true if the annotation is `dict[tuple[T1, T2], ...]` where tuple has exactly 2 elements.
fn is_annotation_dict_with_tuple_keys(annotation: &Expr, semantic: &SemanticModel) -> bool {
// Check if it's a subscript: dict[...]
let Expr::Subscript(subscript) = annotation else {
return false;
};
// Check if it's dict or typing.Dict
if !semantic.match_builtin_expr(subscript.value.as_ref(), "dict")
&& !semantic.match_typing_expr(subscript.value.as_ref(), "Dict")
{
return false;
}
// Extract the slice (should be a tuple: (key_type, value_type))
let Expr::Tuple(tuple) = subscript.slice.as_ref() else {
return false;
};
// dict[K, V] format - check if K is tuple with 2 elements
if let [key, _value] = tuple.elts.as_slice() {
return is_tuple_type_with_two_elements(key, semantic);
}
false
}
/// Returns true if the expression represents a tuple type with exactly 2 elements.
fn is_tuple_type_with_two_elements(expr: &Expr, semantic: &SemanticModel) -> bool {
// Handle tuple[...] subscript
if let Expr::Subscript(subscript) = expr {
// Check if it's tuple or typing.Tuple
if semantic.match_builtin_expr(subscript.value.as_ref(), "tuple")
|| semantic.match_typing_expr(subscript.value.as_ref(), "Tuple")
{
// Check the slice - tuple[T1, T2]
if let Expr::Tuple(tuple_slice) = subscript.slice.as_ref() {
return tuple_slice.elts.len() == 2;
}
return false;
}
}
false
}

View File

@@ -17,16 +17,6 @@ use crate::rules::flake8_unused_arguments::rules::is_not_implemented_stub_with_v
/// Unused `self` parameters are usually a sign of a method that could be
/// replaced by a function, class method, or static method.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Converting an instance method into a static method or class method may
/// cause type checkers to complain about a violation of the Liskov
/// Substitution Principle if it means that the method now incompatibly
/// overrides a method defined on a superclass. Explicitly decorating an
/// overriding method with `@override` signals to Ruff that the method is
/// intended to override a superclass method and that a type checker will
/// enforce that it does so; Ruff therefore knows that it should not enforce
/// rules about unused `self` parameters on such methods.
///
/// ## Example
/// ```python
/// class Person:
@@ -48,8 +38,6 @@ use crate::rules::flake8_unused_arguments::rules::is_not_implemented_stub_with_v
/// def greeting():
/// print("Greetings friend!")
/// ```
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "v0.0.286")]
pub(crate) struct NoSelfUse {

View File

@@ -1,9 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{
self as ast,
helpers::map_callable,
visitor::{Visitor, walk_expr, walk_stmt},
};
use ruff_python_ast as ast;
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
use ruff_text_size::Ranged;
use crate::Violation;
@@ -53,54 +50,65 @@ impl Violation for StopIterationReturn {
}
/// PLR1708
pub(crate) fn stop_iteration_return(checker: &Checker, function_def: &ast::StmtFunctionDef) {
let mut analyzer = GeneratorAnalyzer {
checker,
has_yield: false,
stop_iteration_raises: Vec::new(),
pub(crate) fn stop_iteration_return(checker: &Checker, raise_stmt: &ast::StmtRaise) {
// Fast-path: only continue if this is `raise StopIteration` (with or without args)
let Some(exc) = &raise_stmt.exc else {
return;
};
analyzer.visit_body(&function_def.body);
if analyzer.has_yield {
for raise_stmt in analyzer.stop_iteration_raises {
checker.report_diagnostic(StopIterationReturn, raise_stmt.range());
let is_stop_iteration = match exc.as_ref() {
ast::Expr::Call(ast::ExprCall { func, .. }) => {
checker.semantic().match_builtin_expr(func, "StopIteration")
}
expr => checker.semantic().match_builtin_expr(expr, "StopIteration"),
};
if !is_stop_iteration {
return;
}
// Now check the (more expensive) generator context
if !in_generator_context(checker) {
return;
}
checker.report_diagnostic(StopIterationReturn, raise_stmt.range());
}
struct GeneratorAnalyzer<'a, 'b> {
checker: &'a Checker<'b>,
has_yield: bool,
stop_iteration_raises: Vec<&'a ast::StmtRaise>,
}
impl<'a> Visitor<'a> for GeneratorAnalyzer<'a, '_> {
fn visit_stmt(&mut self, stmt: &'a ast::Stmt) {
match stmt {
ast::Stmt::FunctionDef(_) => {}
ast::Stmt::Raise(raise @ ast::StmtRaise { exc: Some(exc), .. }) => {
if self
.checker
.semantic()
.match_builtin_expr(map_callable(exc), "StopIteration")
{
self.stop_iteration_raises.push(raise);
}
walk_stmt(self, stmt);
/// Returns true if we're inside a function that contains any `yield`/`yield from`.
fn in_generator_context(checker: &Checker) -> bool {
for scope in checker.semantic().current_scopes() {
if let ruff_python_semantic::ScopeKind::Function(function_def) = scope.kind {
if contains_yield_statement(&function_def.body) {
return true;
}
_ => walk_stmt(self, stmt),
}
}
false
}
fn visit_expr(&mut self, expr: &'a ast::Expr) {
match expr {
ast::Expr::Lambda(_) => {}
ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) => {
self.has_yield = true;
/// Check if a statement list contains any yield statements
fn contains_yield_statement(body: &[ast::Stmt]) -> bool {
struct YieldFinder {
found: bool,
}
impl Visitor<'_> for YieldFinder {
fn visit_expr(&mut self, expr: &ast::Expr) {
if matches!(expr, ast::Expr::Yield(_) | ast::Expr::YieldFrom(_)) {
self.found = true;
} else {
walk_expr(self, expr);
}
_ => walk_expr(self, expr),
}
}
let mut finder = YieldFinder { found: false };
for stmt in body {
walk_stmt(&mut finder, stmt);
if finder.found {
return true;
}
}
false
}

View File

@@ -12,16 +12,6 @@ use crate::checkers::ast::Checker;
/// By default, this rule allows up to five arguments, as configured by the
/// [`lint.pylint.max-args`] option.
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Changing the signature of a subclass method may cause type checkers to
/// complain about a violation of the Liskov Substitution Principle if it
/// means that the method now incompatibly overrides a method defined on a
/// superclass. Explicitly decorating an overriding method with `@override`
/// signals to Ruff that the method is intended to override a superclass
/// method and that a type checker will enforce that it does so; Ruff
/// therefore knows that it should not enforce rules about methods having
/// too many arguments.
///
/// ## Why is this bad?
/// Functions with many arguments are harder to understand, maintain, and call.
/// Consider refactoring functions with many arguments into smaller functions
@@ -53,8 +43,6 @@ use crate::checkers::ast::Checker;
///
/// ## Options
/// - `lint.pylint.max-args`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.238")]
pub(crate) struct TooManyArguments {

View File

@@ -21,16 +21,6 @@ use crate::checkers::ast::Checker;
/// with fewer arguments, using objects to group related arguments, or migrating to
/// [keyword-only arguments](https://docs.python.org/3/tutorial/controlflow.html#special-parameters).
///
/// This rule exempts methods decorated with [`@typing.override`][override].
/// Changing the signature of a subclass method may cause type checkers to
/// complain about a violation of the Liskov Substitution Principle if it
/// means that the method now incompatibly overrides a method defined on a
/// superclass. Explicitly decorating an overriding method with `@override`
/// signals to Ruff that the method is intended to override a superclass
/// method and that a type checker will enforce that it does so; Ruff
/// therefore knows that it should not enforce rules about methods having
/// too many arguments.
///
/// ## Example
///
/// ```python
@@ -51,8 +41,6 @@ use crate::checkers::ast::Checker;
///
/// ## Options
/// - `lint.pylint.max-positional-args`
///
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "v0.1.7")]
pub(crate) struct TooManyPositionalArguments {

View File

@@ -39,61 +39,3 @@ help: Add a call to `.items()`
18 |
19 |
note: This is an unsafe fix and may change runtime behavior
PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
--> dict_iter_missing_items.py:37:13
|
35 | empty_dict = {}
36 | empty_dict["x"] = 1
37 | for k, v in empty_dict:
| ^^^^^^^^^^
38 | pass
|
help: Add a call to `.items()`
34 | # Empty dict cases
35 | empty_dict = {}
36 | empty_dict["x"] = 1
- for k, v in empty_dict:
37 + for k, v in empty_dict.items():
38 | pass
39 |
40 | empty_dict_annotated_tuple_keys: dict[tuple[int, str], bool] = {}
note: This is an unsafe fix and may change runtime behavior
PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
--> dict_iter_missing_items.py:46:13
|
44 | empty_dict_unannotated = {}
45 | empty_dict_unannotated[("x", "y")] = True
46 | for k, v in empty_dict_unannotated:
| ^^^^^^^^^^^^^^^^^^^^^^
47 | pass
|
help: Add a call to `.items()`
43 |
44 | empty_dict_unannotated = {}
45 | empty_dict_unannotated[("x", "y")] = True
- for k, v in empty_dict_unannotated:
46 + for k, v in empty_dict_unannotated.items():
47 | pass
48 |
49 | empty_dict_annotated_str_keys: dict[str, int] = {}
note: This is an unsafe fix and may change runtime behavior
PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
--> dict_iter_missing_items.py:51:13
|
49 | empty_dict_annotated_str_keys: dict[str, int] = {}
50 | empty_dict_annotated_str_keys["x"] = 1
51 | for k, v in empty_dict_annotated_str_keys:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | pass
|
help: Add a call to `.items()`
48 |
49 | empty_dict_annotated_str_keys: dict[str, int] = {}
50 | empty_dict_annotated_str_keys["x"] = 1
- for k, v in empty_dict_annotated_str_keys:
51 + for k, v in empty_dict_annotated_str_keys.items():
52 | pass
note: This is an unsafe fix and may change runtime behavior

View File

@@ -107,24 +107,3 @@ PLR1708 Explicit `raise StopIteration` in generator
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:149:9
|
147 | yield 1
148 | class C:
149 | raise StopIteration # Should trigger
| ^^^^^^^^^^^^^^^^^^^
150 | yield C
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:154:5
|
152 | # https://github.com/astral-sh/ruff/pull/21177#discussion_r2539702728
153 | def foo():
154 | raise StopIteration((yield 1)) # Should trigger
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use `return` instead

View File

@@ -97,8 +97,7 @@ mod tests {
#[test_case(Rule::MapIntVersionParsing, Path::new("RUF048_1.py"))]
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
#[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"))]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_1.py"))]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))]
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
#[test_case(Rule::FalsyDictGetFallback, Path::new("RUF056.py"))]
#[test_case(Rule::UnnecessaryRound, Path::new("RUF057.py"))]
@@ -115,7 +114,6 @@ mod tests {
#[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))]
#[test_case(Rule::LoggingEagerConversion, Path::new("RUF065_0.py"))]
#[test_case(Rule::LoggingEagerConversion, Path::new("RUF065_1.py"))]
#[test_case(Rule::PropertyWithoutReturn, Path::new("RUF066.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]
@@ -623,8 +621,8 @@ mod tests {
Ok(())
}
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"), r"^_+", 1)]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"), r"", 2)]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"^_+", 1)]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"", 2)]
fn custom_regexp_preset(
rule_code: Rule,
path: &Path,

View File

@@ -2,9 +2,7 @@ use std::str::FromStr;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_literal::cformat::{
CConversionFlags, CFormatPart, CFormatSpec, CFormatString, CFormatType,
};
use ruff_python_literal::cformat::{CFormatPart, CFormatString, CFormatType};
use ruff_python_literal::format::FormatConversion;
use ruff_text_size::Ranged;
@@ -197,8 +195,7 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
}
// %s with oct() - suggest using %#o instead
FormatConversion::Str
if checker.semantic().match_builtin_expr(func.as_ref(), "oct")
&& !has_complex_conversion_specifier(spec) =>
if checker.semantic().match_builtin_expr(func.as_ref(), "oct") =>
{
checker.report_diagnostic(
LoggingEagerConversion {
@@ -210,8 +207,7 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
}
// %s with hex() - suggest using %#x instead
FormatConversion::Str
if checker.semantic().match_builtin_expr(func.as_ref(), "hex")
&& !has_complex_conversion_specifier(spec) =>
if checker.semantic().match_builtin_expr(func.as_ref(), "hex") =>
{
checker.report_diagnostic(
LoggingEagerConversion {
@@ -226,23 +222,3 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
}
}
}
/// Check if a conversion specifier has complex flags or precision that make `oct()` or `hex()` necessary.
///
/// Returns `true` if any of these conditions are met:
/// - Flag `0` (zero-pad) is used, flag `-` (left-adjust) is not used, and minimum width is specified
/// - Flag ` ` (blank sign) is used
/// - Flag `+` (sign char) is used
/// - Precision is specified
fn has_complex_conversion_specifier(spec: &CFormatSpec) -> bool {
if spec.flags.intersects(CConversionFlags::ZERO_PAD)
&& !spec.flags.intersects(CConversionFlags::LEFT_ADJUST)
&& spec.min_field_width.is_some()
{
return true;
}
spec.flags
.intersects(CConversionFlags::BLANK_SIGN | CConversionFlags::SIGN_CHAR)
|| spec.precision.is_some()
}

View File

@@ -35,7 +35,6 @@ pub(crate) use non_octal_permissions::*;
pub(crate) use none_not_at_end_of_union::*;
pub(crate) use parenthesize_chained_operators::*;
pub(crate) use post_init_default::*;
pub(crate) use property_without_return::*;
pub(crate) use pytest_raises_ambiguous_pattern::*;
pub(crate) use quadratic_list_summation::*;
pub(crate) use redirected_noqa::*;
@@ -100,7 +99,6 @@ mod non_octal_permissions;
mod none_not_at_end_of_union;
mod parenthesize_chained_operators;
mod post_init_default;
mod property_without_return;
mod pytest_raises_ambiguous_pattern;
mod quadratic_list_summation;
mod redirected_noqa;

View File

@@ -1,119 +0,0 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
use ruff_python_ast::{Expr, Stmt, StmtFunctionDef};
use ruff_python_semantic::analyze::{function_type, visibility};
use crate::checkers::ast::Checker;
use crate::{FixAvailability, Violation};
/// ## What it does
/// Detects class `@property` methods that does not have a `return` statement.
///
/// ## Why is this bad?
/// Property methods are expected to return a computed value, a missing return in a property usually indicates an implementation mistake.
///
/// ## Example
/// ```python
/// class User:
/// @property
/// def full_name(self):
/// f"{self.first_name} {self.last_name}"
/// ```
///
/// Use instead:
/// ```python
/// class User:
/// @property
/// def full_name(self):
/// return f"{self.first_name} {self.last_name}"
/// ```
///
/// ## References
/// - [Python documentation: The property class](https://docs.python.org/3/library/functions.html#property)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.7")]
pub(crate) struct PropertyWithoutReturn {
name: String,
}
impl Violation for PropertyWithoutReturn {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
#[derive_message_formats]
fn message(&self) -> String {
let Self { name } = self;
format!("`{name}` is a property without a `return` statement")
}
}
/// RUF066
pub(crate) fn property_without_return(checker: &Checker, function_def: &StmtFunctionDef) {
let semantic = checker.semantic();
if checker.source_type.is_stub() || semantic.in_protocol_or_abstract_method() {
return;
}
let StmtFunctionDef {
decorator_list,
body,
name,
..
} = function_def;
if !visibility::is_property(decorator_list, [], semantic)
|| visibility::is_overload(decorator_list, semantic)
|| function_type::is_stub(function_def, semantic)
{
return;
}
let mut visitor = PropertyVisitor::default();
visitor.visit_body(body);
if visitor.found {
return;
}
checker.report_diagnostic(
PropertyWithoutReturn {
name: name.to_string(),
},
function_def.identifier(),
);
}
#[derive(Default)]
struct PropertyVisitor {
found: bool,
}
// NOTE: We are actually searching for the presence of
// `yield`/`yield from`/`raise`/`return` statement/expression,
// as having one of those indicates that there's likely no implementation mistake
impl Visitor<'_> for PropertyVisitor {
fn visit_expr(&mut self, expr: &Expr) {
if self.found {
return;
}
match expr {
Expr::Yield(_) | Expr::YieldFrom(_) => self.found = true,
_ => walk_expr(self, expr),
}
}
fn visit_stmt(&mut self, stmt: &Stmt) {
if self.found {
return;
}
match stmt {
Stmt::Return(_) | Stmt::Raise(_) => self.found = true,
Stmt::FunctionDef(_) => {
// Do not recurse into nested functions; they're evaluated separately.
}
_ => walk_stmt(self, stmt),
}
}
}

View File

@@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_dunder;
use ruff_python_semantic::{Binding, BindingId, BindingKind, ScopeKind};
use ruff_python_semantic::{Binding, BindingId};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_text_size::Ranged;
@@ -111,7 +111,7 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
return;
}
// We only emit the lint on local variables.
// We only emit the lint on variables defined via assignments.
//
// ## Why not also emit the lint on function parameters?
//
@@ -127,30 +127,8 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
// autofixing the diagnostic for assignments. See:
// - <https://github.com/astral-sh/ruff/issues/14790>
// - <https://github.com/astral-sh/ruff/issues/14799>
match binding.kind {
BindingKind::Annotation
| BindingKind::Argument
| BindingKind::NamedExprAssignment
| BindingKind::Assignment
| BindingKind::LoopVar
| BindingKind::WithItemVar
| BindingKind::BoundException
| BindingKind::UnboundException(_) => {}
BindingKind::TypeParam
| BindingKind::Global(_)
| BindingKind::Nonlocal(_, _)
| BindingKind::Builtin
| BindingKind::ClassDefinition(_)
| BindingKind::FunctionDefinition(_)
| BindingKind::Export(_)
| BindingKind::FutureImport
| BindingKind::Import(_)
| BindingKind::FromImport(_)
| BindingKind::SubmoduleImport(_)
| BindingKind::Deletion
| BindingKind::ConditionalDeletion(_)
| BindingKind::DunderClassCell => return,
if !binding.kind.is_assignment() {
return;
}
// This excludes `global` and `nonlocal` variables.
@@ -160,12 +138,9 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
let semantic = checker.semantic();
// Only variables defined in function and generator scopes
// Only variables defined in function scopes
let scope = &semantic.scopes[binding.scope];
if !matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::Generator { .. }
) {
if !scope.kind.is_function() {
return;
}

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF052 [*] Local dummy variable `_var` is accessed
--> RUF052_0.py:92:9
--> RUF052.py:92:9
|
90 | class Class_:
91 | def fun(self):
@@ -24,7 +24,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_list` is accessed
--> RUF052_0.py:99:5
--> RUF052.py:99:5
|
98 | def fun():
99 | _list = "built-in" # [RUF052]
@@ -45,7 +45,7 @@ help: Prefer using trailing underscores to avoid shadowing a built-in
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052_0.py:106:5
--> RUF052.py:106:5
|
104 | def fun():
105 | global x
@@ -67,7 +67,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052_0.py:113:5
--> RUF052.py:113:5
|
111 | def bar():
112 | nonlocal x
@@ -90,7 +90,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052_0.py:120:5
--> RUF052.py:120:5
|
118 | def fun():
119 | x = "local"
@@ -112,7 +112,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052_0.py:128:5
--> RUF052.py:128:5
|
127 | def unfixables():
128 | _GLOBAL_1 = "foo"
@@ -123,7 +123,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052_0.py:136:5
--> RUF052.py:136:5
|
135 | # unfixable because the rename would shadow a local variable
136 | _local = "local3" # [RUF052]
@@ -133,7 +133,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052_0.py:140:9
--> RUF052.py:140:9
|
139 | def nested():
140 | _GLOBAL_1 = "foo"
@@ -144,7 +144,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052_0.py:145:9
--> RUF052.py:145:9
|
144 | # unfixable because the rename would shadow a variable from the outer function
145 | _local = "local4"
@@ -154,7 +154,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 [*] Local dummy variable `_P` is accessed
--> RUF052_0.py:153:5
--> RUF052.py:153:5
|
151 | from collections import namedtuple
152 |
@@ -184,7 +184,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_T` is accessed
--> RUF052_0.py:154:5
--> RUF052.py:154:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -213,7 +213,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT` is accessed
--> RUF052_0.py:155:5
--> RUF052.py:155:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -242,7 +242,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_E` is accessed
--> RUF052_0.py:156:5
--> RUF052.py:156:5
|
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
155 | _NT = NamedTuple("_NT", [("foo", int)])
@@ -270,7 +270,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT2` is accessed
--> RUF052_0.py:157:5
--> RUF052.py:157:5
|
155 | _NT = NamedTuple("_NT", [("foo", int)])
156 | _E = Enum("_E", ["a", "b", "c"])
@@ -297,7 +297,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT3` is accessed
--> RUF052_0.py:158:5
--> RUF052.py:158:5
|
156 | _E = Enum("_E", ["a", "b", "c"])
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
@@ -323,7 +323,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_DynamicClass` is accessed
--> RUF052_0.py:159:5
--> RUF052.py:159:5
|
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
@@ -347,7 +347,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NotADynamicClass` is accessed
--> RUF052_0.py:160:5
--> RUF052.py:160:5
|
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
159 | _DynamicClass = type("_DynamicClass", (), {})
@@ -371,7 +371,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_dummy_var` is accessed
--> RUF052_0.py:182:5
--> RUF052.py:182:5
|
181 | def foo():
182 | _dummy_var = 42
@@ -396,7 +396,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 Local dummy variable `_dummy_var` is accessed
--> RUF052_0.py:192:5
--> RUF052.py:192:5
|
190 | # Unfixable because both possible candidates for the new name are shadowed
191 | # in the scope of one of the references to the variable

View File

@@ -1,494 +0,0 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:21:9
|
20 | # Should detect used dummy variable
21 | for _item in my_list:
| ^^^^^
22 | print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
18 | my_list = [{"foo": 1}, {"foo": 2}]
19 |
20 | # Should detect used dummy variable
- for _item in my_list:
- print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
21 + for item in my_list:
22 + print(item["foo"]) # RUF052: Local dummy variable `_item` is accessed
23 |
24 | # Should detect used dummy variable
25 | for _index, _value in enumerate(my_list):
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_index` is accessed
--> RUF052_1.py:25:9
|
24 | # Should detect used dummy variable
25 | for _index, _value in enumerate(my_list):
| ^^^^^^
26 | result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
help: Remove leading underscores
22 | print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
23 |
24 | # Should detect used dummy variable
- for _index, _value in enumerate(my_list):
- result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
25 + for index, _value in enumerate(my_list):
26 + result = index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
27 |
28 | # List Comprehensions
29 | def test_list_comprehensions():
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_value` is accessed
--> RUF052_1.py:25:17
|
24 | # Should detect used dummy variable
25 | for _index, _value in enumerate(my_list):
| ^^^^^^
26 | result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
help: Remove leading underscores
22 | print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
23 |
24 | # Should detect used dummy variable
- for _index, _value in enumerate(my_list):
- result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
25 + for _index, value in enumerate(my_list):
26 + result = _index + value["foo"] # RUF052: Both `_index` and `_value` are accessed
27 |
28 | # List Comprehensions
29 | def test_list_comprehensions():
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:33:32
|
32 | # Should detect used dummy variable
33 | result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
| ^^^^^
34 |
35 | # Should detect used dummy variable in nested comprehension
|
help: Remove leading underscores
30 | my_list = [{"foo": 1}, {"foo": 2}]
31 |
32 | # Should detect used dummy variable
- result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
33 + result = [item["foo"] for item in my_list] # RUF052: Local dummy variable `_item` is accessed
34 |
35 | # Should detect used dummy variable in nested comprehension
36 | nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:36:33
|
35 | # Should detect used dummy variable in nested comprehension
36 | nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
| ^^^^^
37 | # RUF052: Both `_item` and `_sublist` are accessed
|
help: Remove leading underscores
33 | result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
34 |
35 | # Should detect used dummy variable in nested comprehension
- nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
36 + nested = [[item["foo"] for item in _sublist] for _sublist in [my_list, my_list]]
37 | # RUF052: Both `_item` and `_sublist` are accessed
38 |
39 | # Should detect with conditions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_sublist` is accessed
--> RUF052_1.py:36:56
|
35 | # Should detect used dummy variable in nested comprehension
36 | nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
| ^^^^^^^^
37 | # RUF052: Both `_item` and `_sublist` are accessed
|
help: Remove leading underscores
33 | result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
34 |
35 | # Should detect used dummy variable in nested comprehension
- nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
36 + nested = [[_item["foo"] for _item in sublist] for sublist in [my_list, my_list]]
37 | # RUF052: Both `_item` and `_sublist` are accessed
38 |
39 | # Should detect with conditions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:40:34
|
39 | # Should detect with conditions
40 | filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
| ^^^^^
41 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
37 | # RUF052: Both `_item` and `_sublist` are accessed
38 |
39 | # Should detect with conditions
- filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
40 + filtered = [item["foo"] for item in my_list if item["foo"] > 0]
41 | # RUF052: Local dummy variable `_item` is accessed
42 |
43 | # Dict Comprehensions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:48:48
|
47 | # Should detect used dummy variable
48 | result = {_item["key"]: _item["value"] for _item in my_list}
| ^^^^^
49 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
45 | my_list = [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
46 |
47 | # Should detect used dummy variable
- result = {_item["key"]: _item["value"] for _item in my_list}
48 + result = {item["key"]: item["value"] for item in my_list}
49 | # RUF052: Local dummy variable `_item` is accessed
50 |
51 | # Should detect with enumerate
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_index` is accessed
--> RUF052_1.py:52:43
|
51 | # Should detect with enumerate
52 | indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
| ^^^^^^
53 | # RUF052: Both `_index` and `_item` are accessed
|
help: Remove leading underscores
49 | # RUF052: Local dummy variable `_item` is accessed
50 |
51 | # Should detect with enumerate
- indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
52 + indexed = {index: _item["value"] for index, _item in enumerate(my_list)}
53 | # RUF052: Both `_index` and `_item` are accessed
54 |
55 | # Should detect in nested dict comprehension
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:52:51
|
51 | # Should detect with enumerate
52 | indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
| ^^^^^
53 | # RUF052: Both `_index` and `_item` are accessed
|
help: Remove leading underscores
49 | # RUF052: Local dummy variable `_item` is accessed
50 |
51 | # Should detect with enumerate
- indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
52 + indexed = {_index: item["value"] for _index, item in enumerate(my_list)}
53 | # RUF052: Both `_index` and `_item` are accessed
54 |
55 | # Should detect in nested dict comprehension
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_inner` is accessed
--> RUF052_1.py:56:59
|
55 | # Should detect in nested dict comprehension
56 | nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
| ^^^^^^
57 | for _outer, sublist in enumerate([my_list])}
58 | # RUF052: `_outer`, `_inner` are accessed
|
help: Remove leading underscores
53 | # RUF052: Both `_index` and `_item` are accessed
54 |
55 | # Should detect in nested dict comprehension
- nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
56 + nested = {_outer: {inner["key"]: inner["value"] for inner in sublist}
57 | for _outer, sublist in enumerate([my_list])}
58 | # RUF052: `_outer`, `_inner` are accessed
59 |
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_outer` is accessed
--> RUF052_1.py:57:19
|
55 | # Should detect in nested dict comprehension
56 | nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
57 | for _outer, sublist in enumerate([my_list])}
| ^^^^^^
58 | # RUF052: `_outer`, `_inner` are accessed
|
help: Remove leading underscores
53 | # RUF052: Both `_index` and `_item` are accessed
54 |
55 | # Should detect in nested dict comprehension
- nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
- for _outer, sublist in enumerate([my_list])}
56 + nested = {outer: {_inner["key"]: _inner["value"] for _inner in sublist}
57 + for outer, sublist in enumerate([my_list])}
58 | # RUF052: `_outer`, `_inner` are accessed
59 |
60 | # Set Comprehensions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:65:39
|
64 | # Should detect used dummy variable
65 | unique_values = {_item["foo"] for _item in my_list}
| ^^^^^
66 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
62 | my_list = [{"foo": 1}, {"foo": 2}, {"foo": 1}] # Note: duplicate values
63 |
64 | # Should detect used dummy variable
- unique_values = {_item["foo"] for _item in my_list}
65 + unique_values = {item["foo"] for item in my_list}
66 | # RUF052: Local dummy variable `_item` is accessed
67 |
68 | # Should detect with conditions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:69:38
|
68 | # Should detect with conditions
69 | filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
| ^^^^^
70 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
66 | # RUF052: Local dummy variable `_item` is accessed
67 |
68 | # Should detect with conditions
- filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
69 + filtered_set = {item["foo"] for item in my_list if item["foo"] > 0}
70 | # RUF052: Local dummy variable `_item` is accessed
71 |
72 | # Should detect with complex expression
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:73:39
|
72 | # Should detect with complex expression
73 | processed = {_item["foo"] * 2 for _item in my_list}
| ^^^^^
74 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
70 | # RUF052: Local dummy variable `_item` is accessed
71 |
72 | # Should detect with complex expression
- processed = {_item["foo"] * 2 for _item in my_list}
73 + processed = {item["foo"] * 2 for item in my_list}
74 | # RUF052: Local dummy variable `_item` is accessed
75 |
76 | # Generator Expressions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:81:29
|
80 | # Should detect used dummy variable
81 | gen = (_item["foo"] for _item in my_list)
| ^^^^^
82 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
78 | my_list = [{"foo": 1}, {"foo": 2}]
79 |
80 | # Should detect used dummy variable
- gen = (_item["foo"] for _item in my_list)
81 + gen = (item["foo"] for item in my_list)
82 | # RUF052: Local dummy variable `_item` is accessed
83 |
84 | # Should detect when passed to function
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:85:34
|
84 | # Should detect when passed to function
85 | total = sum(_item["foo"] for _item in my_list)
| ^^^^^
86 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
82 | # RUF052: Local dummy variable `_item` is accessed
83 |
84 | # Should detect when passed to function
- total = sum(_item["foo"] for _item in my_list)
85 + total = sum(item["foo"] for item in my_list)
86 | # RUF052: Local dummy variable `_item` is accessed
87 |
88 | # Should detect with multiple generators
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052_1.py:89:27
|
88 | # Should detect with multiple generators
89 | pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
| ^^
90 | # RUF052: Both `_x` and `_y` are accessed
|
help: Remove leading underscores
86 | # RUF052: Local dummy variable `_item` is accessed
87 |
88 | # Should detect with multiple generators
- pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
89 + pairs = ((x, _y) for x in range(3) for _y in range(3) if x != _y)
90 | # RUF052: Both `_x` and `_y` are accessed
91 |
92 | # Should detect in nested generator
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_y` is accessed
--> RUF052_1.py:89:46
|
88 | # Should detect with multiple generators
89 | pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
| ^^
90 | # RUF052: Both `_x` and `_y` are accessed
|
help: Remove leading underscores
86 | # RUF052: Local dummy variable `_item` is accessed
87 |
88 | # Should detect with multiple generators
- pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
89 + pairs = ((_x, y) for _x in range(3) for y in range(3) if _x != y)
90 | # RUF052: Both `_x` and `_y` are accessed
91 |
92 | # Should detect in nested generator
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_inner` is accessed
--> RUF052_1.py:93:41
|
92 | # Should detect in nested generator
93 | nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
| ^^^^^^
94 | # RUF052: `_inner` and `_sublist` are accessed
|
help: Remove leading underscores
90 | # RUF052: Both `_x` and `_y` are accessed
91 |
92 | # Should detect in nested generator
- nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
93 + nested_gen = (sum(inner["foo"] for inner in sublist) for _sublist in [my_list] for sublist in _sublist)
94 | # RUF052: `_inner` and `_sublist` are accessed
95 |
96 | # Complex Examples with Multiple Comprehension Types
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_sublist` is accessed
--> RUF052_1.py:93:64
|
92 | # Should detect in nested generator
93 | nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
| ^^^^^^^^
94 | # RUF052: `_inner` and `_sublist` are accessed
|
help: Prefer using trailing underscores to avoid shadowing a variable
90 | # RUF052: Both `_x` and `_y` are accessed
91 |
92 | # Should detect in nested generator
- nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
93 + nested_gen = (sum(_inner["foo"] for _inner in sublist) for sublist_ in [my_list] for sublist in sublist_)
94 | # RUF052: `_inner` and `_sublist` are accessed
95 |
96 | # Complex Examples with Multiple Comprehension Types
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_val` is accessed
--> RUF052_1.py:102:30
|
100 | # Should detect in mixed comprehensions
101 | result = [
102 | {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
| ^^^^
103 | for _record in data
104 | ]
|
help: Remove leading underscores
99 |
100 | # Should detect in mixed comprehensions
101 | result = [
- {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
102 + {_key: [val * 2 for val in _record["items"]] for _key in ["doubled"]}
103 | for _record in data
104 | ]
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_key` is accessed
--> RUF052_1.py:102:60
|
100 | # Should detect in mixed comprehensions
101 | result = [
102 | {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
| ^^^^
103 | for _record in data
104 | ]
|
help: Remove leading underscores
99 |
100 | # Should detect in mixed comprehensions
101 | result = [
- {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
102 + {key: [_val * 2 for _val in _record["items"]] for key in ["doubled"]}
103 | for _record in data
104 | ]
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_record` is accessed
--> RUF052_1.py:103:13
|
101 | result = [
102 | {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
103 | for _record in data
| ^^^^^^^
104 | ]
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
|
help: Remove leading underscores
99 |
100 | # Should detect in mixed comprehensions
101 | result = [
- {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
- for _record in data
102 + {_key: [_val * 2 for _val in record["items"]] for _key in ["doubled"]}
103 + for record in data
104 | ]
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
106 |
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:108:43
|
107 | # Should detect in generator passed to list constructor
108 | gen_list = list(_item["items"][0] for _item in data)
| ^^^^^
109 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
106 |
107 | # Should detect in generator passed to list constructor
- gen_list = list(_item["items"][0] for _item in data)
108 + gen_list = list(item["items"][0] for item in data)
109 | # RUF052: Local dummy variable `_item` is accessed
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,31 +0,0 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF066 `name` is a property without a `return` statement
--> RUF066.py:7:9
|
5 | class User: # Test normal class properties
6 | @property
7 | def name(self): # ERROR: No return
| ^^^^
8 | f"{self.first_name} {self.last_name}"
|
RUF066 `nested` is a property without a `return` statement
--> RUF066.py:18:9
|
17 | @property
18 | def nested(self): # ERROR: Property itself doesn't return
| ^^^^^^
19 | def inner():
20 | return 0
|
RUF066 `prop2` is a property without a `return` statement
--> RUF066.py:43:9
|
42 | @property
43 | def prop2(self): # ERROR: Not returning something (even when we are inside an ABC)
| ^^^^^
44 | 50
|

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF052 [*] Local dummy variable `_var` is accessed
--> RUF052_0.py:92:9
--> RUF052.py:92:9
|
90 | class Class_:
91 | def fun(self):
@@ -24,7 +24,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_list` is accessed
--> RUF052_0.py:99:5
--> RUF052.py:99:5
|
98 | def fun():
99 | _list = "built-in" # [RUF052]
@@ -45,7 +45,7 @@ help: Prefer using trailing underscores to avoid shadowing a built-in
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052_0.py:106:5
--> RUF052.py:106:5
|
104 | def fun():
105 | global x
@@ -67,7 +67,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052_0.py:113:5
--> RUF052.py:113:5
|
111 | def bar():
112 | nonlocal x
@@ -90,7 +90,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052_0.py:120:5
--> RUF052.py:120:5
|
118 | def fun():
119 | x = "local"
@@ -112,7 +112,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052_0.py:128:5
--> RUF052.py:128:5
|
127 | def unfixables():
128 | _GLOBAL_1 = "foo"
@@ -123,7 +123,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052_0.py:136:5
--> RUF052.py:136:5
|
135 | # unfixable because the rename would shadow a local variable
136 | _local = "local3" # [RUF052]
@@ -133,7 +133,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052_0.py:140:9
--> RUF052.py:140:9
|
139 | def nested():
140 | _GLOBAL_1 = "foo"
@@ -144,7 +144,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052_0.py:145:9
--> RUF052.py:145:9
|
144 | # unfixable because the rename would shadow a variable from the outer function
145 | _local = "local4"
@@ -154,7 +154,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 [*] Local dummy variable `_P` is accessed
--> RUF052_0.py:153:5
--> RUF052.py:153:5
|
151 | from collections import namedtuple
152 |
@@ -184,7 +184,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_T` is accessed
--> RUF052_0.py:154:5
--> RUF052.py:154:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -213,7 +213,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT` is accessed
--> RUF052_0.py:155:5
--> RUF052.py:155:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -242,7 +242,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_E` is accessed
--> RUF052_0.py:156:5
--> RUF052.py:156:5
|
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
155 | _NT = NamedTuple("_NT", [("foo", int)])
@@ -270,7 +270,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT2` is accessed
--> RUF052_0.py:157:5
--> RUF052.py:157:5
|
155 | _NT = NamedTuple("_NT", [("foo", int)])
156 | _E = Enum("_E", ["a", "b", "c"])
@@ -297,7 +297,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT3` is accessed
--> RUF052_0.py:158:5
--> RUF052.py:158:5
|
156 | _E = Enum("_E", ["a", "b", "c"])
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
@@ -323,7 +323,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_DynamicClass` is accessed
--> RUF052_0.py:159:5
--> RUF052.py:159:5
|
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
@@ -347,7 +347,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NotADynamicClass` is accessed
--> RUF052_0.py:160:5
--> RUF052.py:160:5
|
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
159 | _DynamicClass = type("_DynamicClass", (), {})
@@ -371,7 +371,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_dummy_var` is accessed
--> RUF052_0.py:182:5
--> RUF052.py:182:5
|
181 | def foo():
182 | _dummy_var = 42
@@ -396,7 +396,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 Local dummy variable `_dummy_var` is accessed
--> RUF052_0.py:192:5
--> RUF052.py:192:5
|
190 | # Unfixable because both possible candidates for the new name are shadowed
191 | # in the scope of one of the references to the variable

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF052 Local dummy variable `_var` is accessed
--> RUF052_0.py:92:9
--> RUF052.py:92:9
|
90 | class Class_:
91 | def fun(self):
@@ -13,7 +13,7 @@ RUF052 Local dummy variable `_var` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_list` is accessed
--> RUF052_0.py:99:5
--> RUF052.py:99:5
|
98 | def fun():
99 | _list = "built-in" # [RUF052]
@@ -23,7 +23,7 @@ RUF052 Local dummy variable `_list` is accessed
help: Prefer using trailing underscores to avoid shadowing a built-in
RUF052 Local dummy variable `_x` is accessed
--> RUF052_0.py:106:5
--> RUF052.py:106:5
|
104 | def fun():
105 | global x
@@ -34,7 +34,7 @@ RUF052 Local dummy variable `_x` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `x` is accessed
--> RUF052_0.py:110:3
--> RUF052.py:110:3
|
109 | def foo():
110 | x = "outer"
@@ -44,7 +44,7 @@ RUF052 Local dummy variable `x` is accessed
|
RUF052 Local dummy variable `_x` is accessed
--> RUF052_0.py:113:5
--> RUF052.py:113:5
|
111 | def bar():
112 | nonlocal x
@@ -56,7 +56,7 @@ RUF052 Local dummy variable `_x` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_x` is accessed
--> RUF052_0.py:120:5
--> RUF052.py:120:5
|
118 | def fun():
119 | x = "local"
@@ -67,7 +67,7 @@ RUF052 Local dummy variable `_x` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052_0.py:128:5
--> RUF052.py:128:5
|
127 | def unfixables():
128 | _GLOBAL_1 = "foo"
@@ -78,7 +78,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052_0.py:136:5
--> RUF052.py:136:5
|
135 | # unfixable because the rename would shadow a local variable
136 | _local = "local3" # [RUF052]
@@ -88,7 +88,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052_0.py:140:9
--> RUF052.py:140:9
|
139 | def nested():
140 | _GLOBAL_1 = "foo"
@@ -99,7 +99,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052_0.py:145:9
--> RUF052.py:145:9
|
144 | # unfixable because the rename would shadow a variable from the outer function
145 | _local = "local4"
@@ -109,7 +109,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_P` is accessed
--> RUF052_0.py:153:5
--> RUF052.py:153:5
|
151 | from collections import namedtuple
152 |
@@ -121,7 +121,7 @@ RUF052 Local dummy variable `_P` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_T` is accessed
--> RUF052_0.py:154:5
--> RUF052.py:154:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -132,7 +132,7 @@ RUF052 Local dummy variable `_T` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_NT` is accessed
--> RUF052_0.py:155:5
--> RUF052.py:155:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -144,7 +144,7 @@ RUF052 Local dummy variable `_NT` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_E` is accessed
--> RUF052_0.py:156:5
--> RUF052.py:156:5
|
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
155 | _NT = NamedTuple("_NT", [("foo", int)])
@@ -156,7 +156,7 @@ RUF052 Local dummy variable `_E` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_NT2` is accessed
--> RUF052_0.py:157:5
--> RUF052.py:157:5
|
155 | _NT = NamedTuple("_NT", [("foo", int)])
156 | _E = Enum("_E", ["a", "b", "c"])
@@ -168,7 +168,7 @@ RUF052 Local dummy variable `_NT2` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_NT3` is accessed
--> RUF052_0.py:158:5
--> RUF052.py:158:5
|
156 | _E = Enum("_E", ["a", "b", "c"])
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
@@ -180,7 +180,7 @@ RUF052 Local dummy variable `_NT3` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_DynamicClass` is accessed
--> RUF052_0.py:159:5
--> RUF052.py:159:5
|
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
@@ -191,7 +191,7 @@ RUF052 Local dummy variable `_DynamicClass` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_NotADynamicClass` is accessed
--> RUF052_0.py:160:5
--> RUF052.py:160:5
|
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
159 | _DynamicClass = type("_DynamicClass", (), {})
@@ -202,18 +202,8 @@ RUF052 Local dummy variable `_NotADynamicClass` is accessed
|
help: Remove leading underscores
RUF052 Local dummy variable `other` is accessed
--> RUF052_0.py:177:13
|
175 | return
176 | _seen.add(self)
177 | for other in self.connected:
| ^^^^^
178 | other.recurse(_seen=_seen)
|
RUF052 Local dummy variable `_dummy_var` is accessed
--> RUF052_0.py:182:5
--> RUF052.py:182:5
|
181 | def foo():
182 | _dummy_var = 42
@@ -224,7 +214,7 @@ RUF052 Local dummy variable `_dummy_var` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_dummy_var` is accessed
--> RUF052_0.py:192:5
--> RUF052.py:192:5
|
190 | # Unfixable because both possible candidates for the new name are shadowed
191 | # in the scope of one of the references to the variable

View File

@@ -773,14 +773,10 @@ pub trait StringFlags: Copy {
}
/// The total length of the string's closer.
/// This is always equal to `self.quote_len()`, except when the string is unclosed,
/// in which case the length is zero.
/// This is always equal to `self.quote_len()`,
/// but is provided here for symmetry with the `opener_len()` method.
fn closer_len(self) -> TextSize {
if self.is_unclosed() {
TextSize::default()
} else {
self.quote_len()
}
self.quote_len()
}
fn as_any_string_flags(self) -> AnyStringFlags {

View File

@@ -193,58 +193,3 @@ def foo():
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
):
pass
# Regression tests for https://github.com/astral-sh/ruff/issues/19226
if '' and (not #
0):
pass
if '' and (not #
(0)
):
pass
if '' and (not
( #
0
)):
pass
if (
not
# comment
(a)):
pass
if not ( # comment
a):
pass
if not (
# comment
(a)):
pass
if not (
# comment
a):
pass
not (# comment
(a))
(-#comment
(a))
if ( # a
# b
not # c
# d
( # e
# f
a # g
# h
) # i
# j
):
pass

View File

@@ -1,149 +0,0 @@
# Test cases for fmt: skip on compound statements that fit on one line
# Basic single-line compound statements
def simple_func(): return "hello" # fmt: skip
if True: print("condition met") # fmt: skip
for i in range(5): print(i) # fmt: skip
while x < 10: x += 1 # fmt: skip
# With expressions that would normally trigger formatting
def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip
if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip
# Nested compound statements (outer should be preserved)
if True:
for i in range(10): print(i) # fmt: skip
# Multiple statements in body (should not apply - multiline)
if True:
x = 1
y = 2 # fmt: skip
# With decorators - decorated function on one line
@overload
def decorated_func(x: int) -> str: return str(x) # fmt: skip
@property
def prop_method(self): return self._value # fmt: skip
# Class definitions on one line
class SimpleClass: pass # fmt: skip
class GenericClass(Generic[T]): pass # fmt: skip
# Try/except blocks
try: risky_operation() # fmt: skip
except ValueError: handle_error() # fmt: skip
except: handle_any_error() # fmt: skip
else: success_case() # fmt: skip
finally: cleanup() # fmt: skip
# Match statements (Python 3.10+)
match value:
case 1: print("one") # fmt: skip
case _: print("other") # fmt: skip
# With statements
with open("file.txt") as f: content = f.read() # fmt: skip
with context_manager() as cm: result = cm.process() # fmt: skip
# Async variants
async def async_func(): return await some_call() # fmt: skip
async for item in async_iterator(): await process(item) # fmt: skip
async with async_context() as ctx: await ctx.work() # fmt: skip
# Complex expressions that would normally format
def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip
if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip
# Edge case: comment positioning
def func_with_comment(): # some comment
return "value" # fmt: skip
# Edge case: multiple fmt: skip (only last one should matter)
def multiple_skip(): return "test" # fmt: skip # fmt: skip
# Should NOT be affected (already multiline)
def multiline_func():
return "this should format normally"
if long_condition_that_spans \
and continues_on_next_line:
print("multiline condition")
# Mix of skipped and non-skipped
for i in range(10): print(f"item {i}") # fmt: skip
for j in range(5):
print(f"formatted item {j}")
# With trailing comma that would normally be removed
def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip
# Dictionary/list comprehensions
def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip
def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip
# Lambda in one-liner
def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip
# String formatting that would normally be reformatted
def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip
# loop else clauses
for i in range(2): print(i) # fmt: skip
else: print("this") # fmt: skip
while foo(): print(i) # fmt: skip
else: print("this") # fmt: skip
# again but only the first skip
for i in range(2): print(i) # fmt: skip
else: print("this")
while foo(): print(i) # fmt: skip
else: print("this")
# again but only the second skip
for i in range(2): print(i)
else: print("this") # fmt: skip
while foo(): print(i)
else: print("this") # fmt: skip
# multiple statements in body
if True: print("this"); print("that") # fmt: skip
# Examples with more comments
try: risky_operation() # fmt: skip
# leading 1
except ValueError: handle_error() # fmt: skip
# leading 2
except: handle_any_error() # fmt: skip
# leading 3
else: success_case() # fmt: skip
# leading 4
finally: cleanup() # fmt: skip
# trailing
# multi-line before colon (should remain as is)
if (
long_condition
): a + b # fmt: skip
# over-indented comment example
# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910
# and https://github.com/astral-sh/ruff/pull/21185
for x in it: foo()
# comment
else: bar() # fmt: skip
if this(
'is a long',
# commented
'condition'
): with_a_skip # fmt: skip

View File

@@ -1890,11 +1890,9 @@ fn handle_lambda_comment<'a>(
CommentPlacement::Default(comment)
}
/// Move an end-of-line comment between a unary op and its operand after the operand by marking
/// it as dangling.
/// Move comment between a unary op and its operand before the unary op by marking them as trailing.
///
/// For example, given:
///
/// ```python
/// (
/// not # comment
@@ -1902,13 +1900,8 @@ fn handle_lambda_comment<'a>(
/// )
/// ```
///
/// the `# comment` will be attached as a dangling comment on the unary op and formatted as:
///
/// ```python
/// (
/// not True # comment
/// )
/// ```
/// The `# comment` will be attached as a dangling comment on the enclosing node, to ensure that
/// it remains on the same line as the operator.
fn handle_unary_op_comment<'a>(
comment: DecoratedComment<'a>,
unary_op: &'a ast::ExprUnaryOp,
@@ -1930,8 +1923,8 @@ fn handle_unary_op_comment<'a>(
let up_to = tokenizer
.find(|token| token.kind == SimpleTokenKind::LParen)
.map_or(unary_op.operand.start(), |lparen| lparen.start());
if comment.end() < up_to && comment.line_position().is_end_of_line() {
CommentPlacement::dangling(unary_op, comment)
if comment.end() < up_to {
CommentPlacement::leading(unary_op, comment)
} else {
CommentPlacement::Default(comment)
}

View File

@@ -1,8 +1,6 @@
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprUnaryOp;
use ruff_python_ast::UnaryOp;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_text_size::Ranged;
use crate::comments::trailing_comments;
use crate::expression::parentheses::{
@@ -41,25 +39,20 @@ impl FormatNodeRule<ExprUnaryOp> for FormatExprUnaryOp {
// ```
trailing_comments(dangling).fmt(f)?;
// Insert a line break if the operand has comments but itself is not parenthesized or if the
// operand is parenthesized but has a leading comment before the parentheses.
// Insert a line break if the operand has comments but itself is not parenthesized.
// ```python
// if (
// not
// # comment
// a):
// pass
//
// if 1 and (
// not
// # comment
// (
// a
// )
// ):
// pass
// a)
// ```
if needs_line_break(item, f.context()) {
if comments.has_leading(operand.as_ref())
&& !is_expression_parenthesized(
operand.as_ref().into(),
f.context().comments().ranges(),
f.context().source(),
)
{
hard_line_break().fmt(f)?;
} else if op.is_not() {
space().fmt(f)?;
@@ -83,51 +76,17 @@ impl NeedsParentheses for ExprUnaryOp {
context: &PyFormatContext,
) -> OptionalParentheses {
if parent.is_expr_await() {
return OptionalParentheses::Always;
}
if needs_line_break(self, context) {
return OptionalParentheses::Always;
}
if is_expression_parenthesized(
OptionalParentheses::Always
} else if is_expression_parenthesized(
self.operand.as_ref().into(),
context.comments().ranges(),
context.source(),
) {
return OptionalParentheses::Never;
OptionalParentheses::Never
} else if context.comments().has(self.operand.as_ref()) {
OptionalParentheses::Always
} else {
self.operand.needs_parentheses(self.into(), context)
}
if context.comments().has(self.operand.as_ref()) {
return OptionalParentheses::Always;
}
self.operand.needs_parentheses(self.into(), context)
}
}
/// Returns `true` if the unary operator will have a hard line break between the operator and its
/// operand and thus requires parentheses.
fn needs_line_break(item: &ExprUnaryOp, context: &PyFormatContext) -> bool {
let comments = context.comments();
let parenthesized_operand_range = parenthesized_range(
item.operand.as_ref().into(),
item.into(),
comments.ranges(),
context.source(),
);
let leading_operand_comments = comments.leading(item.operand.as_ref());
let has_leading_comments_before_parens = parenthesized_operand_range.is_some_and(|range| {
leading_operand_comments
.iter()
.any(|comment| comment.start() < range.start())
});
!leading_operand_comments.is_empty()
&& !is_expression_parenthesized(
item.operand.as_ref().into(),
context.comments().ranges(),
context.source(),
)
|| has_leading_comments_before_parens
}

View File

@@ -7,7 +7,7 @@ use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::preview::is_remove_parens_around_except_types_enabled;
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::suite::SuiteKind;
#[derive(Copy, Clone, Default)]
@@ -55,68 +55,77 @@ impl FormatNodeRule<ExceptHandlerExceptHandler> for FormatExceptHandlerExceptHan
write!(
f,
[clause(
ClauseHeader::ExceptHandler(item),
&format_with(|f: &mut PyFormatter| {
write!(
f,
[
token("except"),
match except_handler_kind {
ExceptHandlerKind::Regular => None,
ExceptHandlerKind::Starred => Some(token("*")),
}
]
)?;
[
clause_header(
ClauseHeader::ExceptHandler(item),
dangling_comments,
&format_with(|f: &mut PyFormatter| {
write!(
f,
[
token("except"),
match except_handler_kind {
ExceptHandlerKind::Regular => None,
ExceptHandlerKind::Starred => Some(token("*")),
}
]
)?;
match type_.as_deref() {
// For tuples of exception types without an `as` name and on 3.14+, the
// parentheses are optional.
//
// ```py
// try:
// ...
// except BaseException, Exception: # Ok
// ...
// ```
Some(Expr::Tuple(tuple))
if f.options().target_version() >= PythonVersion::PY314
&& is_remove_parens_around_except_types_enabled(f.context())
&& name.is_none() =>
{
write!(
f,
[
space(),
tuple.format().with_options(TupleParentheses::NeverPreserve)
]
)?;
}
Some(type_) => {
write!(
f,
[
space(),
maybe_parenthesize_expression(
type_,
item,
Parenthesize::IfBreaks
match type_.as_deref() {
// For tuples of exception types without an `as` name and on 3.14+, the
// parentheses are optional.
//
// ```py
// try:
// ...
// except BaseException, Exception: # Ok
// ...
// ```
Some(Expr::Tuple(tuple))
if f.options().target_version() >= PythonVersion::PY314
&& is_remove_parens_around_except_types_enabled(
f.context(),
)
]
)?;
if let Some(name) = name {
write!(f, [space(), token("as"), space(), name.format()])?;
&& name.is_none() =>
{
write!(
f,
[
space(),
tuple
.format()
.with_options(TupleParentheses::NeverPreserve)
]
)?;
}
Some(type_) => {
write!(
f,
[
space(),
maybe_parenthesize_expression(
type_,
item,
Parenthesize::IfBreaks
)
]
)?;
if let Some(name) = name {
write!(f, [space(), token("as"), space(), name.format()])?;
}
}
_ => {}
}
_ => {}
}
Ok(())
}),
dangling_comments,
body,
SuiteKind::other(self.last_suite_in_statement),
)]
Ok(())
}),
),
clause_body(
body,
SuiteKind::other(self.last_suite_in_statement),
dangling_comments
),
]
)
}
}

View File

@@ -5,7 +5,7 @@ use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::pattern::maybe_parenthesize_pattern;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -46,18 +46,23 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
write!(
f,
[clause(
ClauseHeader::MatchCase(item),
&format_args![
token("case"),
space(),
maybe_parenthesize_pattern(pattern, item),
format_guard
],
dangling_item_comments,
body,
SuiteKind::other(self.last_suite_in_statement),
)]
[
clause_header(
ClauseHeader::MatchCase(item),
dangling_item_comments,
&format_args![
token("case"),
space(),
maybe_parenthesize_pattern(pattern, item),
format_guard
],
),
clause_body(
body,
SuiteKind::other(self.last_suite_in_statement),
dangling_item_comments
),
]
)
}
}

View File

@@ -5,12 +5,11 @@ use ruff_python_ast::{
StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith, Suite,
};
use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::comments::{SourceComment, leading_alternate_branch_comments, trailing_comments};
use crate::statement::suite::{SuiteKind, as_only_an_ellipsis};
use crate::verbatim::{verbatim_text, write_suppressed_clause_header};
use crate::verbatim::write_suppressed_clause_header;
use crate::{has_skip_comment, prelude::*};
/// The header of a compound statement clause.
@@ -37,41 +36,7 @@ pub(crate) enum ClauseHeader<'a> {
OrElse(ElseClause<'a>),
}
impl<'a> ClauseHeader<'a> {
/// Returns the last child in the clause body immediately following this clause header.
///
/// For most clauses, this is the last statement in
/// the primary body. For clauses like `try`, it specifically returns the last child
/// in the `try` body, not the `except`/`else`/`finally` clauses.
///
/// This is similar to [`ruff_python_ast::AnyNodeRef::last_child_in_body`]
/// but restricted to the clause.
pub(crate) fn last_child_in_clause(self) -> Option<AnyNodeRef<'a>> {
match self {
ClauseHeader::Class(StmtClassDef { body, .. })
| ClauseHeader::Function(StmtFunctionDef { body, .. })
| ClauseHeader::If(StmtIf { body, .. })
| ClauseHeader::ElifElse(ElifElseClause { body, .. })
| ClauseHeader::Try(StmtTry { body, .. })
| ClauseHeader::MatchCase(MatchCase { body, .. })
| ClauseHeader::For(StmtFor { body, .. })
| ClauseHeader::While(StmtWhile { body, .. })
| ClauseHeader::With(StmtWith { body, .. })
| ClauseHeader::ExceptHandler(ExceptHandlerExceptHandler { body, .. })
| ClauseHeader::OrElse(
ElseClause::Try(StmtTry { orelse: body, .. })
| ElseClause::For(StmtFor { orelse: body, .. })
| ElseClause::While(StmtWhile { orelse: body, .. }),
)
| ClauseHeader::TryFinally(StmtTry {
finalbody: body, ..
}) => body.last().map(AnyNodeRef::from),
ClauseHeader::Match(StmtMatch { cases, .. }) => cases
.last()
.and_then(|case| case.body.last().map(AnyNodeRef::from)),
}
}
impl ClauseHeader<'_> {
/// The range from the clause keyword up to and including the final colon.
pub(crate) fn range(self, source: &str) -> FormatResult<TextRange> {
let keyword_range = self.first_keyword_range(source)?;
@@ -373,28 +338,6 @@ impl<'a> ClauseHeader<'a> {
}
}
impl<'a> From<ClauseHeader<'a>> for AnyNodeRef<'a> {
fn from(value: ClauseHeader<'a>) -> Self {
match value {
ClauseHeader::Class(stmt_class_def) => stmt_class_def.into(),
ClauseHeader::Function(stmt_function_def) => stmt_function_def.into(),
ClauseHeader::If(stmt_if) => stmt_if.into(),
ClauseHeader::ElifElse(elif_else_clause) => elif_else_clause.into(),
ClauseHeader::Try(stmt_try) => stmt_try.into(),
ClauseHeader::ExceptHandler(except_handler_except_handler) => {
except_handler_except_handler.into()
}
ClauseHeader::TryFinally(stmt_try) => stmt_try.into(),
ClauseHeader::Match(stmt_match) => stmt_match.into(),
ClauseHeader::MatchCase(match_case) => match_case.into(),
ClauseHeader::For(stmt_for) => stmt_for.into(),
ClauseHeader::While(stmt_while) => stmt_while.into(),
ClauseHeader::With(stmt_with) => stmt_with.into(),
ClauseHeader::OrElse(else_clause) => else_clause.into(),
}
}
}
#[derive(Copy, Clone)]
pub(crate) enum ElseClause<'a> {
Try(&'a StmtTry),
@@ -402,16 +345,6 @@ pub(crate) enum ElseClause<'a> {
While(&'a StmtWhile),
}
impl<'a> From<ElseClause<'a>> for AnyNodeRef<'a> {
fn from(value: ElseClause<'a>) -> Self {
match value {
ElseClause::Try(stmt_try) => stmt_try.into(),
ElseClause::For(stmt_for) => stmt_for.into(),
ElseClause::While(stmt_while) => stmt_while.into(),
}
}
}
pub(crate) struct FormatClauseHeader<'a, 'ast> {
header: ClauseHeader<'a>,
/// How to format the clause header
@@ -445,6 +378,22 @@ where
}
}
impl<'a> FormatClauseHeader<'a, '_> {
/// Sets the leading comments that precede an alternate branch.
#[must_use]
pub(crate) fn with_leading_comments<N>(
mut self,
comments: &'a [SourceComment],
last_node: Option<N>,
) -> Self
where
N: Into<AnyNodeRef<'a>>,
{
self.leading_comments = Some((comments, last_node.map(Into::into)));
self
}
}
impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
if let Some((leading_comments, last_node)) = self.leading_comments {
@@ -474,13 +423,13 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> {
}
}
struct FormatClauseBody<'a> {
pub(crate) struct FormatClauseBody<'a> {
body: &'a Suite,
kind: SuiteKind,
trailing_comments: &'a [SourceComment],
}
fn clause_body<'a>(
pub(crate) fn clause_body<'a>(
body: &'a Suite,
kind: SuiteKind,
trailing_comments: &'a [SourceComment],
@@ -516,84 +465,6 @@ impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
}
}
pub(crate) struct FormatClause<'a, 'ast> {
header: ClauseHeader<'a>,
/// How to format the clause header
header_formatter: Argument<'a, PyFormatContext<'ast>>,
/// Leading comments coming before the branch, together with the previous node, if any. Only relevant
/// for alternate branches.
leading_comments: Option<(&'a [SourceComment], Option<AnyNodeRef<'a>>)>,
/// The trailing comments coming after the colon.
trailing_colon_comment: &'a [SourceComment],
body: &'a Suite,
kind: SuiteKind,
}
impl<'a, 'ast> FormatClause<'a, 'ast> {
/// Sets the leading comments that precede an alternate branch.
#[must_use]
pub(crate) fn with_leading_comments<N>(
mut self,
comments: &'a [SourceComment],
last_node: Option<N>,
) -> Self
where
N: Into<AnyNodeRef<'a>>,
{
self.leading_comments = Some((comments, last_node.map(Into::into)));
self
}
fn clause_header(&self) -> FormatClauseHeader<'a, 'ast> {
FormatClauseHeader {
header: self.header,
formatter: self.header_formatter,
leading_comments: self.leading_comments,
trailing_colon_comment: self.trailing_colon_comment,
}
}
fn clause_body(&self) -> FormatClauseBody<'a> {
clause_body(self.body, self.kind, self.trailing_colon_comment)
}
}
/// Formats a clause, handling the case where the compound
/// statement lies on a single line with `# fmt: skip` and
/// should be suppressed.
pub(crate) fn clause<'a, 'ast, Content>(
header: ClauseHeader<'a>,
header_formatter: &'a Content,
trailing_colon_comment: &'a [SourceComment],
body: &'a Suite,
kind: SuiteKind,
) -> FormatClause<'a, 'ast>
where
Content: Format<PyFormatContext<'ast>>,
{
FormatClause {
header,
header_formatter: Argument::new(header_formatter),
leading_comments: None,
trailing_colon_comment,
body,
kind,
}
}
impl<'ast> Format<PyFormatContext<'ast>> for FormatClause<'_, 'ast> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
match should_suppress_clause(self, f)? {
SuppressClauseHeader::Yes {
last_child_in_clause,
} => write_suppressed_clause(self, f, last_child_in_clause),
SuppressClauseHeader::No => {
write!(f, [self.clause_header(), self.clause_body()])
}
}
}
}
/// Finds the range of `keyword` starting the search at `start_position`.
///
/// If the start position is at the end of the previous statement, the
@@ -716,96 +587,3 @@ fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResu
}
}
}
fn should_suppress_clause<'a>(
clause: &FormatClause<'a, '_>,
f: &mut Formatter<PyFormatContext<'_>>,
) -> FormatResult<SuppressClauseHeader<'a>> {
let source = f.context().source();
let Some(last_child_in_clause) = clause.header.last_child_in_clause() else {
return Ok(SuppressClauseHeader::No);
};
// Early return if we don't have a skip comment
// to avoid computing header range in the common case
if !has_skip_comment(
f.context().comments().trailing(last_child_in_clause),
source,
) {
return Ok(SuppressClauseHeader::No);
}
let clause_start = clause.header.range(source)?.end();
let clause_range = TextRange::new(clause_start, last_child_in_clause.end());
// Only applies to clauses on a single line
if source.contains_line_break(clause_range) {
return Ok(SuppressClauseHeader::No);
}
Ok(SuppressClauseHeader::Yes {
last_child_in_clause,
})
}
#[cold]
fn write_suppressed_clause(
clause: &FormatClause,
f: &mut Formatter<PyFormatContext<'_>>,
last_child_in_clause: AnyNodeRef,
) -> FormatResult<()> {
if let Some((leading_comments, last_node)) = clause.leading_comments {
leading_alternate_branch_comments(leading_comments, last_node).fmt(f)?;
}
let header = clause.header;
let clause_start = header.first_keyword_range(f.context().source())?.start();
let comments = f.context().comments().clone();
let clause_end = last_child_in_clause.end();
// Write the outer comments and format the node as verbatim
write!(
f,
[
source_position(clause_start),
verbatim_text(TextRange::new(clause_start, clause_end)),
source_position(clause_end),
trailing_comments(comments.trailing(last_child_in_clause)),
hard_line_break()
]
)?;
// We mark comments in the header as formatted as in
// the implementation of [`write_suppressed_clause_header`].
//
// Note that the header may be multi-line and contain
// various comments since we only require that the range
// starting at the _colon_ and ending at the `# fmt: skip`
// fits on one line.
header.visit(&mut |child| {
for comment in comments.leading_trailing(child) {
comment.mark_formatted();
}
comments.mark_verbatim_node_comments_formatted(child);
});
// Similarly we mark the comments in the body as formatted.
// Note that the trailing comments for the last child in the
// body have already been handled above.
for stmt in clause.body {
comments.mark_verbatim_node_comments_formatted(stmt.into());
}
Ok(())
}
enum SuppressClauseHeader<'a> {
No,
Yes {
last_child_in_clause: AnyNodeRef<'a>,
},
}

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