Compare commits

..

2 Commits

Author SHA1 Message Date
Brent Westbrook
79d526cd91 attempting a fix 2025-11-11 16:33:59 -05:00
Brent Westbrook
376571eed1 add test files from #21385 2025-11-11 15:48:47 -05:00
287 changed files with 3677 additions and 13888 deletions

View File

@@ -7,10 +7,6 @@ serial = { max-threads = 1 }
filter = 'binary(file_watching)'
test-group = 'serial'
[[profile.default.overrides]]
filter = 'binary(e2e)'
test-group = 'serial'
[profile.ci]
# Print out output for failing tests as soon as they fail, and also at the end
# of the run (for easy scrollability).

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@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
with:
enable-cache: "true"
- name: ty mdtests (GitHub annotations)
@@ -319,17 +319,19 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
with:
enable-cache: "true"
- name: "Run tests"
run: cargo nextest run --cargo-profile profiling --all-features
- name: "Run doctests"
run: cargo test --doc --profile profiling --all-features
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
cargo-test-other:
strategy:
@@ -352,11 +354,11 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
with:
enable-cache: "true"
- name: "Run tests"
@@ -462,7 +464,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
shared-key: ruff-linux-debug
@@ -497,7 +499,7 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -532,7 +534,7 @@ jobs:
ref: ${{ github.event.pull_request.base.ref }}
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
with:
python-version: ${{ env.PYTHON_VERSION }}
activate-environment: true
@@ -638,7 +640,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -697,7 +699,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -748,7 +750,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -792,7 +794,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
with:
python-version: 3.13
activate-environment: true
@@ -947,13 +949,13 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-codspeed
@@ -961,7 +963,7 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
uses: CodSpeedHQ/action@bb005fe1c1eea036d3894f02c049cb6b154a1c27 # v4.3.3
with:
mode: instrumentation
run: cargo codspeed run
@@ -987,13 +989,13 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-codspeed
@@ -1001,7 +1003,7 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
uses: CodSpeedHQ/action@bb005fe1c1eea036d3894f02c049cb6b154a1c27 # v4.3.3
with:
mode: instrumentation
run: cargo codspeed run
@@ -1027,13 +1029,13 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-codspeed
@@ -1041,7 +1043,7 @@ jobs:
run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
uses: CodSpeedHQ/action@bb005fe1c1eea036d3894f02c049cb6b154a1c27 # v4.3.3
env:
# enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't
# appear to provide much useful insight for our walltime benchmarks right now

View File

@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- 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@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
@@ -80,7 +80,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- 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@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
pattern: wheels-*

View File

@@ -77,7 +77,7 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- 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@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- 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@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -207,22 +207,17 @@ jobs:
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
if: ${{ success() }}
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-nextest
- name: "Install cargo insta"
if: ${{ success() }}
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
tool: cargo-insta
- name: Update snapshots
if: ${{ success() }}
run: |
cargo r \
--profile=profiling \
-p ty_completion_eval \
-- all --tasks ./crates/ty_completion_eval/completion-evaluation-tasks.csv
# The `cargo insta` docs indicate that `--unreferenced=delete` might be a good option,
# but from local testing it appears to just revert all changes made by `cargo insta test --accept`.
#

View File

@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
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@11aa5472cf9d6b9e019c401505a093112942d7bf"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
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@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
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@11aa5472cf9d6b9e019c401505a093112942d7bf"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
ecosystem-analyzer \
--verbose \

View File

@@ -1,58 +1,5 @@
# Changelog
## 0.14.5
Released on 2025-11-13.
### Preview features
- \[`flake8-simplify`\] Apply `SIM113` when index variable is of type `int` ([#21395](https://github.com/astral-sh/ruff/pull/21395))
- \[`pydoclint`\] Fix false positive when Sphinx directives follow a "Raises" section (`DOC502`) ([#20535](https://github.com/astral-sh/ruff/pull/20535))
- \[`pydoclint`\] Support NumPy-style comma-separated parameters (`DOC102`) ([#20972](https://github.com/astral-sh/ruff/pull/20972))
- \[`refurb`\] Auto-fix annotated assignments (`FURB101`) ([#21278](https://github.com/astral-sh/ruff/pull/21278))
- \[`ruff`\] Ignore `str()` when not used for simple conversion (`RUF065`) ([#21330](https://github.com/astral-sh/ruff/pull/21330))
### Bug fixes
- Fix syntax error false positive on alternative `match` patterns ([#21362](https://github.com/astral-sh/ruff/pull/21362))
- \[`flake8-simplify`\] Fix false positive for iterable initializers with generator arguments (`SIM222`) ([#21187](https://github.com/astral-sh/ruff/pull/21187))
- \[`pyupgrade`\] Fix false positive on relative imports from local `.builtins` module (`UP029`) ([#21309](https://github.com/astral-sh/ruff/pull/21309))
- \[`pyupgrade`\] Consistently set the deprecated tag (`UP035`) ([#21396](https://github.com/astral-sh/ruff/pull/21396))
### Rule changes
- \[`refurb`\] Detect empty f-strings (`FURB105`) ([#21348](https://github.com/astral-sh/ruff/pull/21348))
### CLI
- Add option to provide a reason to `--add-noqa` ([#21294](https://github.com/astral-sh/ruff/pull/21294))
- Add upstream linter URL to `ruff linter --output-format=json` ([#21316](https://github.com/astral-sh/ruff/pull/21316))
- Add color to `--help` ([#21337](https://github.com/astral-sh/ruff/pull/21337))
### Documentation
- Add a new "Opening a PR" section to the contribution guide ([#21298](https://github.com/astral-sh/ruff/pull/21298))
- Added the PyScripter IDE to the list of "Who is using Ruff?" ([#21402](https://github.com/astral-sh/ruff/pull/21402))
- Update PyCharm setup instructions ([#21409](https://github.com/astral-sh/ruff/pull/21409))
- \[`flake8-annotations`\] Add link to `allow-star-arg-any` option (`ANN401`) ([#21326](https://github.com/astral-sh/ruff/pull/21326))
### Other changes
- \[`configuration`\] Improve error message when `line-length` exceeds `u16::MAX` ([#21329](https://github.com/astral-sh/ruff/pull/21329))
### Contributors
- [@njhearp](https://github.com/njhearp)
- [@11happy](https://github.com/11happy)
- [@hugovk](https://github.com/hugovk)
- [@Gankra](https://github.com/Gankra)
- [@ntBre](https://github.com/ntBre)
- [@pyscripter](https://github.com/pyscripter)
- [@danparizher](https://github.com/danparizher)
- [@MichaReiser](https://github.com/MichaReiser)
- [@henryiii](https://github.com/henryiii)
- [@charliecloudberry](https://github.com/charliecloudberry)
## 0.14.4
Released on 2025-11-06.

33
Cargo.lock generated
View File

@@ -1238,9 +1238,9 @@ dependencies = [
[[package]]
name = "get-size-derive2"
version = "0.7.2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff47daa61505c85af126e9dd64af6a342a33dc0cccfe1be74ceadc7d352e6efd"
checksum = "46b134aa084df7c3a513a1035c52f623e4b3065dfaf3d905a4f28a2e79b5bb3f"
dependencies = [
"attribute-derive",
"quote",
@@ -1249,9 +1249,9 @@ dependencies = [
[[package]]
name = "get-size2"
version = "0.7.2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af"
checksum = "c0d51c9f2e956a517619ad9e7eaebc7a573f9c49b38152e12eade750f89156f9"
dependencies = [
"compact_str",
"get-size-derive2",
@@ -1575,9 +1575,9 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.18.3"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88"
checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65"
dependencies = [
"console 0.16.1",
"portable-atomic",
@@ -2606,9 +2606,9 @@ dependencies = [
[[package]]
name = "quick-junit"
version = "0.5.2"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ee9342d671fae8d66b3ae9fd7a9714dfd089c04d2a8b1ec0436ef77aee15e5f"
checksum = "3ed1a693391a16317257103ad06a88c6529ac640846021da7c435a06fffdacd7"
dependencies = [
"chrono",
"indexmap",
@@ -2621,9 +2621,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.38.4"
version = "0.37.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
dependencies = [
"memchr",
]
@@ -2858,7 +2858,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.5"
version = "0.14.4"
dependencies = [
"anyhow",
"argfile",
@@ -3115,7 +3115,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.5"
version = "0.14.4"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3470,7 +3470,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.5"
version = "0.14.4"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3586,7 +3586,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
dependencies = [
"boxcar",
"compact_str",
@@ -3610,12 +3610,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
dependencies = [
"proc-macro2",
"quote",
@@ -4521,7 +4521,6 @@ dependencies = [
"serde",
"serde_json",
"shellexpand",
"smallvec",
"tempfile",
"thiserror 2.0.17",
"tracing",

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 = "a885bb4c4c192741b8a17418fef81a71e33d111e", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "05a9af7f554b64b8aadc2eeb6f2caf73d0408d09", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

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.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.5/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.4/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.4/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.5
rev: v0.14.4
hooks:
# Run the linter.
- id: ruff-check
@@ -491,7 +491,6 @@ Ruff is used by a number of major open-source projects and companies, including:
- [PyTorch](https://github.com/pytorch/pytorch)
- [Pydantic](https://github.com/pydantic/pydantic)
- [Pylint](https://github.com/PyCQA/pylint)
- [PyScripter](https://github.com/pyscripter/pyscripter)
- [PyVista](https://github.com/pyvista/pyvista)
- [Reflex](https://github.com/reflex-dev/reflex)
- [River](https://github.com/online-ml/river)

View File

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

View File

@@ -167,7 +167,6 @@ pub enum AnalyzeCommand {
}
#[derive(Clone, Debug, clap::Parser)]
#[expect(clippy::struct_excessive_bools)]
pub struct AnalyzeGraphCommand {
/// List of files or directories to include.
#[clap(help = "List of files or directories to include [default: .]")]
@@ -194,12 +193,6 @@ pub struct AnalyzeGraphCommand {
/// Path to a virtual environment to use for resolving additional dependencies
#[arg(long)]
python: Option<PathBuf>,
/// Include imports that are only used for type checking (i.e., imports within `if TYPE_CHECKING:` blocks).
/// Use `--no-type-checking-imports` to exclude imports that are only used for type checking.
#[arg(long, overrides_with("no_type_checking_imports"))]
type_checking_imports: bool,
#[arg(long, overrides_with("type_checking_imports"), hide = true)]
no_type_checking_imports: bool,
}
// The `Parser` derive is for ruff_dev, for ruff `Args` would be sufficient
@@ -846,10 +839,6 @@ impl AnalyzeGraphCommand {
string_imports_min_dots: self.min_dots,
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
target_version: self.target_version.map(ast::PythonVersion::from),
type_checking_imports: resolve_bool_arg(
self.type_checking_imports,
self.no_type_checking_imports,
),
..ExplicitConfigOverrides::default()
};
@@ -1346,7 +1335,6 @@ struct ExplicitConfigOverrides {
extension: Option<Vec<ExtensionPair>>,
detect_string_imports: Option<bool>,
string_imports_min_dots: Option<usize>,
type_checking_imports: Option<bool>,
}
impl ConfigurationTransformer for ExplicitConfigOverrides {
@@ -1437,9 +1425,6 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
if let Some(string_imports_min_dots) = &self.string_imports_min_dots {
config.analyze.string_imports_min_dots = Some(*string_imports_min_dots);
}
if let Some(type_checking_imports) = &self.type_checking_imports {
config.analyze.type_checking_imports = Some(*type_checking_imports);
}
config
}

View File

@@ -105,7 +105,6 @@ pub(crate) fn analyze_graph(
let settings = resolver.resolve(path);
let string_imports = settings.analyze.string_imports;
let include_dependencies = settings.analyze.include_dependencies.get(path).cloned();
let type_checking_imports = settings.analyze.type_checking_imports;
// Skip excluded files.
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
@@ -168,7 +167,6 @@ pub(crate) fn analyze_graph(
&path,
package.as_deref(),
string_imports,
type_checking_imports,
)
.unwrap_or_else(|err| {
warn!("Failed to generate import map for {path}: {err}");

View File

@@ -1,193 +0,0 @@
use std::process::Command;
use insta_cmd::assert_cmd_snapshot;
use crate::CliTest;
#[test]
fn type_checking_imports() -> anyhow::Result<()> {
let test = AnalyzeTest::with_files([
("ruff/__init__.py", ""),
(
"ruff/a.py",
r#"
from typing import TYPE_CHECKING
import ruff.b
if TYPE_CHECKING:
import ruff.c
"#,
),
(
"ruff/b.py",
r#"
if TYPE_CHECKING:
from ruff import c
"#,
),
("ruff/c.py", ""),
])?;
assert_cmd_snapshot!(test.command(), @r###"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py",
"ruff/c.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": []
}
----- stderr -----
"###);
assert_cmd_snapshot!(
test.command()
.arg("--no-type-checking-imports"),
@r###"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [],
"ruff/c.py": []
}
----- stderr -----
"###
);
Ok(())
}
#[test]
fn type_checking_imports_from_config() -> anyhow::Result<()> {
let test = AnalyzeTest::with_files([
("ruff/__init__.py", ""),
(
"ruff/a.py",
r#"
from typing import TYPE_CHECKING
import ruff.b
if TYPE_CHECKING:
import ruff.c
"#,
),
(
"ruff/b.py",
r#"
if TYPE_CHECKING:
from ruff import c
"#,
),
("ruff/c.py", ""),
(
"ruff.toml",
r#"
[analyze]
type-checking-imports = false
"#,
),
])?;
assert_cmd_snapshot!(test.command(), @r###"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [],
"ruff/c.py": []
}
----- stderr -----
"###);
test.write_file(
"ruff.toml",
r#"
[analyze]
type-checking-imports = true
"#,
)?;
assert_cmd_snapshot!(test.command(), @r###"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py",
"ruff/c.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": []
}
----- stderr -----
"###
);
Ok(())
}
struct AnalyzeTest {
cli_test: CliTest,
}
impl AnalyzeTest {
pub(crate) fn new() -> anyhow::Result<Self> {
Ok(Self {
cli_test: CliTest::with_settings(|_, mut settings| {
settings.add_filter(r#"\\\\"#, "/");
settings
})?,
})
}
fn with_files<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<Self> {
let case = Self::new()?;
case.write_files(files)?;
Ok(case)
}
#[expect(unused)]
fn with_file(path: impl AsRef<std::path::Path>, content: &str) -> anyhow::Result<Self> {
let fixture = Self::new()?;
fixture.write_file(path, content)?;
Ok(fixture)
}
fn command(&self) -> Command {
let mut command = self.cli_test.command();
command.arg("analyze").arg("graph").arg("--preview");
command
}
}
impl std::ops::Deref for AnalyzeTest {
type Target = CliTest;
fn deref(&self) -> &Self::Target {
&self.cli_test
}
}

View File

@@ -15,7 +15,6 @@ use std::{
};
use tempfile::TempDir;
mod analyze_graph;
mod format;
mod lint;
@@ -63,7 +62,9 @@ impl CliTest {
files: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> anyhow::Result<Self> {
let case = Self::new()?;
case.write_files(files)?;
for file in files {
case.write_file(file.0, file.1)?;
}
Ok(case)
}
@@ -152,16 +153,6 @@ impl CliTest {
Ok(())
}
pub(crate) fn write_files<'a>(
&self,
files: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> Result<()> {
for file in files {
self.write_file(file.0, file.1)?;
}
Ok(())
}
/// Returns the path to the test directory root.
pub(crate) fn root(&self) -> &Path {
&self.project_dir

View File

@@ -9,6 +9,7 @@ info:
- concise
- "--show-settings"
- test.py
snapshot_kind: text
---
success: true
exit_code: 0
@@ -283,6 +284,5 @@ analyze.target_version = 3.10
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -12,6 +12,7 @@ info:
- UP007
- test.py
- "-"
snapshot_kind: text
---
success: true
exit_code: 0
@@ -285,6 +286,5 @@ analyze.target_version = 3.11
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -13,6 +13,7 @@ info:
- UP007
- test.py
- "-"
snapshot_kind: text
---
success: true
exit_code: 0
@@ -287,6 +288,5 @@ analyze.target_version = 3.11
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -14,6 +14,7 @@ info:
- py310
- test.py
- "-"
snapshot_kind: text
---
success: true
exit_code: 0
@@ -287,6 +288,5 @@ analyze.target_version = 3.10
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -11,6 +11,7 @@ info:
- "--select"
- UP007
- foo/test.py
snapshot_kind: text
---
success: true
exit_code: 0
@@ -284,6 +285,5 @@ analyze.target_version = 3.11
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -11,6 +11,7 @@ info:
- "--select"
- UP007
- foo/test.py
snapshot_kind: text
---
success: true
exit_code: 0
@@ -284,6 +285,5 @@ analyze.target_version = 3.10
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -283,6 +283,5 @@ analyze.target_version = 3.10
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -283,6 +283,5 @@ analyze.target_version = 3.10
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -9,6 +9,7 @@ info:
- concise
- test.py
- "--show-settings"
snapshot_kind: text
---
success: true
exit_code: 0
@@ -283,6 +284,5 @@ analyze.target_version = 3.11
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -396,6 +396,5 @@ analyze.target_version = 3.7
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
analyze.type_checking_imports = true
----- stderr -----

View File

@@ -71,13 +71,16 @@ impl Display for Benchmark<'_> {
}
}
fn check_project(db: &ProjectDatabase, project_name: &str, max_diagnostics: usize) {
fn check_project(db: &ProjectDatabase, max_diagnostics: usize) {
let result = db.check();
let diagnostics = result.len();
assert!(
diagnostics > 1 && diagnostics <= max_diagnostics,
"Expected between 1 and {max_diagnostics} diagnostics on project '{project_name}' but got {diagnostics}",
"Expected between {} and {} diagnostics but got {}",
1,
max_diagnostics,
diagnostics
);
}
@@ -181,7 +184,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY39,
},
5000,
1000,
);
static SYMPY: Benchmark = Benchmark::new(
@@ -223,7 +226,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
900,
800,
);
#[track_caller]
@@ -231,11 +234,11 @@ fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) {
bencher
.with_inputs(|| benchmark.setup_iteration())
.bench_local_refs(|db| {
check_project(db, benchmark.project.name, benchmark.max_diagnostics);
check_project(db, benchmark.max_diagnostics);
});
}
#[bench(args=[&ALTAIR, &FREQTRADE, &TANJUN], sample_size=2, sample_count=3)]
#[bench(args=[&ALTAIR, &FREQTRADE, &PYDANTIC, &TANJUN], sample_size=2, sample_count=3)]
fn small(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
@@ -245,12 +248,12 @@ fn medium(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(args=[&SYMPY, &PYDANTIC], sample_size=1, sample_count=2)]
#[bench(args=[&SYMPY], sample_size=1, sample_count=2)]
fn large(bencher: Bencher, benchmark: &Benchmark) {
run_single_threaded(bencher, benchmark);
}
#[bench(args=[&ALTAIR], sample_size=3, sample_count=8)]
#[bench(args=[&PYDANTIC], sample_size=3, sample_count=8)]
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
@@ -258,7 +261,7 @@ fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
.with_inputs(|| benchmark.setup_iteration())
.bench_local_values(|db| {
thread_pool.install(|| {
check_project(&db, benchmark.project.name, benchmark.max_diagnostics);
check_project(&db, benchmark.max_diagnostics);
db
})
});
@@ -282,7 +285,7 @@ fn main() {
// branch when looking up the ingredient index.
{
let db = TANJUN.setup_iteration();
check_project(&db, TANJUN.project.name, TANJUN.max_diagnostics);
check_project(&db, TANJUN.max_diagnostics);
}
divan::main();

View File

@@ -7,7 +7,6 @@ use ruff_source_file::LineIndex;
use crate::Db;
use crate::files::{File, FilePath};
use crate::system::System;
/// Reads the source text of a python text file (must be valid UTF8) or notebook.
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
@@ -16,7 +15,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
let _span = tracing::trace_span!("source_text", file = %path).entered();
let mut read_error = None;
let kind = if is_notebook(db.system(), path) {
let kind = if is_notebook(file.path(db)) {
file.read_to_notebook(db)
.unwrap_or_else(|error| {
tracing::debug!("Failed to read notebook '{path}': {error}");
@@ -41,17 +40,18 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
}
}
fn is_notebook(system: &dyn System, path: &FilePath) -> bool {
let source_type = match path {
FilePath::System(path) => system.source_type(path),
FilePath::SystemVirtual(system_virtual) => system.virtual_path_source_type(system_virtual),
FilePath::Vendored(_) => return false,
};
let with_extension_fallback =
source_type.or_else(|| PySourceType::try_from_extension(path.extension()?));
with_extension_fallback == Some(PySourceType::Ipynb)
fn is_notebook(path: &FilePath) -> bool {
match path {
FilePath::System(system) => system.extension().is_some_and(|extension| {
PySourceType::try_from_extension(extension) == Some(PySourceType::Ipynb)
}),
FilePath::SystemVirtual(system_virtual) => {
system_virtual.extension().is_some_and(|extension| {
PySourceType::try_from_extension(extension) == Some(PySourceType::Ipynb)
})
}
FilePath::Vendored(_) => false,
}
}
/// The source text of a file containing python code.

View File

@@ -9,7 +9,6 @@ pub use os::OsSystem;
use filetime::FileTime;
use ruff_notebook::{Notebook, NotebookError};
use ruff_python_ast::PySourceType;
use std::error::Error;
use std::fmt::{Debug, Formatter};
use std::path::{Path, PathBuf};
@@ -17,11 +16,12 @@ use std::{fmt, io};
pub use test::{DbWithTestSystem, DbWithWritableSystem, InMemorySystem, TestSystem};
use walk_directory::WalkDirectoryBuilder;
use crate::file_revision::FileRevision;
pub use self::path::{
DeduplicatedNestedPathsIter, SystemPath, SystemPathBuf, SystemVirtualPath,
SystemVirtualPathBuf, deduplicate_nested_paths,
};
use crate::file_revision::FileRevision;
mod memory_fs;
#[cfg(feature = "os")]
@@ -66,35 +66,6 @@ pub trait System: Debug + Sync + Send {
/// See [dunce::canonicalize] for more information.
fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf>;
/// Returns the source type for `path` if known or `None`.
///
/// The default is to always return `None`, assuming the system
/// has no additional information and that the caller should
/// rely on the file extension instead.
///
/// This is primarily used for the LSP integration to respect
/// the chosen language (or the fact that it is a notebook) in
/// the editor.
fn source_type(&self, path: &SystemPath) -> Option<PySourceType> {
let _ = path;
None
}
/// Returns the source type for `path` if known or `None`.
///
/// The default is to always return `None`, assuming the system
/// has no additional information and that the caller should
/// rely on the file extension instead.
///
/// This is primarily used for the LSP integration to respect
/// the chosen language (or the fact that it is a notebook) in
/// the editor.
fn virtual_path_source_type(&self, path: &SystemVirtualPath) -> Option<PySourceType> {
let _ = path;
None
}
/// Reads the content of the file at `path` into a [`String`].
fn read_to_string(&self, path: &SystemPath) -> Result<String>;

View File

@@ -14,21 +14,14 @@ pub(crate) struct Collector<'a> {
string_imports: StringImports,
/// The collected imports from the Python AST.
imports: Vec<CollectedImport>,
/// Whether to detect type checking imports
type_checking_imports: bool,
}
impl<'a> Collector<'a> {
pub(crate) fn new(
module_path: Option<&'a [String]>,
string_imports: StringImports,
type_checking_imports: bool,
) -> Self {
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: StringImports) -> Self {
Self {
module_path,
string_imports,
imports: Vec::new(),
type_checking_imports,
}
}
@@ -98,25 +91,10 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
}
}
}
Stmt::If(ast::StmtIf {
test,
body,
elif_else_clauses,
range: _,
node_index: _,
}) => {
// Skip TYPE_CHECKING blocks if not requested
if self.type_checking_imports || !is_type_checking_condition(test) {
self.visit_body(body);
}
for clause in elif_else_clauses {
self.visit_elif_else_clause(clause);
}
}
Stmt::FunctionDef(_)
| Stmt::ClassDef(_)
| Stmt::While(_)
| Stmt::If(_)
| Stmt::With(_)
| Stmt::Match(_)
| Stmt::Try(_)
@@ -174,30 +152,6 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
}
}
/// Check if an expression is a `TYPE_CHECKING` condition.
///
/// Returns `true` for:
/// - `TYPE_CHECKING`
/// - `typing.TYPE_CHECKING`
///
/// NOTE: Aliased `TYPE_CHECKING`, i.e. `import typing.TYPE_CHECKING as TC; if TC: ...`
/// will not be detected!
fn is_type_checking_condition(expr: &Expr) -> bool {
match expr {
// `if TYPE_CHECKING:`
Expr::Name(ast::ExprName { id, .. }) => id.as_str() == "TYPE_CHECKING",
// `if typing.TYPE_CHECKING:`
Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
attr.as_str() == "TYPE_CHECKING"
&& matches!(
value.as_ref(),
Expr::Name(ast::ExprName { id, .. }) if id.as_str() == "typing"
)
}
_ => false,
}
}
#[derive(Debug)]
pub(crate) enum CollectedImport {
/// The import was part of an `import` statement.

View File

@@ -30,7 +30,6 @@ impl ModuleImports {
path: &SystemPath,
package: Option<&SystemPath>,
string_imports: StringImports,
type_checking_imports: bool,
) -> Result<Self> {
// Parse the source code.
let parsed = parse(source, ParseOptions::from(source_type))?;
@@ -39,12 +38,8 @@ impl ModuleImports {
package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path()));
// Collect the imports.
let imports = Collector::new(
module_path.as_deref(),
string_imports,
type_checking_imports,
)
.collect(parsed.syntax());
let imports =
Collector::new(module_path.as_deref(), string_imports).collect(parsed.syntax());
// Resolve the imports.
let mut resolved_imports = ModuleImports::default();

View File

@@ -6,7 +6,7 @@ use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, CacheKey)]
#[derive(Debug, Default, Clone, CacheKey)]
pub struct AnalyzeSettings {
pub exclude: FilePatternSet,
pub preview: PreviewMode,
@@ -14,21 +14,6 @@ pub struct AnalyzeSettings {
pub string_imports: StringImports,
pub include_dependencies: BTreeMap<PathBuf, (PathBuf, Vec<String>)>,
pub extension: ExtensionMapping,
pub type_checking_imports: bool,
}
impl Default for AnalyzeSettings {
fn default() -> Self {
Self {
exclude: FilePatternSet::default(),
preview: PreviewMode::default(),
target_version: PythonVersion::default(),
string_imports: StringImports::default(),
include_dependencies: BTreeMap::default(),
extension: ExtensionMapping::default(),
type_checking_imports: true,
}
}
}
impl fmt::Display for AnalyzeSettings {
@@ -44,7 +29,6 @@ impl fmt::Display for AnalyzeSettings {
self.string_imports,
self.extension | debug,
self.include_dependencies | debug,
self.type_checking_imports,
]
}
Ok(())

View File

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

View File

@@ -46,8 +46,7 @@ def func():
def func():
# SIM113
# https://github.com/astral-sh/ruff/pull/21395
# OK (index doesn't start at 0
idx = 10
for x in range(5):
g(x, idx)

View File

@@ -371,61 +371,6 @@ class Foo:
"""
return
# DOC102 - Test case from issue #20959: comma-separated parameters
def leq(x: object, y: object) -> bool:
"""Compare two objects for loose equality.
Parameters
----------
x1, x2 : object
Objects.
Returns
-------
bool
Whether the objects are identical or equal.
"""
return x is y or x == y
# OK - comma-separated parameters that match function signature
def compare_values(x1: int, x2: int) -> bool:
"""Compare two integer values.
Parameters
----------
x1, x2 : int
Values to compare.
Returns
-------
bool
True if values are equal.
"""
return x1 == x2
# DOC102 - mixed comma-separated and regular parameters
def process_data(data, x1: str, x2: str) -> str:
"""Process data with multiple string parameters.
Parameters
----------
data : list
Input data to process.
x1, x2 : str
String parameters for processing.
extra_param : str
Extra parameter not in signature.
Returns
-------
str
Processed result.
"""
return f"{x1}{x2}{len(data)}"
# OK
def baz(x: int) -> int:
"""
@@ -444,21 +389,3 @@ def baz(x: int) -> int:
int
"""
return x
# OK - comma-separated parameters without type annotations
def add_numbers(a, b):
"""
Adds two numbers and returns the result.
Parameters
----------
a, b
The numbers to add.
Returns
-------
int
The sum of the two numbers.
"""
return a + b

View File

@@ -83,37 +83,6 @@ def calculate_speed(distance: float, time: float) -> float:
raise
# DOC502 regression for Sphinx directive after Raises (issue #18959)
def foo():
"""First line.
Raises:
ValueError:
some text
.. versionadded:: 0.7.0
The ``init_kwargs`` argument.
"""
raise ValueError
# DOC502 regression for following section with colons
def example_with_following_section():
"""Summary.
Returns:
str: The resulting expression.
Raises:
ValueError: If the unit is not valid.
Relation to `time_range_lookup`:
- Handles the "start of" modifier.
- Example: "start of month" → `DATETRUNC()`.
"""
raise ValueError
# This should NOT trigger DOC502 because OSError is explicitly re-raised
def f():
"""Do nothing.

View File

@@ -117,33 +117,3 @@ def calculate_speed(distance: float, time: float) -> float:
except TypeError:
print("Not a number? Shame on you!")
raise
# DOC502 regression for Sphinx directive after Raises (issue #18959)
def foo():
"""First line.
Raises
------
ValueError
some text
.. versionadded:: 0.7.0
The ``init_kwargs`` argument.
"""
raise ValueError
# Make sure we don't bail out on a Sphinx directive in the description of one
# of the exceptions
def foo():
"""First line.
Raises
------
ValueError
some text
.. math:: e^{xception}
ZeroDivisionError
Will not be raised, DOC502
"""
raise ValueError

View File

@@ -152,13 +152,4 @@ import json
data = {"price": 100}
with open("test.json", "wb") as f:
f.write(json.dumps(data, indent=4).encode("utf-8"))
# See: https://github.com/astral-sh/ruff/issues/21381
with open("tmp_path/pyproject.toml", "w") as f:
f.write(dedent(
"""
[project]
other = 1.234
""",
))
f.write(json.dumps(data, indent=4).encode("utf-8"))

View File

@@ -132,9 +132,3 @@ class AWithQuotes:
final_variable: 'Final[list[int]]' = []
class_variable_without_subscript: 'ClassVar' = []
final_variable_without_subscript: 'Final' = []
# Reassignment of a ClassVar should not trigger RUF012
class P:
class_variable: ClassVar[list] = [10, 20, 30, 40, 50]
class_variable = [*class_variable[0::1], *class_variable[2::3]]

View File

@@ -860,17 +860,23 @@ impl SemanticSyntaxContext for Checker<'_> {
}
fn is_bound_parameter(&self, name: &str) -> bool {
match self.semantic.current_scope().kind {
ScopeKind::Function(ast::StmtFunctionDef { parameters, .. }) => {
parameters.includes(name)
for scope in self.semantic.current_scopes() {
match scope.kind {
ScopeKind::Class(_) => return false,
ScopeKind::Function(ast::StmtFunctionDef { parameters, .. })
| ScopeKind::Lambda(ast::ExprLambda {
parameters: Some(parameters),
..
}) => return parameters.includes(name),
ScopeKind::Lambda(_)
| ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
| ScopeKind::DunderClassCell => {}
}
ScopeKind::Class(_)
| ScopeKind::Lambda(_)
| ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
| ScopeKind::DunderClassCell => false,
}
false
}
}

View File

@@ -83,7 +83,7 @@ impl<'a> Importer<'a> {
.into_edit(&required_import)
} else {
// Insert at the start of the file.
Insertion::start_of_file(self.python_ast, self.source, self.stylist, None)
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
.into_edit(&required_import)
}
}
@@ -113,7 +113,7 @@ impl<'a> Importer<'a> {
Insertion::end_of_statement(stmt, self.source, self.stylist)
} else {
// Insert at the start of the file.
Insertion::start_of_file(self.python_ast, self.source, self.stylist, None)
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
};
let add_import_edit = insertion.into_edit(&content);
@@ -498,7 +498,7 @@ impl<'a> Importer<'a> {
Insertion::end_of_statement(stmt, self.source, self.stylist)
} else {
// Insert at the start of the file.
Insertion::start_of_file(self.python_ast, self.source, self.stylist, None)
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
};
if insertion.is_inline() {
Err(anyhow::anyhow!(

View File

@@ -269,8 +269,3 @@ pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSett
pub(crate) const fn is_extended_i18n_function_matching_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()
}

View File

@@ -61,7 +61,6 @@ mod tests {
#[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))]
#[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))]
#[test_case(Rule::EnumerateForLoop, Path::new("SIM113.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -1,8 +1,6 @@
use crate::preview::is_enumerate_for_loop_int_index_enabled;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt};
use ruff_python_ast::{self as ast, Expr, Int, Number, Operator, Stmt};
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged;
@@ -13,9 +11,6 @@ use crate::checkers::ast::Checker;
/// Checks for `for` loops with explicit loop-index variables that can be replaced
/// with `enumerate()`.
///
/// In [preview], this rule checks for index variables initialized with any integer rather than only
/// a literal zero.
///
/// ## Why is this bad?
/// When iterating over a sequence, it's often desirable to keep track of the
/// index of each element alongside the element itself. Prefer the `enumerate`
@@ -40,8 +35,6 @@ use crate::checkers::ast::Checker;
///
/// ## References
/// - [Python documentation: `enumerate`](https://docs.python.org/3/library/functions.html#enumerate)
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.2.0")]
pub(crate) struct EnumerateForLoop {
@@ -89,21 +82,17 @@ pub(crate) fn enumerate_for_loop(checker: &Checker, for_stmt: &ast::StmtFor) {
continue;
}
// Ensure that the index variable was initialized to 0 (or instance of `int` if preview is enabled).
// Ensure that the index variable was initialized to 0.
let Some(value) = typing::find_binding_value(binding, checker.semantic()) else {
continue;
};
if !(matches!(
if !matches!(
value,
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: Number::Int(Int::ZERO),
..
})
) || matches!(
ResolvedPythonType::from(value),
ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer))
) && is_enumerate_for_loop_int_index_enabled(checker.settings()))
{
) {
continue;
}

View File

@@ -1,60 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
--> SIM113.py:6:9
|
4 | for x in range(5):
5 | g(x, idx)
6 | idx += 1
| ^^^^^^^^
7 | h(x)
|
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
--> SIM113.py:17:9
|
15 | if g(x):
16 | break
17 | idx += 1
| ^^^^^^^^
18 | sum += h(x, idx)
|
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
--> SIM113.py:27:9
|
25 | g(x)
26 | h(x, y)
27 | idx += 1
| ^^^^^^^^
|
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
--> SIM113.py:36:9
|
34 | for x in range(5):
35 | sum += h(x, idx)
36 | idx += 1
| ^^^^^^^^
|
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
--> SIM113.py:44:9
|
42 | for x in range(5):
43 | g(x, idx)
44 | idx += 1
| ^^^^^^^^
45 | h(x)
|
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
--> SIM113.py:54:9
|
52 | for x in range(5):
53 | g(x, idx)
54 | idx += 1
| ^^^^^^^^
55 | h(x)
|

View File

@@ -661,31 +661,19 @@ fn parse_parameters_numpy(content: &str, content_start: TextSize) -> Vec<Paramet
.is_some_and(|first_char| !first_char.is_whitespace())
{
if let Some(before_colon) = entry.split(':').next() {
let param_line = before_colon.trim_end();
let param = before_colon.trim_end();
let param_name = param.trim_start_matches('*');
if is_identifier(param_name) {
let param_start = line_start + indentation.text_len();
let param_end = param_start + param.text_len();
// Split on commas to handle comma-separated parameters
let mut current_offset = TextSize::from(0);
for param_part in param_line.split(',') {
let param_part_trimmed = param_part.trim();
let param_name = param_part_trimmed.trim_start_matches('*');
if is_identifier(param_name) {
// Calculate the position of this specific parameter part within the line
// Account for leading whitespace that gets trimmed
let param_start_in_line = current_offset
+ (param_part.text_len() - param_part_trimmed.text_len());
let param_start =
line_start + indentation.text_len() + param_start_in_line;
entries.push(ParameterEntry {
name: param_name,
range: TextRange::at(
content_start + param_start,
param_part_trimmed.text_len(),
),
});
}
// Update offset for next iteration: add the part length plus comma length
current_offset = current_offset + param_part.text_len() + ','.text_len();
entries.push(ParameterEntry {
name: param_name,
range: TextRange::new(
content_start + param_start,
content_start + param_end,
),
});
}
}
}
@@ -722,30 +710,12 @@ fn parse_raises(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName
/// ```
fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = Vec::new();
let mut lines = content.lines().peekable();
let Some(first) = lines.peek() else {
return entries;
};
let indentation = &first[..first.len() - first.trim_start().len()];
for potential in lines {
if let Some(entry) = potential.strip_prefix(indentation) {
if let Some(first_char) = entry.chars().next() {
if !first_char.is_whitespace() {
if let Some(colon_idx) = entry.find(':') {
let entry = entry[..colon_idx].trim();
if !entry.is_empty() {
entries.push(QualifiedName::user_defined(entry));
}
}
}
}
} else {
// If we can't strip the expected indentation, check if this is a dedented line
// (not blank) - if so, break early as we've reached the end of this section
if !potential.trim().is_empty() {
break;
}
}
for potential in content.lines() {
let Some(colon_idx) = potential.find(':') else {
continue;
};
let entry = potential[..colon_idx].trim();
entries.push(QualifiedName::user_defined(entry));
}
entries
}
@@ -769,12 +739,6 @@ fn parse_raises_numpy(content: &str) -> Vec<QualifiedName<'_>> {
let indentation = &dashes[..dashes.len() - dashes.trim_start().len()];
for potential in lines {
if let Some(entry) = potential.strip_prefix(indentation) {
// Check for Sphinx directives (lines starting with ..) - these indicate the end of the
// section. In numpy-style, exceptions are dedented to the same level as sphinx
// directives.
if entry.starts_with("..") {
break;
}
if let Some(first_char) = entry.chars().next() {
if !first_char.is_whitespace() {
entries.push(QualifiedName::user_defined(entry.trim_end()));

View File

@@ -95,23 +95,3 @@ DOC502 Raised exception is not explicitly raised: `DivisionByZero`
82 | return distance / time
|
help: Remove `DivisionByZero` from the docstring
DOC502 Raised exception is not explicitly raised: `ZeroDivisionError`
--> DOC502_numpy.py:139:5
|
137 | # of the exceptions
138 | def foo():
139 | / """First line.
140 | |
141 | | Raises
142 | | ------
143 | | ValueError
144 | | some text
145 | | .. math:: e^{xception}
146 | | ZeroDivisionError
147 | | Will not be raised, DOC502
148 | | """
| |_______^
149 | raise ValueError
|
help: Remove `ZeroDivisionError` from the docstring

View File

@@ -187,36 +187,3 @@ DOC102 Documented parameter `a` is not in the function's signature
302 | b
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `x1` is not in the function's signature
--> DOC102_numpy.py:380:5
|
378 | Parameters
379 | ----------
380 | x1, x2 : object
| ^^
381 | Objects.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `x2` is not in the function's signature
--> DOC102_numpy.py:380:9
|
378 | Parameters
379 | ----------
380 | x1, x2 : object
| ^^
381 | Objects.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `extra_param` is not in the function's signature
--> DOC102_numpy.py:418:5
|
416 | x1, x2 : str
417 | String parameters for processing.
418 | extra_param : str
| ^^^^^^^^^^^
419 | Extra parameter not in signature.
|
help: Remove the extraneous parameter from the docstring

View File

@@ -766,12 +766,11 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport
}
for operation in fixer.with_renames() {
let mut diagnostic = checker.report_diagnostic(
checker.report_diagnostic(
DeprecatedImport {
deprecation: Deprecation::WithRename(operation),
},
import_from_stmt.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
}
}

View File

@@ -2,15 +2,17 @@ use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{
self as ast, Expr, Stmt,
relocate::relocate_expr,
visitor::{self, Visitor},
};
use ruff_text_size::Ranged;
use ruff_python_codegen::Generator;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet;
use crate::importer::ImportRequest;
use crate::rules::refurb::helpers::{FileOpen, find_file_opens};
use crate::{FixAvailability, Locator, Violation};
use crate::{FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `open` and `write` that can be replaced by `pathlib`
@@ -127,7 +129,7 @@ impl<'a> Visitor<'a> for WriteMatcher<'a, '_> {
let open = self.candidates.remove(open);
if self.loop_counter == 0 {
let suggestion = make_suggestion(&open, content, self.checker.locator());
let suggestion = make_suggestion(&open, content, self.checker.generator());
let mut diagnostic = self.checker.report_diagnostic(
WriteWholeFile {
@@ -170,21 +172,27 @@ fn match_write_call(expr: &Expr) -> Option<(&Expr, &Expr)> {
Some((&*attr.value, call.arguments.args.first()?))
}
fn make_suggestion(open: &FileOpen<'_>, arg: &Expr, locator: &Locator) -> String {
let method_name = open.mode.pathlib_method();
let arg_code = locator.slice(arg.range());
if open.keywords.is_empty() {
format!("{method_name}({arg_code})")
} else {
format!(
"{method_name}({arg_code}, {})",
itertools::join(
open.keywords.iter().map(|kw| locator.slice(kw.range())),
", "
)
)
}
fn make_suggestion(open: &FileOpen<'_>, arg: &Expr, generator: Generator) -> String {
let name = ast::ExprName {
id: open.mode.pathlib_method(),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
let mut arg = arg.clone();
relocate_expr(&mut arg, TextRange::default());
let call = ast::ExprCall {
func: Box::new(name.into()),
arguments: ast::Arguments {
args: Box::new([arg]),
keywords: open.keywords.iter().copied().cloned().collect(),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
},
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};
generator.expr(&call.into())
}
fn generate_fix(

View File

@@ -279,34 +279,3 @@ help: Replace with `Path("test.json")....`
- with open("test.json", "wb") as f:
- f.write(json.dumps(data, indent=4).encode("utf-8"))
155 + pathlib.Path("test.json").write_bytes(json.dumps(data, indent=4).encode("utf-8"))
156 |
157 | # See: https://github.com/astral-sh/ruff/issues/21381
158 | with open("tmp_path/pyproject.toml", "w") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....`
--> FURB103.py:158:6
|
157 | # See: https://github.com/astral-sh/ruff/issues/21381
158 | with open("tmp_path/pyproject.toml", "w") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
159 | f.write(dedent(
160 | """
|
help: Replace with `Path("tmp_path/pyproject.toml")....`
148 |
149 | # See: https://github.com/astral-sh/ruff/issues/20785
150 | import json
151 + import pathlib
152 |
153 | data = {"price": 100}
154 |
--------------------------------------------------------------------------------
156 | f.write(json.dumps(data, indent=4).encode("utf-8"))
157 |
158 | # See: https://github.com/astral-sh/ruff/issues/21381
- with open("tmp_path/pyproject.toml", "w") as f:
- f.write(dedent(
159 + pathlib.Path("tmp_path/pyproject.toml").write_text(dedent(
160 | """
161 | [project]
162 | other = 1.234

View File

@@ -209,34 +209,3 @@ help: Replace with `Path("test.json")....`
- with open("test.json", "wb") as f:
- f.write(json.dumps(data, indent=4).encode("utf-8"))
155 + pathlib.Path("test.json").write_bytes(json.dumps(data, indent=4).encode("utf-8"))
156 |
157 | # See: https://github.com/astral-sh/ruff/issues/21381
158 | with open("tmp_path/pyproject.toml", "w") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....`
--> FURB103.py:158:6
|
157 | # See: https://github.com/astral-sh/ruff/issues/21381
158 | with open("tmp_path/pyproject.toml", "w") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
159 | f.write(dedent(
160 | """
|
help: Replace with `Path("tmp_path/pyproject.toml")....`
148 |
149 | # See: https://github.com/astral-sh/ruff/issues/20785
150 | import json
151 + import pathlib
152 |
153 | data = {"price": 100}
154 |
--------------------------------------------------------------------------------
156 | f.write(json.dumps(data, indent=4).encode("utf-8"))
157 |
158 | # See: https://github.com/astral-sh/ruff/issues/21381
- with open("tmp_path/pyproject.toml", "w") as f:
- f.write(dedent(
159 + pathlib.Path("tmp_path/pyproject.toml").write_text(dedent(
160 | """
161 | [project]
162 | other = 1.234

View File

@@ -1,7 +1,6 @@
use rustc_hash::FxHashSet;
use ruff_python_ast::{self as ast, Stmt};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Stmt};
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
use ruff_text_size::Ranged;
@@ -97,9 +96,6 @@ impl Violation for MutableClassDefault {
/// RUF012
pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClassDef) {
// Collect any `ClassVar`s we find in case they get reassigned later.
let mut class_var_targets = FxHashSet::default();
for statement in &class_def.body {
match statement {
Stmt::AnnAssign(ast::StmtAnnAssign {
@@ -108,12 +104,6 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas
value: Some(value),
..
}) => {
if let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
if is_class_var_annotation(annotation, checker.semantic()) {
class_var_targets.insert(id);
}
}
if !is_special_attribute(target)
&& is_mutable_expr(value, checker.semantic())
&& !is_class_var_annotation(annotation, checker.semantic())
@@ -133,12 +123,8 @@ pub(crate) fn mutable_class_default(checker: &Checker, class_def: &ast::StmtClas
}
}
Stmt::Assign(ast::StmtAssign { value, targets, .. }) => {
if !targets.iter().all(|target| {
is_special_attribute(target)
|| target
.as_name_expr()
.is_some_and(|name| class_var_targets.contains(&name.id))
}) && is_mutable_expr(value, checker.semantic())
if !targets.iter().all(is_special_attribute)
&& is_mutable_expr(value, checker.semantic())
{
// Avoid, e.g., Pydantic and msgspec models, which end up copying defaults on instance creation.
if has_default_copy_semantics(class_def, checker.semantic()) {

View File

@@ -294,33 +294,19 @@ impl CellOffsets {
}
/// Returns `true` if the given range contains a cell boundary.
///
/// A range starting at the cell boundary isn't considered to contain the cell boundary
/// as it starts right after it. A range starting before a cell boundary
/// and ending exactly at the boundary is considered to contain the cell boundary.
///
/// # Examples
/// Cell 1:
///
/// ```py
/// import c
/// ```
///
/// Cell 2:
///
/// ```py
/// import os
/// ```
///
/// The range `import c`..`import os`, contains a cell boundary because it starts before cell 2 and ends in cell 2 (`end=cell2_boundary`).
/// The `import os` contains no cell boundary because it starts at the start of cell 2 (at the cell boundary) but doesn't cross into another cell.
pub fn has_cell_boundary(&self, range: TextRange) -> bool {
let after_range_start = self.partition_point(|offset| *offset <= range.start());
let Some(boundary) = self.get(after_range_start).copied() else {
return false;
};
range.contains_inclusive(boundary)
self.binary_search_by(|offset| {
if range.start() <= *offset {
if range.end() < *offset {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
} else {
std::cmp::Ordering::Less
}
})
.is_ok()
}
/// Returns an iterator over [`TextRange`]s covered by each cell.

View File

@@ -39,7 +39,7 @@ impl NotebookIndex {
/// Returns an iterator over the starting rows of each cell (1-based).
///
/// This yields one entry per Python cell (skipping over Markdown cell).
/// This yields one entry per Python cell (skipping over Makrdown cell).
pub fn iter(&self) -> impl Iterator<Item = CellStart> + '_ {
self.cell_starts.iter().copied()
}
@@ -47,7 +47,7 @@ impl NotebookIndex {
/// Translates the given [`LineColumn`] based on the indexing table.
///
/// This will translate the row/column in the concatenated source code
/// to the row/column in the Jupyter Notebook cell.
/// to the row/column in the Jupyter Notebook.
pub fn translate_line_column(&self, source_location: &LineColumn) -> LineColumn {
LineColumn {
line: self
@@ -60,7 +60,7 @@ impl NotebookIndex {
/// Translates the given [`SourceLocation`] based on the indexing table.
///
/// This will translate the line/character in the concatenated source code
/// to the line/character in the Jupyter Notebook cell.
/// to the line/character in the Jupyter Notebook.
pub fn translate_source_location(&self, source_location: &SourceLocation) -> SourceLocation {
SourceLocation {
line: self

View File

@@ -13,7 +13,7 @@ use thiserror::Error;
use ruff_diagnostics::{SourceMap, SourceMarker};
use ruff_source_file::{NewlineWithTrailingNewline, OneIndexed, UniversalNewlineIterator};
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::TextSize;
use crate::cell::CellOffsets;
use crate::index::NotebookIndex;
@@ -294,7 +294,7 @@ impl Notebook {
}
}
/// Build and return the [`NotebookIndex`].
/// Build and return the [`JupyterIndex`].
///
/// ## Notes
///
@@ -388,21 +388,6 @@ impl Notebook {
&self.cell_offsets
}
/// Returns the start offset of the cell at index `cell` in the concatenated
/// text document.
pub fn cell_offset(&self, cell: OneIndexed) -> Option<TextSize> {
self.cell_offsets.get(cell.to_zero_indexed()).copied()
}
/// Returns the text range in the concatenated document of the cell
/// with index `cell`.
pub fn cell_range(&self, cell: OneIndexed) -> Option<TextRange> {
let start = self.cell_offsets.get(cell.to_zero_indexed()).copied()?;
let end = self.cell_offsets.get(cell.to_zero_indexed() + 1).copied()?;
Some(TextRange::new(start, end))
}
/// Return `true` if the notebook has a trailing newline, `false` otherwise.
pub fn trailing_newline(&self) -> bool {
self.trailing_newline

View File

@@ -169,53 +169,3 @@ result = (
# dangling before dot
.b # trailing end-of-line
)
# Regression test for https://github.com/astral-sh/ruff/issues/19350
variable = (
(something) # a comment
.first_method("some string")
)
variable = (
something # a commentdddddddddddddddddddddddddddddd
.first_method("some string")
)
if (
(something) # a commentdddddddddddddddddddddddddddddd
.first_method("some string")
): pass
variable = (
(something # a comment
).first_method("some string")
)
if (
(something # a comment
).first_method("some string") # second comment
): pass
variable = ( # 1
# 2
(something) # 3
# 4
.first_method("some string") # 5
# 6
) # 7
if (
(something
# trailing own line on value
)
.first_method("some string")
): ...
variable = (
(something
# 1
) # 2
.first_method("some string")
)

View File

@@ -0,0 +1,8 @@
[
{
"preview": "disabled"
},
{
"preview": "enabled"
}
]

View File

@@ -125,6 +125,13 @@ lambda a, /, c: a
*x: x
)
(
lambda
# comment
*x,
**y: x
)
(
lambda
# comment 1
@@ -135,6 +142,17 @@ lambda a, /, c: a
x
)
(
lambda
# comment 1
*
# comment 2
x,
**y:
# comment 3
x
)
(
lambda # comment 1
* # comment 2
@@ -142,6 +160,14 @@ lambda a, /, c: a
x
)
(
lambda # comment 1
* # comment 2
x,
y: # comment 3
x
)
lambda *x\
:x
@@ -196,6 +222,17 @@ lambda: ( # comment
x
)
(
lambda # 1
# 2
x, # 3
# 4
y
: # 5
# 6
x
)
(
lambda
x,
@@ -204,6 +241,71 @@ lambda: ( # comment
z
)
# Leading
lambda x: (
lambda y: lambda z: x
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ z # Trailing
) # Trailing
# Leading
lambda x: lambda y: lambda z: [
x,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
z
] # Trailing
# Trailing
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: d
# Regression tests for https://github.com/astral-sh/ruff/issues/8179

View File

@@ -294,39 +294,3 @@ if parent_body:
# d
# e
#f
# Compare behavior with `while`/`else` comment placement
if True: pass
# 1
else:
pass
if True:
pass
# 2
else:
pass
if True: pass
# 3
else:
pass
if True: pass
# 4
else:
pass
def foo():
if True:
pass
# 5
else:
pass
if True:
first;second
# 6
else:
pass

View File

@@ -28,37 +28,3 @@ while (
and anotherCondition or aThirdCondition # trailing third condition
): # comment
print("Do something")
while True: pass
# 1
else:
pass
while True:
pass
# 2
else:
pass
while True: pass
# 3
else:
pass
while True: pass
# 4
else:
pass
def foo():
while True:
pass
# 5
else:
pass
while True:
first;second
# 6
else:
pass

View File

@@ -1042,33 +1042,4 @@ else: # trailing comment
assert_debug_snapshot!(comments.debug(test_case.source_code));
}
#[test]
fn while_else_indented_comment_between_branches() {
let source = r"while True: pass
# comment
else:
pass
";
let test_case = CommentsTestCase::from_code(source);
let comments = test_case.to_comments();
assert_debug_snapshot!(comments.debug(test_case.source_code));
}
#[test]
fn while_else_very_indented_comment_between_branches() {
let source = r"while True:
pass
# comment
else:
pass
";
let test_case = CommentsTestCase::from_code(source);
let comments = test_case.to_comments();
assert_debug_snapshot!(comments.debug(test_case.source_code));
}
}

View File

@@ -8,7 +8,7 @@ use ruff_python_trivia::{
find_only_token_in_range, first_non_trivia_token, indentation_at_offset,
};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_text_size::{Ranged, TextLen, TextRange};
use std::cmp::Ordering;
use crate::comments::visitor::{CommentPlacement, DecoratedComment};
@@ -602,35 +602,9 @@ fn handle_own_line_comment_between_branches<'a>(
// following branch or if it a trailing comment of the previous body's last statement.
let comment_indentation = comment_indentation_after(preceding, comment.range(), source);
let preceding_indentation = indentation(source, &preceding).map_or_else(
// If `indentation` returns `None`, then there is leading
// content before the preceding node. In this case, we
// always treat the comment as being less-indented than the
// preceding. For example:
//
// ```python
// if True: pass
// # leading on `else`
// else:
// pass
// ```
// Note we even do this if the comment is very indented
// (which matches `black`'s behavior as of 2025.11.11)
//
// ```python
// if True: pass
// # leading on `else`
// else:
// pass
// ```
|| {
comment_indentation
// This can be any positive number - we just
// want to hit the `Less` branch below
+ TextSize::new(1)
},
ruff_text_size::TextLen::text_len,
);
let preceding_indentation = indentation(source, &preceding)
.unwrap_or_default()
.text_len();
// Compare to the last statement in the body
match comment_indentation.cmp(&preceding_indentation) {
@@ -704,41 +678,8 @@ fn handle_own_line_comment_after_branch<'a>(
preceding: AnyNodeRef<'a>,
source: &str,
) -> CommentPlacement<'a> {
// If the preceding node has a body, we want the last child - e.g.
//
// ```python
// if True:
// def foo():
// something
// last_child
// # comment
// else:
// pass
// ```
//
// Otherwise, the preceding node may be the last statement in the body
// of the preceding branch, in which case we can take it as our
// `last_child` here - e.g.
//
// ```python
// if True:
// something
// last_child
// # comment
// else:
// pass
// ```
let last_child = match preceding.last_child_in_body() {
Some(last) => last,
None if comment.following_node().is_some_and(|following| {
following.is_first_statement_in_alternate_body(comment.enclosing_node())
}) =>
{
preceding
}
_ => {
return CommentPlacement::Default(comment);
}
let Some(last_child) = preceding.last_child_in_body() else {
return CommentPlacement::Default(comment);
};
// We only care about the length because indentations with mixed spaces and tabs are only valid if

View File

@@ -1,21 +0,0 @@
---
source: crates/ruff_python_formatter/src/comments/mod.rs
expression: comments.debug(test_case.source_code)
---
{
Node {
kind: StmtWhile,
range: 0..45,
source: `while True: pass⏎`,
}: {
"leading": [],
"dangling": [
SourceComment {
text: "# comment",
position: OwnLine,
formatted: false,
},
],
"trailing": [],
},
}

View File

@@ -1,21 +0,0 @@
---
source: crates/ruff_python_formatter/src/comments/mod.rs
expression: comments.debug(test_case.source_code)
---
{
Node {
kind: StmtPass,
range: 16..20,
source: `pass`,
}: {
"leading": [],
"dangling": [],
"trailing": [
SourceComment {
text: "# comment",
position: OwnLine,
formatted: false,
},
],
},
}

View File

@@ -179,22 +179,7 @@ impl NeedsParentheses for ExprAttribute {
context.comments().ranges(),
context.source(),
) {
// We have to avoid creating syntax errors like
// ```python
// variable = (something) # trailing
// .my_attribute
// ```
// See https://github.com/astral-sh/ruff/issues/19350
if context
.comments()
.trailing(self.value.as_ref())
.iter()
.any(|comment| comment.line_position().is_end_of_line())
{
OptionalParentheses::Multiline
} else {
OptionalParentheses::Never
}
OptionalParentheses::Never
} else {
self.value.needs_parentheses(self.into(), context)
}

View File

@@ -4,6 +4,7 @@ use ruff_python_ast::ExprLambda;
use ruff_text_size::Ranged;
use crate::comments::dangling_comments;
use crate::comments::leading_comments;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
use crate::other::parameters::ParametersParentheses;
use crate::prelude::*;
@@ -33,24 +34,45 @@ impl FormatNodeRule<ExprLambda> for FormatExprLambda {
if dangling_before_parameters.is_empty() {
write!(f, [space()])?;
} else {
write!(f, [dangling_comments(dangling_before_parameters)])?;
}
write!(
f,
[parameters
.format()
.with_options(ParametersParentheses::Never)]
)?;
group(&format_with(|f: &mut PyFormatter| {
if f.context().node_level().is_parenthesized()
&& (parameters.len() > 1 || !dangling_before_parameters.is_empty())
{
let end_of_line_start = dangling_before_parameters
.partition_point(|comment| comment.line_position().is_end_of_line());
let (same_line_comments, own_line_comments) =
dangling_before_parameters.split_at(end_of_line_start);
write!(f, [token(":")])?;
dangling_comments(same_line_comments).fmt(f)?;
if dangling_after_parameters.is_empty() {
write!(f, [space()])?;
} else {
write!(f, [dangling_comments(dangling_after_parameters)])?;
}
write![
f,
[
soft_line_break(),
leading_comments(own_line_comments),
parameters
.format()
.with_options(ParametersParentheses::Never),
]
]
} else {
parameters
.format()
.with_options(ParametersParentheses::Never)
.fmt(f)
}?;
write!(f, [token(":")])?;
if dangling_after_parameters.is_empty() {
write!(f, [space()])
} else {
write!(f, [dangling_comments(dangling_after_parameters)])
}
}))
.fmt(f)?;
} else {
write!(f, [token(":")])?;

View File

@@ -241,7 +241,7 @@ impl FormatNodeRule<Parameters> for FormatParameters {
let num_parameters = item.len();
if self.parentheses == ParametersParentheses::Never {
write!(f, [group(&format_inner), dangling_comments(dangling)])
write!(f, [format_inner, dangling_comments(dangling)])
} else if num_parameters == 0 {
let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f);
// No parameters, format any dangling comments between `()`

View File

@@ -175,56 +175,6 @@ result = (
# dangling before dot
.b # trailing end-of-line
)
# Regression test for https://github.com/astral-sh/ruff/issues/19350
variable = (
(something) # a comment
.first_method("some string")
)
variable = (
something # a commentdddddddddddddddddddddddddddddd
.first_method("some string")
)
if (
(something) # a commentdddddddddddddddddddddddddddddd
.first_method("some string")
): pass
variable = (
(something # a comment
).first_method("some string")
)
if (
(something # a comment
).first_method("some string") # second comment
): pass
variable = ( # 1
# 2
(something) # 3
# 4
.first_method("some string") # 5
# 6
) # 7
if (
(something
# trailing own line on value
)
.first_method("some string")
): ...
variable = (
(something
# 1
) # 2
.first_method("some string")
)
```
## Output
@@ -378,54 +328,4 @@ result = (
# dangling before dot
.b # trailing end-of-line
)
# Regression test for https://github.com/astral-sh/ruff/issues/19350
variable = (
(something) # a comment
.first_method("some string")
)
variable = something.first_method( # a commentdddddddddddddddddddddddddddddd
"some string"
)
if (
(something) # a commentdddddddddddddddddddddddddddddd
.first_method("some string")
):
pass
variable = (
something # a comment
).first_method("some string")
if (
(
something # a comment
).first_method("some string") # second comment
):
pass
variable = ( # 1
# 2
(something) # 3
# 4
.first_method("some string") # 5
# 6
) # 7
if (
something
# trailing own line on value
).first_method("some string"):
...
variable = (
(
something
# 1
) # 2
.first_method("some string")
)
```

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py
snapshot_kind: text
---
## Input
```python
@@ -132,6 +131,13 @@ lambda a, /, c: a
*x: x
)
(
lambda
# comment
*x,
**y: x
)
(
lambda
# comment 1
@@ -142,6 +148,17 @@ lambda a, /, c: a
x
)
(
lambda
# comment 1
*
# comment 2
x,
**y:
# comment 3
x
)
(
lambda # comment 1
* # comment 2
@@ -149,6 +166,14 @@ lambda a, /, c: a
x
)
(
lambda # comment 1
* # comment 2
x,
y: # comment 3
x
)
lambda *x\
:x
@@ -203,6 +228,17 @@ lambda: ( # comment
x
)
(
lambda # 1
# 2
x, # 3
# 4
y
: # 5
# 6
x
)
(
lambda
x,
@@ -211,6 +247,71 @@ lambda: ( # comment
z
)
# Leading
lambda x: (
lambda y: lambda z: x
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ z # Trailing
) # Trailing
# Leading
lambda x: lambda y: lambda z: [
x,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
z
] # Trailing
# Trailing
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: d
# Regression tests for https://github.com/astral-sh/ruff/issues/8179
@@ -237,7 +338,22 @@ def a():
```
## Output
## Outputs
### Output 1
```
indent-style = space
line-width = 88
indent-width = 4
quote-style = Double
line-ending = LineFeed
magic-trailing-comma = Respect
docstring-code = Disabled
docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
```
```python
# Leading
lambda x: x # Trailing
@@ -301,7 +417,8 @@ a = (
)
a = (
lambda x, # Dangling
lambda
x, # Dangling
y: 1
)
@@ -367,6 +484,13 @@ lambda a, /, c: a
*x: x
)
(
lambda
# comment
*x,
**y: x
)
(
lambda
# comment 1
@@ -376,6 +500,16 @@ lambda a, /, c: a
x
)
(
lambda
# comment 1
# comment 2
*x,
**y:
# comment 3
x
)
(
lambda # comment 1
# comment 2
@@ -383,6 +517,14 @@ lambda a, /, c: a
x
)
(
lambda # comment 1
# comment 2
*x,
y: # comment 3
x
)
lambda *x: x
(
@@ -435,11 +577,87 @@ lambda: ( # comment
)
(
lambda x,
lambda # 1
# 2
x, # 3
# 4
y: # 5
# 6
x
)
(
lambda
x,
# comment
y: z
)
# Leading
lambda x: (
lambda y: lambda z: x
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ z # Trailing
) # Trailing
# Leading
lambda x: lambda y: lambda z: [
x,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
z,
] # Trailing
# Trailing
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(
*args, **kwargs
), e=1, f=2, g=2: d
@@ -451,7 +669,8 @@ def a():
c,
d,
e,
f=lambda self,
f=lambda
self,
*args,
**kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs),
)
@@ -462,7 +681,365 @@ def a():
c,
d,
e,
f=lambda self,
f=lambda
self,
araa,
kkkwargs,
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
args,
kwargs,
e=1,
f=2,
g=2: d,
g=10,
)
```
### Output 2
```
indent-style = space
line-width = 88
indent-width = 4
quote-style = Double
line-ending = LineFeed
magic-trailing-comma = Respect
docstring-code = Disabled
docstring-code-line-width = "dynamic"
preview = Enabled
target_version = 3.10
source_type = Python
```
```python
# Leading
lambda x: x # Trailing
# Trailing
# Leading
lambda x, y: x # Trailing
# Trailing
# Leading
lambda x, y: x, y # Trailing
# Trailing
# Leading
lambda x, /, y: x # Trailing
# Trailing
# Leading
lambda x: lambda y: lambda z: x # Trailing
# Trailing
# Leading
lambda x: lambda y: lambda z: (x, y, z) # Trailing
# Trailing
# Leading
lambda x: lambda y: lambda z: (x, y, z) # Trailing
# Trailing
# Leading
lambda x: lambda y: lambda z: (
x,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
z,
) # Trailing
# Trailing
a = (
lambda: # Dangling
1
)
a = (
lambda
x, # Dangling
y: 1
)
# Regression test: lambda empty arguments ranges were too long, leading to unstable
# formatting
(
lambda: ( #
),
)
# lambda arguments don't have parentheses, so we never add a magic trailing comma ...
def f(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = lambda x: y,
):
pass
# ...but we do preserve a trailing comma after the arguments
a = lambda b,: 0
lambda a,: 0
lambda *args,: 0
lambda **kwds,: 0
lambda a, *args,: 0
lambda a, **kwds,: 0
lambda *args, b,: 0
lambda *, b,: 0
lambda *args, **kwds,: 0
lambda a, *args, b,: 0
lambda a, *, b,: 0
lambda a, *args, **kwds,: 0
lambda *args, b, **kwds,: 0
lambda *, b, **kwds,: 0
lambda a, *args, b, **kwds,: 0
lambda a, *, b, **kwds,: 0
lambda a, /: a
lambda a, /, c: a
# Dangling comments without parameters.
(
lambda: # 3
None
)
(
lambda:
# 3
None
)
(
lambda: # 1
# 2
# 3
# 4
None # 5
)
(
lambda
# comment
*x: x
)
(
lambda
# comment
*x,
**y: x
)
(
lambda
# comment 1
# comment 2
*x:
# comment 3
x
)
(
lambda
# comment 1
# comment 2
*x,
**y:
# comment 3
x
)
(
lambda # comment 1
# comment 2
*x: # comment 3
x
)
(
lambda # comment 1
# comment 2
*x,
y: # comment 3
x
)
lambda *x: x
(
lambda
# comment
*x: x
)
lambda: ( # comment
x
)
(
lambda: # comment
x
)
(
lambda:
# comment
x
)
(
lambda: # comment
x
)
(
lambda:
# comment
x
)
(
lambda: # comment
( # comment
x
)
)
(
lambda # 1
# 2
x: # 3
# 4
# 5
# 6
x
)
(
lambda # 1
# 2
x, # 3
# 4
y: # 5
# 6
x
)
(
lambda
x,
# comment
y: z
)
# Leading
lambda x: (
lambda y: lambda z: x
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ y
+ z # Trailing
) # Trailing
# Leading
lambda x: lambda y: lambda z: [
x,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
y,
z,
] # Trailing
# Trailing
lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(
*args, **kwargs
), e=1, f=2, g=2: d
# Regression tests for https://github.com/astral-sh/ruff/issues/8179
def a():
return b(
c,
d,
e,
f=lambda
self,
*args,
**kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs),
)
def a():
return b(
c,
d,
e,
f=lambda
self,
araa,
kkkwargs,
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/form_feed.py
snapshot_kind: text
---
## Input
```python
@@ -17,7 +18,7 @@ else:
# Regression test for: https://github.com/astral-sh/ruff/issues/7624
if symbol is not None:
request["market"] = market["id"]
# "remaining_volume": "0.0",
# "remaining_volume": "0.0",
else:
pass
```

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/if.py
snapshot_kind: text
---
## Input
```python
@@ -300,42 +301,6 @@ if parent_body:
# d
# e
#f
# Compare behavior with `while`/`else` comment placement
if True: pass
# 1
else:
pass
if True:
pass
# 2
else:
pass
if True: pass
# 3
else:
pass
if True: pass
# 4
else:
pass
def foo():
if True:
pass
# 5
else:
pass
if True:
first;second
# 6
else:
pass
```
## Output
@@ -642,48 +607,6 @@ if parent_body:
# d
# e
# f
# Compare behavior with `while`/`else` comment placement
if True:
pass
# 1
else:
pass
if True:
pass
# 2
else:
pass
if True:
pass
# 3
else:
pass
if True:
pass
# 4
else:
pass
def foo():
if True:
pass
# 5
else:
pass
if True:
first
second
# 6
else:
pass
```

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py
snapshot_kind: text
---
## Input
```python
@@ -34,40 +35,6 @@ while (
and anotherCondition or aThirdCondition # trailing third condition
): # comment
print("Do something")
while True: pass
# 1
else:
pass
while True:
pass
# 2
else:
pass
while True: pass
# 3
else:
pass
while True: pass
# 4
else:
pass
def foo():
while True:
pass
# 5
else:
pass
while True:
first;second
# 6
else:
pass
```
## Output
@@ -103,44 +70,4 @@ while (
or aThirdCondition # trailing third condition
): # comment
print("Do something")
while True:
pass
# 1
else:
pass
while True:
pass
# 2
else:
pass
while True:
pass
# 3
else:
pass
while True:
pass
# 4
else:
pass
def foo():
while True:
pass
# 5
else:
pass
while True:
first
second
# 6
else:
pass
```

View File

@@ -10,7 +10,7 @@ use ruff_python_parser::{TokenKind, Tokens};
use ruff_python_trivia::is_python_whitespace;
use ruff_python_trivia::{PythonWhitespace, textwrap::indent};
use ruff_source_file::{LineRanges, UniversalNewlineIterator};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{Ranged, TextSize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Placement<'a> {
@@ -37,7 +37,7 @@ pub struct Insertion<'a> {
impl<'a> Insertion<'a> {
/// Create an [`Insertion`] to insert (e.g.) an import statement at the start of a given
/// file or cell, along with a prefix and suffix to use for the insertion.
/// file, along with a prefix and suffix to use for the insertion.
///
/// For example, given the following code:
///
@@ -49,26 +49,7 @@ impl<'a> Insertion<'a> {
///
/// The insertion returned will begin at the start of the `import os` statement, and will
/// include a trailing newline.
///
/// If `within_range` is set, the insertion will be limited to the specified range. That is,
/// the insertion is constrained to the given range rather than the start of the file.
/// This is used for insertions in notebook cells where the source code and AST are for
/// the entire notebook but the insertion should be constrained to a specific cell.
pub fn start_of_file(
body: &[Stmt],
contents: &str,
stylist: &Stylist,
within_range: Option<TextRange>,
) -> Insertion<'static> {
let body = within_range
.map(|range| {
let start = body.partition_point(|stmt| stmt.start() < range.start());
let end = body.partition_point(|stmt| stmt.end() <= range.end());
&body[start..end]
})
.unwrap_or(body);
pub fn start_of_file(body: &[Stmt], contents: &str, stylist: &Stylist) -> Insertion<'static> {
// Skip over any docstrings.
let mut location = if let Some(mut location) = match_docstring_end(body) {
// If the first token after the docstring is a semicolon, insert after the semicolon as
@@ -85,10 +66,6 @@ impl<'a> Insertion<'a> {
// Otherwise, advance to the next row.
contents.full_line_end(location)
} else if let Some(range) = within_range
&& range.start() != TextSize::ZERO
{
range.start()
} else {
contents.bom_start_offset()
};
@@ -397,12 +374,7 @@ mod tests {
fn insert(contents: &str) -> Result<Insertion<'_>> {
let parsed = parse_module(contents)?;
let stylist = Stylist::from_tokens(parsed.tokens(), contents);
Ok(Insertion::start_of_file(
parsed.suite(),
contents,
&stylist,
None,
))
Ok(Insertion::start_of_file(parsed.suite(), contents, &stylist))
}
let contents = "";

View File

@@ -2107,10 +2107,8 @@ pub trait SemanticSyntaxContext {
fn report_semantic_error(&self, error: SemanticSyntaxError);
/// Returns `true` if the visitor is inside a `for` or `while` loop.
fn in_loop_context(&self) -> bool;
/// Returns `true` if `name` is a bound parameter in the current function or lambda scope.
fn is_bound_parameter(&self, name: &str) -> bool;
}

View File

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

View File

@@ -232,9 +232,6 @@ impl Configuration {
include_dependencies: analyze
.include_dependencies
.unwrap_or(analyze_defaults.include_dependencies),
type_checking_imports: analyze
.type_checking_imports
.unwrap_or(analyze_defaults.type_checking_imports),
};
let lint = self.lint;
@@ -1280,7 +1277,6 @@ pub struct AnalyzeConfiguration {
pub detect_string_imports: Option<bool>,
pub string_imports_min_dots: Option<usize>,
pub include_dependencies: Option<BTreeMap<PathBuf, (PathBuf, Vec<String>)>>,
pub type_checking_imports: Option<bool>,
}
impl AnalyzeConfiguration {
@@ -1307,7 +1303,6 @@ impl AnalyzeConfiguration {
})
.collect::<BTreeMap<_, _>>()
}),
type_checking_imports: options.type_checking_imports,
})
}
@@ -1322,7 +1317,6 @@ impl AnalyzeConfiguration {
.string_imports_min_dots
.or(config.string_imports_min_dots),
include_dependencies: self.include_dependencies.or(config.include_dependencies),
type_checking_imports: self.type_checking_imports.or(config.type_checking_imports),
}
}
}

View File

@@ -3892,18 +3892,6 @@ pub struct AnalyzeOptions {
"#
)]
pub include_dependencies: Option<BTreeMap<PathBuf, Vec<String>>>,
/// Whether to include imports that are only used for type checking (i.e., imports within `if TYPE_CHECKING:` blocks).
/// When enabled (default), type-checking-only imports are included in the import graph.
/// When disabled, they are excluded.
#[option(
default = "true",
value_type = "bool",
example = r#"
# Exclude type-checking-only imports from the graph
type-checking-imports = false
"#
)]
pub type_checking_imports: Option<bool>,
}
/// Like [`LintCommonOptions`], but with any `#[serde(flatten)]` fields inlined. This leads to far,

View File

@@ -153,7 +153,7 @@ fn both_warnings_and_errors() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -171,14 +171,14 @@ fn both_warnings_and_errors() -> anyhow::Result<()> {
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^^^^
| ^
|
info: rule `non-subscriptable` is enabled by default
Found 2 diagnostics
----- stderr -----
");
"###);
Ok(())
}
@@ -193,7 +193,7 @@ fn both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()>
"###,
)?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r"
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -211,14 +211,14 @@ fn both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()>
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^^^^
| ^
|
info: rule `non-subscriptable` is enabled by default
Found 2 diagnostics
----- stderr -----
");
"###);
Ok(())
}
@@ -233,7 +233,7 @@ fn exit_zero_is_true() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r"
assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -251,14 +251,14 @@ fn exit_zero_is_true() -> anyhow::Result<()> {
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^^^^
| ^
|
info: rule `non-subscriptable` is enabled by default
Found 2 diagnostics
----- stderr -----
");
"###);
Ok(())
}

View File

@@ -617,7 +617,7 @@ fn gitlab_diagnostics() -> anyhow::Result<()> {
let _s = settings.bind_to_scope();
assert_cmd_snapshot!(case.command().arg("--output-format=gitlab").arg("--warn").arg("unresolved-reference")
.env("CI_PROJECT_DIR", case.project_dir), @r#"
.env("CI_PROJECT_DIR", case.project_dir), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -655,14 +655,14 @@ fn gitlab_diagnostics() -> anyhow::Result<()> {
},
"end": {
"line": 3,
"column": 11
"column": 8
}
}
}
}
]
----- stderr -----
"#);
"###);
Ok(())
}
@@ -677,15 +677,15 @@ fn github_diagnostics() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--output-format=github").arg("--warn").arg("unresolved-reference"), @r"
assert_cmd_snapshot!(case.command().arg("--output-format=github").arg("--warn").arg("unresolved-reference"), @r###"
success: false
exit_code: 1
----- stdout -----
::warning title=ty (unresolved-reference),file=<temp_dir>/test.py,line=2,col=7,endLine=2,endColumn=8::test.py:2:7: unresolved-reference: Name `x` used when not defined
::error title=ty (non-subscriptable),file=<temp_dir>/test.py,line=3,col=7,endLine=3,endColumn=11::test.py:3:7: non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
::error title=ty (non-subscriptable),file=<temp_dir>/test.py,line=3,col=7,endLine=3,endColumn=8::test.py:3:7: non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
----- stderr -----
");
"###);
Ok(())
}

View File

@@ -323,231 +323,6 @@ fn python_version_inferred_from_system_installation() -> anyhow::Result<()> {
Ok(())
}
/// This attempts to simulate the tangled web of symlinks that a homebrew install has
/// which can easily confuse us if we're ever told to use it.
///
/// The main thing this is regression-testing is a panic in one *extremely* specific case
/// that you have to try really hard to hit (but vscode, hilariously, did hit).
#[cfg(unix)]
#[test]
fn python_argument_trapped_in_a_symlink_factory() -> anyhow::Result<()> {
let case = CliTest::with_files([
// This is the real python binary.
(
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13",
"",
),
// There's a real site-packages here (although it's basically empty).
(
"opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages/foo.py",
"",
),
// There's also a real site-packages here (although it's basically empty).
("opt/homebrew/lib/python3.13/site-packages/bar.py", ""),
// This has the real stdlib, but the site-packages in this dir is a symlink.
(
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/abc.py",
"",
),
// It's important that this our faux-homebrew not be in the same dir as our working directory
// to reproduce the crash, don't ask me why.
(
"project/test.py",
"\
import foo
import bar
import colorama
",
),
])?;
// many python symlinks pointing to a single real python (the longest path)
case.write_symlink(
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13",
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3",
)?;
case.write_symlink(
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3",
"opt/homebrew/Cellar/python@3.13/3.13.5/bin/python3",
)?;
case.write_symlink(
"opt/homebrew/Cellar/python@3.13/3.13.5/bin/python3",
"opt/homebrew/bin/python3",
)?;
// the "real" python's site-packages is a symlink to a different dir
case.write_symlink(
"opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages",
"opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages",
)?;
// Try all 4 pythons with absolute paths to our fauxbrew install
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.arg("--python").arg(case.root().join("opt/homebrew/bin/python3")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `foo`
--> test.py:1:8
|
1 | import foo
| ^^^
2 | import bar
3 | import colorama
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/lib/python3.13/site-packages (site-packages)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-import]: Cannot resolve imported module `colorama`
--> test.py:3:8
|
1 | import foo
2 | import bar
3 | import colorama
| ^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/lib/python3.13/site-packages (site-packages)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 2 diagnostics
----- stderr -----
");
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.arg("--python").arg(case.root().join("opt/homebrew/Cellar/python@3.13/3.13.5/bin/python3")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `bar`
--> test.py:2:8
|
1 | import foo
2 | import bar
| ^^^
3 | import colorama
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages (site-packages)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-import]: Cannot resolve imported module `colorama`
--> test.py:3:8
|
1 | import foo
2 | import bar
3 | import colorama
| ^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages (site-packages)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 2 diagnostics
----- stderr -----
");
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.arg("--python").arg(case.root().join("opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `bar`
--> test.py:2:8
|
1 | import foo
2 | import bar
| ^^^
3 | import colorama
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-import]: Cannot resolve imported module `colorama`
--> test.py:3:8
|
1 | import foo
2 | import bar
3 | import colorama
| ^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 2 diagnostics
----- stderr -----
");
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.arg("--python").arg(case.root().join("opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/bin/python3.13")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `bar`
--> test.py:2:8
|
1 | import foo
2 | import bar
| ^^^
3 | import colorama
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-import]: Cannot resolve imported module `colorama`
--> test.py:3:8
|
1 | import foo
2 | import bar
3 | import colorama
| ^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 2 diagnostics
----- stderr -----
");
Ok(())
}
/// On Unix systems, it's common for a Python installation at `.venv/bin/python` to only be a symlink
/// to a system Python installation. We must be careful not to resolve the symlink too soon!
/// If we do, we will incorrectly add the system installation's `site-packages` as a search path,

View File

@@ -10,14 +10,12 @@ import-deprioritizes-type_check_only,main.py,1,1
import-deprioritizes-type_check_only,main.py,2,1
import-deprioritizes-type_check_only,main.py,3,2
import-deprioritizes-type_check_only,main.py,4,3
import-keyword-completion,main.py,0,1
internal-typeshed-hidden,main.py,0,4
none-completion,main.py,0,2
internal-typeshed-hidden,main.py,0,5
none-completion,main.py,0,11
numpy-array,main.py,0,
numpy-array,main.py,1,1
object-attr-instance-methods,main.py,0,1
object-attr-instance-methods,main.py,1,1
pass-keyword-completion,main.py,0,1
raise-uses-base-exception,main.py,0,2
scope-existing-over-new-import,main.py,0,1
scope-prioritize-closer,main.py,0,2
@@ -25,4 +23,4 @@ scope-simple-long-identifier,main.py,0,1
tstring-completions,main.py,0,1
ty-extensions-lower-stdlib,main.py,0,8
type-var-typing-over-ast,main.py,0,3
type-var-typing-over-ast,main.py,1,279
type-var-typing-over-ast,main.py,1,277
1 name file index rank
10 import-deprioritizes-type_check_only main.py 2 1
11 import-deprioritizes-type_check_only main.py 3 2
12 import-deprioritizes-type_check_only main.py 4 3
13 import-keyword-completion internal-typeshed-hidden main.py 0 1 5
14 internal-typeshed-hidden none-completion main.py 0 4 11
none-completion main.py 0 2
15 numpy-array main.py 0
16 numpy-array main.py 1 1
17 object-attr-instance-methods main.py 0 1
18 object-attr-instance-methods main.py 1 1
pass-keyword-completion main.py 0 1
19 raise-uses-base-exception main.py 0 2
20 scope-existing-over-new-import main.py 0 1
21 scope-prioritize-closer main.py 0 2
23 tstring-completions main.py 0 1
24 ty-extensions-lower-stdlib main.py 0 8
25 type-var-typing-over-ast main.py 0 3
26 type-var-typing-over-ast main.py 1 279 277

View File

@@ -1,2 +0,0 @@
[settings]
auto-import = false

View File

@@ -1 +0,0 @@
from collections im<CURSOR: import>

View File

@@ -1,5 +0,0 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@@ -1,8 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }

View File

@@ -1,2 +0,0 @@
[settings]
auto-import = false

View File

@@ -1,3 +0,0 @@
match x:
case int():
pa<CURSOR: pass>

View File

@@ -1,5 +0,0 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@@ -1,8 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }

File diff suppressed because it is too large Load Diff

View File

@@ -855,7 +855,7 @@ fn convert_resolved_definitions_to_targets(
}
ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => {
// For file ranges, navigate to the specific range within the file
crate::NavigationTarget::from(file_range)
crate::NavigationTarget::new(file_range.file(), file_range.range())
}
})
.collect()

View File

@@ -1592,111 +1592,6 @@ a = Test()
");
}
#[test]
fn float_annotation() {
let test = CursorTest::builder()
.source(
"main.py",
"
a: float<CURSOR> = 3.14
",
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main.py:2:4
|
2 | a: float = 3.14
| ^^^^^
|
info[goto-definition]: Definition
--> stdlib/builtins.pyi:661:7
|
660 | @disjoint_base
661 | class float:
| ^^^^^
662 | """Convert a string or number to a floating-point number, if possible."""
|
info: Source
--> main.py:2:4
|
2 | a: float = 3.14
| ^^^^^
|
"#);
}
#[test]
fn complex_annotation() {
let test = CursorTest::builder()
.source(
"main.py",
"
a: complex<CURSOR> = 3.14
",
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main.py:2:4
|
2 | a: complex = 3.14
| ^^^^^^^
|
info[goto-definition]: Definition
--> stdlib/builtins.pyi:661:7
|
660 | @disjoint_base
661 | class float:
| ^^^^^
662 | """Convert a string or number to a floating-point number, if possible."""
|
info: Source
--> main.py:2:4
|
2 | a: complex = 3.14
| ^^^^^^^
|
info[goto-definition]: Definition
--> stdlib/builtins.pyi:822:7
|
821 | @disjoint_base
822 | class complex:
| ^^^^^^^
823 | """Create a complex number from a string or numbers.
|
info: Source
--> main.py:2:4
|
2 | a: complex = 3.14
| ^^^^^^^
|
"#);
}
/// Regression test for <https://github.com/astral-sh/ty/issues/1451>.
/// We must ensure we respect re-import convention for stub files for
/// imports in builtins.pyi.

View File

@@ -199,13 +199,13 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:915:7
--> stdlib/builtins.pyi:913:7
|
914 | @disjoint_base
915 | class str(Sequence[str]):
912 | @disjoint_base
913 | class str(Sequence[str]):
| ^^^
916 | """str(object='') -> str
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
914 | """str(object='') -> str
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:1
@@ -227,13 +227,13 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:915:7
--> stdlib/builtins.pyi:913:7
|
914 | @disjoint_base
915 | class str(Sequence[str]):
912 | @disjoint_base
913 | class str(Sequence[str]):
| ^^^
916 | """str(object='') -> str
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
914 | """str(object='') -> str
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:2:10
@@ -334,13 +334,13 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:915:7
--> stdlib/builtins.pyi:913:7
|
914 | @disjoint_base
915 | class str(Sequence[str]):
912 | @disjoint_base
913 | class str(Sequence[str]):
| ^^^
916 | """str(object='') -> str
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
914 | """str(object='') -> str
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:6
@@ -368,13 +368,13 @@ mod tests {
// is an int. Navigating to `str` would match pyright's behavior.
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:348:7
--> stdlib/builtins.pyi:346:7
|
347 | @disjoint_base
348 | class int:
345 | @disjoint_base
346 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
347 | """int([x]) -> integer
348 | int(x, base=10) -> integer
|
info: Source
--> main.py:4:6
@@ -401,13 +401,13 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:2920:7
--> stdlib/builtins.pyi:2918:7
|
2919 | @disjoint_base
2920 | class dict(MutableMapping[_KT, _VT]):
2917 | @disjoint_base
2918 | class dict(MutableMapping[_KT, _VT]):
| ^^^^
2921 | """dict() -> new empty dictionary
2922 | dict(mapping) -> new dictionary initialized from a mapping object's
2919 | """dict() -> new empty dictionary
2920 | dict(mapping) -> new dictionary initialized from a mapping object's
|
info: Source
--> main.py:6:5
@@ -431,13 +431,13 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:915:7
--> stdlib/builtins.pyi:913:7
|
914 | @disjoint_base
915 | class str(Sequence[str]):
912 | @disjoint_base
913 | class str(Sequence[str]):
| ^^^
916 | """str(object='') -> str
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
914 | """str(object='') -> str
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:3:5
@@ -523,13 +523,13 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:915:7
--> stdlib/builtins.pyi:913:7
|
914 | @disjoint_base
915 | class str(Sequence[str]):
912 | @disjoint_base
913 | class str(Sequence[str]):
| ^^^
916 | """str(object='') -> str
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
914 | """str(object='') -> str
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:15
@@ -570,13 +570,13 @@ f(**kwargs<CURSOR>)
|
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:915:7
--> stdlib/builtins.pyi:913:7
|
914 | @disjoint_base
915 | class str(Sequence[str]):
912 | @disjoint_base
913 | class str(Sequence[str]):
| ^^^
916 | """str(object='') -> str
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
914 | """str(object='') -> str
915 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:3:5

View File

@@ -2634,40 +2634,6 @@ def ab(a: int, *, c: int):
");
}
#[test]
fn hover_float_annotation() {
let test = cursor_test(
r#"
a: float<CURSOR> = 3.14
"#,
);
assert_snapshot!(test.hover(), @r"
int | float
---------------------------------------------
Convert a string or number to a floating-point number, if possible.
---------------------------------------------
```python
int | float
```
---
```text
Convert a string or number to a floating-point number, if possible.
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:4
|
2 | a: float = 3.14
| ^^^^^- Cursor offset
| |
| source
|
");
}
impl CursorTest {
fn hover(&self) -> String {
use std::fmt::Write;

View File

@@ -20,7 +20,6 @@ use rustc_hash::FxHashMap;
use ruff_db::files::File;
use ruff_db::parsed::ParsedModuleRef;
use ruff_db::source::source_text;
use ruff_diagnostics::Edit;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
@@ -77,7 +76,6 @@ impl<'a> Importer<'a> {
parsed: &'a ParsedModuleRef,
) -> Self {
let imports = TopLevelImports::find(parsed);
Self {
db,
file,
@@ -147,14 +145,10 @@ impl<'a> Importer<'a> {
let request = request.avoid_conflicts(self.db, self.file, members);
let mut symbol_text: Box<str> = request.member.into();
let Some(response) = self.find(&request, members.at) else {
let insertion = if let Some(future) = self.find_last_future_import(members.at) {
let insertion = if let Some(future) = self.find_last_future_import() {
Insertion::end_of_statement(future.stmt, self.source, self.stylist)
} else {
let range = source_text(self.db, self.file)
.as_notebook()
.and_then(|notebook| notebook.cell_offsets().containing_range(members.at));
Insertion::start_of_file(self.parsed.suite(), self.source, self.stylist, range)
Insertion::start_of_file(self.parsed.suite(), self.source, self.stylist)
};
let import = insertion.into_edit(&request.to_string());
if matches!(request.style, ImportStyle::Import) {
@@ -215,9 +209,6 @@ impl<'a> Importer<'a> {
available_at: TextSize,
) -> Option<ImportResponse<'importer, 'a>> {
let mut choice = None;
let source = source_text(self.db, self.file);
let notebook = source.as_notebook();
for import in &self.imports {
// If the import statement comes after the spot where we
// need the symbol, then we conservatively assume that
@@ -235,22 +226,7 @@ impl<'a> Importer<'a> {
if import.stmt.start() >= available_at {
return choice;
}
if let Some(response) = import.satisfies(self.db, self.file, request) {
let partial = matches!(response.kind, ImportResponseKind::Partial { .. });
// The LSP doesn't support edits across cell boundaries.
// Skip over imports that only partially satisfy the import
// because they would require changes to the import (across cell boundaries).
if partial
&& let Some(notebook) = notebook
&& notebook
.cell_offsets()
.has_cell_boundary(TextRange::new(import.stmt.start(), available_at))
{
continue;
}
if choice
.as_ref()
.is_none_or(|c| !c.kind.is_prioritized_over(&response.kind))
@@ -271,21 +247,9 @@ impl<'a> Importer<'a> {
}
/// Find the last `from __future__` import statement in the AST.
fn find_last_future_import(&self, at: TextSize) -> Option<&'a AstImport> {
let source = source_text(self.db, self.file);
let notebook = source.as_notebook();
fn find_last_future_import(&self) -> Option<&'a AstImport> {
self.imports
.iter()
.take_while(|import| import.stmt.start() <= at)
// Skip over imports from other cells.
.skip_while(|import| {
notebook.is_some_and(|notebook| {
notebook
.cell_offsets()
.has_cell_boundary(TextRange::new(import.stmt.start(), at))
})
})
.take_while(|import| {
import
.stmt

File diff suppressed because it is too large Load Diff

View File

@@ -132,20 +132,6 @@ impl NavigationTarget {
pub fn full_range(&self) -> TextRange {
self.full_range
}
pub fn full_file_range(&self) -> FileRange {
FileRange::new(self.file, self.full_range)
}
}
impl From<FileRange> for NavigationTarget {
fn from(value: FileRange) -> Self {
Self {
file: value.file(),
focus_range: value.range(),
full_range: value.range(),
}
}
}
/// Specifies the kind of reference operation.

File diff suppressed because it is too large Load Diff

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