Compare commits
105 Commits
dcreager/f
...
0.14.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecab623fb2 | ||
|
|
42f152108a | ||
|
|
594b7b04d3 | ||
|
|
b5b4917d7f | ||
|
|
0084e94f78 | ||
|
|
566c959add | ||
|
|
8bcfc198b8 | ||
|
|
c534bfaf01 | ||
|
|
5e1b2eef57 | ||
|
|
98681b9356 | ||
|
|
3ed537e9f1 | ||
|
|
53efc82989 | ||
|
|
666f488f1b | ||
|
|
aef2fad0c5 | ||
|
|
df66946b89 | ||
|
|
efb23b01af | ||
|
|
e5818d89fd | ||
|
|
a7d48ffd40 | ||
|
|
77f8fa6906 | ||
|
|
7c7f8d1a17 | ||
|
|
761031f729 | ||
|
|
792ec3e96e | ||
|
|
c7107a5a90 | ||
|
|
e0f3a064b9 | ||
|
|
e2e21508dc | ||
|
|
2c0c5ff4e7 | ||
|
|
adf4f1e3f4 | ||
|
|
3dbbb76654 | ||
|
|
b72120f7ef | ||
|
|
33713a7e2a | ||
|
|
5364256190 | ||
|
|
81c97e9e94 | ||
|
|
294f863523 | ||
|
|
4628180fac | ||
|
|
de32247f30 | ||
|
|
209ea06592 | ||
|
|
88bfc32dfc | ||
|
|
66d233134f | ||
|
|
15cb41c1f9 | ||
|
|
eddb9ad38d | ||
|
|
b19ddca69b | ||
|
|
747c39a26a | ||
|
|
dd15656deb | ||
|
|
adf095e889 | ||
|
|
bfd65c4215 | ||
|
|
0631e72187 | ||
|
|
bab688b76c | ||
|
|
7e277667d1 | ||
|
|
d379f3826f | ||
|
|
6f9265d78d | ||
|
|
0c6d652b5f | ||
|
|
03fe560164 | ||
|
|
68343e7edf | ||
|
|
a57e291311 | ||
|
|
f317a71682 | ||
|
|
35bfcff24d | ||
|
|
474b00568a | ||
|
|
3b23d3c041 | ||
|
|
3f4875313f | ||
|
|
8327f262ff | ||
|
|
8c4a9d8808 | ||
|
|
907e7f7705 | ||
|
|
7e8915d76e | ||
|
|
680297aef8 | ||
|
|
314a6e58ed | ||
|
|
918fc2c773 | ||
|
|
e642874cf1 | ||
|
|
aec225d825 | ||
|
|
d24c891a4b | ||
|
|
859f9ec21a | ||
|
|
3410041b4c | ||
|
|
f2ce5e561a | ||
|
|
f495c6d4ae | ||
|
|
768bb24cdf | ||
|
|
492d676736 | ||
|
|
ddc1417f22 | ||
|
|
040aa7463b | ||
|
|
09d457aa52 | ||
|
|
438ef334d3 | ||
|
|
6cc502781f | ||
|
|
e2a1d1a8eb | ||
|
|
040b482cf7 | ||
|
|
03dfbf21eb | ||
|
|
e3c78d8203 | ||
|
|
a9b3caf181 | ||
|
|
629258241f | ||
|
|
762c44527e | ||
|
|
59c6cb521d | ||
|
|
54dba15088 | ||
|
|
1af318534a | ||
|
|
553e568624 | ||
|
|
cdef3f5ab8 | ||
|
|
6178822427 | ||
|
|
6b7adb0537 | ||
|
|
06941c1987 | ||
|
|
eb7c098d6b | ||
|
|
1b28fc1f14 | ||
|
|
290a5720cb | ||
|
|
c4767f5aa8 | ||
|
|
6e84f4fd7a | ||
|
|
78ce17ce8f | ||
|
|
0761ea42d9 | ||
|
|
416e2267da | ||
|
|
02c102da88 | ||
|
|
29c24bc8a6 |
44
.github/workflows/ci.yaml
vendored
44
.github/workflows/ci.yaml
vendored
@@ -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@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -284,6 +284,10 @@ jobs:
|
||||
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
|
||||
- name: Dogfood ty on py-fuzzer
|
||||
run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
|
||||
- name: Dogfood ty on the scripts directory
|
||||
run: uv run --project=./scripts cargo run -p ty check --project=./scripts
|
||||
- name: Dogfood ty on ty_benchmark
|
||||
run: uv run --project=./scripts/ty_benchmark cargo run -p ty check --project=./scripts/ty_benchmark
|
||||
# Check for broken links in the documentation.
|
||||
- run: cargo doc --all --no-deps
|
||||
env:
|
||||
@@ -319,11 +323,11 @@ 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@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
@@ -352,11 +356,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@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
@@ -462,7 +466,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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
shared-key: ruff-linux-debug
|
||||
@@ -497,7 +501,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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
# Run all code generation scripts, and verify that the current output is
|
||||
@@ -532,7 +536,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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
activate-environment: true
|
||||
@@ -638,7 +642,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -697,7 +701,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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -748,7 +752,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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -792,7 +796,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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
with:
|
||||
python-version: 3.13
|
||||
activate-environment: true
|
||||
@@ -947,13 +951,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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -987,13 +991,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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1027,13 +1031,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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
|
||||
4
.github/workflows/mypy_primer.yaml
vendored
4
.github/workflows/mypy_primer.yaml
vendored
@@ -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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
@@ -81,7 +81,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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
|
||||
2
.github/workflows/publish-pypi.yml
vendored
2
.github/workflows/publish-pypi.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
10
.github/workflows/sync_typeshed.yaml
vendored
10
.github/workflows/sync_typeshed.yaml
vendored
@@ -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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- 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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- 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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
@@ -207,12 +207,12 @@ jobs:
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
if: ${{ success() }}
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
if: ${{ success() }}
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: Update snapshots
|
||||
|
||||
4
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
4
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
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@e5c5f5b2d762af91b28490537fe0077334165693"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--repository ruff \
|
||||
|
||||
4
.github/workflows/ty-ecosystem-report.yaml
vendored
4
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
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@e5c5f5b2d762af91b28490537fe0077334165693"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--verbose \
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -5,5 +5,6 @@
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"search.exclude": {
|
||||
"**/*.snap": true
|
||||
}
|
||||
},
|
||||
"ty.diagnosticMode": "openFilesOnly"
|
||||
}
|
||||
|
||||
78
CHANGELOG.md
78
CHANGELOG.md
@@ -1,5 +1,83 @@
|
||||
# Changelog
|
||||
|
||||
## 0.14.7
|
||||
|
||||
Released on 2025-11-28.
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-bandit`\] Handle string literal bindings in suspicious-url-open-usage (`S310`) ([#21469](https://github.com/astral-sh/ruff/pull/21469))
|
||||
- \[`pylint`\] Fix `PLR1708` false positives on nested functions ([#21177](https://github.com/astral-sh/ruff/pull/21177))
|
||||
- \[`pylint`\] Fix suppression for empty dict without tuple key annotation (`PLE1141`) ([#21290](https://github.com/astral-sh/ruff/pull/21290))
|
||||
- \[`ruff`\] Add rule `RUF066` to detect unnecessary class properties ([#21535](https://github.com/astral-sh/ruff/pull/21535))
|
||||
- \[`ruff`\] Catch more dummy variable uses (`RUF052`) ([#19799](https://github.com/astral-sh/ruff/pull/19799))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [server] Set severity for non-rule diagnostics ([#21559](https://github.com/astral-sh/ruff/pull/21559))
|
||||
- \[`flake8-implicit-str-concat`\] Avoid invalid fix in (`ISC003`) ([#21517](https://github.com/astral-sh/ruff/pull/21517))
|
||||
- \[`parser`\] Fix panic when parsing IPython escape command expressions ([#21480](https://github.com/astral-sh/ruff/pull/21480))
|
||||
|
||||
### CLI
|
||||
|
||||
- Show partial fixability indicator in statistics output ([#21513](https://github.com/astral-sh/ruff/pull/21513))
|
||||
|
||||
### Contributors
|
||||
|
||||
- [@mikeleppane](https://github.com/mikeleppane)
|
||||
- [@senekor](https://github.com/senekor)
|
||||
- [@ShaharNaveh](https://github.com/ShaharNaveh)
|
||||
- [@JumboBear](https://github.com/JumboBear)
|
||||
- [@prakhar1144](https://github.com/prakhar1144)
|
||||
- [@tsvikas](https://github.com/tsvikas)
|
||||
- [@danparizher](https://github.com/danparizher)
|
||||
- [@chirizxc](https://github.com/chirizxc)
|
||||
- [@AlexWaygood](https://github.com/AlexWaygood)
|
||||
- [@MichaReiser](https://github.com/MichaReiser)
|
||||
|
||||
## 0.14.6
|
||||
|
||||
Released on 2025-11-21.
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-bandit`\] Support new PySNMP API paths (`S508`, `S509`) ([#21374](https://github.com/astral-sh/ruff/pull/21374))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Adjust own-line comment placement between branches ([#21185](https://github.com/astral-sh/ruff/pull/21185))
|
||||
- Avoid syntax error when formatting attribute expressions with outer parentheses, parenthesized value, and trailing comment on value ([#20418](https://github.com/astral-sh/ruff/pull/20418))
|
||||
- Fix panic when formatting comments in unary expressions ([#21501](https://github.com/astral-sh/ruff/pull/21501))
|
||||
- Respect `fmt: skip` for compound statements on a single line ([#20633](https://github.com/astral-sh/ruff/pull/20633))
|
||||
- \[`refurb`\] Fix `FURB103` autofix ([#21454](https://github.com/astral-sh/ruff/pull/21454))
|
||||
- \[`ruff`\] Fix false positive for complex conversion specifiers in `logging-eager-conversion` (`RUF065`) ([#21464](https://github.com/astral-sh/ruff/pull/21464))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`ruff`\] Avoid false positive on `ClassVar` reassignment (`RUF012`) ([#21478](https://github.com/astral-sh/ruff/pull/21478))
|
||||
|
||||
### CLI
|
||||
|
||||
- Render hyperlinks for lint errors ([#21514](https://github.com/astral-sh/ruff/pull/21514))
|
||||
- Add a `ruff analyze` option to skip over imports in `TYPE_CHECKING` blocks ([#21472](https://github.com/astral-sh/ruff/pull/21472))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Limit `eglot-format` hook to eglot-managed Python buffers ([#21459](https://github.com/astral-sh/ruff/pull/21459))
|
||||
- Mention `force-exclude` in "Configuration > Python file discovery" ([#21500](https://github.com/astral-sh/ruff/pull/21500))
|
||||
|
||||
### Contributors
|
||||
|
||||
- [@ntBre](https://github.com/ntBre)
|
||||
- [@dylwil3](https://github.com/dylwil3)
|
||||
- [@gauthsvenkat](https://github.com/gauthsvenkat)
|
||||
- [@MichaReiser](https://github.com/MichaReiser)
|
||||
- [@thamer](https://github.com/thamer)
|
||||
- [@Ruchir28](https://github.com/Ruchir28)
|
||||
- [@thejcannon](https://github.com/thejcannon)
|
||||
- [@danparizher](https://github.com/danparizher)
|
||||
- [@chirizxc](https://github.com/chirizxc)
|
||||
|
||||
## 0.14.5
|
||||
|
||||
Released on 2025-11-13.
|
||||
|
||||
56
Cargo.lock
generated
56
Cargo.lock
generated
@@ -442,9 +442,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.51"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -452,9 +452,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.51"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1016,7 +1016,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1108,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1255,7 +1255,7 @@ checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"get-size-derive2",
|
||||
"hashbrown 0.16.0",
|
||||
"hashbrown 0.16.1",
|
||||
"indexmap",
|
||||
"smallvec",
|
||||
]
|
||||
@@ -1353,9 +1353,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
]
|
||||
@@ -1564,12 +1564,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.12.0"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
|
||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -1763,7 +1763,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2859,7 +2859,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.5"
|
||||
version = "0.14.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -3117,7 +3117,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.5"
|
||||
version = "0.14.7"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3127,7 +3127,7 @@ dependencies = [
|
||||
"fern",
|
||||
"glob",
|
||||
"globset",
|
||||
"hashbrown 0.16.0",
|
||||
"hashbrown 0.16.1",
|
||||
"imperative",
|
||||
"insta",
|
||||
"is-macro",
|
||||
@@ -3472,7 +3472,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.5"
|
||||
version = "0.14.7"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3570,7 +3570,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3588,7 +3588,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=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3612,12 +3612,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=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
|
||||
|
||||
[[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=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3935,9 +3935,9 @@ checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.110"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3971,7 +3971,7 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4462,7 +4462,7 @@ dependencies = [
|
||||
"drop_bomb",
|
||||
"get-size2",
|
||||
"glob",
|
||||
"hashbrown 0.16.0",
|
||||
"hashbrown 0.16.1",
|
||||
"indexmap",
|
||||
"indoc",
|
||||
"insta",
|
||||
@@ -4474,6 +4474,7 @@ dependencies = [
|
||||
"quickcheck_macros",
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
"ruff_memory_usage",
|
||||
@@ -4519,6 +4520,7 @@ dependencies = [
|
||||
"lsp-types",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -4559,6 +4561,7 @@ dependencies = [
|
||||
"path-slash",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
"ruff_index",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -4600,6 +4603,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
"ruff_notebook",
|
||||
"ruff_python_formatter",
|
||||
"ruff_source_file",
|
||||
@@ -5020,7 +5024,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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 = "17bc55d699565e5a1cb1bd42363b905af2f9f3e7", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
|
||||
@@ -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.7/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.7/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.7
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.14.5"
|
||||
version = "0.14.7"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -34,9 +34,21 @@ struct ExpandedStatistics<'a> {
|
||||
code: Option<&'a SecondaryCode>,
|
||||
name: &'static str,
|
||||
count: usize,
|
||||
fixable: bool,
|
||||
#[serde(rename = "fixable")]
|
||||
all_fixable: bool,
|
||||
fixable_count: usize,
|
||||
}
|
||||
|
||||
impl ExpandedStatistics<'_> {
|
||||
fn any_fixable(&self) -> bool {
|
||||
self.fixable_count > 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Accumulator type for grouping diagnostics by code.
|
||||
/// Format: (`code`, `representative_diagnostic`, `total_count`, `fixable_count`)
|
||||
type DiagnosticGroup<'a> = (Option<&'a SecondaryCode>, &'a Diagnostic, usize, usize);
|
||||
|
||||
pub(crate) struct Printer {
|
||||
format: OutputFormat,
|
||||
log_level: LogLevel,
|
||||
@@ -133,7 +145,7 @@ impl Printer {
|
||||
if fixables.applicable > 0 {
|
||||
writeln!(
|
||||
writer,
|
||||
"{fix_prefix} {} fixable with the --fix option.",
|
||||
"{fix_prefix} {} fixable with the `--fix` option.",
|
||||
fixables.applicable
|
||||
)?;
|
||||
}
|
||||
@@ -256,35 +268,41 @@ impl Printer {
|
||||
diagnostics: &Diagnostics,
|
||||
writer: &mut dyn Write,
|
||||
) -> Result<()> {
|
||||
let required_applicability = self.unsafe_fixes.required_applicability();
|
||||
let statistics: Vec<ExpandedStatistics> = diagnostics
|
||||
.inner
|
||||
.iter()
|
||||
.map(|message| (message.secondary_code(), message))
|
||||
.sorted_by_key(|(code, message)| (*code, message.fixable()))
|
||||
.fold(
|
||||
vec![],
|
||||
|mut acc: Vec<((Option<&SecondaryCode>, &Diagnostic), usize)>, (code, message)| {
|
||||
if let Some(((prev_code, _prev_message), count)) = acc.last_mut() {
|
||||
if *prev_code == code {
|
||||
*count += 1;
|
||||
return acc;
|
||||
.sorted_by_key(|diagnostic| diagnostic.secondary_code())
|
||||
.fold(vec![], |mut acc: Vec<DiagnosticGroup>, diagnostic| {
|
||||
let is_fixable = diagnostic
|
||||
.fix()
|
||||
.is_some_and(|fix| fix.applies(required_applicability));
|
||||
let code = diagnostic.secondary_code();
|
||||
|
||||
if let Some((prev_code, _prev_message, count, fixable_count)) = acc.last_mut() {
|
||||
if *prev_code == code {
|
||||
*count += 1;
|
||||
if is_fixable {
|
||||
*fixable_count += 1;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
acc.push(((code, message), 1));
|
||||
acc
|
||||
}
|
||||
acc.push((code, diagnostic, 1, usize::from(is_fixable)));
|
||||
acc
|
||||
})
|
||||
.iter()
|
||||
.map(
|
||||
|&(code, message, count, fixable_count)| ExpandedStatistics {
|
||||
code,
|
||||
name: message.name(),
|
||||
count,
|
||||
// Backward compatibility: `fixable` is true only when all violations are fixable.
|
||||
// See: https://github.com/astral-sh/ruff/pull/21513
|
||||
all_fixable: fixable_count == count,
|
||||
fixable_count,
|
||||
},
|
||||
)
|
||||
.iter()
|
||||
.map(|&((code, message), count)| ExpandedStatistics {
|
||||
code,
|
||||
name: message.name(),
|
||||
count,
|
||||
fixable: if let Some(fix) = message.fix() {
|
||||
fix.applies(self.unsafe_fixes.required_applicability())
|
||||
} else {
|
||||
false
|
||||
},
|
||||
})
|
||||
.sorted_by_key(|statistic| Reverse(statistic.count))
|
||||
.collect();
|
||||
|
||||
@@ -308,13 +326,14 @@ impl Printer {
|
||||
.map(|statistic| statistic.code.map_or(0, |s| s.len()))
|
||||
.max()
|
||||
.unwrap();
|
||||
let any_fixable = statistics.iter().any(|statistic| statistic.fixable);
|
||||
let any_fixable = statistics.iter().any(ExpandedStatistics::any_fixable);
|
||||
|
||||
let fixable = format!("[{}] ", "*".cyan());
|
||||
let all_fixable = format!("[{}] ", "*".cyan());
|
||||
let partially_fixable = format!("[{}] ", "-".cyan());
|
||||
let unfixable = "[ ] ";
|
||||
|
||||
// By default, we mimic Flake8's `--statistics` format.
|
||||
for statistic in statistics {
|
||||
for statistic in &statistics {
|
||||
writeln!(
|
||||
writer,
|
||||
"{:>count_width$}\t{:<code_width$}\t{}{}",
|
||||
@@ -326,8 +345,10 @@ impl Printer {
|
||||
.red()
|
||||
.bold(),
|
||||
if any_fixable {
|
||||
if statistic.fixable {
|
||||
&fixable
|
||||
if statistic.all_fixable {
|
||||
&all_fixable
|
||||
} else if statistic.any_fixable() {
|
||||
&partially_fixable
|
||||
} else {
|
||||
unfixable
|
||||
}
|
||||
|
||||
@@ -1043,7 +1043,7 @@ def mvce(keys, values):
|
||||
----- stdout -----
|
||||
1 C416 [*] unnecessary-comprehension
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the --fix option.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
@@ -1073,7 +1073,8 @@ def mvce(keys, values):
|
||||
"code": "C416",
|
||||
"name": "unnecessary-comprehension",
|
||||
"count": 1,
|
||||
"fixable": false
|
||||
"fixable": false,
|
||||
"fixable_count": 0
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1106,7 +1107,8 @@ def mvce(keys, values):
|
||||
"code": "C416",
|
||||
"name": "unnecessary-comprehension",
|
||||
"count": 1,
|
||||
"fixable": true
|
||||
"fixable": true,
|
||||
"fixable_count": 1
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1114,6 +1116,54 @@ def mvce(keys, values):
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_statistics_json_partial_fix() {
|
||||
let mut cmd = RuffCheck::default()
|
||||
.args([
|
||||
"--select",
|
||||
"UP035",
|
||||
"--statistics",
|
||||
"--output-format",
|
||||
"json",
|
||||
])
|
||||
.build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin("from typing import List, AsyncGenerator"), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
[
|
||||
{
|
||||
"code": "UP035",
|
||||
"name": "deprecated-import",
|
||||
"count": 2,
|
||||
"fixable": false,
|
||||
"fixable_count": 1
|
||||
}
|
||||
]
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_statistics_partial_fix() {
|
||||
let mut cmd = RuffCheck::default()
|
||||
.args(["--select", "UP035", "--statistics"])
|
||||
.build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin("from typing import List, AsyncGenerator"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
2 UP035 [-] deprecated-import
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_statistics_syntax_errors() {
|
||||
let mut cmd = RuffCheck::default()
|
||||
@@ -1810,7 +1860,7 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the --fix option.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
@@ -1853,7 +1903,7 @@ fn check_shows_unsafe_fixes_with_opt_in() {
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
[*] 2 fixable with the --fix option.
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
@@ -59,8 +59,6 @@ divan = { workspace = true, optional = true }
|
||||
anyhow = { workspace = true }
|
||||
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true }
|
||||
criterion = { workspace = true, default-features = false, optional = true }
|
||||
rayon = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
@@ -88,3 +86,7 @@ mimalloc = { workspace = true }
|
||||
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dev-dependencies]
|
||||
tikv-jemallocator = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rustc-hash = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
|
||||
@@ -120,7 +120,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY310,
|
||||
},
|
||||
600,
|
||||
1070,
|
||||
);
|
||||
|
||||
static FREQTRADE: Benchmark = Benchmark::new(
|
||||
@@ -143,7 +143,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
525,
|
||||
600,
|
||||
);
|
||||
|
||||
static PANDAS: Benchmark = Benchmark::new(
|
||||
@@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
3000,
|
||||
4000,
|
||||
);
|
||||
|
||||
static PYDANTIC: Benchmark = Benchmark::new(
|
||||
@@ -181,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY39,
|
||||
},
|
||||
5000,
|
||||
7000,
|
||||
);
|
||||
|
||||
static SYMPY: Benchmark = Benchmark::new(
|
||||
@@ -223,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
900,
|
||||
950,
|
||||
);
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.5"
|
||||
version = "0.14.7"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -45,3 +45,22 @@ urllib.request.urlopen(urllib.request.Request(url))
|
||||
# https://github.com/astral-sh/ruff/issues/15522
|
||||
map(urllib.request.urlopen, [])
|
||||
foo = urllib.request.urlopen
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/21462
|
||||
path = "https://example.com/data.csv"
|
||||
urllib.request.urlretrieve(path, "data.csv")
|
||||
url = "https://example.com/api"
|
||||
urllib.request.Request(url)
|
||||
|
||||
# Test resolved f-strings and concatenated string literals
|
||||
fstring_url = f"https://example.com/data.csv"
|
||||
urllib.request.urlopen(fstring_url)
|
||||
urllib.request.Request(fstring_url)
|
||||
|
||||
concatenated_url = "https://" + "example.com/data.csv"
|
||||
urllib.request.urlopen(concatenated_url)
|
||||
urllib.request.Request(concatenated_url)
|
||||
|
||||
nested_concatenated = "http://" + "example.com" + "/data.csv"
|
||||
urllib.request.urlopen(nested_concatenated)
|
||||
urllib.request.Request(nested_concatenated)
|
||||
|
||||
@@ -208,3 +208,17 @@ _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
|
||||
_ = f"b {t"abc" \
|
||||
t"def"} g"
|
||||
|
||||
|
||||
# Explicit concatenation with either operand being
|
||||
# a string literal that wraps across multiple lines (in parentheses)
|
||||
# reports diagnostic - no autofix.
|
||||
# See https://github.com/astral-sh/ruff/issues/19757
|
||||
_ = "abc" + (
|
||||
"def"
|
||||
"ghi"
|
||||
)
|
||||
|
||||
_ = (
|
||||
"abc"
|
||||
"def"
|
||||
) + "ghi"
|
||||
|
||||
@@ -30,3 +30,23 @@ for a, b in d_tuple:
|
||||
pass
|
||||
for a, b in d_tuple_annotated:
|
||||
pass
|
||||
|
||||
# Empty dict cases
|
||||
empty_dict = {}
|
||||
empty_dict["x"] = 1
|
||||
for k, v in empty_dict:
|
||||
pass
|
||||
|
||||
empty_dict_annotated_tuple_keys: dict[tuple[int, str], bool] = {}
|
||||
for k, v in empty_dict_annotated_tuple_keys:
|
||||
pass
|
||||
|
||||
empty_dict_unannotated = {}
|
||||
empty_dict_unannotated[("x", "y")] = True
|
||||
for k, v in empty_dict_unannotated:
|
||||
pass
|
||||
|
||||
empty_dict_annotated_str_keys: dict[str, int] = {}
|
||||
empty_dict_annotated_str_keys["x"] = 1
|
||||
for k, v in empty_dict_annotated_str_keys:
|
||||
pass
|
||||
|
||||
@@ -129,3 +129,26 @@ def generator_with_lambda():
|
||||
yield 1
|
||||
func = lambda x: x # Just a regular lambda
|
||||
yield 2
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/21162
|
||||
def foo():
|
||||
def g():
|
||||
yield 1
|
||||
raise StopIteration # Should not trigger
|
||||
|
||||
|
||||
def foo():
|
||||
def g():
|
||||
raise StopIteration # Should not trigger
|
||||
yield 1
|
||||
|
||||
# https://github.com/astral-sh/ruff/pull/21177#pullrequestreview-3430209718
|
||||
def foo():
|
||||
yield 1
|
||||
class C:
|
||||
raise StopIteration # Should trigger
|
||||
yield C
|
||||
|
||||
# https://github.com/astral-sh/ruff/pull/21177#discussion_r2539702728
|
||||
def foo():
|
||||
raise StopIteration((yield 1)) # Should trigger
|
||||
109
crates/ruff_linter/resources/test/fixtures/ruff/RUF052_1.py
vendored
Normal file
109
crates/ruff_linter/resources/test/fixtures/ruff/RUF052_1.py
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
# Correct usage in loop and comprehension
|
||||
def process_data():
|
||||
return 42
|
||||
def test_correct_dummy_usage():
|
||||
my_list = [{"foo": 1}, {"foo": 2}]
|
||||
|
||||
# Should NOT detect - dummy variable is not used
|
||||
[process_data() for _ in my_list] # OK: `_` is ignored by rule
|
||||
|
||||
# Should NOT detect - dummy variable is not used
|
||||
[item["foo"] for item in my_list] # OK: not a dummy variable name
|
||||
|
||||
# Should NOT detect - dummy variable is not used
|
||||
[42 for _unused in my_list] # OK: `_unused` is not accessed
|
||||
|
||||
# Regular For Loops
|
||||
def test_for_loops():
|
||||
my_list = [{"foo": 1}, {"foo": 2}]
|
||||
|
||||
# Should detect used dummy variable
|
||||
for _item in my_list:
|
||||
print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
|
||||
|
||||
# Should detect used dummy variable
|
||||
for _index, _value in enumerate(my_list):
|
||||
result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
||||
|
||||
# List Comprehensions
|
||||
def test_list_comprehensions():
|
||||
my_list = [{"foo": 1}, {"foo": 2}]
|
||||
|
||||
# Should detect used dummy variable
|
||||
result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
|
||||
|
||||
# Should detect used dummy variable in nested comprehension
|
||||
nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
|
||||
# RUF052: Both `_item` and `_sublist` are accessed
|
||||
|
||||
# Should detect with conditions
|
||||
filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
|
||||
# RUF052: Local dummy variable `_item` is accessed
|
||||
|
||||
# Dict Comprehensions
|
||||
def test_dict_comprehensions():
|
||||
my_list = [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
|
||||
|
||||
# Should detect used dummy variable
|
||||
result = {_item["key"]: _item["value"] for _item in my_list}
|
||||
# RUF052: Local dummy variable `_item` is accessed
|
||||
|
||||
# Should detect with enumerate
|
||||
indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
|
||||
# RUF052: Both `_index` and `_item` are accessed
|
||||
|
||||
# Should detect in nested dict comprehension
|
||||
nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
|
||||
for _outer, sublist in enumerate([my_list])}
|
||||
# RUF052: `_outer`, `_inner` are accessed
|
||||
|
||||
# Set Comprehensions
|
||||
def test_set_comprehensions():
|
||||
my_list = [{"foo": 1}, {"foo": 2}, {"foo": 1}] # Note: duplicate values
|
||||
|
||||
# Should detect used dummy variable
|
||||
unique_values = {_item["foo"] for _item in my_list}
|
||||
# RUF052: Local dummy variable `_item` is accessed
|
||||
|
||||
# Should detect with conditions
|
||||
filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
|
||||
# RUF052: Local dummy variable `_item` is accessed
|
||||
|
||||
# Should detect with complex expression
|
||||
processed = {_item["foo"] * 2 for _item in my_list}
|
||||
# RUF052: Local dummy variable `_item` is accessed
|
||||
|
||||
# Generator Expressions
|
||||
def test_generator_expressions():
|
||||
my_list = [{"foo": 1}, {"foo": 2}]
|
||||
|
||||
# Should detect used dummy variable
|
||||
gen = (_item["foo"] for _item in my_list)
|
||||
# RUF052: Local dummy variable `_item` is accessed
|
||||
|
||||
# Should detect when passed to function
|
||||
total = sum(_item["foo"] for _item in my_list)
|
||||
# RUF052: Local dummy variable `_item` is accessed
|
||||
|
||||
# Should detect with multiple generators
|
||||
pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
|
||||
# RUF052: Both `_x` and `_y` are accessed
|
||||
|
||||
# Should detect in nested generator
|
||||
nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
|
||||
# RUF052: `_inner` and `_sublist` are accessed
|
||||
|
||||
# Complex Examples with Multiple Comprehension Types
|
||||
def test_mixed_comprehensions():
|
||||
data = [{"items": [1, 2, 3]}, {"items": [4, 5, 6]}]
|
||||
|
||||
# Should detect in mixed comprehensions
|
||||
result = [
|
||||
{_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
|
||||
for _record in data
|
||||
]
|
||||
# RUF052: `_key`, `_val`, and `_record` are all accessed
|
||||
|
||||
# Should detect in generator passed to list constructor
|
||||
gen_list = list(_item["items"][0] for _item in data)
|
||||
# RUF052: Local dummy variable `_item` is accessed
|
||||
70
crates/ruff_linter/resources/test/fixtures/ruff/RUF066.py
vendored
Normal file
70
crates/ruff_linter/resources/test/fixtures/ruff/RUF066.py
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
import abc
|
||||
import typing
|
||||
|
||||
|
||||
class User: # Test normal class properties
|
||||
@property
|
||||
def name(self): # ERROR: No return
|
||||
f"{self.first_name} {self.last_name}"
|
||||
|
||||
@property
|
||||
def age(self): # OK: Returning something
|
||||
return 100
|
||||
|
||||
def method(self): # OK: Not a property
|
||||
x = 1
|
||||
|
||||
@property
|
||||
def nested(self): # ERROR: Property itself doesn't return
|
||||
def inner():
|
||||
return 0
|
||||
|
||||
@property
|
||||
def stub(self): ... # OK: A stub; doesn't return anything
|
||||
|
||||
|
||||
class UserMeta(metaclass=abc.ABCMeta): # Test properies inside of an ABC class
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def abstr_prop1(self): ... # OK: Abstract methods doesn't need to return anything
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def abstr_prop2(self): # OK: Abstract methods doesn't need to return anything
|
||||
"""
|
||||
A cool docstring
|
||||
"""
|
||||
|
||||
@property
|
||||
def prop1(self): # OK: Returning a value
|
||||
return 1
|
||||
|
||||
@property
|
||||
def prop2(self): # ERROR: Not returning something (even when we are inside an ABC)
|
||||
50
|
||||
|
||||
def method(self): # OK: Not a property
|
||||
x = 1
|
||||
|
||||
|
||||
def func(): # OK: Not a property
|
||||
x = 1
|
||||
|
||||
|
||||
class Proto(typing.Protocol): # Tests for a Protocol class
|
||||
@property
|
||||
def prop1(self) -> int: ... # OK: A stub property
|
||||
|
||||
|
||||
class File: # Extra tests for things like yield/yield from/raise
|
||||
@property
|
||||
def stream1(self): # OK: Yields something
|
||||
yield
|
||||
|
||||
@property
|
||||
def stream2(self): # OK: Yields from something
|
||||
yield from self.stream1
|
||||
|
||||
@property
|
||||
def children(self): # OK: Raises
|
||||
raise ValueError("File does not have children")
|
||||
@@ -17,7 +17,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:8:5: F841 [
|
||||
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
|
||||
crates/ruff_linter/resources/test/project/project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
Found 7 errors.
|
||||
[*] 7 potentially fixable with the --fix option.
|
||||
[*] 7 potentially fixable with the `--fix` option.
|
||||
```
|
||||
|
||||
Running from the project directory itself should exhibit the same behavior:
|
||||
@@ -32,7 +32,7 @@ examples/docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but n
|
||||
project/file.py:1:8: F401 [*] `os` imported but unused
|
||||
project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
Found 7 errors.
|
||||
[*] 7 potentially fixable with the --fix option.
|
||||
[*] 7 potentially fixable with the `--fix` option.
|
||||
```
|
||||
|
||||
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
|
||||
@@ -43,7 +43,7 @@ files:
|
||||
docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
Found 2 errors.
|
||||
[*] 2 potentially fixable with the --fix option.
|
||||
[*] 2 potentially fixable with the `--fix` option.
|
||||
```
|
||||
|
||||
`--config` should force Ruff to use the specified `pyproject.toml` for all files, and resolve
|
||||
@@ -61,7 +61,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:4:27: F401
|
||||
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
|
||||
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
|
||||
Found 9 errors.
|
||||
[*] 9 potentially fixable with the --fix option.
|
||||
[*] 9 potentially fixable with the `--fix` option.
|
||||
```
|
||||
|
||||
Running from a parent directory should "ignore" the `exclude` (hence, `concepts/file.py` gets
|
||||
@@ -74,7 +74,7 @@ docs/docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
excluded/script.py:5:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
Found 4 errors.
|
||||
[*] 4 potentially fixable with the --fix option.
|
||||
[*] 4 potentially fixable with the `--fix` option.
|
||||
```
|
||||
|
||||
Passing an excluded directory directly should report errors in the contained files:
|
||||
@@ -83,7 +83,7 @@ Passing an excluded directory directly should report errors in the contained fil
|
||||
∴ cargo run -p ruff -- check crates/ruff_linter/resources/test/project/examples/excluded/
|
||||
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 potentially fixable with the --fix option.
|
||||
[*] 1 potentially fixable with the `--fix` option.
|
||||
```
|
||||
|
||||
Unless we `--force-exclude`:
|
||||
|
||||
@@ -131,6 +131,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.is_rule_enabled(Rule::GeneratorReturnFromIterMethod) {
|
||||
flake8_pyi::rules::bad_generator_return_type(function_def, checker);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::StopIterationReturn) {
|
||||
pylint::rules::stop_iteration_return(checker, function_def);
|
||||
}
|
||||
if checker.source_type.is_stub() {
|
||||
if checker.is_rule_enabled(Rule::StrOrReprDefinedInStub) {
|
||||
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
|
||||
@@ -344,6 +347,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.is_rule_enabled(Rule::InvalidArgumentName) {
|
||||
pep8_naming::rules::invalid_argument_name_function(checker, function_def);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::PropertyWithoutReturn) {
|
||||
ruff::rules::property_without_return(checker, function_def);
|
||||
}
|
||||
}
|
||||
Stmt::Return(_) => {
|
||||
if checker.is_rule_enabled(Rule::ReturnInInit) {
|
||||
@@ -950,9 +956,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.is_rule_enabled(Rule::MisplacedBareRaise) {
|
||||
pylint::rules::misplaced_bare_raise(checker, raise);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::StopIterationReturn) {
|
||||
pylint::rules::stop_iteration_return(checker, raise);
|
||||
}
|
||||
}
|
||||
Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => {
|
||||
if checker.is_rule_enabled(Rule::GlobalStatement) {
|
||||
|
||||
@@ -1058,6 +1058,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "063") => rules::ruff::rules::AccessAnnotationsFromClassDict,
|
||||
(Ruff, "064") => rules::ruff::rules::NonOctalPermissions,
|
||||
(Ruff, "065") => rules::ruff::rules::LoggingEagerConversion,
|
||||
(Ruff, "066") => rules::ruff::rules::PropertyWithoutReturn,
|
||||
|
||||
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
|
||||
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,
|
||||
|
||||
@@ -279,3 +279,10 @@ pub(crate) const fn is_extended_snmp_api_path_detection_enabled(settings: &Linte
|
||||
pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/21469
|
||||
pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled(
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::assert_diagnostics;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_diagnostics, assert_diagnostics_diff};
|
||||
|
||||
#[test_case(Rule::Assert, Path::new("S101.py"))]
|
||||
#[test_case(Rule::BadFilePermissions, Path::new("S103.py"))]
|
||||
@@ -112,14 +112,19 @@ mod tests {
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
|
||||
assert_diagnostics_diff!(
|
||||
snapshot,
|
||||
Path::new("flake8_bandit").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
preview: PreviewMode::Disabled,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
&LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
use itertools::Either;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{self as ast, Arguments, Decorator, Expr, ExprCall, Operator};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::analyze::typing::find_binding_value;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_suspicious_function_reference_enabled;
|
||||
use crate::preview::{
|
||||
is_s310_resolve_string_literal_bindings_enabled, is_suspicious_function_reference_enabled,
|
||||
};
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for calls to `pickle` functions or modules that wrap them.
|
||||
@@ -1016,6 +1021,25 @@ fn suspicious_function(
|
||||
|| has_prefix(chars.skip_while(|c| c.is_whitespace()), "https://")
|
||||
}
|
||||
|
||||
/// Resolves `expr` to its binding and checks if the resolved expression starts with an HTTP or HTTPS prefix.
|
||||
fn expression_starts_with_http_prefix(
|
||||
expr: &Expr,
|
||||
semantic: &SemanticModel,
|
||||
settings: &LinterSettings,
|
||||
) -> bool {
|
||||
let resolved_expression = if is_s310_resolve_string_literal_bindings_enabled(settings)
|
||||
&& let Some(name_expr) = expr.as_name_expr()
|
||||
&& let Some(binding_id) = semantic.only_binding(name_expr)
|
||||
&& let Some(value) = find_binding_value(semantic.binding(binding_id), semantic)
|
||||
{
|
||||
value
|
||||
} else {
|
||||
expr
|
||||
};
|
||||
|
||||
leading_chars(resolved_expression).is_some_and(has_http_prefix)
|
||||
}
|
||||
|
||||
/// Return the leading characters for an expression, if it's a string literal, f-string, or
|
||||
/// string concatenation.
|
||||
fn leading_chars(expr: &Expr) -> Option<impl Iterator<Item = char> + Clone + '_> {
|
||||
@@ -1139,17 +1163,19 @@ fn suspicious_function(
|
||||
// URLOpen (`Request`)
|
||||
["urllib", "request", "Request"] | ["six", "moves", "urllib", "request", "Request"] => {
|
||||
if let Some(arguments) = arguments {
|
||||
// If the `url` argument is a string literal or an f-string, allow `http` and `https` schemes.
|
||||
// If the `url` argument is a string literal (including resolved bindings), allow `http` and `https` schemes.
|
||||
if arguments.args.iter().all(|arg| !arg.is_starred_expr())
|
||||
&& arguments
|
||||
.keywords
|
||||
.iter()
|
||||
.all(|keyword| keyword.arg.is_some())
|
||||
{
|
||||
if arguments
|
||||
.find_argument_value("url", 0)
|
||||
.and_then(leading_chars)
|
||||
.is_some_and(has_http_prefix)
|
||||
if let Some(url_expr) = arguments.find_argument_value("url", 0)
|
||||
&& expression_starts_with_http_prefix(
|
||||
url_expr,
|
||||
checker.semantic(),
|
||||
checker.settings(),
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1186,19 +1212,25 @@ fn suspicious_function(
|
||||
name.segments() == ["urllib", "request", "Request"]
|
||||
})
|
||||
{
|
||||
if arguments
|
||||
.find_argument_value("url", 0)
|
||||
.and_then(leading_chars)
|
||||
.is_some_and(has_http_prefix)
|
||||
if let Some(url_expr) = arguments.find_argument_value("url", 0)
|
||||
&& expression_starts_with_http_prefix(
|
||||
url_expr,
|
||||
checker.semantic(),
|
||||
checker.settings(),
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the `url` argument is a string literal, allow `http` and `https` schemes.
|
||||
// If the `url` argument is a string literal (including resolved bindings), allow `http` and `https` schemes.
|
||||
Some(expr) => {
|
||||
if leading_chars(expr).is_some_and(has_http_prefix) {
|
||||
if expression_starts_with_http_prefix(
|
||||
expr,
|
||||
checker.semantic(),
|
||||
checker.settings(),
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,3 +254,84 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
|
||||
42 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:51:1
|
||||
|
|
||||
49 | # https://github.com/astral-sh/ruff/issues/21462
|
||||
50 | path = "https://example.com/data.csv"
|
||||
51 | urllib.request.urlretrieve(path, "data.csv")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
52 | url = "https://example.com/api"
|
||||
53 | urllib.request.Request(url)
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:53:1
|
||||
|
|
||||
51 | urllib.request.urlretrieve(path, "data.csv")
|
||||
52 | url = "https://example.com/api"
|
||||
53 | urllib.request.Request(url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
54 |
|
||||
55 | # Test resolved f-strings and concatenated string literals
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:57:1
|
||||
|
|
||||
55 | # Test resolved f-strings and concatenated string literals
|
||||
56 | fstring_url = f"https://example.com/data.csv"
|
||||
57 | urllib.request.urlopen(fstring_url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
58 | urllib.request.Request(fstring_url)
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:58:1
|
||||
|
|
||||
56 | fstring_url = f"https://example.com/data.csv"
|
||||
57 | urllib.request.urlopen(fstring_url)
|
||||
58 | urllib.request.Request(fstring_url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
59 |
|
||||
60 | concatenated_url = "https://" + "example.com/data.csv"
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:61:1
|
||||
|
|
||||
60 | concatenated_url = "https://" + "example.com/data.csv"
|
||||
61 | urllib.request.urlopen(concatenated_url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
62 | urllib.request.Request(concatenated_url)
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:62:1
|
||||
|
|
||||
60 | concatenated_url = "https://" + "example.com/data.csv"
|
||||
61 | urllib.request.urlopen(concatenated_url)
|
||||
62 | urllib.request.Request(concatenated_url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
63 |
|
||||
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:65:1
|
||||
|
|
||||
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
|
||||
65 | urllib.request.urlopen(nested_concatenated)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
66 | urllib.request.Request(nested_concatenated)
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:66:1
|
||||
|
|
||||
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
|
||||
65 | urllib.request.urlopen(nested_concatenated)
|
||||
66 | urllib.request.Request(nested_concatenated)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
|
||||
--> S301.py:3:1
|
||||
|
|
||||
1 | import pickle
|
||||
2 |
|
||||
3 | pickle.loads()
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 2
|
||||
|
||||
--- Added ---
|
||||
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
|
||||
--> S301.py:7:5
|
||||
|
|
||||
@@ -19,6 +19,7 @@ S301 `pickle` and modules that wrap it can be unsafe when used to deserialize un
|
||||
8 | foo = pickle.load
|
||||
|
|
||||
|
||||
|
||||
S301 `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue
|
||||
--> S301.py:8:7
|
||||
|
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S307 Use of possibly insecure function; consider using `ast.literal_eval`
|
||||
--> S307.py:3:7
|
||||
|
|
||||
1 | import os
|
||||
2 |
|
||||
3 | print(eval("1+1")) # S307
|
||||
| ^^^^^^^^^^^
|
||||
4 | print(eval("os.getcwd()")) # S307
|
||||
|
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
S307 Use of possibly insecure function; consider using `ast.literal_eval`
|
||||
--> S307.py:4:7
|
||||
|
|
||||
3 | print(eval("1+1")) # S307
|
||||
4 | print(eval("os.getcwd()")) # S307
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 2
|
||||
|
||||
--- Added ---
|
||||
S307 Use of possibly insecure function; consider using `ast.literal_eval`
|
||||
--> S307.py:16:5
|
||||
|
|
||||
@@ -28,6 +19,7 @@ S307 Use of possibly insecure function; consider using `ast.literal_eval`
|
||||
17 | foo = eval
|
||||
|
|
||||
|
||||
|
||||
S307 Use of possibly insecure function; consider using `ast.literal_eval`
|
||||
--> S307.py:17:7
|
||||
|
|
||||
|
||||
@@ -1,60 +1,37 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:6:5
|
||||
|
|
||||
4 | def bad_func():
|
||||
5 | inject = "harmful_input"
|
||||
6 | mark_safe(inject)
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
7 | mark_safe("I will add" + inject + "to my string")
|
||||
8 | mark_safe("I will add %s to my string" % inject)
|
||||
|
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:7:5
|
||||
|
|
||||
5 | inject = "harmful_input"
|
||||
6 | mark_safe(inject)
|
||||
7 | mark_safe("I will add" + inject + "to my string")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
8 | mark_safe("I will add %s to my string" % inject)
|
||||
9 | mark_safe("I will add {} to my string".format(inject))
|
||||
|
|
||||
--- Summary ---
|
||||
Removed: 2
|
||||
Added: 4
|
||||
|
||||
--- Removed ---
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:8:5
|
||||
--> S308.py:16:1
|
||||
|
|
||||
6 | mark_safe(inject)
|
||||
7 | mark_safe("I will add" + inject + "to my string")
|
||||
8 | mark_safe("I will add %s to my string" % inject)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
9 | mark_safe("I will add {} to my string".format(inject))
|
||||
10 | mark_safe(f"I will add {inject} to my string")
|
||||
16 | @mark_safe
|
||||
| ^^^^^^^^^^
|
||||
17 | def some_func():
|
||||
18 | return '<script>alert("evil!")</script>'
|
||||
|
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:9:5
|
||||
|
|
||||
7 | mark_safe("I will add" + inject + "to my string")
|
||||
8 | mark_safe("I will add %s to my string" % inject)
|
||||
9 | mark_safe("I will add {} to my string".format(inject))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
10 | mark_safe(f"I will add {inject} to my string")
|
||||
|
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:10:5
|
||||
--> S308.py:36:1
|
||||
|
|
||||
8 | mark_safe("I will add %s to my string" % inject)
|
||||
9 | mark_safe("I will add {} to my string".format(inject))
|
||||
10 | mark_safe(f"I will add {inject} to my string")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
11 |
|
||||
12 | def good_func():
|
||||
36 | @mark_safe
|
||||
| ^^^^^^^^^^
|
||||
37 | def some_func():
|
||||
38 | return '<script>alert("evil!")</script>'
|
||||
|
|
||||
|
||||
|
||||
|
||||
--- Added ---
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:16:2
|
||||
|
|
||||
@@ -64,59 +41,6 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
18 | return '<script>alert("evil!")</script>'
|
||||
|
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:26:5
|
||||
|
|
||||
24 | def bad_func():
|
||||
25 | inject = "harmful_input"
|
||||
26 | mark_safe(inject)
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
27 | mark_safe("I will add" + inject + "to my string")
|
||||
28 | mark_safe("I will add %s to my string" % inject)
|
||||
|
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:27:5
|
||||
|
|
||||
25 | inject = "harmful_input"
|
||||
26 | mark_safe(inject)
|
||||
27 | mark_safe("I will add" + inject + "to my string")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
28 | mark_safe("I will add %s to my string" % inject)
|
||||
29 | mark_safe("I will add {} to my string".format(inject))
|
||||
|
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:28:5
|
||||
|
|
||||
26 | mark_safe(inject)
|
||||
27 | mark_safe("I will add" + inject + "to my string")
|
||||
28 | mark_safe("I will add %s to my string" % inject)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
29 | mark_safe("I will add {} to my string".format(inject))
|
||||
30 | mark_safe(f"I will add {inject} to my string")
|
||||
|
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:29:5
|
||||
|
|
||||
27 | mark_safe("I will add" + inject + "to my string")
|
||||
28 | mark_safe("I will add %s to my string" % inject)
|
||||
29 | mark_safe("I will add {} to my string".format(inject))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
30 | mark_safe(f"I will add {inject} to my string")
|
||||
|
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:30:5
|
||||
|
|
||||
28 | mark_safe("I will add %s to my string" % inject)
|
||||
29 | mark_safe("I will add {} to my string".format(inject))
|
||||
30 | mark_safe(f"I will add {inject} to my string")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
31 |
|
||||
32 | def good_func():
|
||||
|
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:36:2
|
||||
@@ -127,6 +51,7 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
38 | return '<script>alert("evil!")</script>'
|
||||
|
|
||||
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:42:5
|
||||
|
|
||||
@@ -136,6 +61,7 @@ S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
43 | foo = mark_safe
|
||||
|
|
||||
|
||||
|
||||
S308 Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
--> S308.py:43:7
|
||||
|
|
||||
|
||||
@@ -1,260 +1,106 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:6:1
|
||||
|
|
||||
4 | urllib.request.urlopen(url=f'http://www.google.com')
|
||||
5 | urllib.request.urlopen(url='http://' + 'www' + '.google.com')
|
||||
6 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
7 | urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
|
||||
8 | urllib.request.urlopen('http://www.google.com')
|
||||
|
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:7:1
|
||||
|
|
||||
5 | urllib.request.urlopen(url='http://' + 'www' + '.google.com')
|
||||
6 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
|
||||
7 | urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
8 | urllib.request.urlopen('http://www.google.com')
|
||||
9 | urllib.request.urlopen(f'http://www.google.com')
|
||||
|
|
||||
--- Summary ---
|
||||
Removed: 8
|
||||
Added: 2
|
||||
|
||||
--- Removed ---
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:10:1
|
||||
--> S310.py:51:1
|
||||
|
|
||||
8 | urllib.request.urlopen('http://www.google.com')
|
||||
9 | urllib.request.urlopen(f'http://www.google.com')
|
||||
10 | urllib.request.urlopen('file:///foo/bar/baz')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
11 | urllib.request.urlopen(url)
|
||||
49 | # https://github.com/astral-sh/ruff/issues/21462
|
||||
50 | path = "https://example.com/data.csv"
|
||||
51 | urllib.request.urlretrieve(path, "data.csv")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
52 | url = "https://example.com/api"
|
||||
53 | urllib.request.Request(url)
|
||||
|
|
||||
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:11:1
|
||||
--> S310.py:53:1
|
||||
|
|
||||
9 | urllib.request.urlopen(f'http://www.google.com')
|
||||
10 | urllib.request.urlopen('file:///foo/bar/baz')
|
||||
11 | urllib.request.urlopen(url)
|
||||
51 | urllib.request.urlretrieve(path, "data.csv")
|
||||
52 | url = "https://example.com/api"
|
||||
53 | urllib.request.Request(url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
12 |
|
||||
13 | urllib.request.Request(url='http://www.google.com')
|
||||
54 |
|
||||
55 | # Test resolved f-strings and concatenated string literals
|
||||
|
|
||||
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:16:1
|
||||
--> S310.py:57:1
|
||||
|
|
||||
14 | urllib.request.Request(url=f'http://www.google.com')
|
||||
15 | urllib.request.Request(url='http://' + 'www' + '.google.com')
|
||||
16 | urllib.request.Request(url='http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
17 | urllib.request.Request(url=f'http://www.google.com', **kwargs)
|
||||
18 | urllib.request.Request('http://www.google.com')
|
||||
55 | # Test resolved f-strings and concatenated string literals
|
||||
56 | fstring_url = f"https://example.com/data.csv"
|
||||
57 | urllib.request.urlopen(fstring_url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
58 | urllib.request.Request(fstring_url)
|
||||
|
|
||||
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:17:1
|
||||
--> S310.py:58:1
|
||||
|
|
||||
15 | urllib.request.Request(url='http://' + 'www' + '.google.com')
|
||||
16 | urllib.request.Request(url='http://www.google.com', **kwargs)
|
||||
17 | urllib.request.Request(url=f'http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
18 | urllib.request.Request('http://www.google.com')
|
||||
19 | urllib.request.Request(f'http://www.google.com')
|
||||
56 | fstring_url = f"https://example.com/data.csv"
|
||||
57 | urllib.request.urlopen(fstring_url)
|
||||
58 | urllib.request.Request(fstring_url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
59 |
|
||||
60 | concatenated_url = "https://" + "example.com/data.csv"
|
||||
|
|
||||
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:20:1
|
||||
--> S310.py:61:1
|
||||
|
|
||||
18 | urllib.request.Request('http://www.google.com')
|
||||
19 | urllib.request.Request(f'http://www.google.com')
|
||||
20 | urllib.request.Request('file:///foo/bar/baz')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
21 | urllib.request.Request(url)
|
||||
60 | concatenated_url = "https://" + "example.com/data.csv"
|
||||
61 | urllib.request.urlopen(concatenated_url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
62 | urllib.request.Request(concatenated_url)
|
||||
|
|
||||
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:21:1
|
||||
--> S310.py:62:1
|
||||
|
|
||||
19 | urllib.request.Request(f'http://www.google.com')
|
||||
20 | urllib.request.Request('file:///foo/bar/baz')
|
||||
21 | urllib.request.Request(url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
22 |
|
||||
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
60 | concatenated_url = "https://" + "example.com/data.csv"
|
||||
61 | urllib.request.urlopen(concatenated_url)
|
||||
62 | urllib.request.Request(concatenated_url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
63 |
|
||||
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
|
||||
|
|
||||
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:23:1
|
||||
--> S310.py:65:1
|
||||
|
|
||||
21 | urllib.request.Request(url)
|
||||
22 |
|
||||
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
|
||||
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
|
||||
65 | urllib.request.urlopen(nested_concatenated)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
66 | urllib.request.Request(nested_concatenated)
|
||||
|
|
||||
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:24:1
|
||||
--> S310.py:66:1
|
||||
|
|
||||
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
64 | nested_concatenated = "http://" + "example.com" + "/data.csv"
|
||||
65 | urllib.request.urlopen(nested_concatenated)
|
||||
66 | urllib.request.Request(nested_concatenated)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:25:1
|
||||
|
|
||||
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
|
||||
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:26:1
|
||||
|
|
||||
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
|
||||
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
28 | urllib.request.URLopener().open('http://www.google.com')
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:27:1
|
||||
|
|
||||
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
28 | urllib.request.URLopener().open('http://www.google.com')
|
||||
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:28:1
|
||||
|
|
||||
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
28 | urllib.request.URLopener().open('http://www.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
||||
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:29:1
|
||||
|
|
||||
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
28 | urllib.request.URLopener().open('http://www.google.com')
|
||||
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:30:1
|
||||
|
|
||||
28 | urllib.request.URLopener().open('http://www.google.com')
|
||||
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
||||
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
32 | urllib.request.URLopener().open(url)
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:31:1
|
||||
|
|
||||
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
||||
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
32 | urllib.request.URLopener().open(url)
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:32:1
|
||||
|
|
||||
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
32 | urllib.request.URLopener().open(url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
33 |
|
||||
34 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'))
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:37:1
|
||||
|
|
||||
35 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'))
|
||||
36 | urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
|
||||
37 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
38 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
|
||||
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:38:1
|
||||
|
|
||||
36 | urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
|
||||
37 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
|
||||
38 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:41:1
|
||||
|
|
||||
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
42 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:41:24
|
||||
|
|
||||
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
42 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:42:1
|
||||
|
|
||||
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
42 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:42:24
|
||||
|
|
||||
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
42 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
--- Added ---
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:46:5
|
||||
|
|
||||
@@ -264,6 +110,7 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
|
||||
47 | foo = urllib.request.urlopen
|
||||
|
|
||||
|
||||
|
||||
S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
--> S310.py:47:7
|
||||
|
|
||||
@@ -271,4 +118,6 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch
|
||||
46 | map(urllib.request.urlopen, [])
|
||||
47 | foo = urllib.request.urlopen
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
48 |
|
||||
49 | # https://github.com/astral-sh/ruff/issues/21462
|
||||
|
|
||||
|
||||
@@ -1,103 +1,15 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:10:1
|
||||
|
|
||||
9 | # Errors
|
||||
10 | random.Random()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
11 | random.random()
|
||||
12 | random.randrange()
|
||||
|
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:11:1
|
||||
|
|
||||
9 | # Errors
|
||||
10 | random.Random()
|
||||
11 | random.random()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
12 | random.randrange()
|
||||
13 | random.randint()
|
||||
|
|
||||
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:12:1
|
||||
|
|
||||
10 | random.Random()
|
||||
11 | random.random()
|
||||
12 | random.randrange()
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
13 | random.randint()
|
||||
14 | random.choice()
|
||||
|
|
||||
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:13:1
|
||||
|
|
||||
11 | random.random()
|
||||
12 | random.randrange()
|
||||
13 | random.randint()
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
14 | random.choice()
|
||||
15 | random.choices()
|
||||
|
|
||||
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:14:1
|
||||
|
|
||||
12 | random.randrange()
|
||||
13 | random.randint()
|
||||
14 | random.choice()
|
||||
| ^^^^^^^^^^^^^^^
|
||||
15 | random.choices()
|
||||
16 | random.uniform()
|
||||
|
|
||||
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:15:1
|
||||
|
|
||||
13 | random.randint()
|
||||
14 | random.choice()
|
||||
15 | random.choices()
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
16 | random.uniform()
|
||||
17 | random.triangular()
|
||||
|
|
||||
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:16:1
|
||||
|
|
||||
14 | random.choice()
|
||||
15 | random.choices()
|
||||
16 | random.uniform()
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
17 | random.triangular()
|
||||
18 | random.randbytes()
|
||||
|
|
||||
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:17:1
|
||||
|
|
||||
15 | random.choices()
|
||||
16 | random.uniform()
|
||||
17 | random.triangular()
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
18 | random.randbytes()
|
||||
|
|
||||
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:18:1
|
||||
|
|
||||
16 | random.uniform()
|
||||
17 | random.triangular()
|
||||
18 | random.randbytes()
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
19 |
|
||||
20 | # Unrelated
|
||||
|
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 2
|
||||
|
||||
--- Added ---
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:26:5
|
||||
|
|
||||
@@ -107,6 +19,7 @@ S311 Standard pseudo-random generators are not suitable for cryptographic purpos
|
||||
27 | foo = random.randrange
|
||||
|
|
||||
|
||||
|
||||
S311 Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||
--> S311.py:27:7
|
||||
|
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
|
||||
--> S312.py:3:1
|
||||
|
|
||||
1 | from telnetlib import Telnet
|
||||
2 |
|
||||
3 | Telnet("localhost", 23)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 3
|
||||
|
||||
--- Added ---
|
||||
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
|
||||
--> S312.py:7:5
|
||||
|
|
||||
@@ -19,6 +19,7 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
|
||||
8 | foo = Telnet
|
||||
|
|
||||
|
||||
|
||||
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
|
||||
--> S312.py:8:7
|
||||
|
|
||||
@@ -30,6 +31,7 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
|
||||
10 | import telnetlib
|
||||
|
|
||||
|
||||
|
||||
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
|
||||
--> S312.py:11:5
|
||||
|
|
||||
@@ -39,13 +41,3 @@ S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
|
||||
12 |
|
||||
13 | from typing import Annotated
|
||||
|
|
||||
|
||||
S312 Telnet is considered insecure. Use SSH or some other encrypted protocol.
|
||||
--> S312.py:14:24
|
||||
|
|
||||
13 | from typing import Annotated
|
||||
14 | foo: Annotated[Telnet, telnetlib.Telnet()]
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
15 |
|
||||
16 | def _() -> Telnet: ...
|
||||
|
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:3:25
|
||||
|
|
||||
1 | from pysnmp.hlapi import CommunityData
|
||||
2 |
|
||||
3 | CommunityData("public", mpModel=0) # S508
|
||||
| ^^^^^^^^^
|
||||
4 | CommunityData("public", mpModel=1) # S508
|
||||
|
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:4:25
|
||||
|
|
||||
3 | CommunityData("public", mpModel=0) # S508
|
||||
4 | CommunityData("public", mpModel=1) # S508
|
||||
| ^^^^^^^^^
|
||||
5 |
|
||||
6 | CommunityData("public", mpModel=2) # OK
|
||||
|
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 8
|
||||
|
||||
--- Added ---
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:18:46
|
||||
|
|
||||
@@ -32,6 +21,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:19:58
|
||||
|
|
||||
@@ -42,6 +32,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:20:53
|
||||
|
|
||||
@@ -53,6 +44,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:21:45
|
||||
|
|
||||
@@ -64,6 +56,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:22:58
|
||||
|
|
||||
@@ -75,6 +68,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:23:53
|
||||
|
|
||||
@@ -86,6 +80,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:24:45
|
||||
|
|
||||
@@ -96,6 +91,7 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:25:43
|
||||
|
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:4:12
|
||||
|
|
||||
4 | insecure = UsmUserData("securityName") # S509
|
||||
| ^^^^^^^^^^^
|
||||
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
|
||||
|
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:5:16
|
||||
|
|
||||
4 | insecure = UsmUserData("securityName") # S509
|
||||
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
|
||||
| ^^^^^^^^^^^
|
||||
6 |
|
||||
7 | less_insecure = UsmUserData("securityName", "authName", "privName") # OK
|
||||
|
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 4
|
||||
|
||||
--- Added ---
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:15:1
|
||||
|
|
||||
@@ -30,6 +21,7 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv`
|
||||
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
|
||||
|
|
||||
|
||||
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:16:1
|
||||
|
|
||||
@@ -40,6 +32,7 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv`
|
||||
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
||||
|
|
||||
|
||||
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:17:1
|
||||
|
|
||||
@@ -50,6 +43,7 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv`
|
||||
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
||||
|
|
||||
|
||||
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:18:1
|
||||
|
|
||||
|
||||
@@ -25,6 +25,11 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// keyword-only argument, to force callers to be explicit when providing
|
||||
/// the argument.
|
||||
///
|
||||
/// This rule exempts methods decorated with [`@typing.override`][override],
|
||||
/// since changing the signature of a subclass method that overrides a
|
||||
/// superclass method may cause type checkers to complain about a violation of
|
||||
/// the Liskov Substitution Principle.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
@@ -89,6 +94,8 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// ## References
|
||||
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.127")]
|
||||
pub(crate) struct BooleanDefaultValuePositionalArgument;
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// the argument.
|
||||
///
|
||||
/// Dunder methods that define operators are exempt from this rule, as are
|
||||
/// setters and `@override` definitions.
|
||||
/// setters and [`@override`][override] definitions.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
@@ -93,6 +93,8 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// ## References
|
||||
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
|
||||
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.127")]
|
||||
pub(crate) struct BooleanTypeHintPositionalArgument;
|
||||
|
||||
@@ -17,6 +17,8 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
/// non-obvious errors, as readers may mistake the argument for the
|
||||
/// builtin and vice versa.
|
||||
///
|
||||
/// Function definitions decorated with [`@override`][override] or
|
||||
/// [`@overload`][overload] are exempt from this rule by default.
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`lint.flake8-builtins.ignorelist`] configuration option.
|
||||
///
|
||||
@@ -48,6 +50,9 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
/// ## References
|
||||
/// - [_Is it bad practice to use a built-in function name as an attribute or method identifier?_](https://stackoverflow.com/questions/9109333/is-it-bad-practice-to-use-a-built-in-function-name-as-an-attribute-or-method-ide)
|
||||
/// - [_Why is it a bad idea to name a variable `id` in Python?_](https://stackoverflow.com/questions/77552/id-is-a-bad-variable-name-in-python)
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
/// [overload]: https://docs.python.org/3/library/typing.html#typing.overload
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.48")]
|
||||
pub(crate) struct BuiltinArgumentShadowing {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::{self as ast, Expr, Operator};
|
||||
use ruff_python_trivia::is_python_whitespace;
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::AlwaysFixableViolation;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::{Edit, Fix};
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for string literals that are explicitly concatenated (using the
|
||||
@@ -36,14 +36,16 @@ use crate::{Edit, Fix};
|
||||
#[violation_metadata(stable_since = "v0.0.201")]
|
||||
pub(crate) struct ExplicitStringConcatenation;
|
||||
|
||||
impl AlwaysFixableViolation for ExplicitStringConcatenation {
|
||||
impl Violation for ExplicitStringConcatenation {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Explicitly concatenated string should be implicitly concatenated".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Remove redundant '+' operator to implicitly concatenate".to_string()
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Remove redundant '+' operator to implicitly concatenate".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +84,27 @@ pub(crate) fn explicit(checker: &Checker, expr: &Expr) {
|
||||
.locator()
|
||||
.contains_line_break(TextRange::new(left.end(), right.start()))
|
||||
{
|
||||
checker
|
||||
.report_diagnostic(ExplicitStringConcatenation, expr.range())
|
||||
.set_fix(generate_fix(checker, bin_op));
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(ExplicitStringConcatenation, expr.range());
|
||||
|
||||
let is_parenthesized = |expr: &Expr| {
|
||||
parenthesized_range(
|
||||
expr.into(),
|
||||
bin_op.into(),
|
||||
checker.comment_ranges(),
|
||||
checker.source(),
|
||||
)
|
||||
.is_some()
|
||||
};
|
||||
// If either `left` or `right` is parenthesized, generating
|
||||
// a fix would be too involved. Just report the diagnostic.
|
||||
// Currently, attempting `generate_fix` would result in
|
||||
// an invalid code. See: #19757
|
||||
if is_parenthesized(left) || is_parenthesized(right) {
|
||||
return;
|
||||
}
|
||||
|
||||
diagnostic.set_fix(generate_fix(checker, bin_op));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,3 +357,33 @@ help: Remove redundant '+' operator to implicitly concatenate
|
||||
203 | )
|
||||
204 |
|
||||
205 | # nested examples with both t and f-strings
|
||||
|
||||
ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
--> ISC.py:216:5
|
||||
|
|
||||
214 | # reports diagnostic - no autofix.
|
||||
215 | # See https://github.com/astral-sh/ruff/issues/19757
|
||||
216 | _ = "abc" + (
|
||||
| _____^
|
||||
217 | | "def"
|
||||
218 | | "ghi"
|
||||
219 | | )
|
||||
| |_^
|
||||
220 |
|
||||
221 | _ = (
|
||||
|
|
||||
help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
ISC003 Explicitly concatenated string should be implicitly concatenated
|
||||
--> ISC.py:221:5
|
||||
|
|
||||
219 | )
|
||||
220 |
|
||||
221 | _ = (
|
||||
| _____^
|
||||
222 | | "abc"
|
||||
223 | | "def"
|
||||
224 | | ) + "ghi"
|
||||
| |_________^
|
||||
|
|
||||
help: Remove redundant '+' operator to implicitly concatenate
|
||||
|
||||
@@ -89,3 +89,24 @@ ISC002 Implicitly concatenated string literals over multiple lines
|
||||
209 | | t"def"} g"
|
||||
| |__________^
|
||||
|
|
||||
|
||||
ISC002 Implicitly concatenated string literals over multiple lines
|
||||
--> ISC.py:217:5
|
||||
|
|
||||
215 | # See https://github.com/astral-sh/ruff/issues/19757
|
||||
216 | _ = "abc" + (
|
||||
217 | / "def"
|
||||
218 | | "ghi"
|
||||
| |_________^
|
||||
219 | )
|
||||
|
|
||||
|
||||
ISC002 Implicitly concatenated string literals over multiple lines
|
||||
--> ISC.py:222:5
|
||||
|
|
||||
221 | _ = (
|
||||
222 | / "abc"
|
||||
223 | | "def"
|
||||
| |_________^
|
||||
224 | ) + "ghi"
|
||||
|
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::helpers::map_subscript;
|
||||
use ruff_python_ast::whitespace::trailing_comment_start_offset;
|
||||
use ruff_python_ast::{Expr, ExprStringLiteral, Stmt, StmtExpr};
|
||||
use ruff_python_semantic::{ScopeKind, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -101,7 +99,7 @@ pub(crate) fn unnecessary_placeholder(checker: &Checker, body: &[Stmt]) {
|
||||
// Ellipses are significant in protocol methods and abstract methods.
|
||||
// Specifically, Pyright uses the presence of an ellipsis to indicate that
|
||||
// a method is a stub, rather than a default implementation.
|
||||
if in_protocol_or_abstract_method(checker.semantic()) {
|
||||
if checker.semantic().in_protocol_or_abstract_method() {
|
||||
return;
|
||||
}
|
||||
Placeholder::Ellipsis
|
||||
@@ -163,21 +161,3 @@ impl std::fmt::Display for Placeholder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the [`SemanticModel`] is in a `typing.Protocol` subclass or an abstract
|
||||
/// method.
|
||||
fn in_protocol_or_abstract_method(semantic: &SemanticModel) -> bool {
|
||||
semantic.current_scopes().any(|scope| match scope.kind {
|
||||
ScopeKind::Class(class_def) => class_def
|
||||
.bases()
|
||||
.iter()
|
||||
.any(|base| semantic.match_typing_expr(map_subscript(base), "Protocol")),
|
||||
ScopeKind::Function(function_def) => {
|
||||
ruff_python_semantic::analyze::visibility::is_abstract(
|
||||
&function_def.decorator_list,
|
||||
semantic,
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,6 +60,16 @@ impl Violation for UnusedFunctionArgument {
|
||||
/// prefixed with an underscore, or some other value that adheres to the
|
||||
/// [`lint.dummy-variable-rgx`] pattern.
|
||||
///
|
||||
/// This rule exempts methods decorated with [`@typing.override`][override].
|
||||
/// Removing a parameter from a subclass method (or changing a parameter's
|
||||
/// name) may cause type checkers to complain about a violation of the Liskov
|
||||
/// Substitution Principle if it means that the method now incompatibly
|
||||
/// overrides a method defined on a superclass. Explicitly decorating an
|
||||
/// overriding method with `@override` signals to Ruff that the method is
|
||||
/// intended to override a superclass method and that a type checker will
|
||||
/// enforce that it does so; Ruff therefore knows that it should not enforce
|
||||
/// rules about unused arguments on such methods.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Class:
|
||||
@@ -76,6 +86,8 @@ impl Violation for UnusedFunctionArgument {
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.dummy-variable-rgx`
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.168")]
|
||||
pub(crate) struct UnusedMethodArgument {
|
||||
@@ -101,6 +113,16 @@ impl Violation for UnusedMethodArgument {
|
||||
/// prefixed with an underscore, or some other value that adheres to the
|
||||
/// [`lint.dummy-variable-rgx`] pattern.
|
||||
///
|
||||
/// This rule exempts methods decorated with [`@typing.override`][override].
|
||||
/// Removing a parameter from a subclass method (or changing a parameter's
|
||||
/// name) may cause type checkers to complain about a violation of the Liskov
|
||||
/// Substitution Principle if it means that the method now incompatibly
|
||||
/// overrides a method defined on a superclass. Explicitly decorating an
|
||||
/// overriding method with `@override` signals to Ruff that the method is
|
||||
/// intended to override a superclass method and that a type checker will
|
||||
/// enforce that it does so; Ruff therefore knows that it should not enforce
|
||||
/// rules about unused arguments on such methods.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Class:
|
||||
@@ -119,6 +141,8 @@ impl Violation for UnusedMethodArgument {
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.dummy-variable-rgx`
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.168")]
|
||||
pub(crate) struct UnusedClassMethodArgument {
|
||||
@@ -144,6 +168,16 @@ impl Violation for UnusedClassMethodArgument {
|
||||
/// prefixed with an underscore, or some other value that adheres to the
|
||||
/// [`lint.dummy-variable-rgx`] pattern.
|
||||
///
|
||||
/// This rule exempts methods decorated with [`@typing.override`][override].
|
||||
/// Removing a parameter from a subclass method (or changing a parameter's
|
||||
/// name) may cause type checkers to complain about a violation of the Liskov
|
||||
/// Substitution Principle if it means that the method now incompatibly
|
||||
/// overrides a method defined on a superclass. Explicitly decorating an
|
||||
/// overriding method with `@override` signals to Ruff that the method is
|
||||
/// intended to override a superclass method, and that a type checker will
|
||||
/// enforce that it does so; Ruff therefore knows that it should not enforce
|
||||
/// rules about unused arguments on such methods.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Class:
|
||||
@@ -162,6 +196,8 @@ impl Violation for UnusedClassMethodArgument {
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.dummy-variable-rgx`
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.168")]
|
||||
pub(crate) struct UnusedStaticMethodArgument {
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::checkers::ast::Checker;
|
||||
/// > mixedCase is allowed only in contexts where that’s already the
|
||||
/// > prevailing style (e.g. threading.py), to retain backwards compatibility.
|
||||
///
|
||||
/// Methods decorated with `@typing.override` are ignored.
|
||||
/// Methods decorated with [`@typing.override`][override] are ignored.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -43,6 +43,8 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.77")]
|
||||
pub(crate) struct InvalidArgumentName {
|
||||
|
||||
@@ -24,6 +24,11 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// to ignore all functions starting with `test_` from this rule, set the
|
||||
/// [`lint.pep8-naming.extend-ignore-names`] option to `["test_*"]`.
|
||||
///
|
||||
/// This rule exempts methods decorated with [`@typing.override`][override].
|
||||
/// Explicitly decorating a method with `@override` signals to Ruff that the method is intended
|
||||
/// to override a superclass method, and that a type checker will enforce that it does so. Ruff
|
||||
/// therefore knows that it should not enforce naming conventions on such methods.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def myFunction():
|
||||
@@ -41,6 +46,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-variable-names
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.77")]
|
||||
pub(crate) struct InvalidFunctionName {
|
||||
|
||||
@@ -169,7 +169,12 @@ impl Violation for UndocumentedPublicClass {
|
||||
/// If the codebase adheres to a standard format for method docstrings, follow
|
||||
/// that format for consistency.
|
||||
///
|
||||
/// This rule exempts methods decorated with [`@typing.override`][override],
|
||||
/// since it is a common practice to document a method on a superclass but not
|
||||
/// on an overriding method in a subclass.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Cat(Animal):
|
||||
/// def greet(self, happy: bool = True):
|
||||
@@ -180,6 +185,7 @@ impl Violation for UndocumentedPublicClass {
|
||||
/// ```
|
||||
///
|
||||
/// Use instead (in the NumPy docstring format):
|
||||
///
|
||||
/// ```python
|
||||
/// class Cat(Animal):
|
||||
/// def greet(self, happy: bool = True):
|
||||
@@ -202,6 +208,7 @@ impl Violation for UndocumentedPublicClass {
|
||||
/// ```
|
||||
///
|
||||
/// Or (in the Google docstring format):
|
||||
///
|
||||
/// ```python
|
||||
/// class Cat(Animal):
|
||||
/// def greet(self, happy: bool = True):
|
||||
@@ -227,6 +234,8 @@ impl Violation for UndocumentedPublicClass {
|
||||
/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.70")]
|
||||
pub(crate) struct UndocumentedPublicMethod;
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::rules::pylint::helpers::is_known_dunder_method;
|
||||
///
|
||||
/// This rule will detect all methods starting and ending with at least
|
||||
/// one underscore (e.g., `_str_`), but ignores known dunder methods (like
|
||||
/// `__init__`), as well as methods that are marked with `@override`.
|
||||
/// `__init__`), as well as methods that are marked with [`@override`][override].
|
||||
///
|
||||
/// Additional dunder methods names can be allowed via the
|
||||
/// [`lint.pylint.allow-dunder-method-names`] setting.
|
||||
@@ -42,6 +42,8 @@ use crate::rules::pylint::helpers::is_known_dunder_method;
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pylint.allow-dunder-method-names`
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "v0.0.285")]
|
||||
pub(crate) struct BadDunderMethodName {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_python_ast::{Expr, Stmt};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_semantic::analyze::typing::is_dict;
|
||||
@@ -108,15 +108,77 @@ fn is_dict_key_tuple_with_two_elements(binding: &Binding, semantic: &SemanticMod
|
||||
return false;
|
||||
};
|
||||
|
||||
let Stmt::Assign(assign_stmt) = statement else {
|
||||
let (value, annotation) = match statement {
|
||||
Stmt::Assign(assign_stmt) => (assign_stmt.value.as_ref(), None),
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
value: Some(value),
|
||||
annotation,
|
||||
..
|
||||
}) => (value.as_ref(), Some(annotation.as_ref())),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let Expr::Dict(dict_expr) = value else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Expr::Dict(dict_expr) = &*assign_stmt.value else {
|
||||
return false;
|
||||
};
|
||||
// Check if dict is empty
|
||||
let is_empty = dict_expr.is_empty();
|
||||
|
||||
if is_empty {
|
||||
// For empty dicts, check type annotation
|
||||
return annotation
|
||||
.is_some_and(|annotation| is_annotation_dict_with_tuple_keys(annotation, semantic));
|
||||
}
|
||||
|
||||
// For non-empty dicts, check if all keys are 2-tuples
|
||||
dict_expr
|
||||
.iter_keys()
|
||||
.all(|key| matches!(key, Some(Expr::Tuple(tuple)) if tuple.len() == 2))
|
||||
}
|
||||
|
||||
/// Returns true if the annotation is `dict[tuple[T1, T2], ...]` where tuple has exactly 2 elements.
|
||||
fn is_annotation_dict_with_tuple_keys(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
||||
// Check if it's a subscript: dict[...]
|
||||
let Expr::Subscript(subscript) = annotation else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Check if it's dict or typing.Dict
|
||||
if !semantic.match_builtin_expr(subscript.value.as_ref(), "dict")
|
||||
&& !semantic.match_typing_expr(subscript.value.as_ref(), "Dict")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract the slice (should be a tuple: (key_type, value_type))
|
||||
let Expr::Tuple(tuple) = subscript.slice.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// dict[K, V] format - check if K is tuple with 2 elements
|
||||
if let [key, _value] = tuple.elts.as_slice() {
|
||||
return is_tuple_type_with_two_elements(key, semantic);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if the expression represents a tuple type with exactly 2 elements.
|
||||
fn is_tuple_type_with_two_elements(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
// Handle tuple[...] subscript
|
||||
if let Expr::Subscript(subscript) = expr {
|
||||
// Check if it's tuple or typing.Tuple
|
||||
if semantic.match_builtin_expr(subscript.value.as_ref(), "tuple")
|
||||
|| semantic.match_typing_expr(subscript.value.as_ref(), "Tuple")
|
||||
{
|
||||
// Check the slice - tuple[T1, T2]
|
||||
if let Expr::Tuple(tuple_slice) = subscript.slice.as_ref() {
|
||||
return tuple_slice.elts.len() == 2;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@ use crate::rules::flake8_unused_arguments::rules::is_not_implemented_stub_with_v
|
||||
/// Unused `self` parameters are usually a sign of a method that could be
|
||||
/// replaced by a function, class method, or static method.
|
||||
///
|
||||
/// This rule exempts methods decorated with [`@typing.override`][override].
|
||||
/// Converting an instance method into a static method or class method may
|
||||
/// cause type checkers to complain about a violation of the Liskov
|
||||
/// Substitution Principle if it means that the method now incompatibly
|
||||
/// overrides a method defined on a superclass. Explicitly decorating an
|
||||
/// overriding method with `@override` signals to Ruff that the method is
|
||||
/// intended to override a superclass method and that a type checker will
|
||||
/// enforce that it does so; Ruff therefore knows that it should not enforce
|
||||
/// rules about unused `self` parameters on such methods.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Person:
|
||||
@@ -38,6 +48,8 @@ use crate::rules::flake8_unused_arguments::rules::is_not_implemented_stub_with_v
|
||||
/// def greeting():
|
||||
/// print("Greetings friend!")
|
||||
/// ```
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "v0.0.286")]
|
||||
pub(crate) struct NoSelfUse {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
|
||||
use ruff_python_ast::{
|
||||
self as ast,
|
||||
helpers::map_callable,
|
||||
visitor::{Visitor, walk_expr, walk_stmt},
|
||||
};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Violation;
|
||||
@@ -50,65 +53,54 @@ impl Violation for StopIterationReturn {
|
||||
}
|
||||
|
||||
/// PLR1708
|
||||
pub(crate) fn stop_iteration_return(checker: &Checker, raise_stmt: &ast::StmtRaise) {
|
||||
// Fast-path: only continue if this is `raise StopIteration` (with or without args)
|
||||
let Some(exc) = &raise_stmt.exc else {
|
||||
return;
|
||||
pub(crate) fn stop_iteration_return(checker: &Checker, function_def: &ast::StmtFunctionDef) {
|
||||
let mut analyzer = GeneratorAnalyzer {
|
||||
checker,
|
||||
has_yield: false,
|
||||
stop_iteration_raises: Vec::new(),
|
||||
};
|
||||
|
||||
let is_stop_iteration = match exc.as_ref() {
|
||||
ast::Expr::Call(ast::ExprCall { func, .. }) => {
|
||||
checker.semantic().match_builtin_expr(func, "StopIteration")
|
||||
analyzer.visit_body(&function_def.body);
|
||||
|
||||
if analyzer.has_yield {
|
||||
for raise_stmt in analyzer.stop_iteration_raises {
|
||||
checker.report_diagnostic(StopIterationReturn, raise_stmt.range());
|
||||
}
|
||||
expr => checker.semantic().match_builtin_expr(expr, "StopIteration"),
|
||||
};
|
||||
|
||||
if !is_stop_iteration {
|
||||
return;
|
||||
}
|
||||
|
||||
// Now check the (more expensive) generator context
|
||||
if !in_generator_context(checker) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.report_diagnostic(StopIterationReturn, raise_stmt.range());
|
||||
}
|
||||
|
||||
/// Returns true if we're inside a function that contains any `yield`/`yield from`.
|
||||
fn in_generator_context(checker: &Checker) -> bool {
|
||||
for scope in checker.semantic().current_scopes() {
|
||||
if let ruff_python_semantic::ScopeKind::Function(function_def) = scope.kind {
|
||||
if contains_yield_statement(&function_def.body) {
|
||||
return true;
|
||||
struct GeneratorAnalyzer<'a, 'b> {
|
||||
checker: &'a Checker<'b>,
|
||||
has_yield: bool,
|
||||
stop_iteration_raises: Vec<&'a ast::StmtRaise>,
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for GeneratorAnalyzer<'a, '_> {
|
||||
fn visit_stmt(&mut self, stmt: &'a ast::Stmt) {
|
||||
match stmt {
|
||||
ast::Stmt::FunctionDef(_) => {}
|
||||
ast::Stmt::Raise(raise @ ast::StmtRaise { exc: Some(exc), .. }) => {
|
||||
if self
|
||||
.checker
|
||||
.semantic()
|
||||
.match_builtin_expr(map_callable(exc), "StopIteration")
|
||||
{
|
||||
self.stop_iteration_raises.push(raise);
|
||||
}
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
_ => walk_stmt(self, stmt),
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if a statement list contains any yield statements
|
||||
fn contains_yield_statement(body: &[ast::Stmt]) -> bool {
|
||||
struct YieldFinder {
|
||||
found: bool,
|
||||
}
|
||||
|
||||
impl Visitor<'_> for YieldFinder {
|
||||
fn visit_expr(&mut self, expr: &ast::Expr) {
|
||||
if matches!(expr, ast::Expr::Yield(_) | ast::Expr::YieldFrom(_)) {
|
||||
self.found = true;
|
||||
} else {
|
||||
fn visit_expr(&mut self, expr: &'a ast::Expr) {
|
||||
match expr {
|
||||
ast::Expr::Lambda(_) => {}
|
||||
ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) => {
|
||||
self.has_yield = true;
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
_ => walk_expr(self, expr),
|
||||
}
|
||||
}
|
||||
|
||||
let mut finder = YieldFinder { found: false };
|
||||
for stmt in body {
|
||||
walk_stmt(&mut finder, stmt);
|
||||
if finder.found {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -12,6 +12,16 @@ use crate::checkers::ast::Checker;
|
||||
/// By default, this rule allows up to five arguments, as configured by the
|
||||
/// [`lint.pylint.max-args`] option.
|
||||
///
|
||||
/// This rule exempts methods decorated with [`@typing.override`][override].
|
||||
/// Changing the signature of a subclass method may cause type checkers to
|
||||
/// complain about a violation of the Liskov Substitution Principle if it
|
||||
/// means that the method now incompatibly overrides a method defined on a
|
||||
/// superclass. Explicitly decorating an overriding method with `@override`
|
||||
/// signals to Ruff that the method is intended to override a superclass
|
||||
/// method and that a type checker will enforce that it does so; Ruff
|
||||
/// therefore knows that it should not enforce rules about methods having
|
||||
/// too many arguments.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Functions with many arguments are harder to understand, maintain, and call.
|
||||
/// Consider refactoring functions with many arguments into smaller functions
|
||||
@@ -43,6 +53,8 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pylint.max-args`
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.238")]
|
||||
pub(crate) struct TooManyArguments {
|
||||
|
||||
@@ -21,6 +21,16 @@ use crate::checkers::ast::Checker;
|
||||
/// with fewer arguments, using objects to group related arguments, or migrating to
|
||||
/// [keyword-only arguments](https://docs.python.org/3/tutorial/controlflow.html#special-parameters).
|
||||
///
|
||||
/// This rule exempts methods decorated with [`@typing.override`][override].
|
||||
/// Changing the signature of a subclass method may cause type checkers to
|
||||
/// complain about a violation of the Liskov Substitution Principle if it
|
||||
/// means that the method now incompatibly overrides a method defined on a
|
||||
/// superclass. Explicitly decorating an overriding method with `@override`
|
||||
/// signals to Ruff that the method is intended to override a superclass
|
||||
/// method and that a type checker will enforce that it does so; Ruff
|
||||
/// therefore knows that it should not enforce rules about methods having
|
||||
/// too many arguments.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
@@ -41,6 +51,8 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pylint.max-positional-args`
|
||||
///
|
||||
/// [override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "v0.1.7")]
|
||||
pub(crate) struct TooManyPositionalArguments {
|
||||
|
||||
@@ -39,3 +39,61 @@ help: Add a call to `.items()`
|
||||
18 |
|
||||
19 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
|
||||
--> dict_iter_missing_items.py:37:13
|
||||
|
|
||||
35 | empty_dict = {}
|
||||
36 | empty_dict["x"] = 1
|
||||
37 | for k, v in empty_dict:
|
||||
| ^^^^^^^^^^
|
||||
38 | pass
|
||||
|
|
||||
help: Add a call to `.items()`
|
||||
34 | # Empty dict cases
|
||||
35 | empty_dict = {}
|
||||
36 | empty_dict["x"] = 1
|
||||
- for k, v in empty_dict:
|
||||
37 + for k, v in empty_dict.items():
|
||||
38 | pass
|
||||
39 |
|
||||
40 | empty_dict_annotated_tuple_keys: dict[tuple[int, str], bool] = {}
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
|
||||
--> dict_iter_missing_items.py:46:13
|
||||
|
|
||||
44 | empty_dict_unannotated = {}
|
||||
45 | empty_dict_unannotated[("x", "y")] = True
|
||||
46 | for k, v in empty_dict_unannotated:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
47 | pass
|
||||
|
|
||||
help: Add a call to `.items()`
|
||||
43 |
|
||||
44 | empty_dict_unannotated = {}
|
||||
45 | empty_dict_unannotated[("x", "y")] = True
|
||||
- for k, v in empty_dict_unannotated:
|
||||
46 + for k, v in empty_dict_unannotated.items():
|
||||
47 | pass
|
||||
48 |
|
||||
49 | empty_dict_annotated_str_keys: dict[str, int] = {}
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
|
||||
--> dict_iter_missing_items.py:51:13
|
||||
|
|
||||
49 | empty_dict_annotated_str_keys: dict[str, int] = {}
|
||||
50 | empty_dict_annotated_str_keys["x"] = 1
|
||||
51 | for k, v in empty_dict_annotated_str_keys:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
52 | pass
|
||||
|
|
||||
help: Add a call to `.items()`
|
||||
48 |
|
||||
49 | empty_dict_annotated_str_keys: dict[str, int] = {}
|
||||
50 | empty_dict_annotated_str_keys["x"] = 1
|
||||
- for k, v in empty_dict_annotated_str_keys:
|
||||
51 + for k, v in empty_dict_annotated_str_keys.items():
|
||||
52 | pass
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -107,3 +107,24 @@ PLR1708 Explicit `raise StopIteration` in generator
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Use `return` instead
|
||||
|
||||
PLR1708 Explicit `raise StopIteration` in generator
|
||||
--> stop_iteration_return.py:149:9
|
||||
|
|
||||
147 | yield 1
|
||||
148 | class C:
|
||||
149 | raise StopIteration # Should trigger
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
150 | yield C
|
||||
|
|
||||
help: Use `return` instead
|
||||
|
||||
PLR1708 Explicit `raise StopIteration` in generator
|
||||
--> stop_iteration_return.py:154:5
|
||||
|
|
||||
152 | # https://github.com/astral-sh/ruff/pull/21177#discussion_r2539702728
|
||||
153 | def foo():
|
||||
154 | raise StopIteration((yield 1)) # Should trigger
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Use `return` instead
|
||||
|
||||
@@ -97,7 +97,8 @@ mod tests {
|
||||
#[test_case(Rule::MapIntVersionParsing, Path::new("RUF048_1.py"))]
|
||||
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
|
||||
#[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))]
|
||||
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))]
|
||||
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"))]
|
||||
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_1.py"))]
|
||||
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
|
||||
#[test_case(Rule::FalsyDictGetFallback, Path::new("RUF056.py"))]
|
||||
#[test_case(Rule::UnnecessaryRound, Path::new("RUF057.py"))]
|
||||
@@ -114,6 +115,7 @@ mod tests {
|
||||
#[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))]
|
||||
#[test_case(Rule::LoggingEagerConversion, Path::new("RUF065_0.py"))]
|
||||
#[test_case(Rule::LoggingEagerConversion, Path::new("RUF065_1.py"))]
|
||||
#[test_case(Rule::PropertyWithoutReturn, Path::new("RUF066.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
|
||||
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]
|
||||
@@ -621,8 +623,8 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"^_+", 1)]
|
||||
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"", 2)]
|
||||
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"), r"^_+", 1)]
|
||||
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"), r"", 2)]
|
||||
fn custom_regexp_preset(
|
||||
rule_code: Rule,
|
||||
path: &Path,
|
||||
|
||||
@@ -35,6 +35,7 @@ pub(crate) use non_octal_permissions::*;
|
||||
pub(crate) use none_not_at_end_of_union::*;
|
||||
pub(crate) use parenthesize_chained_operators::*;
|
||||
pub(crate) use post_init_default::*;
|
||||
pub(crate) use property_without_return::*;
|
||||
pub(crate) use pytest_raises_ambiguous_pattern::*;
|
||||
pub(crate) use quadratic_list_summation::*;
|
||||
pub(crate) use redirected_noqa::*;
|
||||
@@ -99,6 +100,7 @@ mod non_octal_permissions;
|
||||
mod none_not_at_end_of_union;
|
||||
mod parenthesize_chained_operators;
|
||||
mod post_init_default;
|
||||
mod property_without_return;
|
||||
mod pytest_raises_ambiguous_pattern;
|
||||
mod quadratic_list_summation;
|
||||
mod redirected_noqa;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
|
||||
use ruff_python_ast::{Expr, Stmt, StmtFunctionDef};
|
||||
use ruff_python_semantic::analyze::{function_type, visibility};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Detects class `@property` methods that does not have a `return` statement.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Property methods are expected to return a computed value, a missing return in a property usually indicates an implementation mistake.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class User:
|
||||
/// @property
|
||||
/// def full_name(self):
|
||||
/// f"{self.first_name} {self.last_name}"
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class User:
|
||||
/// @property
|
||||
/// def full_name(self):
|
||||
/// return f"{self.first_name} {self.last_name}"
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: The property class](https://docs.python.org/3/library/functions.html#property)
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.14.7")]
|
||||
pub(crate) struct PropertyWithoutReturn {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Violation for PropertyWithoutReturn {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self { name } = self;
|
||||
format!("`{name}` is a property without a `return` statement")
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF066
|
||||
pub(crate) fn property_without_return(checker: &Checker, function_def: &StmtFunctionDef) {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
if checker.source_type.is_stub() || semantic.in_protocol_or_abstract_method() {
|
||||
return;
|
||||
}
|
||||
|
||||
let StmtFunctionDef {
|
||||
decorator_list,
|
||||
body,
|
||||
name,
|
||||
..
|
||||
} = function_def;
|
||||
|
||||
if !visibility::is_property(decorator_list, [], semantic)
|
||||
|| visibility::is_overload(decorator_list, semantic)
|
||||
|| function_type::is_stub(function_def, semantic)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut visitor = PropertyVisitor::default();
|
||||
visitor.visit_body(body);
|
||||
if visitor.found {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.report_diagnostic(
|
||||
PropertyWithoutReturn {
|
||||
name: name.to_string(),
|
||||
},
|
||||
function_def.identifier(),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PropertyVisitor {
|
||||
found: bool,
|
||||
}
|
||||
|
||||
// NOTE: We are actually searching for the presence of
|
||||
// `yield`/`yield from`/`raise`/`return` statement/expression,
|
||||
// as having one of those indicates that there's likely no implementation mistake
|
||||
impl Visitor<'_> for PropertyVisitor {
|
||||
fn visit_expr(&mut self, expr: &Expr) {
|
||||
if self.found {
|
||||
return;
|
||||
}
|
||||
|
||||
match expr {
|
||||
Expr::Yield(_) | Expr::YieldFrom(_) => self.found = true,
|
||||
_ => walk_expr(self, expr),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||
if self.found {
|
||||
return;
|
||||
}
|
||||
|
||||
match stmt {
|
||||
Stmt::Return(_) | Stmt::Raise(_) => self.found = true,
|
||||
Stmt::FunctionDef(_) => {
|
||||
// Do not recurse into nested functions; they're evaluated separately.
|
||||
}
|
||||
_ => walk_stmt(self, stmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::helpers::is_dunder;
|
||||
use ruff_python_semantic::{Binding, BindingId};
|
||||
use ruff_python_semantic::{Binding, BindingId, BindingKind, ScopeKind};
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -111,7 +111,7 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
|
||||
return;
|
||||
}
|
||||
|
||||
// We only emit the lint on variables defined via assignments.
|
||||
// We only emit the lint on local variables.
|
||||
//
|
||||
// ## Why not also emit the lint on function parameters?
|
||||
//
|
||||
@@ -127,8 +127,30 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
|
||||
// autofixing the diagnostic for assignments. See:
|
||||
// - <https://github.com/astral-sh/ruff/issues/14790>
|
||||
// - <https://github.com/astral-sh/ruff/issues/14799>
|
||||
if !binding.kind.is_assignment() {
|
||||
return;
|
||||
match binding.kind {
|
||||
BindingKind::Annotation
|
||||
| BindingKind::Argument
|
||||
| BindingKind::NamedExprAssignment
|
||||
| BindingKind::Assignment
|
||||
| BindingKind::LoopVar
|
||||
| BindingKind::WithItemVar
|
||||
| BindingKind::BoundException
|
||||
| BindingKind::UnboundException(_) => {}
|
||||
|
||||
BindingKind::TypeParam
|
||||
| BindingKind::Global(_)
|
||||
| BindingKind::Nonlocal(_, _)
|
||||
| BindingKind::Builtin
|
||||
| BindingKind::ClassDefinition(_)
|
||||
| BindingKind::FunctionDefinition(_)
|
||||
| BindingKind::Export(_)
|
||||
| BindingKind::FutureImport
|
||||
| BindingKind::Import(_)
|
||||
| BindingKind::FromImport(_)
|
||||
| BindingKind::SubmoduleImport(_)
|
||||
| BindingKind::Deletion
|
||||
| BindingKind::ConditionalDeletion(_)
|
||||
| BindingKind::DunderClassCell => return,
|
||||
}
|
||||
|
||||
// This excludes `global` and `nonlocal` variables.
|
||||
@@ -138,9 +160,12 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
|
||||
|
||||
let semantic = checker.semantic();
|
||||
|
||||
// Only variables defined in function scopes
|
||||
// Only variables defined in function and generator scopes
|
||||
let scope = &semantic.scopes[binding.scope];
|
||||
if !scope.kind.is_function() {
|
||||
if !matches!(
|
||||
scope.kind,
|
||||
ScopeKind::Function(_) | ScopeKind::Generator { .. }
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF052 [*] Local dummy variable `_var` is accessed
|
||||
--> RUF052.py:92:9
|
||||
--> RUF052_0.py:92:9
|
||||
|
|
||||
90 | class Class_:
|
||||
91 | def fun(self):
|
||||
@@ -24,7 +24,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_list` is accessed
|
||||
--> RUF052.py:99:5
|
||||
--> RUF052_0.py:99:5
|
||||
|
|
||||
98 | def fun():
|
||||
99 | _list = "built-in" # [RUF052]
|
||||
@@ -45,7 +45,7 @@ help: Prefer using trailing underscores to avoid shadowing a built-in
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_x` is accessed
|
||||
--> RUF052.py:106:5
|
||||
--> RUF052_0.py:106:5
|
||||
|
|
||||
104 | def fun():
|
||||
105 | global x
|
||||
@@ -67,7 +67,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_x` is accessed
|
||||
--> RUF052.py:113:5
|
||||
--> RUF052_0.py:113:5
|
||||
|
|
||||
111 | def bar():
|
||||
112 | nonlocal x
|
||||
@@ -90,7 +90,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_x` is accessed
|
||||
--> RUF052.py:120:5
|
||||
--> RUF052_0.py:120:5
|
||||
|
|
||||
118 | def fun():
|
||||
119 | x = "local"
|
||||
@@ -112,7 +112,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
--> RUF052.py:128:5
|
||||
--> RUF052_0.py:128:5
|
||||
|
|
||||
127 | def unfixables():
|
||||
128 | _GLOBAL_1 = "foo"
|
||||
@@ -123,7 +123,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_local` is accessed
|
||||
--> RUF052.py:136:5
|
||||
--> RUF052_0.py:136:5
|
||||
|
|
||||
135 | # unfixable because the rename would shadow a local variable
|
||||
136 | _local = "local3" # [RUF052]
|
||||
@@ -133,7 +133,7 @@ RUF052 Local dummy variable `_local` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
--> RUF052.py:140:9
|
||||
--> RUF052_0.py:140:9
|
||||
|
|
||||
139 | def nested():
|
||||
140 | _GLOBAL_1 = "foo"
|
||||
@@ -144,7 +144,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_local` is accessed
|
||||
--> RUF052.py:145:9
|
||||
--> RUF052_0.py:145:9
|
||||
|
|
||||
144 | # unfixable because the rename would shadow a variable from the outer function
|
||||
145 | _local = "local4"
|
||||
@@ -154,7 +154,7 @@ RUF052 Local dummy variable `_local` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 [*] Local dummy variable `_P` is accessed
|
||||
--> RUF052.py:153:5
|
||||
--> RUF052_0.py:153:5
|
||||
|
|
||||
151 | from collections import namedtuple
|
||||
152 |
|
||||
@@ -184,7 +184,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_T` is accessed
|
||||
--> RUF052.py:154:5
|
||||
--> RUF052_0.py:154:5
|
||||
|
|
||||
153 | _P = ParamSpec("_P")
|
||||
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
|
||||
@@ -213,7 +213,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_NT` is accessed
|
||||
--> RUF052.py:155:5
|
||||
--> RUF052_0.py:155:5
|
||||
|
|
||||
153 | _P = ParamSpec("_P")
|
||||
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
|
||||
@@ -242,7 +242,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_E` is accessed
|
||||
--> RUF052.py:156:5
|
||||
--> RUF052_0.py:156:5
|
||||
|
|
||||
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
|
||||
155 | _NT = NamedTuple("_NT", [("foo", int)])
|
||||
@@ -270,7 +270,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_NT2` is accessed
|
||||
--> RUF052.py:157:5
|
||||
--> RUF052_0.py:157:5
|
||||
|
|
||||
155 | _NT = NamedTuple("_NT", [("foo", int)])
|
||||
156 | _E = Enum("_E", ["a", "b", "c"])
|
||||
@@ -297,7 +297,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_NT3` is accessed
|
||||
--> RUF052.py:158:5
|
||||
--> RUF052_0.py:158:5
|
||||
|
|
||||
156 | _E = Enum("_E", ["a", "b", "c"])
|
||||
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
|
||||
@@ -323,7 +323,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_DynamicClass` is accessed
|
||||
--> RUF052.py:159:5
|
||||
--> RUF052_0.py:159:5
|
||||
|
|
||||
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
|
||||
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
|
||||
@@ -347,7 +347,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_NotADynamicClass` is accessed
|
||||
--> RUF052.py:160:5
|
||||
--> RUF052_0.py:160:5
|
||||
|
|
||||
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
|
||||
159 | _DynamicClass = type("_DynamicClass", (), {})
|
||||
@@ -371,7 +371,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_dummy_var` is accessed
|
||||
--> RUF052.py:182:5
|
||||
--> RUF052_0.py:182:5
|
||||
|
|
||||
181 | def foo():
|
||||
182 | _dummy_var = 42
|
||||
@@ -396,7 +396,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 Local dummy variable `_dummy_var` is accessed
|
||||
--> RUF052.py:192:5
|
||||
--> RUF052_0.py:192:5
|
||||
|
|
||||
190 | # Unfixable because both possible candidates for the new name are shadowed
|
||||
191 | # in the scope of one of the references to the variable
|
||||
@@ -0,0 +1,494 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:21:9
|
||||
|
|
||||
20 | # Should detect used dummy variable
|
||||
21 | for _item in my_list:
|
||||
| ^^^^^
|
||||
22 | print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
18 | my_list = [{"foo": 1}, {"foo": 2}]
|
||||
19 |
|
||||
20 | # Should detect used dummy variable
|
||||
- for _item in my_list:
|
||||
- print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
|
||||
21 + for item in my_list:
|
||||
22 + print(item["foo"]) # RUF052: Local dummy variable `_item` is accessed
|
||||
23 |
|
||||
24 | # Should detect used dummy variable
|
||||
25 | for _index, _value in enumerate(my_list):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_index` is accessed
|
||||
--> RUF052_1.py:25:9
|
||||
|
|
||||
24 | # Should detect used dummy variable
|
||||
25 | for _index, _value in enumerate(my_list):
|
||||
| ^^^^^^
|
||||
26 | result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
22 | print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
|
||||
23 |
|
||||
24 | # Should detect used dummy variable
|
||||
- for _index, _value in enumerate(my_list):
|
||||
- result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
||||
25 + for index, _value in enumerate(my_list):
|
||||
26 + result = index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
||||
27 |
|
||||
28 | # List Comprehensions
|
||||
29 | def test_list_comprehensions():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_value` is accessed
|
||||
--> RUF052_1.py:25:17
|
||||
|
|
||||
24 | # Should detect used dummy variable
|
||||
25 | for _index, _value in enumerate(my_list):
|
||||
| ^^^^^^
|
||||
26 | result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
22 | print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
|
||||
23 |
|
||||
24 | # Should detect used dummy variable
|
||||
- for _index, _value in enumerate(my_list):
|
||||
- result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
||||
25 + for _index, value in enumerate(my_list):
|
||||
26 + result = _index + value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
||||
27 |
|
||||
28 | # List Comprehensions
|
||||
29 | def test_list_comprehensions():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:33:32
|
||||
|
|
||||
32 | # Should detect used dummy variable
|
||||
33 | result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
|
||||
| ^^^^^
|
||||
34 |
|
||||
35 | # Should detect used dummy variable in nested comprehension
|
||||
|
|
||||
help: Remove leading underscores
|
||||
30 | my_list = [{"foo": 1}, {"foo": 2}]
|
||||
31 |
|
||||
32 | # Should detect used dummy variable
|
||||
- result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
|
||||
33 + result = [item["foo"] for item in my_list] # RUF052: Local dummy variable `_item` is accessed
|
||||
34 |
|
||||
35 | # Should detect used dummy variable in nested comprehension
|
||||
36 | nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:36:33
|
||||
|
|
||||
35 | # Should detect used dummy variable in nested comprehension
|
||||
36 | nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
|
||||
| ^^^^^
|
||||
37 | # RUF052: Both `_item` and `_sublist` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
33 | result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
|
||||
34 |
|
||||
35 | # Should detect used dummy variable in nested comprehension
|
||||
- nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
|
||||
36 + nested = [[item["foo"] for item in _sublist] for _sublist in [my_list, my_list]]
|
||||
37 | # RUF052: Both `_item` and `_sublist` are accessed
|
||||
38 |
|
||||
39 | # Should detect with conditions
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_sublist` is accessed
|
||||
--> RUF052_1.py:36:56
|
||||
|
|
||||
35 | # Should detect used dummy variable in nested comprehension
|
||||
36 | nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
|
||||
| ^^^^^^^^
|
||||
37 | # RUF052: Both `_item` and `_sublist` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
33 | result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
|
||||
34 |
|
||||
35 | # Should detect used dummy variable in nested comprehension
|
||||
- nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
|
||||
36 + nested = [[_item["foo"] for _item in sublist] for sublist in [my_list, my_list]]
|
||||
37 | # RUF052: Both `_item` and `_sublist` are accessed
|
||||
38 |
|
||||
39 | # Should detect with conditions
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:40:34
|
||||
|
|
||||
39 | # Should detect with conditions
|
||||
40 | filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
|
||||
| ^^^^^
|
||||
41 | # RUF052: Local dummy variable `_item` is accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
37 | # RUF052: Both `_item` and `_sublist` are accessed
|
||||
38 |
|
||||
39 | # Should detect with conditions
|
||||
- filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
|
||||
40 + filtered = [item["foo"] for item in my_list if item["foo"] > 0]
|
||||
41 | # RUF052: Local dummy variable `_item` is accessed
|
||||
42 |
|
||||
43 | # Dict Comprehensions
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:48:48
|
||||
|
|
||||
47 | # Should detect used dummy variable
|
||||
48 | result = {_item["key"]: _item["value"] for _item in my_list}
|
||||
| ^^^^^
|
||||
49 | # RUF052: Local dummy variable `_item` is accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
45 | my_list = [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
|
||||
46 |
|
||||
47 | # Should detect used dummy variable
|
||||
- result = {_item["key"]: _item["value"] for _item in my_list}
|
||||
48 + result = {item["key"]: item["value"] for item in my_list}
|
||||
49 | # RUF052: Local dummy variable `_item` is accessed
|
||||
50 |
|
||||
51 | # Should detect with enumerate
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_index` is accessed
|
||||
--> RUF052_1.py:52:43
|
||||
|
|
||||
51 | # Should detect with enumerate
|
||||
52 | indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
|
||||
| ^^^^^^
|
||||
53 | # RUF052: Both `_index` and `_item` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
49 | # RUF052: Local dummy variable `_item` is accessed
|
||||
50 |
|
||||
51 | # Should detect with enumerate
|
||||
- indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
|
||||
52 + indexed = {index: _item["value"] for index, _item in enumerate(my_list)}
|
||||
53 | # RUF052: Both `_index` and `_item` are accessed
|
||||
54 |
|
||||
55 | # Should detect in nested dict comprehension
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:52:51
|
||||
|
|
||||
51 | # Should detect with enumerate
|
||||
52 | indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
|
||||
| ^^^^^
|
||||
53 | # RUF052: Both `_index` and `_item` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
49 | # RUF052: Local dummy variable `_item` is accessed
|
||||
50 |
|
||||
51 | # Should detect with enumerate
|
||||
- indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
|
||||
52 + indexed = {_index: item["value"] for _index, item in enumerate(my_list)}
|
||||
53 | # RUF052: Both `_index` and `_item` are accessed
|
||||
54 |
|
||||
55 | # Should detect in nested dict comprehension
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_inner` is accessed
|
||||
--> RUF052_1.py:56:59
|
||||
|
|
||||
55 | # Should detect in nested dict comprehension
|
||||
56 | nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
|
||||
| ^^^^^^
|
||||
57 | for _outer, sublist in enumerate([my_list])}
|
||||
58 | # RUF052: `_outer`, `_inner` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
53 | # RUF052: Both `_index` and `_item` are accessed
|
||||
54 |
|
||||
55 | # Should detect in nested dict comprehension
|
||||
- nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
|
||||
56 + nested = {_outer: {inner["key"]: inner["value"] for inner in sublist}
|
||||
57 | for _outer, sublist in enumerate([my_list])}
|
||||
58 | # RUF052: `_outer`, `_inner` are accessed
|
||||
59 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_outer` is accessed
|
||||
--> RUF052_1.py:57:19
|
||||
|
|
||||
55 | # Should detect in nested dict comprehension
|
||||
56 | nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
|
||||
57 | for _outer, sublist in enumerate([my_list])}
|
||||
| ^^^^^^
|
||||
58 | # RUF052: `_outer`, `_inner` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
53 | # RUF052: Both `_index` and `_item` are accessed
|
||||
54 |
|
||||
55 | # Should detect in nested dict comprehension
|
||||
- nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
|
||||
- for _outer, sublist in enumerate([my_list])}
|
||||
56 + nested = {outer: {_inner["key"]: _inner["value"] for _inner in sublist}
|
||||
57 + for outer, sublist in enumerate([my_list])}
|
||||
58 | # RUF052: `_outer`, `_inner` are accessed
|
||||
59 |
|
||||
60 | # Set Comprehensions
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:65:39
|
||||
|
|
||||
64 | # Should detect used dummy variable
|
||||
65 | unique_values = {_item["foo"] for _item in my_list}
|
||||
| ^^^^^
|
||||
66 | # RUF052: Local dummy variable `_item` is accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
62 | my_list = [{"foo": 1}, {"foo": 2}, {"foo": 1}] # Note: duplicate values
|
||||
63 |
|
||||
64 | # Should detect used dummy variable
|
||||
- unique_values = {_item["foo"] for _item in my_list}
|
||||
65 + unique_values = {item["foo"] for item in my_list}
|
||||
66 | # RUF052: Local dummy variable `_item` is accessed
|
||||
67 |
|
||||
68 | # Should detect with conditions
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:69:38
|
||||
|
|
||||
68 | # Should detect with conditions
|
||||
69 | filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
|
||||
| ^^^^^
|
||||
70 | # RUF052: Local dummy variable `_item` is accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
66 | # RUF052: Local dummy variable `_item` is accessed
|
||||
67 |
|
||||
68 | # Should detect with conditions
|
||||
- filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
|
||||
69 + filtered_set = {item["foo"] for item in my_list if item["foo"] > 0}
|
||||
70 | # RUF052: Local dummy variable `_item` is accessed
|
||||
71 |
|
||||
72 | # Should detect with complex expression
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:73:39
|
||||
|
|
||||
72 | # Should detect with complex expression
|
||||
73 | processed = {_item["foo"] * 2 for _item in my_list}
|
||||
| ^^^^^
|
||||
74 | # RUF052: Local dummy variable `_item` is accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
70 | # RUF052: Local dummy variable `_item` is accessed
|
||||
71 |
|
||||
72 | # Should detect with complex expression
|
||||
- processed = {_item["foo"] * 2 for _item in my_list}
|
||||
73 + processed = {item["foo"] * 2 for item in my_list}
|
||||
74 | # RUF052: Local dummy variable `_item` is accessed
|
||||
75 |
|
||||
76 | # Generator Expressions
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:81:29
|
||||
|
|
||||
80 | # Should detect used dummy variable
|
||||
81 | gen = (_item["foo"] for _item in my_list)
|
||||
| ^^^^^
|
||||
82 | # RUF052: Local dummy variable `_item` is accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
78 | my_list = [{"foo": 1}, {"foo": 2}]
|
||||
79 |
|
||||
80 | # Should detect used dummy variable
|
||||
- gen = (_item["foo"] for _item in my_list)
|
||||
81 + gen = (item["foo"] for item in my_list)
|
||||
82 | # RUF052: Local dummy variable `_item` is accessed
|
||||
83 |
|
||||
84 | # Should detect when passed to function
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:85:34
|
||||
|
|
||||
84 | # Should detect when passed to function
|
||||
85 | total = sum(_item["foo"] for _item in my_list)
|
||||
| ^^^^^
|
||||
86 | # RUF052: Local dummy variable `_item` is accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
82 | # RUF052: Local dummy variable `_item` is accessed
|
||||
83 |
|
||||
84 | # Should detect when passed to function
|
||||
- total = sum(_item["foo"] for _item in my_list)
|
||||
85 + total = sum(item["foo"] for item in my_list)
|
||||
86 | # RUF052: Local dummy variable `_item` is accessed
|
||||
87 |
|
||||
88 | # Should detect with multiple generators
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_x` is accessed
|
||||
--> RUF052_1.py:89:27
|
||||
|
|
||||
88 | # Should detect with multiple generators
|
||||
89 | pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
|
||||
| ^^
|
||||
90 | # RUF052: Both `_x` and `_y` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
86 | # RUF052: Local dummy variable `_item` is accessed
|
||||
87 |
|
||||
88 | # Should detect with multiple generators
|
||||
- pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
|
||||
89 + pairs = ((x, _y) for x in range(3) for _y in range(3) if x != _y)
|
||||
90 | # RUF052: Both `_x` and `_y` are accessed
|
||||
91 |
|
||||
92 | # Should detect in nested generator
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_y` is accessed
|
||||
--> RUF052_1.py:89:46
|
||||
|
|
||||
88 | # Should detect with multiple generators
|
||||
89 | pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
|
||||
| ^^
|
||||
90 | # RUF052: Both `_x` and `_y` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
86 | # RUF052: Local dummy variable `_item` is accessed
|
||||
87 |
|
||||
88 | # Should detect with multiple generators
|
||||
- pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
|
||||
89 + pairs = ((_x, y) for _x in range(3) for y in range(3) if _x != y)
|
||||
90 | # RUF052: Both `_x` and `_y` are accessed
|
||||
91 |
|
||||
92 | # Should detect in nested generator
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_inner` is accessed
|
||||
--> RUF052_1.py:93:41
|
||||
|
|
||||
92 | # Should detect in nested generator
|
||||
93 | nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
|
||||
| ^^^^^^
|
||||
94 | # RUF052: `_inner` and `_sublist` are accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
90 | # RUF052: Both `_x` and `_y` are accessed
|
||||
91 |
|
||||
92 | # Should detect in nested generator
|
||||
- nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
|
||||
93 + nested_gen = (sum(inner["foo"] for inner in sublist) for _sublist in [my_list] for sublist in _sublist)
|
||||
94 | # RUF052: `_inner` and `_sublist` are accessed
|
||||
95 |
|
||||
96 | # Complex Examples with Multiple Comprehension Types
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_sublist` is accessed
|
||||
--> RUF052_1.py:93:64
|
||||
|
|
||||
92 | # Should detect in nested generator
|
||||
93 | nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
|
||||
| ^^^^^^^^
|
||||
94 | # RUF052: `_inner` and `_sublist` are accessed
|
||||
|
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
90 | # RUF052: Both `_x` and `_y` are accessed
|
||||
91 |
|
||||
92 | # Should detect in nested generator
|
||||
- nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
|
||||
93 + nested_gen = (sum(_inner["foo"] for _inner in sublist) for sublist_ in [my_list] for sublist in sublist_)
|
||||
94 | # RUF052: `_inner` and `_sublist` are accessed
|
||||
95 |
|
||||
96 | # Complex Examples with Multiple Comprehension Types
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_val` is accessed
|
||||
--> RUF052_1.py:102:30
|
||||
|
|
||||
100 | # Should detect in mixed comprehensions
|
||||
101 | result = [
|
||||
102 | {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
|
||||
| ^^^^
|
||||
103 | for _record in data
|
||||
104 | ]
|
||||
|
|
||||
help: Remove leading underscores
|
||||
99 |
|
||||
100 | # Should detect in mixed comprehensions
|
||||
101 | result = [
|
||||
- {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
|
||||
102 + {_key: [val * 2 for val in _record["items"]] for _key in ["doubled"]}
|
||||
103 | for _record in data
|
||||
104 | ]
|
||||
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_key` is accessed
|
||||
--> RUF052_1.py:102:60
|
||||
|
|
||||
100 | # Should detect in mixed comprehensions
|
||||
101 | result = [
|
||||
102 | {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
|
||||
| ^^^^
|
||||
103 | for _record in data
|
||||
104 | ]
|
||||
|
|
||||
help: Remove leading underscores
|
||||
99 |
|
||||
100 | # Should detect in mixed comprehensions
|
||||
101 | result = [
|
||||
- {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
|
||||
102 + {key: [_val * 2 for _val in _record["items"]] for key in ["doubled"]}
|
||||
103 | for _record in data
|
||||
104 | ]
|
||||
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_record` is accessed
|
||||
--> RUF052_1.py:103:13
|
||||
|
|
||||
101 | result = [
|
||||
102 | {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
|
||||
103 | for _record in data
|
||||
| ^^^^^^^
|
||||
104 | ]
|
||||
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
99 |
|
||||
100 | # Should detect in mixed comprehensions
|
||||
101 | result = [
|
||||
- {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
|
||||
- for _record in data
|
||||
102 + {_key: [_val * 2 for _val in record["items"]] for _key in ["doubled"]}
|
||||
103 + for record in data
|
||||
104 | ]
|
||||
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
|
||||
106 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_item` is accessed
|
||||
--> RUF052_1.py:108:43
|
||||
|
|
||||
107 | # Should detect in generator passed to list constructor
|
||||
108 | gen_list = list(_item["items"][0] for _item in data)
|
||||
| ^^^^^
|
||||
109 | # RUF052: Local dummy variable `_item` is accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
|
||||
106 |
|
||||
107 | # Should detect in generator passed to list constructor
|
||||
- gen_list = list(_item["items"][0] for _item in data)
|
||||
108 + gen_list = list(item["items"][0] for item in data)
|
||||
109 | # RUF052: Local dummy variable `_item` is accessed
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF066 `name` is a property without a `return` statement
|
||||
--> RUF066.py:7:9
|
||||
|
|
||||
5 | class User: # Test normal class properties
|
||||
6 | @property
|
||||
7 | def name(self): # ERROR: No return
|
||||
| ^^^^
|
||||
8 | f"{self.first_name} {self.last_name}"
|
||||
|
|
||||
|
||||
RUF066 `nested` is a property without a `return` statement
|
||||
--> RUF066.py:18:9
|
||||
|
|
||||
17 | @property
|
||||
18 | def nested(self): # ERROR: Property itself doesn't return
|
||||
| ^^^^^^
|
||||
19 | def inner():
|
||||
20 | return 0
|
||||
|
|
||||
|
||||
RUF066 `prop2` is a property without a `return` statement
|
||||
--> RUF066.py:43:9
|
||||
|
|
||||
42 | @property
|
||||
43 | def prop2(self): # ERROR: Not returning something (even when we are inside an ABC)
|
||||
| ^^^^^
|
||||
44 | 50
|
||||
|
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF052 [*] Local dummy variable `_var` is accessed
|
||||
--> RUF052.py:92:9
|
||||
--> RUF052_0.py:92:9
|
||||
|
|
||||
90 | class Class_:
|
||||
91 | def fun(self):
|
||||
@@ -24,7 +24,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_list` is accessed
|
||||
--> RUF052.py:99:5
|
||||
--> RUF052_0.py:99:5
|
||||
|
|
||||
98 | def fun():
|
||||
99 | _list = "built-in" # [RUF052]
|
||||
@@ -45,7 +45,7 @@ help: Prefer using trailing underscores to avoid shadowing a built-in
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_x` is accessed
|
||||
--> RUF052.py:106:5
|
||||
--> RUF052_0.py:106:5
|
||||
|
|
||||
104 | def fun():
|
||||
105 | global x
|
||||
@@ -67,7 +67,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_x` is accessed
|
||||
--> RUF052.py:113:5
|
||||
--> RUF052_0.py:113:5
|
||||
|
|
||||
111 | def bar():
|
||||
112 | nonlocal x
|
||||
@@ -90,7 +90,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_x` is accessed
|
||||
--> RUF052.py:120:5
|
||||
--> RUF052_0.py:120:5
|
||||
|
|
||||
118 | def fun():
|
||||
119 | x = "local"
|
||||
@@ -112,7 +112,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
--> RUF052.py:128:5
|
||||
--> RUF052_0.py:128:5
|
||||
|
|
||||
127 | def unfixables():
|
||||
128 | _GLOBAL_1 = "foo"
|
||||
@@ -123,7 +123,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_local` is accessed
|
||||
--> RUF052.py:136:5
|
||||
--> RUF052_0.py:136:5
|
||||
|
|
||||
135 | # unfixable because the rename would shadow a local variable
|
||||
136 | _local = "local3" # [RUF052]
|
||||
@@ -133,7 +133,7 @@ RUF052 Local dummy variable `_local` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
--> RUF052.py:140:9
|
||||
--> RUF052_0.py:140:9
|
||||
|
|
||||
139 | def nested():
|
||||
140 | _GLOBAL_1 = "foo"
|
||||
@@ -144,7 +144,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_local` is accessed
|
||||
--> RUF052.py:145:9
|
||||
--> RUF052_0.py:145:9
|
||||
|
|
||||
144 | # unfixable because the rename would shadow a variable from the outer function
|
||||
145 | _local = "local4"
|
||||
@@ -154,7 +154,7 @@ RUF052 Local dummy variable `_local` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 [*] Local dummy variable `_P` is accessed
|
||||
--> RUF052.py:153:5
|
||||
--> RUF052_0.py:153:5
|
||||
|
|
||||
151 | from collections import namedtuple
|
||||
152 |
|
||||
@@ -184,7 +184,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_T` is accessed
|
||||
--> RUF052.py:154:5
|
||||
--> RUF052_0.py:154:5
|
||||
|
|
||||
153 | _P = ParamSpec("_P")
|
||||
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
|
||||
@@ -213,7 +213,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_NT` is accessed
|
||||
--> RUF052.py:155:5
|
||||
--> RUF052_0.py:155:5
|
||||
|
|
||||
153 | _P = ParamSpec("_P")
|
||||
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
|
||||
@@ -242,7 +242,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_E` is accessed
|
||||
--> RUF052.py:156:5
|
||||
--> RUF052_0.py:156:5
|
||||
|
|
||||
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
|
||||
155 | _NT = NamedTuple("_NT", [("foo", int)])
|
||||
@@ -270,7 +270,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_NT2` is accessed
|
||||
--> RUF052.py:157:5
|
||||
--> RUF052_0.py:157:5
|
||||
|
|
||||
155 | _NT = NamedTuple("_NT", [("foo", int)])
|
||||
156 | _E = Enum("_E", ["a", "b", "c"])
|
||||
@@ -297,7 +297,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_NT3` is accessed
|
||||
--> RUF052.py:158:5
|
||||
--> RUF052_0.py:158:5
|
||||
|
|
||||
156 | _E = Enum("_E", ["a", "b", "c"])
|
||||
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
|
||||
@@ -323,7 +323,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_DynamicClass` is accessed
|
||||
--> RUF052.py:159:5
|
||||
--> RUF052_0.py:159:5
|
||||
|
|
||||
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
|
||||
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
|
||||
@@ -347,7 +347,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_NotADynamicClass` is accessed
|
||||
--> RUF052.py:160:5
|
||||
--> RUF052_0.py:160:5
|
||||
|
|
||||
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
|
||||
159 | _DynamicClass = type("_DynamicClass", (), {})
|
||||
@@ -371,7 +371,7 @@ help: Remove leading underscores
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 [*] Local dummy variable `_dummy_var` is accessed
|
||||
--> RUF052.py:182:5
|
||||
--> RUF052_0.py:182:5
|
||||
|
|
||||
181 | def foo():
|
||||
182 | _dummy_var = 42
|
||||
@@ -396,7 +396,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF052 Local dummy variable `_dummy_var` is accessed
|
||||
--> RUF052.py:192:5
|
||||
--> RUF052_0.py:192:5
|
||||
|
|
||||
190 | # Unfixable because both possible candidates for the new name are shadowed
|
||||
191 | # in the scope of one of the references to the variable
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF052 Local dummy variable `_var` is accessed
|
||||
--> RUF052.py:92:9
|
||||
--> RUF052_0.py:92:9
|
||||
|
|
||||
90 | class Class_:
|
||||
91 | def fun(self):
|
||||
@@ -13,7 +13,7 @@ RUF052 Local dummy variable `_var` is accessed
|
||||
help: Remove leading underscores
|
||||
|
||||
RUF052 Local dummy variable `_list` is accessed
|
||||
--> RUF052.py:99:5
|
||||
--> RUF052_0.py:99:5
|
||||
|
|
||||
98 | def fun():
|
||||
99 | _list = "built-in" # [RUF052]
|
||||
@@ -23,7 +23,7 @@ RUF052 Local dummy variable `_list` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a built-in
|
||||
|
||||
RUF052 Local dummy variable `_x` is accessed
|
||||
--> RUF052.py:106:5
|
||||
--> RUF052_0.py:106:5
|
||||
|
|
||||
104 | def fun():
|
||||
105 | global x
|
||||
@@ -34,7 +34,7 @@ RUF052 Local dummy variable `_x` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `x` is accessed
|
||||
--> RUF052.py:110:3
|
||||
--> RUF052_0.py:110:3
|
||||
|
|
||||
109 | def foo():
|
||||
110 | x = "outer"
|
||||
@@ -44,7 +44,7 @@ RUF052 Local dummy variable `x` is accessed
|
||||
|
|
||||
|
||||
RUF052 Local dummy variable `_x` is accessed
|
||||
--> RUF052.py:113:5
|
||||
--> RUF052_0.py:113:5
|
||||
|
|
||||
111 | def bar():
|
||||
112 | nonlocal x
|
||||
@@ -56,7 +56,7 @@ RUF052 Local dummy variable `_x` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_x` is accessed
|
||||
--> RUF052.py:120:5
|
||||
--> RUF052_0.py:120:5
|
||||
|
|
||||
118 | def fun():
|
||||
119 | x = "local"
|
||||
@@ -67,7 +67,7 @@ RUF052 Local dummy variable `_x` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
--> RUF052.py:128:5
|
||||
--> RUF052_0.py:128:5
|
||||
|
|
||||
127 | def unfixables():
|
||||
128 | _GLOBAL_1 = "foo"
|
||||
@@ -78,7 +78,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_local` is accessed
|
||||
--> RUF052.py:136:5
|
||||
--> RUF052_0.py:136:5
|
||||
|
|
||||
135 | # unfixable because the rename would shadow a local variable
|
||||
136 | _local = "local3" # [RUF052]
|
||||
@@ -88,7 +88,7 @@ RUF052 Local dummy variable `_local` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
--> RUF052.py:140:9
|
||||
--> RUF052_0.py:140:9
|
||||
|
|
||||
139 | def nested():
|
||||
140 | _GLOBAL_1 = "foo"
|
||||
@@ -99,7 +99,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_local` is accessed
|
||||
--> RUF052.py:145:9
|
||||
--> RUF052_0.py:145:9
|
||||
|
|
||||
144 | # unfixable because the rename would shadow a variable from the outer function
|
||||
145 | _local = "local4"
|
||||
@@ -109,7 +109,7 @@ RUF052 Local dummy variable `_local` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_P` is accessed
|
||||
--> RUF052.py:153:5
|
||||
--> RUF052_0.py:153:5
|
||||
|
|
||||
151 | from collections import namedtuple
|
||||
152 |
|
||||
@@ -121,7 +121,7 @@ RUF052 Local dummy variable `_P` is accessed
|
||||
help: Remove leading underscores
|
||||
|
||||
RUF052 Local dummy variable `_T` is accessed
|
||||
--> RUF052.py:154:5
|
||||
--> RUF052_0.py:154:5
|
||||
|
|
||||
153 | _P = ParamSpec("_P")
|
||||
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
|
||||
@@ -132,7 +132,7 @@ RUF052 Local dummy variable `_T` is accessed
|
||||
help: Remove leading underscores
|
||||
|
||||
RUF052 Local dummy variable `_NT` is accessed
|
||||
--> RUF052.py:155:5
|
||||
--> RUF052_0.py:155:5
|
||||
|
|
||||
153 | _P = ParamSpec("_P")
|
||||
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
|
||||
@@ -144,7 +144,7 @@ RUF052 Local dummy variable `_NT` is accessed
|
||||
help: Remove leading underscores
|
||||
|
||||
RUF052 Local dummy variable `_E` is accessed
|
||||
--> RUF052.py:156:5
|
||||
--> RUF052_0.py:156:5
|
||||
|
|
||||
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
|
||||
155 | _NT = NamedTuple("_NT", [("foo", int)])
|
||||
@@ -156,7 +156,7 @@ RUF052 Local dummy variable `_E` is accessed
|
||||
help: Remove leading underscores
|
||||
|
||||
RUF052 Local dummy variable `_NT2` is accessed
|
||||
--> RUF052.py:157:5
|
||||
--> RUF052_0.py:157:5
|
||||
|
|
||||
155 | _NT = NamedTuple("_NT", [("foo", int)])
|
||||
156 | _E = Enum("_E", ["a", "b", "c"])
|
||||
@@ -168,7 +168,7 @@ RUF052 Local dummy variable `_NT2` is accessed
|
||||
help: Remove leading underscores
|
||||
|
||||
RUF052 Local dummy variable `_NT3` is accessed
|
||||
--> RUF052.py:158:5
|
||||
--> RUF052_0.py:158:5
|
||||
|
|
||||
156 | _E = Enum("_E", ["a", "b", "c"])
|
||||
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
|
||||
@@ -180,7 +180,7 @@ RUF052 Local dummy variable `_NT3` is accessed
|
||||
help: Remove leading underscores
|
||||
|
||||
RUF052 Local dummy variable `_DynamicClass` is accessed
|
||||
--> RUF052.py:159:5
|
||||
--> RUF052_0.py:159:5
|
||||
|
|
||||
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
|
||||
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
|
||||
@@ -191,7 +191,7 @@ RUF052 Local dummy variable `_DynamicClass` is accessed
|
||||
help: Remove leading underscores
|
||||
|
||||
RUF052 Local dummy variable `_NotADynamicClass` is accessed
|
||||
--> RUF052.py:160:5
|
||||
--> RUF052_0.py:160:5
|
||||
|
|
||||
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
|
||||
159 | _DynamicClass = type("_DynamicClass", (), {})
|
||||
@@ -202,8 +202,18 @@ RUF052 Local dummy variable `_NotADynamicClass` is accessed
|
||||
|
|
||||
help: Remove leading underscores
|
||||
|
||||
RUF052 Local dummy variable `other` is accessed
|
||||
--> RUF052_0.py:177:13
|
||||
|
|
||||
175 | return
|
||||
176 | _seen.add(self)
|
||||
177 | for other in self.connected:
|
||||
| ^^^^^
|
||||
178 | other.recurse(_seen=_seen)
|
||||
|
|
||||
|
||||
RUF052 Local dummy variable `_dummy_var` is accessed
|
||||
--> RUF052.py:182:5
|
||||
--> RUF052_0.py:182:5
|
||||
|
|
||||
181 | def foo():
|
||||
182 | _dummy_var = 42
|
||||
@@ -214,7 +224,7 @@ RUF052 Local dummy variable `_dummy_var` is accessed
|
||||
help: Prefer using trailing underscores to avoid shadowing a variable
|
||||
|
||||
RUF052 Local dummy variable `_dummy_var` is accessed
|
||||
--> RUF052.py:192:5
|
||||
--> RUF052_0.py:192:5
|
||||
|
|
||||
190 | # Unfixable because both possible candidates for the new name are shadowed
|
||||
191 | # in the scope of one of the references to the variable
|
||||
@@ -773,10 +773,14 @@ pub trait StringFlags: Copy {
|
||||
}
|
||||
|
||||
/// The total length of the string's closer.
|
||||
/// This is always equal to `self.quote_len()`,
|
||||
/// but is provided here for symmetry with the `opener_len()` method.
|
||||
/// This is always equal to `self.quote_len()`, except when the string is unclosed,
|
||||
/// in which case the length is zero.
|
||||
fn closer_len(self) -> TextSize {
|
||||
self.quote_len()
|
||||
if self.is_unclosed() {
|
||||
TextSize::default()
|
||||
} else {
|
||||
self.quote_len()
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_string_flags(self) -> AnyStringFlags {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# parse_options: {"mode": "ipython"}
|
||||
with (a, ?b)
|
||||
?
|
||||
@@ -0,0 +1,3 @@
|
||||
# parse_options: {"mode": "ipython"}
|
||||
with (a, ?b
|
||||
?
|
||||
@@ -0,0 +1,4 @@
|
||||
# parse_options: {"mode": "ipython"}
|
||||
with a, ?b
|
||||
?
|
||||
x = 1
|
||||
@@ -1115,7 +1115,27 @@ impl RecoveryContextKind {
|
||||
TokenKind::Colon => Some(ListTerminatorKind::ErrorRecovery),
|
||||
_ => None,
|
||||
},
|
||||
WithItemKind::Unparenthesized | WithItemKind::ParenthesizedExpression => p
|
||||
// test_err ipython_help_escape_command_error_recovery_1
|
||||
// # parse_options: {"mode": "ipython"}
|
||||
// with (a, ?b)
|
||||
// ?
|
||||
|
||||
// test_err ipython_help_escape_command_error_recovery_2
|
||||
// # parse_options: {"mode": "ipython"}
|
||||
// with (a, ?b
|
||||
// ?
|
||||
|
||||
// test_err ipython_help_escape_command_error_recovery_3
|
||||
// # parse_options: {"mode": "ipython"}
|
||||
// with a, ?b
|
||||
// ?
|
||||
// x = 1
|
||||
WithItemKind::Unparenthesized => matches!(
|
||||
p.current_token_kind(),
|
||||
TokenKind::Colon | TokenKind::Newline
|
||||
)
|
||||
.then_some(ListTerminatorKind::Regular),
|
||||
WithItemKind::ParenthesizedExpression => p
|
||||
.at(TokenKind::Colon)
|
||||
.then_some(ListTerminatorKind::Regular),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_parser/resources/inline/err/ipython_help_escape_command_error_recovery_1.py
|
||||
---
|
||||
## AST
|
||||
|
||||
```
|
||||
Module(
|
||||
ModModule {
|
||||
node_index: NodeIndex(None),
|
||||
range: 0..52,
|
||||
body: [
|
||||
With(
|
||||
StmtWith {
|
||||
node_index: NodeIndex(None),
|
||||
range: 37..49,
|
||||
is_async: false,
|
||||
items: [
|
||||
WithItem {
|
||||
range: 43..44,
|
||||
node_index: NodeIndex(None),
|
||||
context_expr: Name(
|
||||
ExprName {
|
||||
node_index: NodeIndex(None),
|
||||
range: 43..44,
|
||||
id: Name("a"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
optional_vars: None,
|
||||
},
|
||||
WithItem {
|
||||
range: 47..48,
|
||||
node_index: NodeIndex(None),
|
||||
context_expr: Name(
|
||||
ExprName {
|
||||
node_index: NodeIndex(None),
|
||||
range: 47..48,
|
||||
id: Name("b"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
optional_vars: None,
|
||||
},
|
||||
],
|
||||
body: [],
|
||||
},
|
||||
),
|
||||
IpyEscapeCommand(
|
||||
StmtIpyEscapeCommand {
|
||||
node_index: NodeIndex(None),
|
||||
range: 50..51,
|
||||
kind: Help,
|
||||
value: "",
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
```
|
||||
## Errors
|
||||
|
||||
|
|
||||
1 | # parse_options: {"mode": "ipython"}
|
||||
2 | with (a, ?b)
|
||||
| ^ Syntax Error: Expected `,`, found `?`
|
||||
3 | ?
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
1 | # parse_options: {"mode": "ipython"}
|
||||
2 | with (a, ?b)
|
||||
| ^ Syntax Error: Expected `:`, found newline
|
||||
3 | ?
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
1 | # parse_options: {"mode": "ipython"}
|
||||
2 | with (a, ?b)
|
||||
3 | ?
|
||||
| ^ Syntax Error: Expected an indented block after `with` statement
|
||||
|
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_parser/resources/inline/err/ipython_help_escape_command_error_recovery_2.py
|
||||
---
|
||||
## AST
|
||||
|
||||
```
|
||||
Module(
|
||||
ModModule {
|
||||
node_index: NodeIndex(None),
|
||||
range: 0..51,
|
||||
body: [
|
||||
With(
|
||||
StmtWith {
|
||||
node_index: NodeIndex(None),
|
||||
range: 37..51,
|
||||
is_async: false,
|
||||
items: [
|
||||
WithItem {
|
||||
range: 42..51,
|
||||
node_index: NodeIndex(None),
|
||||
context_expr: Tuple(
|
||||
ExprTuple {
|
||||
node_index: NodeIndex(None),
|
||||
range: 42..51,
|
||||
elts: [
|
||||
Name(
|
||||
ExprName {
|
||||
node_index: NodeIndex(None),
|
||||
range: 43..44,
|
||||
id: Name("a"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
Name(
|
||||
ExprName {
|
||||
node_index: NodeIndex(None),
|
||||
range: 47..48,
|
||||
id: Name("b"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
],
|
||||
ctx: Load,
|
||||
parenthesized: true,
|
||||
},
|
||||
),
|
||||
optional_vars: None,
|
||||
},
|
||||
],
|
||||
body: [],
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
```
|
||||
## Errors
|
||||
|
||||
|
|
||||
1 | # parse_options: {"mode": "ipython"}
|
||||
2 | with (a, ?b
|
||||
| ^ Syntax Error: Expected an expression or a ')'
|
||||
3 | ?
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
1 | # parse_options: {"mode": "ipython"}
|
||||
2 | with (a, ?b
|
||||
3 | ?
|
||||
| ^ Syntax Error: Expected `,`, found `?`
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
2 | with (a, ?b
|
||||
3 | ?
|
||||
| ^ Syntax Error: unexpected EOF while parsing
|
||||
|
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_parser/resources/inline/err/ipython_help_escape_command_error_recovery_3.py
|
||||
---
|
||||
## AST
|
||||
|
||||
```
|
||||
Module(
|
||||
ModModule {
|
||||
node_index: NodeIndex(None),
|
||||
range: 0..56,
|
||||
body: [
|
||||
With(
|
||||
StmtWith {
|
||||
node_index: NodeIndex(None),
|
||||
range: 37..47,
|
||||
is_async: false,
|
||||
items: [
|
||||
WithItem {
|
||||
range: 42..43,
|
||||
node_index: NodeIndex(None),
|
||||
context_expr: Name(
|
||||
ExprName {
|
||||
node_index: NodeIndex(None),
|
||||
range: 42..43,
|
||||
id: Name("a"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
optional_vars: None,
|
||||
},
|
||||
WithItem {
|
||||
range: 46..47,
|
||||
node_index: NodeIndex(None),
|
||||
context_expr: Name(
|
||||
ExprName {
|
||||
node_index: NodeIndex(None),
|
||||
range: 46..47,
|
||||
id: Name("b"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
optional_vars: None,
|
||||
},
|
||||
],
|
||||
body: [],
|
||||
},
|
||||
),
|
||||
IpyEscapeCommand(
|
||||
StmtIpyEscapeCommand {
|
||||
node_index: NodeIndex(None),
|
||||
range: 48..49,
|
||||
kind: Help,
|
||||
value: "",
|
||||
},
|
||||
),
|
||||
Assign(
|
||||
StmtAssign {
|
||||
node_index: NodeIndex(None),
|
||||
range: 50..55,
|
||||
targets: [
|
||||
Name(
|
||||
ExprName {
|
||||
node_index: NodeIndex(None),
|
||||
range: 50..51,
|
||||
id: Name("x"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
],
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
node_index: NodeIndex(None),
|
||||
range: 54..55,
|
||||
value: Int(
|
||||
1,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
```
|
||||
## Errors
|
||||
|
||||
|
|
||||
1 | # parse_options: {"mode": "ipython"}
|
||||
2 | with a, ?b
|
||||
| ^ Syntax Error: Expected `,`, found `?`
|
||||
3 | ?
|
||||
4 | x = 1
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
1 | # parse_options: {"mode": "ipython"}
|
||||
2 | with a, ?b
|
||||
| ^ Syntax Error: Expected `:`, found newline
|
||||
3 | ?
|
||||
4 | x = 1
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
1 | # parse_options: {"mode": "ipython"}
|
||||
2 | with a, ?b
|
||||
3 | ?
|
||||
| ^ Syntax Error: Expected an indented block after `with` statement
|
||||
4 | x = 1
|
||||
|
|
||||
@@ -3,12 +3,13 @@ use std::path::Path;
|
||||
use bitflags::bitflags;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_python_ast::helpers::from_relative_import;
|
||||
use ruff_python_ast::helpers::{from_relative_import, map_subscript};
|
||||
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprContext, PySourceType, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::Imported;
|
||||
use crate::analyze::visibility;
|
||||
use crate::binding::{
|
||||
Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImport, Import,
|
||||
SubmoduleImport,
|
||||
@@ -2153,6 +2154,21 @@ impl<'a> SemanticModel<'a> {
|
||||
function.range() == function_def.range()
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the model is in a `typing.Protocol` subclass or an abstract
|
||||
/// method.
|
||||
pub fn in_protocol_or_abstract_method(&self) -> bool {
|
||||
self.current_scopes().any(|scope| match scope.kind {
|
||||
ScopeKind::Class(class_def) => class_def
|
||||
.bases()
|
||||
.iter()
|
||||
.any(|base| self.match_typing_expr(map_subscript(base), "Protocol")),
|
||||
ScopeKind::Function(function_def) => {
|
||||
visibility::is_abstract(&function_def.decorator_list, self)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ShadowedBinding {
|
||||
|
||||
@@ -283,24 +283,27 @@ fn to_lsp_diagnostic(
|
||||
range = diagnostic_range.to_range(source_kind.source_code(), index, encoding);
|
||||
}
|
||||
|
||||
let (severity, tags, code) = if let Some(code) = code {
|
||||
let code = code.to_string();
|
||||
(
|
||||
Some(severity(&code)),
|
||||
tags(diagnostic),
|
||||
Some(lsp_types::NumberOrString::String(code)),
|
||||
)
|
||||
let (severity, code) = if let Some(code) = code {
|
||||
(severity(code), code.to_string())
|
||||
} else {
|
||||
(None, None, None)
|
||||
(
|
||||
match diagnostic.severity() {
|
||||
ruff_db::diagnostic::Severity::Info => lsp_types::DiagnosticSeverity::INFORMATION,
|
||||
ruff_db::diagnostic::Severity::Warning => lsp_types::DiagnosticSeverity::WARNING,
|
||||
ruff_db::diagnostic::Severity::Error => lsp_types::DiagnosticSeverity::ERROR,
|
||||
ruff_db::diagnostic::Severity::Fatal => lsp_types::DiagnosticSeverity::ERROR,
|
||||
},
|
||||
diagnostic.id().to_string(),
|
||||
)
|
||||
};
|
||||
|
||||
(
|
||||
cell,
|
||||
lsp_types::Diagnostic {
|
||||
range,
|
||||
severity,
|
||||
tags,
|
||||
code,
|
||||
severity: Some(severity),
|
||||
tags: tags(diagnostic),
|
||||
code: Some(lsp_types::NumberOrString::String(code)),
|
||||
code_description: diagnostic.documentation_url().and_then(|url| {
|
||||
Some(lsp_types::CodeDescription {
|
||||
href: lsp_types::Url::parse(url).ok()?,
|
||||
|
||||
@@ -106,6 +106,25 @@ impl TextSize {
|
||||
pub fn checked_sub(self, rhs: TextSize) -> Option<TextSize> {
|
||||
self.raw.checked_sub(rhs.raw).map(|raw| TextSize { raw })
|
||||
}
|
||||
|
||||
/// Saturating addition. Returns maximum `TextSize` if overflow occurred.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn saturating_add(self, rhs: TextSize) -> TextSize {
|
||||
TextSize {
|
||||
raw: self.raw.saturating_add(rhs.raw),
|
||||
}
|
||||
}
|
||||
|
||||
/// Saturating subtraction. Returns minimum `TextSize` if overflow
|
||||
/// occurred.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn saturating_sub(self, rhs: TextSize) -> TextSize {
|
||||
TextSize {
|
||||
raw: self.raw.saturating_sub(rhs.raw),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for TextSize {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.5"
|
||||
version = "0.14.7"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
399
crates/ty/docs/rules.md
generated
399
crates/ty/docs/rules.md
generated
@@ -39,7 +39,7 @@ def test(): -> "int":
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L121" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L134" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L165" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L178" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ f(int) # error
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L191" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L204" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ a = 1
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L216" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L229" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ class C(A, B): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L242" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L255" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -184,13 +184,41 @@ class B(A): ...
|
||||
|
||||
[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
|
||||
## `cyclic-type-alias-definition`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-type-alias-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L281" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for type alias definitions that (directly or mutually) refer to themselves.
|
||||
|
||||
**Why is it bad?**
|
||||
|
||||
Although it is permitted to define a recursive type alias, it is not meaningful
|
||||
to have a type alias whose expansion can only result in itself, and is therefore not allowed.
|
||||
|
||||
**Examples**
|
||||
|
||||
```python
|
||||
type Itself = Itself
|
||||
|
||||
type A = B
|
||||
type B = A
|
||||
```
|
||||
|
||||
## `duplicate-base`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L307" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L342" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -217,7 +245,7 @@ class B(A, A): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L328" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L363" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -329,7 +357,7 @@ def test(): -> "Literal[5]":
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L532" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -359,7 +387,7 @@ class C(A, B): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L556" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L591" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -385,7 +413,7 @@ t[3] # IndexError: tuple index out of range
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L360" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L395" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -474,7 +502,7 @@ an atypical memory layout.
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L610" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L645" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -501,7 +529,7 @@ func("foo") # error: [invalid-argument-type]
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L685" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -529,7 +557,7 @@ a: int = ''
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1809" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1948" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -563,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L672" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L707" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -599,7 +627,7 @@ asyncio.run(main())
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L737" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -623,7 +651,7 @@ class A(42): ... # error: [invalid-base]
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L753" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -650,7 +678,7 @@ with 1:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L774" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -679,7 +707,7 @@ a: str
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L797" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L832" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -717,13 +745,55 @@ except ZeroDivisionError:
|
||||
|
||||
This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes)
|
||||
|
||||
## `invalid-explicit-override`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.28">0.0.1-alpha.28</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1645" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for methods that are decorated with `@override` but do not override any method in a superclass.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Decorating a method with `@override` declares to the type checker that the intention is that it should
|
||||
override a method from a superclass.
|
||||
|
||||
**Example**
|
||||
|
||||
|
||||
```python
|
||||
from typing import override
|
||||
|
||||
class A:
|
||||
@override
|
||||
def foo(self): ... # Error raised here
|
||||
|
||||
class B(A):
|
||||
@override
|
||||
def ffooo(self): ... # Error raised here
|
||||
|
||||
class C:
|
||||
@override
|
||||
def __repr__(self): ... # fine: overrides `object.__repr__`
|
||||
|
||||
class D(A):
|
||||
@override
|
||||
def foo(self): ... # fine: overrides `A.foo`
|
||||
```
|
||||
|
||||
## `invalid-generic-class`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L833" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L868" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -756,7 +826,7 @@ class C[U](Generic[T]): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L577" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L612" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -795,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo!
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L859" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L894" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -830,7 +900,7 @@ def f(t: TypeVar("U")): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L991" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -858,13 +928,120 @@ class B(metaclass=f): ...
|
||||
|
||||
- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
|
||||
|
||||
## `invalid-method-override`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2076" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Detects method overrides that violate the [Liskov Substitution Principle] ("LSP").
|
||||
|
||||
The LSP states that an instance of a subtype should be substitutable for an instance of its supertype.
|
||||
Applied to Python, this means:
|
||||
1. All argument combinations a superclass method accepts
|
||||
must also be accepted by an overriding subclass method.
|
||||
2. The return type of an overriding subclass method must be a subtype
|
||||
of the return type of the superclass method.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Violating the Liskov Substitution Principle will lead to many of ty's assumptions and
|
||||
inferences being incorrect, which will mean that it will fail to catch many possible
|
||||
type errors in your code.
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
class Super:
|
||||
def method(self, x) -> int:
|
||||
return 42
|
||||
|
||||
class Sub(Super):
|
||||
# Liskov violation: `str` is not a subtype of `int`,
|
||||
# but the supertype method promises to return an `int`.
|
||||
def method(self, x) -> str: # error: [invalid-override]
|
||||
return "foo"
|
||||
|
||||
def accepts_super(s: Super) -> int:
|
||||
return s.method(x=42)
|
||||
|
||||
accepts_super(Sub()) # The result of this call is a string, but ty will infer
|
||||
# it to be an `int` due to the violation of the Liskov Substitution Principle.
|
||||
|
||||
class Sub2(Super):
|
||||
# Liskov violation: the superclass method can be called with a `x=`
|
||||
# keyword argument, but the subclass method does not accept it.
|
||||
def method(self, y) -> int: # error: [invalid-override]
|
||||
return 42
|
||||
|
||||
accepts_super(Sub2()) # TypeError at runtime: method() got an unexpected keyword argument 'x'
|
||||
# ty cannot catch this error due to the violation of the Liskov Substitution Principle.
|
||||
```
|
||||
|
||||
**Common issues**
|
||||
|
||||
|
||||
**Why does ty complain about my `__eq__` method?**
|
||||
|
||||
|
||||
`__eq__` and `__ne__` methods in Python are generally expected to accept arbitrary
|
||||
objects as their second argument, for example:
|
||||
|
||||
```python
|
||||
class A:
|
||||
x: int
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
# gracefully handle an object of an unexpected type
|
||||
# without raising an exception
|
||||
if not isinstance(other, A):
|
||||
return False
|
||||
return self.x == other.x
|
||||
```
|
||||
|
||||
If `A.__eq__` here were annotated as only accepting `A` instances for its second argument,
|
||||
it would imply that you wouldn't be able to use `==` between instances of `A` and
|
||||
instances of unrelated classes without an exception possibly being raised. While some
|
||||
classes in Python do indeed behave this way, the strongly held convention is that it should
|
||||
be avoided wherever possible. As part of this check, therefore, ty enforces that `__eq__`
|
||||
and `__ne__` methods accept `object` as their second argument.
|
||||
|
||||
**Why does ty disagree with Ruff about how to write my method?**
|
||||
|
||||
|
||||
Ruff has several rules that will encourage you to rename a parameter, or change its type
|
||||
signature, if it thinks you're falling into a certain anti-pattern. For example, Ruff's
|
||||
[ARG002](https://docs.astral.sh/ruff/rules/unused-method-argument/) rule recommends that an
|
||||
unused parameter should either be removed or renamed to start with `_`. Applying either of
|
||||
these suggestions can cause ty to start reporting an `invalid-method-override` error if
|
||||
the function in question is a method on a subclass that overrides a method on a superclass,
|
||||
and the change would cause the subclass method to no longer accept all argument combinations
|
||||
that the superclass method accepts.
|
||||
|
||||
This can usually be resolved by adding [`@typing.override`][override] to your method
|
||||
definition. Ruff knows that a method decorated with `@typing.override` is intended to
|
||||
override a method by the same name on a superclass, and avoids reporting rules like ARG002
|
||||
for such methods; it knows that the changes recommended by ARG002 would violate the Liskov
|
||||
Substitution Principle.
|
||||
|
||||
Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
|
||||
|
||||
[Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle
|
||||
[override]: https://docs.python.org/3/library/typing.html#typing.override
|
||||
|
||||
## `invalid-named-tuple`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -896,7 +1073,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L932" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L967" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -926,7 +1103,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L983" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1018" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -976,7 +1153,7 @@ def foo(x: int) -> int: ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1082" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1117" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1002,7 +1179,7 @@ def f(a: int = ''): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L887" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L922" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1033,7 +1210,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L442" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L477" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1067,7 +1244,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1102" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1137" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1116,7 +1293,7 @@ def g():
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L631" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L666" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1141,7 +1318,7 @@ def func() -> int:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1145" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1180" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1199,7 +1376,7 @@ TODO #14889
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L911" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L946" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1220,13 +1397,60 @@ IntOrStr = TypeAliasType("IntOrStr", int | str) # okay
|
||||
NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal
|
||||
```
|
||||
|
||||
## `invalid-type-arguments`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for invalid type arguments in explicit type specialization.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Providing the wrong number of type arguments or type arguments that don't
|
||||
satisfy the type variable's bounds or constraints will lead to incorrect
|
||||
type inference and may indicate a misunderstanding of the generic type's
|
||||
interface.
|
||||
|
||||
**Examples**
|
||||
|
||||
|
||||
Using legacy type variables:
|
||||
```python
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T1 = TypeVar('T1', int, str)
|
||||
T2 = TypeVar('T2', bound=int)
|
||||
|
||||
class Foo1(Generic[T1]): ...
|
||||
class Foo2(Generic[T2]): ...
|
||||
|
||||
Foo1[bytes] # error: bytes does not satisfy T1's constraints
|
||||
Foo2[str] # error: str does not satisfy T2's bound
|
||||
```
|
||||
|
||||
Using PEP 695 type variables:
|
||||
```python
|
||||
class Foo[T]: ...
|
||||
class Bar[T, U]: ...
|
||||
|
||||
Foo[int, str] # error: too many arguments
|
||||
Bar[int] # error: too few arguments
|
||||
```
|
||||
|
||||
## `invalid-type-checking-constant`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1184" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1219" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1256,7 +1480,7 @@ TYPE_CHECKING = ''
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1208" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1243" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1286,7 +1510,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1260" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1295" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1320,7 +1544,7 @@ f(10) # Error
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1232" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1267" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1354,7 +1578,7 @@ class C:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1288" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1389,7 +1613,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1317" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1352" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1414,7 +1638,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1910" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2049" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1447,7 +1671,7 @@ alice["age"] # KeyError
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1336" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1476,7 +1700,7 @@ func("string") # error: [no-matching-overload]
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1359" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1394" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1500,7 +1724,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1377" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1520,13 +1744,46 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
pass
|
||||
```
|
||||
|
||||
## `override-of-final-method`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1618" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for methods on subclasses that override superclass methods decorated with `@final`.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Decorating a method with `@final` declares to the type checker that it should not be
|
||||
overridden on any subclass.
|
||||
|
||||
**Example**
|
||||
|
||||
|
||||
```python
|
||||
from typing import final
|
||||
|
||||
class A:
|
||||
@final
|
||||
def foo(self): ...
|
||||
|
||||
class B(A):
|
||||
def foo(self): ... # Error raised here
|
||||
```
|
||||
|
||||
## `parameter-already-assigned`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1428" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1504" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1553,7 +1810,7 @@ f(1, x=2) # Error raised here
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1663" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1802" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1611,7 +1868,7 @@ def test(): -> "int":
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1785" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1924" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1641,7 +1898,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1519" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1595" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1670,7 +1927,7 @@ class B(A): ... # Error raised here
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1703" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1697,7 +1954,7 @@ f("foo") # Error raised here
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1542" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1681" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1725,7 +1982,7 @@ def _(x: int):
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1585" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1724" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1771,7 +2028,7 @@ class A:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1642" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1781" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1798,7 +2055,7 @@ f(x=1, y=2) # Error raised here
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1684" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1823" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1826,7 +2083,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1706" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1845" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1851,7 +2108,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1725" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1864" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1876,7 +2133,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1397" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1473" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1913,7 +2170,7 @@ b1 < b2 < b1 # exception raised here
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1744" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1883" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1941,7 +2198,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1766" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1905" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1966,7 +2223,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L471" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2007,7 +2264,7 @@ class SubProto(BaseProto, Protocol):
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.16">0.0.1-alpha.16</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L321" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2034,7 +2291,7 @@ old_func() # emits [deprecated] diagnostic
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ignore-comment-unknown-rule" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L40" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L47" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2065,7 +2322,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-ignore-comment" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L65" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L72" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2095,7 +2352,7 @@ a = 20 / 0 # type: ignore
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1449" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1525" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2123,7 +2380,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L139" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L152" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2155,7 +2412,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1547" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2187,7 +2444,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1837" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1976" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2214,7 +2471,7 @@ cast(int, f()) # Redundant
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1624" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1763" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2238,7 +2495,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1858" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1997" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2296,7 +2553,7 @@ def g():
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L720" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L755" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2335,7 +2592,7 @@ class D(C): ... # error: [unsupported-base]
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1026" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1061" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2398,7 +2655,7 @@ def foo(x: int | str) -> int | str:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L268" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L303" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2422,7 +2679,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1497" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1573" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2450,7 +2707,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unused-ignore-comment" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L15" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L22" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
||||
@@ -313,7 +313,8 @@ impl MainLoop {
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
let display_config = DisplayDiagnosticConfig::default()
|
||||
.format(terminal_settings.output_format.into())
|
||||
.color(colored::control::SHOULD_COLORIZE.should_colorize());
|
||||
.color(colored::control::SHOULD_COLORIZE.should_colorize())
|
||||
.show_fix_diff(true);
|
||||
|
||||
if check_revision == revision {
|
||||
if db.project().files(db).is_empty() {
|
||||
|
||||
@@ -37,7 +37,8 @@ fn config_override_python_version() -> anyhow::Result<()> {
|
||||
5 | print(sys.last_exc)
|
||||
| ^^^^^^^^^^^^
|
||||
|
|
||||
info: Python 3.11 was assumed when accessing `last_exc`
|
||||
info: The member may be available on other Python versions or platforms
|
||||
info: Python 3.11 was assumed when resolving the `last_exc` attribute
|
||||
--> pyproject.toml:3:18
|
||||
|
|
||||
2 | [tool.ty.environment]
|
||||
@@ -1179,6 +1180,8 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
import os
|
||||
|
||||
os.grantpt(1) # only available on unix, Python 3.13 or newer
|
||||
|
||||
from typing import LiteralString # added in Python 3.11
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
@@ -1194,8 +1197,11 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
3 |
|
||||
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
|
||||
| ^^^^^^^^^^
|
||||
5 |
|
||||
6 | from typing import LiteralString # added in Python 3.11
|
||||
|
|
||||
info: Python 3.10 was assumed when accessing `grantpt`
|
||||
info: The member may be available on other Python versions or platforms
|
||||
info: Python 3.10 was assumed when resolving the `grantpt` attribute
|
||||
--> ty.toml:3:18
|
||||
|
|
||||
2 | [environment]
|
||||
@@ -1205,7 +1211,26 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
|
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
error[unresolved-import]: Module `typing` has no member `LiteralString`
|
||||
--> main.py:6:20
|
||||
|
|
||||
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
|
||||
5 |
|
||||
6 | from typing import LiteralString # added in Python 3.11
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: The member may be available on other Python versions or platforms
|
||||
info: Python 3.10 was assumed when resolving imports
|
||||
--> ty.toml:3:18
|
||||
|
|
||||
2 | [environment]
|
||||
3 | python-version = "3.10"
|
||||
| ^^^^^^ Python 3.10 assumed due to this configuration setting
|
||||
4 | python-platform = "linux"
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
@@ -1225,6 +1250,8 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
import os
|
||||
|
||||
os.grantpt(1) # only available on unix, Python 3.13 or newer
|
||||
|
||||
from typing import LiteralString # added in Python 3.11
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
@@ -18,11 +18,11 @@ 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
|
||||
raise-uses-base-exception,main.py,0,1
|
||||
scope-existing-over-new-import,main.py,0,1
|
||||
scope-prioritize-closer,main.py,0,2
|
||||
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,278
|
||||
|
||||
|
@@ -8,13 +8,12 @@ use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
|
||||
///
|
||||
/// Returns symbols from all files in the workspace and dependencies, filtered
|
||||
/// by the query.
|
||||
pub fn all_symbols<'db>(db: &'db dyn Db, query: &str) -> Vec<AllSymbolInfo<'db>> {
|
||||
pub fn all_symbols<'db>(db: &'db dyn Db, query: &QueryPattern) -> Vec<AllSymbolInfo<'db>> {
|
||||
// If the query is empty, return immediately to avoid expensive file scanning
|
||||
if query.is_empty() {
|
||||
if query.will_match_everything() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let query = QueryPattern::new(query);
|
||||
let results = std::sync::Mutex::new(Vec::new());
|
||||
{
|
||||
let modules = all_modules(db);
|
||||
@@ -144,7 +143,7 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
|
||||
|
||||
impl CursorTest {
|
||||
fn all_symbols(&self, query: &str) -> String {
|
||||
let symbols = all_symbols(&self.db, query);
|
||||
let symbols = all_symbols(&self.db, &QueryPattern::fuzzy(query));
|
||||
|
||||
if symbols.is_empty() {
|
||||
return "No symbols found".to_string();
|
||||
|
||||
43
crates/ty_ide/src/code_action.rs
Normal file
43
crates/ty_ide/src/code_action.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::{completion, find_node::covering_node};
|
||||
use ruff_db::{files::File, parsed::parsed_module};
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_text_size::TextRange;
|
||||
use ty_project::Db;
|
||||
use ty_python_semantic::types::UNRESOLVED_REFERENCE;
|
||||
|
||||
/// A `QuickFix` Code Action
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QuickFix {
|
||||
pub title: String,
|
||||
pub edits: Vec<Edit>,
|
||||
pub preferred: bool,
|
||||
}
|
||||
|
||||
pub fn code_actions(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
diagnostic_range: TextRange,
|
||||
diagnostic_id: &str,
|
||||
) -> Option<Vec<QuickFix>> {
|
||||
let registry = db.lint_registry();
|
||||
let Ok(lint_id) = registry.get(diagnostic_id) else {
|
||||
return None;
|
||||
};
|
||||
if lint_id.name() == UNRESOLVED_REFERENCE.name() {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
let node = covering_node(parsed.syntax().into(), diagnostic_range).node();
|
||||
let symbol = &node.expr_name()?.id;
|
||||
|
||||
let fixes = completion::missing_imports(db, file, &parsed, symbol, node)
|
||||
.into_iter()
|
||||
.map(|import| QuickFix {
|
||||
title: import.label,
|
||||
edits: vec![import.edit],
|
||||
preferred: true,
|
||||
})
|
||||
.collect();
|
||||
Some(fixes)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ use crate::references::{ReferencesMode, references};
|
||||
use crate::{Db, ReferenceTarget};
|
||||
use ruff_db::files::File;
|
||||
use ruff_text_size::TextSize;
|
||||
use ty_python_semantic::SemanticModel;
|
||||
|
||||
/// Find all document highlights for a symbol at the given position.
|
||||
/// Document highlights are limited to the current file only.
|
||||
@@ -13,9 +14,10 @@ pub fn document_highlights(
|
||||
) -> Option<Vec<ReferenceTarget>> {
|
||||
let parsed = ruff_db::parsed::parsed_module(db, file);
|
||||
let module = parsed.load(db);
|
||||
let model = SemanticModel::new(db, file);
|
||||
|
||||
// Get the definitions for the symbol at the cursor position
|
||||
let goto_target = find_goto_target(&module, offset)?;
|
||||
let goto_target = find_goto_target(&model, &module, offset)?;
|
||||
|
||||
// Use DocumentHighlights mode which limits search to current file only
|
||||
references(db, file, &goto_target, ReferencesMode::DocumentHighlights)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,11 @@ use crate::references::{ReferencesMode, references};
|
||||
use crate::{Db, ReferenceTarget};
|
||||
use ruff_db::files::File;
|
||||
use ruff_text_size::TextSize;
|
||||
use ty_python_semantic::SemanticModel;
|
||||
|
||||
/// Find all references to a symbol at the given position.
|
||||
/// Search for references across all files in the project.
|
||||
pub fn goto_references(
|
||||
pub fn find_references(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
offset: TextSize,
|
||||
@@ -14,9 +15,10 @@ pub fn goto_references(
|
||||
) -> Option<Vec<ReferenceTarget>> {
|
||||
let parsed = ruff_db::parsed::parsed_module(db, file);
|
||||
let module = parsed.load(db);
|
||||
let model = SemanticModel::new(db, file);
|
||||
|
||||
// Get the definitions for the symbol at the cursor position
|
||||
let goto_target = find_goto_target(&module, offset)?;
|
||||
let goto_target = find_goto_target(&model, &module, offset)?;
|
||||
|
||||
let mode = if include_declaration {
|
||||
ReferencesMode::References
|
||||
@@ -39,7 +41,7 @@ mod tests {
|
||||
impl CursorTest {
|
||||
fn references(&self) -> String {
|
||||
let Some(mut reference_results) =
|
||||
goto_references(&self.db, self.cursor.file, self.cursor.offset, true)
|
||||
find_references(&self.db, self.cursor.file, self.cursor.offset, true)
|
||||
else {
|
||||
return "No references found".to_string();
|
||||
};
|
||||
@@ -82,7 +84,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parameter_references_in_function() {
|
||||
fn parameter_references_in_function() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def calculate_sum(<CURSOR>value: int) -> int:
|
||||
@@ -147,29 +149,28 @@ result = calculate_sum(value=42)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // TODO: Enable when nonlocal support is fully implemented in goto.rs
|
||||
fn test_nonlocal_variable_references() {
|
||||
fn nonlocal_variable_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def outer_function():
|
||||
coun<CURSOR>ter = 0
|
||||
|
||||
|
||||
def increment():
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
return counter
|
||||
|
||||
|
||||
def decrement():
|
||||
nonlocal counter
|
||||
counter -= 1
|
||||
return counter
|
||||
|
||||
|
||||
# Use counter in outer scope
|
||||
initial = counter
|
||||
increment()
|
||||
decrement()
|
||||
final = counter
|
||||
|
||||
|
||||
return increment, decrement
|
||||
",
|
||||
);
|
||||
@@ -181,7 +182,7 @@ def outer_function():
|
||||
2 | def outer_function():
|
||||
3 | counter = 0
|
||||
| ^^^^^^^
|
||||
4 |
|
||||
4 |
|
||||
5 | def increment():
|
||||
|
|
||||
|
||||
@@ -212,7 +213,7 @@ def outer_function():
|
||||
7 | counter += 1
|
||||
8 | return counter
|
||||
| ^^^^^^^
|
||||
9 |
|
||||
9 |
|
||||
10 | def decrement():
|
||||
|
|
||||
|
||||
@@ -243,7 +244,7 @@ def outer_function():
|
||||
12 | counter -= 1
|
||||
13 | return counter
|
||||
| ^^^^^^^
|
||||
14 |
|
||||
14 |
|
||||
15 | # Use counter in outer scope
|
||||
|
|
||||
|
||||
@@ -264,15 +265,14 @@ def outer_function():
|
||||
18 | decrement()
|
||||
19 | final = counter
|
||||
| ^^^^^^^
|
||||
20 |
|
||||
20 |
|
||||
21 | return increment, decrement
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // TODO: Enable when global support is fully implemented in goto.rs
|
||||
fn test_global_variable_references() {
|
||||
fn global_variable_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
glo<CURSOR>bal_counter = 0
|
||||
@@ -389,7 +389,7 @@ final_value = global_counter
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_except_handler_variable_references() {
|
||||
fn except_handler_variable_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
try:
|
||||
@@ -450,7 +450,7 @@ except ValueError as err:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_match_as_references() {
|
||||
fn pattern_match_as_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
match x:
|
||||
@@ -498,7 +498,7 @@ match x:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_match_mapping_rest_references() {
|
||||
fn pattern_match_mapping_rest_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
match data:
|
||||
@@ -553,7 +553,7 @@ match data:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_definition_references() {
|
||||
fn function_definition_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def my_func<CURSOR>tion():
|
||||
@@ -632,7 +632,7 @@ value = my_function
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_class_definition_references() {
|
||||
fn class_definition_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
class My<CURSOR>Class:
|
||||
@@ -711,7 +711,741 @@ cls = MyClass
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_file_function_references() {
|
||||
fn references_string_annotation1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCla<CURSOR>ss" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | a: "MyClass" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:4:7
|
||||
|
|
||||
2 | a: "MyClass" = 1
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| ^^^^^^^
|
||||
5 | """some docs"""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_string_annotation2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyCl<CURSOR>ass" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:12
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:4:7
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| ^^^^^^^
|
||||
5 | """some docs"""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_string_annotation3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None |<CURSOR> MyClass" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @"No references found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_string_annotation4() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyClass<CURSOR>" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:12
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:4:7
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| ^^^^^^^
|
||||
5 | """some docs"""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_string_annotation5() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyClass"<CURSOR> = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @"No references found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_string_annotation_dangling1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCl<CURSOR>ass |" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @"No references found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_string_annotation_dangling2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCl<CURSOR>ass | No" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | a: "MyClass | No" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:4:7
|
||||
|
|
||||
2 | a: "MyClass | No" = 1
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| ^^^^^^^
|
||||
5 | """some docs"""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_string_annotation_dangling3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyClass | N<CURSOR>o" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @"No references found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_rest_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_rest_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_as_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_as_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_keyword_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, button=a<CURSOR>b):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:11:17
|
||||
|
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
11 | x = ab
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_keyword_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, button=ab):
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:11:17
|
||||
|
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
11 | x = ab
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_class_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Cl<CURSOR>ick(x, button=ab):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class Click:
|
||||
| ^^^^^
|
||||
3 | __match_args__ = ("position", "button")
|
||||
4 | def __init__(self, pos, btn):
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:8:20
|
||||
|
|
||||
6 | self.button: str = btn
|
||||
7 |
|
||||
8 | def my_func(event: Click):
|
||||
| ^^^^^
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:10:14
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^^^^
|
||||
11 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_class_field_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, but<CURSOR>ton=ab):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @"No references found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:2:37
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:2:46
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:2:37
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:2:46
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_spec_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:3:43
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:3:53
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_spec_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:3:43
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:3:53
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_tuple_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:2:38
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:2:50
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_tuple_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:2:38
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:2:50
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_file_function_references() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"utils.py",
|
||||
@@ -737,7 +1471,7 @@ from utils import func
|
||||
class DataProcessor:
|
||||
def __init__(self):
|
||||
self.multiplier = func
|
||||
|
||||
|
||||
def process(self, value):
|
||||
return func(value)
|
||||
",
|
||||
@@ -801,14 +1535,14 @@ class DataProcessor:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_file_class_attribute_references() {
|
||||
fn multi_file_class_attribute_references() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"models.py",
|
||||
"
|
||||
class MyModel:
|
||||
a<CURSOR>ttr = 42
|
||||
|
||||
|
||||
def get_attribute(self):
|
||||
return MyModel.attr
|
||||
",
|
||||
@@ -879,7 +1613,7 @@ def process_model():
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_alias_references_should_not_resolve_to_original() {
|
||||
fn import_alias_references_should_not_resolve_to_original() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"original.py",
|
||||
@@ -15,7 +15,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use ty_python_semantic::ResolvedDefinition;
|
||||
use ty_python_semantic::types::Type;
|
||||
use ty_python_semantic::types::ide_support::{
|
||||
call_signature_details, definitions_for_keyword_argument,
|
||||
call_signature_details, call_type_simplified_by_overloads, definitions_for_keyword_argument,
|
||||
};
|
||||
use ty_python_semantic::{
|
||||
HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol,
|
||||
@@ -61,6 +61,7 @@ pub(crate) enum GotoTarget<'a> {
|
||||
/// ```
|
||||
ImportModuleComponent {
|
||||
module_name: String,
|
||||
level: u32,
|
||||
component_index: usize,
|
||||
component_range: TextRange,
|
||||
},
|
||||
@@ -189,20 +190,31 @@ pub(crate) enum GotoTarget<'a> {
|
||||
/// The call of the callable
|
||||
call: &'a ast::ExprCall,
|
||||
},
|
||||
|
||||
/// Go to on a sub-expression of a string annotation's sub-AST
|
||||
///
|
||||
/// ```py
|
||||
/// x: "int | None"
|
||||
/// ^^^^
|
||||
/// ```
|
||||
///
|
||||
/// This is equivalent to `GotoTarget::Expression` but the expression
|
||||
/// isn't actually in the AST.
|
||||
StringAnnotationSubexpr {
|
||||
/// The string literal that is a string annotation.
|
||||
string_expr: &'a ast::ExprStringLiteral,
|
||||
/// The range to query in the sub-AST for the sub-expression.
|
||||
subrange: TextRange,
|
||||
/// If the expression is a Name of some kind this is the name (just a cached result).
|
||||
name: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The resolved definitions for a `GotoTarget`
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum DefinitionsOrTargets<'db> {
|
||||
/// We computed actual Definitions we can do followup queries on.
|
||||
Definitions(Vec<ResolvedDefinition<'db>>),
|
||||
/// We directly computed a navigation.
|
||||
///
|
||||
/// We can't get docs or usefully compute goto-definition for this.
|
||||
Targets(crate::NavigationTargets),
|
||||
}
|
||||
pub(crate) struct Definitions<'db>(pub Vec<ResolvedDefinition<'db>>);
|
||||
|
||||
impl<'db> DefinitionsOrTargets<'db> {
|
||||
impl<'db> Definitions<'db> {
|
||||
pub(crate) fn from_ty(db: &'db dyn crate::Db, ty: Type<'db>) -> Option<Self> {
|
||||
let ty_def = ty.definition(db)?;
|
||||
let resolved = match ty_def {
|
||||
@@ -213,11 +225,12 @@ impl<'db> DefinitionsOrTargets<'db> {
|
||||
| ty_python_semantic::types::TypeDefinition::Function(definition)
|
||||
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
|
||||
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)
|
||||
| ty_python_semantic::types::TypeDefinition::SpecialForm(definition)
|
||||
| ty_python_semantic::types::TypeDefinition::NewType(definition) => {
|
||||
ResolvedDefinition::Definition(definition)
|
||||
}
|
||||
};
|
||||
Some(DefinitionsOrTargets::Definitions(vec![resolved]))
|
||||
Some(Definitions(vec![resolved]))
|
||||
}
|
||||
|
||||
/// Get the "goto-declaration" interpretation of this definition
|
||||
@@ -225,14 +238,9 @@ impl<'db> DefinitionsOrTargets<'db> {
|
||||
/// In this case it basically returns exactly what was found.
|
||||
pub(crate) fn declaration_targets(
|
||||
self,
|
||||
db: &'db dyn crate::Db,
|
||||
db: &'db dyn ty_python_semantic::Db,
|
||||
) -> Option<crate::NavigationTargets> {
|
||||
match self {
|
||||
DefinitionsOrTargets::Definitions(definitions) => {
|
||||
definitions_to_navigation_targets(db, None, definitions)
|
||||
}
|
||||
DefinitionsOrTargets::Targets(targets) => Some(targets),
|
||||
}
|
||||
definitions_to_navigation_targets(db, None, self.0)
|
||||
}
|
||||
|
||||
/// Get the "goto-definition" interpretation of this definition
|
||||
@@ -241,14 +249,9 @@ impl<'db> DefinitionsOrTargets<'db> {
|
||||
/// if the definition we have is found in a stub file.
|
||||
pub(crate) fn definition_targets(
|
||||
self,
|
||||
db: &'db dyn crate::Db,
|
||||
db: &'db dyn ty_python_semantic::Db,
|
||||
) -> Option<crate::NavigationTargets> {
|
||||
match self {
|
||||
DefinitionsOrTargets::Definitions(definitions) => {
|
||||
definitions_to_navigation_targets(db, Some(&StubMapper::new(db)), definitions)
|
||||
}
|
||||
DefinitionsOrTargets::Targets(targets) => Some(targets),
|
||||
}
|
||||
definitions_to_navigation_targets(db, Some(&StubMapper::new(db)), self.0)
|
||||
}
|
||||
|
||||
/// Get the docstring for this definition
|
||||
@@ -257,13 +260,7 @@ impl<'db> DefinitionsOrTargets<'db> {
|
||||
/// so this will check both the goto-declarations and goto-definitions (in that order)
|
||||
/// and return the first one found.
|
||||
pub(crate) fn docstring(self, db: &'db dyn crate::Db) -> Option<Docstring> {
|
||||
let definitions = match self {
|
||||
DefinitionsOrTargets::Definitions(definitions) => definitions,
|
||||
// Can't find docs for these
|
||||
// (make more cases DefinitionOrTargets::Definitions to get more docs!)
|
||||
DefinitionsOrTargets::Targets(_) => return None,
|
||||
};
|
||||
for definition in &definitions {
|
||||
for definition in &self.0 {
|
||||
// If we got a docstring from the original definition, use it
|
||||
if let Some(docstring) = definition.docstring(db) {
|
||||
return Some(Docstring::new(docstring));
|
||||
@@ -276,7 +273,7 @@ impl<'db> DefinitionsOrTargets<'db> {
|
||||
let stub_mapper = StubMapper::new(db);
|
||||
|
||||
// Try to find the corresponding implementation definition
|
||||
for definition in stub_mapper.map_definitions(definitions) {
|
||||
for definition in stub_mapper.map_definitions(self.0) {
|
||||
if let Some(docstring) = definition.docstring(db) {
|
||||
return Some(Docstring::new(docstring));
|
||||
}
|
||||
@@ -301,31 +298,68 @@ impl GotoTarget<'_> {
|
||||
// (i.e. the type of `MyClass` in `MyClass()` is `<class MyClass>` and not `() -> MyClass`)
|
||||
GotoTarget::Call { callable, .. } => callable.inferred_type(model),
|
||||
GotoTarget::TypeParamTypeVarName(typevar) => typevar.inferred_type(model),
|
||||
GotoTarget::ImportModuleComponent {
|
||||
module_name,
|
||||
component_index,
|
||||
level,
|
||||
..
|
||||
} => {
|
||||
// We don't currently support hovering the bare `.` so there is always a name
|
||||
let module = import_name(module_name, *component_index);
|
||||
model.resolve_module_type(Some(module), *level)?
|
||||
}
|
||||
GotoTarget::StringAnnotationSubexpr {
|
||||
string_expr,
|
||||
subrange,
|
||||
..
|
||||
} => {
|
||||
let (subast, _submodel) = model.enter_string_annotation(string_expr)?;
|
||||
let submod = subast.syntax();
|
||||
let subnode = covering_node(submod.into(), *subrange).node();
|
||||
|
||||
// The type checker knows the type of the full annotation but nothing else
|
||||
if AnyNodeRef::from(&*submod.body) == subnode {
|
||||
string_expr.inferred_type(model)
|
||||
} else {
|
||||
// TODO: force the typechecker to tell us its secrets
|
||||
// (it computes but then immediately discards these types)
|
||||
return None;
|
||||
}
|
||||
}
|
||||
GotoTarget::BinOp { expression, .. } => {
|
||||
let (_, ty) = ty_python_semantic::definitions_for_bin_op(model, expression)?;
|
||||
ty
|
||||
}
|
||||
GotoTarget::UnaryOp { expression, .. } => {
|
||||
let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?;
|
||||
ty
|
||||
}
|
||||
// TODO: Support identifier targets
|
||||
GotoTarget::PatternMatchRest(_)
|
||||
| GotoTarget::PatternKeywordArgument(_)
|
||||
| GotoTarget::PatternMatchStarName(_)
|
||||
| GotoTarget::PatternMatchAsName(_)
|
||||
| GotoTarget::ImportModuleComponent { .. }
|
||||
| GotoTarget::TypeParamParamSpecName(_)
|
||||
| GotoTarget::TypeParamTypeVarTupleName(_)
|
||||
| GotoTarget::NonLocal { .. }
|
||||
| GotoTarget::Globals { .. } => return None,
|
||||
GotoTarget::BinOp { expression, .. } => {
|
||||
let (_, ty) =
|
||||
ty_python_semantic::definitions_for_bin_op(model.db(), model, expression)?;
|
||||
ty
|
||||
}
|
||||
GotoTarget::UnaryOp { expression, .. } => {
|
||||
let (_, ty) =
|
||||
ty_python_semantic::definitions_for_unary_op(model.db(), model, expression)?;
|
||||
ty
|
||||
}
|
||||
};
|
||||
|
||||
Some(ty)
|
||||
}
|
||||
|
||||
/// Try to get a simplified display of this callable type by resolving overloads
|
||||
pub(crate) fn call_type_simplified_by_overloads(
|
||||
&self,
|
||||
model: &SemanticModel,
|
||||
) -> Option<String> {
|
||||
if let GotoTarget::Call { call, .. } = self {
|
||||
call_type_simplified_by_overloads(model, call)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the definitions for this goto target.
|
||||
///
|
||||
/// The `alias_resolution` parameter controls whether import aliases
|
||||
@@ -340,84 +374,56 @@ impl GotoTarget<'_> {
|
||||
/// as just returning a raw `NavigationTarget`.
|
||||
pub(crate) fn get_definition_targets<'db>(
|
||||
&self,
|
||||
file: ruff_db::files::File,
|
||||
db: &'db dyn crate::Db,
|
||||
model: &SemanticModel<'db>,
|
||||
alias_resolution: ImportAliasResolution,
|
||||
) -> Option<DefinitionsOrTargets<'db>> {
|
||||
use crate::NavigationTarget;
|
||||
|
||||
match self {
|
||||
GotoTarget::Expression(expression) => definitions_for_expression(db, file, expression)
|
||||
.map(DefinitionsOrTargets::Definitions),
|
||||
|
||||
) -> Option<Definitions<'db>> {
|
||||
let definitions = match self {
|
||||
GotoTarget::Expression(expression) => definitions_for_expression(model, *expression),
|
||||
// For already-defined symbols, they are their own definitions
|
||||
GotoTarget::FunctionDef(function) => {
|
||||
let model = SemanticModel::new(db, file);
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(function.definition(&model)),
|
||||
]))
|
||||
}
|
||||
GotoTarget::FunctionDef(function) => Some(vec![ResolvedDefinition::Definition(
|
||||
function.definition(model),
|
||||
)]),
|
||||
|
||||
GotoTarget::ClassDef(class) => {
|
||||
let model = SemanticModel::new(db, file);
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(class.definition(&model)),
|
||||
]))
|
||||
}
|
||||
GotoTarget::ClassDef(class) => Some(vec![ResolvedDefinition::Definition(
|
||||
class.definition(model),
|
||||
)]),
|
||||
|
||||
GotoTarget::Parameter(parameter) => {
|
||||
let model = SemanticModel::new(db, file);
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(parameter.definition(&model)),
|
||||
]))
|
||||
}
|
||||
GotoTarget::Parameter(parameter) => Some(vec![ResolvedDefinition::Definition(
|
||||
parameter.definition(model),
|
||||
)]),
|
||||
|
||||
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
|
||||
GotoTarget::ImportSymbolAlias {
|
||||
alias, import_from, ..
|
||||
} => {
|
||||
let symbol_name = alias.name.as_str();
|
||||
Some(DefinitionsOrTargets::Definitions(
|
||||
definitions_for_imported_symbol(
|
||||
db,
|
||||
file,
|
||||
import_from,
|
||||
symbol_name,
|
||||
alias_resolution,
|
||||
),
|
||||
Some(definitions_for_imported_symbol(
|
||||
model,
|
||||
import_from,
|
||||
symbol_name,
|
||||
alias_resolution,
|
||||
))
|
||||
}
|
||||
|
||||
GotoTarget::ImportModuleComponent {
|
||||
module_name,
|
||||
component_index,
|
||||
level,
|
||||
..
|
||||
} => {
|
||||
// Handle both `import foo.bar` and `from foo.bar import baz` where offset is within module component
|
||||
let components: Vec<&str> = module_name.split('.').collect();
|
||||
|
||||
// Build the module name up to and including the component containing the offset
|
||||
let target_module_name = components[..=*component_index].join(".");
|
||||
|
||||
// Try to resolve the module
|
||||
definitions_for_module(db, &target_module_name)
|
||||
// We don't currently support hovering the bare `.` so there is always a name
|
||||
let module = import_name(module_name, *component_index);
|
||||
definitions_for_module(model, Some(module), *level)
|
||||
}
|
||||
|
||||
// Handle import aliases (offset within 'z' in "import x.y as z")
|
||||
GotoTarget::ImportModuleAlias { alias } => {
|
||||
if alias_resolution == ImportAliasResolution::ResolveAliases {
|
||||
let full_module_name = alias.name.as_str();
|
||||
// Try to resolve the module
|
||||
definitions_for_module(db, full_module_name)
|
||||
definitions_for_module(model, Some(alias.name.as_str()), 0)
|
||||
} else {
|
||||
let alias_range = alias.asname.as_ref().unwrap().range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget {
|
||||
file,
|
||||
focus_range: alias_range,
|
||||
full_range: alias.range(),
|
||||
}),
|
||||
))
|
||||
alias.asname.as_ref().map(|name| {
|
||||
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,77 +431,128 @@ impl GotoTarget<'_> {
|
||||
GotoTarget::KeywordArgument {
|
||||
keyword,
|
||||
call_expression,
|
||||
} => Some(DefinitionsOrTargets::Definitions(
|
||||
definitions_for_keyword_argument(db, file, keyword, call_expression),
|
||||
} => Some(definitions_for_keyword_argument(
|
||||
model,
|
||||
keyword,
|
||||
call_expression,
|
||||
)),
|
||||
|
||||
// For exception variables, they are their own definitions (like parameters)
|
||||
GotoTarget::ExceptVariable(except_handler) => {
|
||||
let model = SemanticModel::new(db, file);
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(except_handler.definition(&model)),
|
||||
]))
|
||||
Some(vec![ResolvedDefinition::Definition(
|
||||
except_handler.definition(model),
|
||||
)])
|
||||
}
|
||||
|
||||
// For pattern match rest variables, they are their own definitions
|
||||
// Patterns are glorified assignments but we have to look them up by ident
|
||||
// because they're not expressions
|
||||
GotoTarget::PatternMatchRest(pattern_mapping) => {
|
||||
if let Some(rest_name) = &pattern_mapping.rest {
|
||||
let range = rest_name.range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
pattern_mapping.rest.as_ref().map(|name| {
|
||||
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
|
||||
})
|
||||
}
|
||||
|
||||
// For pattern match as names, they are their own definitions
|
||||
GotoTarget::PatternMatchAsName(pattern_as) => {
|
||||
if let Some(name) = &pattern_as.name {
|
||||
let range = name.range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
GotoTarget::PatternMatchAsName(pattern_as) => pattern_as.name.as_ref().map(|name| {
|
||||
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
|
||||
}),
|
||||
|
||||
GotoTarget::PatternKeywordArgument(pattern_keyword) => {
|
||||
let name = &pattern_keyword.attr;
|
||||
Some(definitions_for_name(
|
||||
model,
|
||||
name.as_str(),
|
||||
AnyNodeRef::Identifier(name),
|
||||
))
|
||||
}
|
||||
|
||||
GotoTarget::PatternMatchStarName(pattern_star) => {
|
||||
pattern_star.name.as_ref().map(|name| {
|
||||
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
|
||||
})
|
||||
}
|
||||
|
||||
// For callables, both the definition of the callable and the actual function impl are relevant.
|
||||
//
|
||||
// Prefer the function impl over the callable so that its docstrings win if defined.
|
||||
GotoTarget::Call { callable, call } => {
|
||||
let mut definitions = definitions_for_callable(db, file, call);
|
||||
let mut definitions = definitions_for_callable(model, call);
|
||||
let expr_definitions =
|
||||
definitions_for_expression(db, file, callable).unwrap_or_default();
|
||||
definitions_for_expression(model, *callable).unwrap_or_default();
|
||||
definitions.extend(expr_definitions);
|
||||
|
||||
if definitions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(DefinitionsOrTargets::Definitions(definitions))
|
||||
Some(definitions)
|
||||
}
|
||||
}
|
||||
|
||||
GotoTarget::BinOp { expression, .. } => {
|
||||
let model = SemanticModel::new(db, file);
|
||||
|
||||
let (definitions, _) =
|
||||
ty_python_semantic::definitions_for_bin_op(db, &model, expression)?;
|
||||
ty_python_semantic::definitions_for_bin_op(model, expression)?;
|
||||
|
||||
Some(DefinitionsOrTargets::Definitions(definitions))
|
||||
Some(definitions)
|
||||
}
|
||||
|
||||
GotoTarget::UnaryOp { expression, .. } => {
|
||||
let model = SemanticModel::new(db, file);
|
||||
let (definitions, _) =
|
||||
ty_python_semantic::definitions_for_unary_op(db, &model, expression)?;
|
||||
ty_python_semantic::definitions_for_unary_op(model, expression)?;
|
||||
|
||||
Some(DefinitionsOrTargets::Definitions(definitions))
|
||||
Some(definitions)
|
||||
}
|
||||
|
||||
_ => None,
|
||||
}
|
||||
// String annotations sub-expressions require us to recurse into the sub-AST
|
||||
GotoTarget::StringAnnotationSubexpr {
|
||||
string_expr,
|
||||
subrange,
|
||||
..
|
||||
} => {
|
||||
let (subast, submodel) = model.enter_string_annotation(string_expr)?;
|
||||
let subexpr = covering_node(subast.syntax().into(), *subrange)
|
||||
.node()
|
||||
.as_expr_ref()?;
|
||||
definitions_for_expression(&submodel, subexpr)
|
||||
}
|
||||
|
||||
// nonlocal and global are essentially loads, but again they're statements,
|
||||
// so we need to look them up by ident
|
||||
GotoTarget::NonLocal { identifier } | GotoTarget::Globals { identifier } => {
|
||||
Some(definitions_for_name(
|
||||
model,
|
||||
identifier.as_str(),
|
||||
AnyNodeRef::Identifier(identifier),
|
||||
))
|
||||
}
|
||||
|
||||
// These are declarations of sorts, but they're stmts and not exprs, so look up by ident.
|
||||
GotoTarget::TypeParamTypeVarName(type_var) => {
|
||||
let name = &type_var.name;
|
||||
Some(definitions_for_name(
|
||||
model,
|
||||
name.as_str(),
|
||||
AnyNodeRef::Identifier(name),
|
||||
))
|
||||
}
|
||||
|
||||
GotoTarget::TypeParamParamSpecName(name) => {
|
||||
let name = &name.name;
|
||||
Some(definitions_for_name(
|
||||
model,
|
||||
name.as_str(),
|
||||
AnyNodeRef::Identifier(name),
|
||||
))
|
||||
}
|
||||
|
||||
GotoTarget::TypeParamTypeVarTupleName(name) => {
|
||||
let name = &name.name;
|
||||
Some(definitions_for_name(
|
||||
model,
|
||||
name.as_str(),
|
||||
AnyNodeRef::Identifier(name),
|
||||
))
|
||||
}
|
||||
};
|
||||
definitions.map(Definitions)
|
||||
}
|
||||
|
||||
/// Returns the text representation of this goto target.
|
||||
@@ -513,6 +570,7 @@ impl GotoTarget<'_> {
|
||||
ast::ExprRef::Attribute(attr) => Some(Cow::Borrowed(attr.attr.as_str())),
|
||||
_ => None,
|
||||
},
|
||||
GotoTarget::StringAnnotationSubexpr { name, .. } => name.as_deref().map(Cow::Borrowed),
|
||||
GotoTarget::FunctionDef(function) => Some(Cow::Borrowed(function.name.as_str())),
|
||||
GotoTarget::ClassDef(class) => Some(Cow::Borrowed(class.name.as_str())),
|
||||
GotoTarget::Parameter(parameter) => Some(Cow::Borrowed(parameter.name.as_str())),
|
||||
@@ -573,6 +631,7 @@ impl GotoTarget<'_> {
|
||||
|
||||
/// Creates a `GotoTarget` from a `CoveringNode` and an offset within the node
|
||||
pub(crate) fn from_covering_node<'a>(
|
||||
model: &SemanticModel,
|
||||
covering_node: &crate::find_node::CoveringNode<'a>,
|
||||
offset: TextSize,
|
||||
tokens: &Tokens,
|
||||
@@ -619,6 +678,7 @@ impl GotoTarget<'_> {
|
||||
{
|
||||
return Some(GotoTarget::ImportModuleComponent {
|
||||
module_name: full_name.to_string(),
|
||||
level: 0,
|
||||
component_index,
|
||||
component_range,
|
||||
});
|
||||
@@ -659,14 +719,12 @@ impl GotoTarget<'_> {
|
||||
// Handle offset within module name in from import statements
|
||||
if let Some(module_expr) = &from.module {
|
||||
let full_module_name = module_expr.to_string();
|
||||
|
||||
if let Some((component_index, component_range)) = find_module_component(
|
||||
&full_module_name,
|
||||
module_expr.range.start(),
|
||||
offset,
|
||||
) {
|
||||
if let Some((component_index, component_range)) =
|
||||
find_module_component(&full_module_name, module_expr.start(), offset)
|
||||
{
|
||||
return Some(GotoTarget::ImportModuleComponent {
|
||||
module_name: full_module_name,
|
||||
level: from.level,
|
||||
component_index,
|
||||
component_range,
|
||||
});
|
||||
@@ -773,6 +831,31 @@ impl GotoTarget<'_> {
|
||||
Some(GotoTarget::Expression(unary.into()))
|
||||
}
|
||||
|
||||
node @ AnyNodeRef::ExprStringLiteral(string_expr) => {
|
||||
// Check if we've clicked on a sub-GotoTarget inside a string annotation's sub-AST
|
||||
if let Some((subast, submodel)) = model.enter_string_annotation(string_expr)
|
||||
&& let Some(GotoTarget::Expression(subexpr)) = find_goto_target_impl(
|
||||
&submodel,
|
||||
subast.tokens(),
|
||||
subast.syntax().into(),
|
||||
offset,
|
||||
)
|
||||
{
|
||||
let name = match subexpr {
|
||||
ast::ExprRef::Name(name) => Some(name.id.to_string()),
|
||||
ast::ExprRef::Attribute(attr) => Some(attr.attr.to_string()),
|
||||
_ => None,
|
||||
};
|
||||
Some(GotoTarget::StringAnnotationSubexpr {
|
||||
string_expr,
|
||||
subrange: subexpr.range(),
|
||||
name,
|
||||
})
|
||||
} else {
|
||||
node.as_expr_ref().map(GotoTarget::Expression)
|
||||
}
|
||||
}
|
||||
|
||||
node => {
|
||||
// Check if this is seemingly a callable being invoked (the `x` in `x(...)`)
|
||||
let parent = covering_node.parent();
|
||||
@@ -808,6 +891,7 @@ impl Ranged for GotoTarget<'_> {
|
||||
GotoTarget::ImportModuleComponent {
|
||||
component_range, ..
|
||||
} => *component_range,
|
||||
GotoTarget::StringAnnotationSubexpr { subrange, .. } => *subrange,
|
||||
GotoTarget::ImportModuleAlias { alias } => alias.asname.as_ref().unwrap().range,
|
||||
GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range,
|
||||
GotoTarget::KeywordArgument { keyword, .. } => keyword.arg.as_ref().unwrap().range,
|
||||
@@ -827,9 +911,9 @@ impl Ranged for GotoTarget<'_> {
|
||||
}
|
||||
|
||||
/// Converts a collection of `ResolvedDefinition` items into `NavigationTarget` items.
|
||||
fn convert_resolved_definitions_to_targets(
|
||||
db: &dyn crate::Db,
|
||||
definitions: Vec<ty_python_semantic::ResolvedDefinition<'_>>,
|
||||
fn convert_resolved_definitions_to_targets<'db>(
|
||||
db: &'db dyn ty_python_semantic::Db,
|
||||
definitions: Vec<ty_python_semantic::ResolvedDefinition<'db>>,
|
||||
) -> Vec<crate::NavigationTarget> {
|
||||
definitions
|
||||
.into_iter()
|
||||
@@ -863,27 +947,28 @@ fn convert_resolved_definitions_to_targets(
|
||||
|
||||
/// Shared helper to get definitions for an expr (that is presumably a name/attr)
|
||||
fn definitions_for_expression<'db>(
|
||||
db: &'db dyn crate::Db,
|
||||
file: ruff_db::files::File,
|
||||
expression: &ruff_python_ast::ExprRef<'_>,
|
||||
model: &SemanticModel<'db>,
|
||||
expression: ruff_python_ast::ExprRef<'_>,
|
||||
) -> Option<Vec<ResolvedDefinition<'db>>> {
|
||||
match expression {
|
||||
ast::ExprRef::Name(name) => Some(definitions_for_name(db, file, name)),
|
||||
ast::ExprRef::Name(name) => Some(definitions_for_name(
|
||||
model,
|
||||
name.id.as_str(),
|
||||
expression.into(),
|
||||
)),
|
||||
ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute(
|
||||
db, file, attribute,
|
||||
model, attribute,
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn definitions_for_callable<'db>(
|
||||
db: &'db dyn crate::Db,
|
||||
file: ruff_db::files::File,
|
||||
model: &SemanticModel<'db>,
|
||||
call: &ast::ExprCall,
|
||||
) -> Vec<ResolvedDefinition<'db>> {
|
||||
let model = SemanticModel::new(db, file);
|
||||
// Attempt to refine to a specific call
|
||||
let signature_info = call_signature_details(db, &model, call);
|
||||
let signature_info = call_signature_details(model, call);
|
||||
signature_info
|
||||
.into_iter()
|
||||
.filter_map(|signature| signature.definition.map(ResolvedDefinition::Definition))
|
||||
@@ -892,7 +977,7 @@ fn definitions_for_callable<'db>(
|
||||
|
||||
/// Shared helper to map and convert resolved definitions into navigation targets.
|
||||
fn definitions_to_navigation_targets<'db>(
|
||||
db: &dyn crate::Db,
|
||||
db: &dyn ty_python_semantic::Db,
|
||||
stub_mapper: Option<&StubMapper<'db>>,
|
||||
mut definitions: Vec<ty_python_semantic::ResolvedDefinition<'db>>,
|
||||
) -> Option<crate::NavigationTargets> {
|
||||
@@ -907,12 +992,21 @@ fn definitions_to_navigation_targets<'db>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_goto_target(
|
||||
parsed: &ParsedModuleRef,
|
||||
pub(crate) fn find_goto_target<'a>(
|
||||
model: &'a SemanticModel,
|
||||
parsed: &'a ParsedModuleRef,
|
||||
offset: TextSize,
|
||||
) -> Option<GotoTarget<'_>> {
|
||||
let token = parsed
|
||||
.tokens()
|
||||
) -> Option<GotoTarget<'a>> {
|
||||
find_goto_target_impl(model, parsed.tokens(), parsed.syntax().into(), offset)
|
||||
}
|
||||
|
||||
pub(crate) fn find_goto_target_impl<'a>(
|
||||
model: &'a SemanticModel,
|
||||
tokens: &'a Tokens,
|
||||
syntax: AnyNodeRef<'a>,
|
||||
offset: TextSize,
|
||||
) -> Option<GotoTarget<'a>> {
|
||||
let token = tokens
|
||||
.at_offset(offset)
|
||||
.max_by_key(|token| match token.kind() {
|
||||
TokenKind::Name
|
||||
@@ -933,30 +1027,24 @@ pub(crate) fn find_goto_target(
|
||||
return None;
|
||||
}
|
||||
|
||||
let covering_node = covering_node(parsed.syntax().into(), token.range())
|
||||
.find_first(|node| node.is_identifier() || node.is_expression())
|
||||
let covering_node = covering_node(syntax, token.range())
|
||||
.find_first(|node| {
|
||||
node.is_identifier() || node.is_expression() || node.is_stmt_import_from()
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
GotoTarget::from_covering_node(&covering_node, offset, parsed.tokens())
|
||||
GotoTarget::from_covering_node(model, &covering_node, offset, tokens)
|
||||
}
|
||||
|
||||
/// Helper function to resolve a module name and create a navigation target.
|
||||
fn definitions_for_module<'db>(
|
||||
db: &'db dyn crate::Db,
|
||||
module_name_str: &str,
|
||||
) -> Option<DefinitionsOrTargets<'db>> {
|
||||
use ty_python_semantic::{ModuleName, resolve_module};
|
||||
|
||||
if let Some(module_name) = ModuleName::new(module_name_str) {
|
||||
if let Some(resolved_module) = resolve_module(db, &module_name) {
|
||||
if let Some(module_file) = resolved_module.file(db) {
|
||||
return Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Module(module_file),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
model: &SemanticModel<'db>,
|
||||
module: Option<&str>,
|
||||
level: u32,
|
||||
) -> Option<Vec<ResolvedDefinition<'db>>> {
|
||||
let module = model.resolve_module(module, level)?;
|
||||
let file = module.file(model.db())?;
|
||||
Some(vec![ResolvedDefinition::Module(file)])
|
||||
}
|
||||
|
||||
/// Helper function to extract module component information from a dotted module name
|
||||
@@ -970,9 +1058,7 @@ fn find_module_component(
|
||||
|
||||
// Split the module name into components and find which one contains the offset
|
||||
let mut current_pos = 0;
|
||||
let components: Vec<&str> = full_module_name.split('.').collect();
|
||||
|
||||
for (i, component) in components.iter().enumerate() {
|
||||
for (i, component) in full_module_name.split('.').enumerate() {
|
||||
let component_start = current_pos;
|
||||
let component_end = current_pos + component.len();
|
||||
|
||||
@@ -991,3 +1077,16 @@ fn find_module_component(
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Helper to get the module name up to the given component index
|
||||
fn import_name(module_name: &str, component_index: usize) -> &str {
|
||||
// We want everything to the left of the nth `.`
|
||||
// If there's no nth `.` then we want the whole thing.
|
||||
let idx = module_name
|
||||
.match_indices('.')
|
||||
.nth(component_index)
|
||||
.map(|(idx, _)| idx)
|
||||
.unwrap_or(module_name.len());
|
||||
|
||||
&module_name[..idx]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ty_python_semantic::ImportAliasResolution;
|
||||
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
|
||||
|
||||
/// Navigate to the declaration of a symbol.
|
||||
///
|
||||
@@ -16,10 +16,11 @@ pub fn goto_declaration(
|
||||
offset: TextSize,
|
||||
) -> Option<RangedValue<NavigationTargets>> {
|
||||
let module = parsed_module(db, file).load(db);
|
||||
let goto_target = find_goto_target(&module, offset)?;
|
||||
let model = SemanticModel::new(db, file);
|
||||
let goto_target = find_goto_target(&model, &module, offset)?;
|
||||
|
||||
let declaration_targets = goto_target
|
||||
.get_definition_targets(file, db, ImportAliasResolution::ResolveAliases)?
|
||||
.get_definition_targets(&model, ImportAliasResolution::ResolveAliases)?
|
||||
.declaration_targets(db)?;
|
||||
|
||||
Some(RangedValue {
|
||||
@@ -888,6 +889,190 @@ def another_helper(path):
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_string_annotation1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCla<CURSOR>ss" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:7
|
||||
|
|
||||
2 | a: "MyClass" = 1
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| ^^^^^^^
|
||||
5 | """some docs"""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | a: "MyClass" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_string_annotation2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyCl<CURSOR>ass" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:7
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| ^^^^^^^
|
||||
5 | """some docs"""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:12
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_string_annotation3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None |<CURSOR> MyClass" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_string_annotation4() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyClass<CURSOR>" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:7
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| ^^^^^^^
|
||||
5 | """some docs"""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:12
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_string_annotation5() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyClass"<CURSOR> = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_string_annotation_dangling1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCl<CURSOR>ass |" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_string_annotation_dangling2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCl<CURSOR>ass | No" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:7
|
||||
|
|
||||
2 | a: "MyClass | No" = 1
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| ^^^^^^^
|
||||
5 | """some docs"""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | a: "MyClass | No" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_string_annotation_dangling3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyClass | N<CURSOR>o" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_nested_instance_attribute() {
|
||||
let test = cursor_test(
|
||||
@@ -1069,6 +1254,45 @@ def outer():
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_nonlocal_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def outer():
|
||||
xy = "outer_value"
|
||||
|
||||
def inner():
|
||||
nonlocal x<CURSOR>y
|
||||
xy = "modified"
|
||||
return x # Should find the nonlocal x declaration in outer scope
|
||||
|
||||
return inner
|
||||
"#,
|
||||
);
|
||||
|
||||
// Should find the variable declaration in the outer scope, not the nonlocal statement
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | def outer():
|
||||
3 | xy = "outer_value"
|
||||
| ^^
|
||||
4 |
|
||||
5 | def inner():
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:18
|
||||
|
|
||||
5 | def inner():
|
||||
6 | nonlocal xy
|
||||
| ^^
|
||||
7 | xy = "modified"
|
||||
8 | return x # Should find the nonlocal x declaration in outer scope
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_global_binding() {
|
||||
let test = cursor_test(
|
||||
@@ -1103,6 +1327,41 @@ def function():
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_global_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
global_var = "global_value"
|
||||
|
||||
def function():
|
||||
global global_<CURSOR>var
|
||||
global_var = "modified"
|
||||
return global_var # Should find the global variable declaration
|
||||
"#,
|
||||
);
|
||||
|
||||
// Should find the global variable declaration, not the global statement
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:1
|
||||
|
|
||||
2 | global_var = "global_value"
|
||||
| ^^^^^^^^^^
|
||||
3 |
|
||||
4 | def function():
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:12
|
||||
|
|
||||
4 | def function():
|
||||
5 | global global_var
|
||||
| ^^^^^^^^^^
|
||||
6 | global_var = "modified"
|
||||
7 | return global_var # Should find the global variable declaration
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_inherited_attribute() {
|
||||
let test = cursor_test(
|
||||
@@ -1138,6 +1397,486 @@ def function():
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_rest_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_rest_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_as_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_as_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_keyword_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, button=a<CURSOR>b):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_keyword_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, button=ab):
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:11:17
|
||||
|
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
11 | x = ab
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_class_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Cl<CURSOR>ick(x, button=ab):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class Click:
|
||||
| ^^^^^
|
||||
3 | __match_args__ = ("position", "button")
|
||||
4 | def __init__(self, pos, btn):
|
||||
|
|
||||
info: Source
|
||||
--> main.py:10:14
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^^^^
|
||||
11 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_class_field_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, but<CURSOR>ton=ab):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:37
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_spec_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_spec_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:43
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_tuple_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_tuple_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:38
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_property_getter_setter() {
|
||||
let test = cursor_test(
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ty_python_semantic::ImportAliasResolution;
|
||||
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
|
||||
|
||||
/// Navigate to the definition of a symbol.
|
||||
///
|
||||
@@ -17,10 +17,10 @@ pub fn goto_definition(
|
||||
offset: TextSize,
|
||||
) -> Option<RangedValue<NavigationTargets>> {
|
||||
let module = parsed_module(db, file).load(db);
|
||||
let goto_target = find_goto_target(&module, offset)?;
|
||||
|
||||
let model = SemanticModel::new(db, file);
|
||||
let goto_target = find_goto_target(&model, &module, offset)?;
|
||||
let definition_targets = goto_target
|
||||
.get_definition_targets(file, db, ImportAliasResolution::ResolveAliases)?
|
||||
.get_definition_targets(&model, ImportAliasResolution::ResolveAliases)?
|
||||
.definition_targets(db)?;
|
||||
|
||||
Some(RangedValue {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,16 @@
|
||||
reason = "Prefer System trait methods over std methods in ty crates"
|
||||
)]
|
||||
mod all_symbols;
|
||||
mod code_action;
|
||||
mod completion;
|
||||
mod doc_highlights;
|
||||
mod docstring;
|
||||
mod document_symbols;
|
||||
mod find_node;
|
||||
mod find_references;
|
||||
mod goto;
|
||||
mod goto_declaration;
|
||||
mod goto_definition;
|
||||
mod goto_references;
|
||||
mod goto_type_definition;
|
||||
mod hover;
|
||||
mod importer;
|
||||
@@ -27,13 +28,16 @@ mod symbols;
|
||||
mod workspace_symbols;
|
||||
|
||||
pub use all_symbols::{AllSymbolInfo, all_symbols};
|
||||
pub use code_action::{QuickFix, code_actions};
|
||||
pub use completion::{Completion, CompletionKind, CompletionSettings, completion};
|
||||
pub use doc_highlights::document_highlights;
|
||||
pub use document_symbols::document_symbols;
|
||||
pub use find_references::find_references;
|
||||
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
|
||||
pub use goto_references::goto_references;
|
||||
pub use hover::hover;
|
||||
pub use inlay_hints::{InlayHintKind, InlayHintLabel, InlayHintSettings, inlay_hints};
|
||||
pub use inlay_hints::{
|
||||
InlayHintKind, InlayHintLabel, InlayHintSettings, InlayHintTextEdit, inlay_hints,
|
||||
};
|
||||
pub use markup::MarkupKind;
|
||||
pub use references::ReferencesMode;
|
||||
pub use rename::{can_rename, rename};
|
||||
|
||||
@@ -20,7 +20,7 @@ use ruff_python_ast::{
|
||||
};
|
||||
use ruff_python_parser::Tokens;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use ty_python_semantic::ImportAliasResolution;
|
||||
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
|
||||
|
||||
/// Mode for references search behavior
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -48,8 +48,9 @@ pub(crate) fn references(
|
||||
// Get the definitions for the symbol at the cursor position
|
||||
|
||||
// When finding references, do not resolve any local aliases.
|
||||
let model = SemanticModel::new(db, file);
|
||||
let target_definitions_nav = goto_target
|
||||
.get_definition_targets(file, db, ImportAliasResolution::PreserveAliases)?
|
||||
.get_definition_targets(&model, ImportAliasResolution::PreserveAliases)?
|
||||
.definition_targets(db)?;
|
||||
let target_definitions: Vec<NavigationTarget> = target_definitions_nav.into_iter().collect();
|
||||
|
||||
@@ -121,10 +122,10 @@ fn references_for_file(
|
||||
) {
|
||||
let parsed = ruff_db::parsed::parsed_module(db, file);
|
||||
let module = parsed.load(db);
|
||||
let model = SemanticModel::new(db, file);
|
||||
|
||||
let mut finder = LocalReferencesFinder {
|
||||
db,
|
||||
file,
|
||||
model: &model,
|
||||
target_definitions,
|
||||
references,
|
||||
mode,
|
||||
@@ -156,8 +157,7 @@ fn is_symbol_externally_visible(goto_target: &GotoTarget<'_>) -> bool {
|
||||
|
||||
/// AST visitor to find all references to a specific symbol by comparing semantic definitions
|
||||
struct LocalReferencesFinder<'a> {
|
||||
db: &'a dyn Db,
|
||||
file: File,
|
||||
model: &'a SemanticModel<'a>,
|
||||
tokens: &'a Tokens,
|
||||
target_definitions: &'a [NavigationTarget],
|
||||
references: &'a mut Vec<ReferenceTarget>,
|
||||
@@ -219,6 +219,11 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> {
|
||||
self.check_identifier_reference(name);
|
||||
}
|
||||
}
|
||||
AnyNodeRef::PatternMatchStar(pattern_star) if self.should_include_declaration() => {
|
||||
if let Some(name) = &pattern_star.name {
|
||||
self.check_identifier_reference(name);
|
||||
}
|
||||
}
|
||||
AnyNodeRef::PatternMatchMapping(pattern_mapping)
|
||||
if self.should_include_declaration() =>
|
||||
{
|
||||
@@ -226,6 +231,31 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> {
|
||||
self.check_identifier_reference(rest_name);
|
||||
}
|
||||
}
|
||||
AnyNodeRef::TypeParamParamSpec(param_spec) if self.should_include_declaration() => {
|
||||
self.check_identifier_reference(¶m_spec.name);
|
||||
}
|
||||
AnyNodeRef::TypeParamTypeVarTuple(param_tuple) if self.should_include_declaration() => {
|
||||
self.check_identifier_reference(¶m_tuple.name);
|
||||
}
|
||||
AnyNodeRef::TypeParamTypeVar(param_var) if self.should_include_declaration() => {
|
||||
self.check_identifier_reference(¶m_var.name);
|
||||
}
|
||||
AnyNodeRef::ExprStringLiteral(string_expr) if self.should_include_declaration() => {
|
||||
// Highlight the sub-AST of a string annotation
|
||||
if let Some((sub_ast, sub_model)) = self.model.enter_string_annotation(string_expr)
|
||||
{
|
||||
let mut sub_finder = LocalReferencesFinder {
|
||||
model: &sub_model,
|
||||
target_definitions: self.target_definitions,
|
||||
references: self.references,
|
||||
mode: self.mode,
|
||||
tokens: sub_ast.tokens(),
|
||||
target_text: self.target_text,
|
||||
ancestors: Vec::new(),
|
||||
};
|
||||
sub_finder.visit_expr(sub_ast.expr());
|
||||
}
|
||||
}
|
||||
AnyNodeRef::Alias(alias) if self.should_include_declaration() => {
|
||||
// Handle import alias declarations
|
||||
if let Some(asname) = &alias.asname {
|
||||
@@ -284,14 +314,13 @@ impl LocalReferencesFinder<'_> {
|
||||
// the node is fine here. Offsets matter only for import statements
|
||||
// where the identifier might be a multi-part module name.
|
||||
let offset = covering_node.node().start();
|
||||
|
||||
if let Some(goto_target) =
|
||||
GotoTarget::from_covering_node(covering_node, offset, self.tokens)
|
||||
GotoTarget::from_covering_node(self.model, covering_node, offset, self.tokens)
|
||||
{
|
||||
// Get the definitions for this goto target
|
||||
if let Some(current_definitions_nav) = goto_target
|
||||
.get_definition_targets(self.file, self.db, ImportAliasResolution::PreserveAliases)
|
||||
.and_then(|definitions| definitions.declaration_targets(self.db))
|
||||
.get_definition_targets(self.model, ImportAliasResolution::PreserveAliases)
|
||||
.and_then(|definitions| definitions.declaration_targets(self.model.db()))
|
||||
{
|
||||
let current_definitions: Vec<NavigationTarget> =
|
||||
current_definitions_nav.into_iter().collect();
|
||||
@@ -300,7 +329,7 @@ impl LocalReferencesFinder<'_> {
|
||||
// Determine if this is a read or write reference
|
||||
let kind = self.determine_reference_kind(covering_node);
|
||||
let target =
|
||||
ReferenceTarget::new(self.file, covering_node.node().range(), kind);
|
||||
ReferenceTarget::new(self.model.file(), covering_node.node().range(), kind);
|
||||
self.references.push(target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@ use crate::references::{ReferencesMode, references};
|
||||
use crate::{Db, ReferenceTarget};
|
||||
use ruff_db::files::File;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ty_python_semantic::ImportAliasResolution;
|
||||
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
|
||||
|
||||
/// Returns the range of the symbol if it can be renamed, None if not.
|
||||
pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option<ruff_text_size::TextRange> {
|
||||
let parsed = ruff_db::parsed::parsed_module(db, file);
|
||||
let module = parsed.load(db);
|
||||
let model = SemanticModel::new(db, file);
|
||||
|
||||
// Get the definitions for the symbol at the offset
|
||||
let goto_target = find_goto_target(&module, offset)?;
|
||||
let goto_target = find_goto_target(&model, &module, offset)?;
|
||||
|
||||
// Don't allow renaming of import module components
|
||||
if matches!(
|
||||
@@ -24,7 +25,7 @@ pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option<ruff_text
|
||||
let current_file_in_project = is_file_in_project(db, file);
|
||||
|
||||
if let Some(definition_targets) = goto_target
|
||||
.get_definition_targets(file, db, ImportAliasResolution::PreserveAliases)
|
||||
.get_definition_targets(&model, ImportAliasResolution::PreserveAliases)
|
||||
.and_then(|definitions| definitions.declaration_targets(db))
|
||||
{
|
||||
for target in &definition_targets {
|
||||
@@ -58,9 +59,10 @@ pub fn rename(
|
||||
) -> Option<Vec<ReferenceTarget>> {
|
||||
let parsed = ruff_db::parsed::parsed_module(db, file);
|
||||
let module = parsed.load(db);
|
||||
let model = SemanticModel::new(db, file);
|
||||
|
||||
// Get the definitions for the symbol at the offset
|
||||
let goto_target = find_goto_target(&module, offset)?;
|
||||
let goto_target = find_goto_target(&model, &module, offset)?;
|
||||
|
||||
// Clients shouldn't call us with an empty new name, but just in case...
|
||||
if new_name.is_empty() {
|
||||
@@ -337,6 +339,546 @@ class DataProcessor:
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_string_annotation1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCla<CURSOR>ss" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("MyNewClass"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | a: "MyClass" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| -------
|
||||
5 | """some docs"""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_string_annotation2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyCl<CURSOR>ass" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("MyNewClass"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:2:12
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| -------
|
||||
5 | """some docs"""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_string_annotation3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None |<CURSOR> MyClass" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("MyNewClass"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_string_annotation4() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyClass<CURSOR>" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("MyNewClass"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:2:12
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| -------
|
||||
5 | """some docs"""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_string_annotation5() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyClass"<CURSOR> = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("MyNewClass"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_string_annotation_dangling1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCl<CURSOR>ass |" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("MyNewClass"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_string_annotation_dangling2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCl<CURSOR>ass | No" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("MyNewClass"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | a: "MyClass | No" = 1
|
||||
| ^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| -------
|
||||
5 | """some docs"""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_string_annotation_dangling3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyClass | N<CURSOR>o" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("MyNewClass"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
| --
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
| --
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_rest_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
| --
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_rest_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
| --
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_as_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
| --
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_as_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
| --
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_keyword_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, button=a<CURSOR>b):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
| --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_keyword_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, button=ab):
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
| --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_class_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Cl<CURSOR>ick(x, button=ab):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class Click:
|
||||
| ^^^^^
|
||||
3 | __match_args__ = ("position", "button")
|
||||
4 | def __init__(self, pos, btn):
|
||||
|
|
||||
::: main.py:8:20
|
||||
|
|
||||
6 | self.button: str = btn
|
||||
7 |
|
||||
8 | def my_func(event: Click):
|
||||
| -----
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| -----
|
||||
11 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_class_field_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, but<CURSOR>ton=ab):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_spec_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_spec_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_tuple_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_tuple_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cannot_rename_import_module_component() {
|
||||
// Test that we cannot rename parts of module names in import statements
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user