Compare commits
56 Commits
alex/from-
...
gankra/sub
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4be2aad04 | ||
|
|
a2f9e5a680 | ||
|
|
695f2723d4 | ||
|
|
949684677f | ||
|
|
d4f5e51c23 | ||
|
|
73af08a520 | ||
|
|
b60910c82b | ||
|
|
ac532e81e7 | ||
|
|
dd1ac81f15 | ||
|
|
a10c4e2027 | ||
|
|
d6ce7f0fce | ||
|
|
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 |
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
|
||||
|
||||
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@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
|
||||
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@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
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
42
Cargo.lock
generated
42
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]]
|
||||
@@ -3127,7 +3127,7 @@ dependencies = [
|
||||
"fern",
|
||||
"glob",
|
||||
"globset",
|
||||
"hashbrown 0.16.0",
|
||||
"hashbrown 0.16.1",
|
||||
"imperative",
|
||||
"insta",
|
||||
"is-macro",
|
||||
@@ -3570,7 +3570,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -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",
|
||||
@@ -5020,7 +5022,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]]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
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")
|
||||
@@ -347,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) {
|
||||
|
||||
@@ -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
|
||||
|
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -115,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"))]
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
|
||||
@@ -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 {
|
||||
|
||||
298
crates/ty/docs/rules.md
generated
298
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#L131" 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#L175" 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#L201" 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#L226" 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#L252" 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#L278" 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#L339" 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#L360" 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#L564" 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#L588" 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#L392" 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#L642" 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#L682" 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#L1877" 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#L704" 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#L734" 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#L785" 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#L806" 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#L829" 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#L1574" 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#L865" 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#L609" 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#L891" 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#L988" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -858,13 +928,99 @@ 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#L2005" 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.
|
||||
|
||||
[Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle
|
||||
|
||||
## `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#L538" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -896,7 +1052,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#L964" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -926,7 +1082,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#L1015" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -976,7 +1132,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#L1114" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1002,7 +1158,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#L919" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1033,7 +1189,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#L474" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1067,7 +1223,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#L1134" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1116,7 +1272,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#L663" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1141,7 +1297,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#L1177" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1199,7 +1355,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#L943" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1226,7 +1382,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
||||
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#L1216" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1256,7 +1412,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#L1240" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1286,7 +1442,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#L1292" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1320,7 +1476,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#L1264" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1354,7 +1510,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#L1320" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1389,7 +1545,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#L1349" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1414,7 +1570,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#L1978" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1447,7 +1603,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#L1368" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1476,7 +1632,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#L1391" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1500,7 +1656,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#L1409" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1526,7 +1682,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
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#L1460" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1553,7 +1709,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#L1731" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1611,7 +1767,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#L1853" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1641,7 +1797,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#L1551" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1670,7 +1826,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#L1632" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1697,7 +1853,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#L1610" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1725,7 +1881,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#L1653" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1771,7 +1927,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#L1710" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1798,7 +1954,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#L1752" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1826,7 +1982,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#L1774" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1851,7 +2007,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#L1793" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1876,7 +2032,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#L1429" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1913,7 +2069,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#L1812" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1941,7 +2097,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#L1834" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1966,7 +2122,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#L503" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2007,7 +2163,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#L318" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2034,7 +2190,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 +2221,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 +2251,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#L1481" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2123,7 +2279,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#L149" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2155,7 +2311,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#L1503" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2187,7 +2343,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#L1905" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2214,7 +2370,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#L1692" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2238,7 +2394,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#L1926" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2296,7 +2452,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#L752" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2335,7 +2491,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#L1058" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2398,7 +2554,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#L300" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2422,7 +2578,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#L1529" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2450,7 +2606,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
|
||||
|
||||
|
@@ -9,9 +9,10 @@ use ruff_python_ast::name::Name;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::{Token, TokenAt, TokenKind, Tokens};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use ty_python_semantic::types::UnionType;
|
||||
use ty_python_semantic::{
|
||||
Completion as SemanticCompletion, ModuleName, NameKind, SemanticModel,
|
||||
types::{CycleDetector, Type},
|
||||
Completion as SemanticCompletion, KnownModule, ModuleName, NameKind, SemanticModel,
|
||||
types::{CycleDetector, KnownClass, Type},
|
||||
};
|
||||
|
||||
use crate::docstring::Docstring;
|
||||
@@ -82,6 +83,31 @@ impl<'db> Completions<'db> {
|
||||
fn force_add(&mut self, completion: Completion<'db>) {
|
||||
self.items.push(completion);
|
||||
}
|
||||
|
||||
/// Tags completions with whether they are known to be usable in
|
||||
/// a `raise` context.
|
||||
///
|
||||
/// It's possible that some completions are usable in a `raise`
|
||||
/// but aren't marked by this method. That is, false negatives are
|
||||
/// possible but false positives are not.
|
||||
fn tag_raisable(&mut self) {
|
||||
let raisable_type = UnionType::from_elements(
|
||||
self.db,
|
||||
[
|
||||
KnownClass::BaseException.to_subclass_of(self.db),
|
||||
KnownClass::BaseException.to_instance(self.db),
|
||||
],
|
||||
);
|
||||
for item in &mut self.items {
|
||||
let Some(ty) = item.ty else { continue };
|
||||
item.is_definitively_raisable = ty.is_assignable_to(self.db, raisable_type);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes any completion that doesn't satisfy the given predicate.
|
||||
fn retain(&mut self, predicate: impl FnMut(&Completion<'_>) -> bool) {
|
||||
self.items.retain(predicate);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Extend<SemanticCompletion<'db>> for Completions<'db> {
|
||||
@@ -153,6 +179,13 @@ pub struct Completion<'db> {
|
||||
/// Whether this item only exists for type checking purposes and
|
||||
/// will be missing at runtime
|
||||
pub is_type_check_only: bool,
|
||||
/// Whether this item can definitively be used in a `raise` context.
|
||||
///
|
||||
/// Note that this may not always be computed. (i.e., Only computed
|
||||
/// when we are in a `raise` context.) And also note that if this
|
||||
/// is `true`, then it's definitively usable in `raise`, but if
|
||||
/// it's `false`, it _may_ still be usable in `raise`.
|
||||
pub is_definitively_raisable: bool,
|
||||
/// The documentation associated with this item, if
|
||||
/// available.
|
||||
pub documentation: Option<Docstring>,
|
||||
@@ -177,6 +210,7 @@ impl<'db> Completion<'db> {
|
||||
import: None,
|
||||
builtin: semantic.builtin,
|
||||
is_type_check_only,
|
||||
is_definitively_raisable: false,
|
||||
documentation,
|
||||
}
|
||||
}
|
||||
@@ -257,6 +291,7 @@ impl<'db> Completion<'db> {
|
||||
import: None,
|
||||
builtin: false,
|
||||
is_type_check_only: false,
|
||||
is_definitively_raisable: false,
|
||||
documentation: None,
|
||||
}
|
||||
}
|
||||
@@ -271,6 +306,7 @@ impl<'db> Completion<'db> {
|
||||
import: None,
|
||||
builtin: true,
|
||||
is_type_check_only: false,
|
||||
is_definitively_raisable: false,
|
||||
documentation: None,
|
||||
}
|
||||
}
|
||||
@@ -364,6 +400,20 @@ pub fn completion<'db>(
|
||||
}
|
||||
}
|
||||
|
||||
if is_raising_exception(tokens) {
|
||||
completions.tag_raisable();
|
||||
|
||||
// As a special case, and because it's a common footgun, we
|
||||
// specifically disallow `NotImplemented` in this context.
|
||||
// `NotImplementedError` should be used instead. So if we can
|
||||
// definitively detect `NotImplemented`, then we can safely
|
||||
// omit it from suggestions.
|
||||
completions.retain(|item| {
|
||||
let Some(ty) = item.ty else { return true };
|
||||
!ty.is_notimplemented(db)
|
||||
});
|
||||
}
|
||||
|
||||
completions.into_completions()
|
||||
}
|
||||
|
||||
@@ -427,7 +477,8 @@ fn add_unimported_completions<'db>(
|
||||
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
|
||||
|
||||
for symbol in all_symbols(db, &completions.query) {
|
||||
if symbol.module.file(db) == Some(file) {
|
||||
if symbol.module.file(db) == Some(file) || symbol.module.is_known(db, KnownModule::Builtins)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -450,6 +501,7 @@ fn add_unimported_completions<'db>(
|
||||
builtin: false,
|
||||
// TODO: `is_type_check_only` requires inferring the type of the symbol
|
||||
is_type_check_only: false,
|
||||
is_definitively_raisable: false,
|
||||
documentation: None,
|
||||
});
|
||||
}
|
||||
@@ -1263,7 +1315,8 @@ fn find_typed_text(
|
||||
if last.end() < offset || last.range().is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(source[last.range()].to_string())
|
||||
let range = TextRange::new(last.start(), offset);
|
||||
Some(source[range].to_string())
|
||||
}
|
||||
|
||||
/// Whether the last token is in a place where we should not provide completions.
|
||||
@@ -1354,10 +1407,52 @@ fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Opt
|
||||
type_param.name.range.contains_range(range)
|
||||
}
|
||||
ast::AnyNodeRef::StmtFor(stmt_for) => stmt_for.target.range().contains_range(range),
|
||||
// The AST does not produce `ast::AnyNodeRef::Parameter` nodes for keywords
|
||||
// or otherwise invalid syntax. Rather they are captured in a
|
||||
// `ast::AnyNodeRef::Parameters` node as "empty space". To ensure
|
||||
// we still suppress suggestions even when the syntax is technically
|
||||
// invalid we extract the token under the cursor and check if it makes
|
||||
// up that "empty space" inside the Parameters Node. If it does, we know
|
||||
// that we are still binding variables, just that the current state is
|
||||
// syntatically invalid. Hence we suppress autocomplete suggestons
|
||||
// also in those cases.
|
||||
ast::AnyNodeRef::Parameters(params) => {
|
||||
if !params.range.contains_range(range) {
|
||||
return false;
|
||||
}
|
||||
params
|
||||
.iter()
|
||||
.map(|param| param.range())
|
||||
.all(|r| !r.contains_range(range))
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true when the cursor is after a `raise` keyword.
|
||||
fn is_raising_exception(tokens: &[Token]) -> bool {
|
||||
/// The maximum number of tokens we're willing to
|
||||
/// look-behind to find a `raise` keyword.
|
||||
const LIMIT: usize = 10;
|
||||
|
||||
// This only looks for things like `raise foo.bar.baz.qu<CURSOR>`.
|
||||
// Technically, any kind of expression is allowed after `raise`.
|
||||
// But we may not always want to treat it specially. So we're
|
||||
// rather conservative about what we consider "raising an
|
||||
// exception" to be for the purposes of completions. The failure
|
||||
// mode here is that we may wind up suggesting things that
|
||||
// shouldn't be raised. The benefit is that when this heuristic
|
||||
// does work, we won't suggest things that shouldn't be raised.
|
||||
for token in tokens.iter().rev().take(LIMIT) {
|
||||
match token.kind() {
|
||||
TokenKind::Name | TokenKind::Dot => continue,
|
||||
TokenKind::Raise => return true,
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Order completions according to the following rules:
|
||||
///
|
||||
/// 1) Names with no underscore prefix
|
||||
@@ -1370,8 +1465,16 @@ fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Opt
|
||||
/// This has the effect of putting all dunder attributes after "normal"
|
||||
/// attributes, and all single-underscore attributes after dunder attributes.
|
||||
fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering {
|
||||
fn key<'a>(completion: &'a Completion) -> (bool, bool, bool, NameKind, bool, &'a Name) {
|
||||
fn key<'a>(completion: &'a Completion) -> (bool, bool, bool, bool, NameKind, bool, &'a Name) {
|
||||
(
|
||||
// This is only true when we are both in a `raise` context
|
||||
// *and* we know this suggestion is definitively usable
|
||||
// in a `raise` context. So we should sort these before
|
||||
// anything else.
|
||||
!completion.is_definitively_raisable,
|
||||
// When `None`, a completion is for something in the
|
||||
// current module, which we should generally prefer over
|
||||
// something from outside the module.
|
||||
completion.module_name.is_some(),
|
||||
// At time of writing (2025-11-11), keyword completions
|
||||
// are classified as builtins, which makes them sort after
|
||||
@@ -1549,6 +1652,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inside_token() {
|
||||
let test = completion_test_builder(
|
||||
"\
|
||||
foo_bar_baz = 1
|
||||
x = foo<CURSOR>bad
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(
|
||||
test.skip_builtins().build().snapshot(),
|
||||
@"foo_bar_baz",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_keyword_dedup() {
|
||||
let test = completion_test_builder(
|
||||
@@ -5263,6 +5381,45 @@ def foo(p<CURSOR>
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_function_param_keyword() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
def foo(in<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(
|
||||
builder.build().snapshot(),
|
||||
@"<No completions found>",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_function_param_multi_keyword() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
def foo(param, in<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(
|
||||
builder.build().snapshot(),
|
||||
@"<No completions found>",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_function_param_multi_keyword_middle() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
def foo(param, in<CURSOR>, param_two
|
||||
",
|
||||
);
|
||||
assert_snapshot!(
|
||||
builder.build().snapshot(),
|
||||
@"<No completions found>",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completions_in_function_type_param() {
|
||||
let builder = completion_test_builder(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -61,6 +61,7 @@ pub(crate) enum GotoTarget<'a> {
|
||||
/// ```
|
||||
ImportModuleComponent {
|
||||
module_name: String,
|
||||
level: u32,
|
||||
component_index: usize,
|
||||
component_range: TextRange,
|
||||
},
|
||||
@@ -189,6 +190,24 @@ 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`
|
||||
@@ -226,7 +245,7 @@ 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) => {
|
||||
@@ -242,7 +261,7 @@ 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) => {
|
||||
@@ -302,26 +321,51 @@ 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)
|
||||
@@ -333,7 +377,7 @@ impl GotoTarget<'_> {
|
||||
model: &SemanticModel,
|
||||
) -> Option<String> {
|
||||
if let GotoTarget::Call { call, .. } = self {
|
||||
call_type_simplified_by_overloads(model.db(), model, call)
|
||||
call_type_simplified_by_overloads(model, call)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -353,37 +397,25 @@ 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)
|
||||
GotoTarget::Expression(expression) => definitions_for_expression(model, *expression)
|
||||
.map(DefinitionsOrTargets::Definitions),
|
||||
|
||||
// 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(DefinitionsOrTargets::Definitions(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(DefinitionsOrTargets::Definitions(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(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(parameter.definition(model)),
|
||||
])),
|
||||
|
||||
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
|
||||
GotoTarget::ImportSymbolAlias {
|
||||
@@ -392,8 +424,7 @@ impl GotoTarget<'_> {
|
||||
let symbol_name = alias.name.as_str();
|
||||
Some(DefinitionsOrTargets::Definitions(
|
||||
definitions_for_imported_symbol(
|
||||
db,
|
||||
file,
|
||||
model,
|
||||
import_from,
|
||||
symbol_name,
|
||||
alias_resolution,
|
||||
@@ -404,29 +435,23 @@ impl GotoTarget<'_> {
|
||||
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,
|
||||
file: model.file(),
|
||||
focus_range: alias_range,
|
||||
full_range: alias.range(),
|
||||
}),
|
||||
@@ -439,14 +464,13 @@ impl GotoTarget<'_> {
|
||||
keyword,
|
||||
call_expression,
|
||||
} => Some(DefinitionsOrTargets::Definitions(
|
||||
definitions_for_keyword_argument(db, file, keyword, call_expression),
|
||||
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)),
|
||||
ResolvedDefinition::Definition(except_handler.definition(model)),
|
||||
]))
|
||||
}
|
||||
|
||||
@@ -455,7 +479,10 @@ impl GotoTarget<'_> {
|
||||
if let Some(rest_name) = &pattern_mapping.rest {
|
||||
let range = rest_name.range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
|
||||
crate::NavigationTargets::single(NavigationTarget::new(
|
||||
model.file(),
|
||||
range,
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
@@ -467,7 +494,10 @@ impl GotoTarget<'_> {
|
||||
if let Some(name) = &pattern_as.name {
|
||||
let range = name.range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
|
||||
crate::NavigationTargets::single(NavigationTarget::new(
|
||||
model.file(),
|
||||
range,
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
@@ -478,9 +508,9 @@ impl GotoTarget<'_> {
|
||||
//
|
||||
// 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() {
|
||||
@@ -491,23 +521,46 @@ impl GotoTarget<'_> {
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
_ => 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)
|
||||
.map(DefinitionsOrTargets::Definitions)
|
||||
}
|
||||
GotoTarget::NonLocal { identifier } | GotoTarget::Globals { identifier } => {
|
||||
Some(DefinitionsOrTargets::Definitions(definitions_for_name(
|
||||
model,
|
||||
identifier.as_str(),
|
||||
AnyNodeRef::Identifier(identifier),
|
||||
)))
|
||||
}
|
||||
|
||||
// TODO: implement these
|
||||
GotoTarget::PatternKeywordArgument(..)
|
||||
| GotoTarget::PatternMatchStarName(..)
|
||||
| GotoTarget::TypeParamTypeVarName(..)
|
||||
| GotoTarget::TypeParamParamSpecName(..)
|
||||
| GotoTarget::TypeParamTypeVarTupleName(..) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +579,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())),
|
||||
@@ -586,6 +640,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,
|
||||
@@ -632,6 +687,7 @@ impl GotoTarget<'_> {
|
||||
{
|
||||
return Some(GotoTarget::ImportModuleComponent {
|
||||
module_name: full_name.to_string(),
|
||||
level: 0,
|
||||
component_index,
|
||||
component_range,
|
||||
});
|
||||
@@ -672,14 +728,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,
|
||||
});
|
||||
@@ -786,6 +840,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();
|
||||
@@ -821,6 +900,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,
|
||||
@@ -840,9 +920,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()
|
||||
@@ -876,27 +956,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))
|
||||
@@ -905,7 +986,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> {
|
||||
@@ -920,12 +1001,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
|
||||
@@ -946,30 +1036,26 @@ 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,
|
||||
model: &SemanticModel<'db>,
|
||||
module: Option<&str>,
|
||||
level: u32,
|
||||
) -> 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
|
||||
let module = model.resolve_module(module, level)?;
|
||||
let file = module.file(model.db())?;
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Module(file),
|
||||
]))
|
||||
}
|
||||
|
||||
/// Helper function to extract module component information from a dotted module name
|
||||
@@ -983,9 +1069,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();
|
||||
|
||||
@@ -1004,3 +1088,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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 references to a symbol at the given position.
|
||||
/// Search for references across all files in the project.
|
||||
@@ -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
|
||||
@@ -147,7 +149,6 @@ result = calculate_sum(value=42)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // TODO: Enable when nonlocal support is fully implemented in goto.rs
|
||||
fn test_nonlocal_variable_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
@@ -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,14 +265,13 @@ 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() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
@@ -710,6 +710,194 @@ cls = MyClass
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 test_multi_file_function_references() {
|
||||
let test = CursorTest::builder()
|
||||
|
||||
@@ -11,9 +11,9 @@ pub fn goto_type_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 ty = goto_target.inferred_type(&model)?;
|
||||
|
||||
tracing::debug!("Inferred type of covering node is {}", ty.display(db));
|
||||
@@ -285,6 +285,300 @@ mod tests {
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_import_module() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
import l<CURSOR>ib
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib.py:1:1
|
||||
|
|
||||
1 | a = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:8
|
||||
|
|
||||
2 | import lib
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_import_module_multi1() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
import li<CURSOR>b.submod
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib/__init__.py", "b = 7").unwrap();
|
||||
test.write_file("lib/submod.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib/__init__.py:1:1
|
||||
|
|
||||
1 | b = 7
|
||||
| ^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:8
|
||||
|
|
||||
2 | import lib.submod
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_import_module_multi2() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
import lib.subm<CURSOR>od
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib/__init__.py", "b = 7").unwrap();
|
||||
test.write_file("lib/submod.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib/submod.py:1:1
|
||||
|
|
||||
1 | a = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:12
|
||||
|
|
||||
2 | import lib.submod
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_from_import_module() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
from l<CURSOR>ib import a
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib.py:1:1
|
||||
|
|
||||
1 | a = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:6
|
||||
|
|
||||
2 | from lib import a
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_from_import_module_multi1() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
from li<CURSOR>b.submod import a
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib/__init__.py", "b = 7").unwrap();
|
||||
test.write_file("lib/submod.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib/__init__.py:1:1
|
||||
|
|
||||
1 | b = 7
|
||||
| ^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:6
|
||||
|
|
||||
2 | from lib.submod import a
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_from_import_module_multi2() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
from lib.subm<CURSOR>od import a
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib/__init__.py", "b = 7").unwrap();
|
||||
test.write_file("lib/submod.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib/submod.py:1:1
|
||||
|
|
||||
1 | a = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:10
|
||||
|
|
||||
2 | from lib.submod import a
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_from_import_rel1() {
|
||||
let mut test = CursorTest::builder()
|
||||
.source(
|
||||
"lib/sub/__init__.py",
|
||||
r#"
|
||||
from .bot.bot<CURSOR>mod import *
|
||||
sub = 2
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
test.write_file("lib/__init__.py", "lib = 1").unwrap();
|
||||
// test.write_file("lib/sub/__init__.py", "sub = 2").unwrap();
|
||||
test.write_file("lib/sub/bot/__init__.py", "bot = 3")
|
||||
.unwrap();
|
||||
test.write_file("lib/sub/submod.py", "submod = 21").unwrap();
|
||||
test.write_file("lib/sub/bot/botmod.py", "botmod = 31")
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib/sub/bot/botmod.py:1:1
|
||||
|
|
||||
1 | botmod = 31
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> lib/sub/__init__.py:2:11
|
||||
|
|
||||
2 | from .bot.botmod import *
|
||||
| ^^^^^^
|
||||
3 | sub = 2
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_from_import_rel2() {
|
||||
let mut test = CursorTest::builder()
|
||||
.source(
|
||||
"lib/sub/__init__.py",
|
||||
r#"
|
||||
from .bo<CURSOR>t.botmod import *
|
||||
sub = 2
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
test.write_file("lib/__init__.py", "lib = 1").unwrap();
|
||||
// test.write_file("lib/sub/__init__.py", "sub = 2").unwrap();
|
||||
test.write_file("lib/sub/bot/__init__.py", "bot = 3")
|
||||
.unwrap();
|
||||
test.write_file("lib/sub/submod.py", "submod = 21").unwrap();
|
||||
test.write_file("lib/sub/bot/botmod.py", "botmod = 31")
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib/sub/bot/__init__.py:1:1
|
||||
|
|
||||
1 | bot = 3
|
||||
| ^^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> lib/sub/__init__.py:2:7
|
||||
|
|
||||
2 | from .bot.botmod import *
|
||||
| ^^^
|
||||
3 | sub = 2
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_from_import_rel3() {
|
||||
let mut test = CursorTest::builder()
|
||||
.source(
|
||||
"lib/sub/__init__.py",
|
||||
r#"
|
||||
from .<CURSOR>bot.botmod import *
|
||||
sub = 2
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
test.write_file("lib/__init__.py", "lib = 1").unwrap();
|
||||
// test.write_file("lib/sub/__init__.py", "sub = 2").unwrap();
|
||||
test.write_file("lib/sub/bot/__init__.py", "bot = 3")
|
||||
.unwrap();
|
||||
test.write_file("lib/sub/submod.py", "submod = 21").unwrap();
|
||||
test.write_file("lib/sub/bot/botmod.py", "botmod = 31")
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib/sub/bot/__init__.py:1:1
|
||||
|
|
||||
1 | bot = 3
|
||||
| ^^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> lib/sub/__init__.py:2:7
|
||||
|
|
||||
2 | from .bot.botmod import *
|
||||
| ^^^
|
||||
3 | sub = 2
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_from_import_rel4() {
|
||||
let mut test = CursorTest::builder()
|
||||
.source(
|
||||
"lib/sub/__init__.py",
|
||||
r#"
|
||||
from .<CURSOR> import submod
|
||||
sub = 2
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
test.write_file("lib/__init__.py", "lib = 1").unwrap();
|
||||
// test.write_file("lib/sub/__init__.py", "sub = 2").unwrap();
|
||||
test.write_file("lib/sub/bot/__init__.py", "bot = 3")
|
||||
.unwrap();
|
||||
test.write_file("lib/sub/submod.py", "submod = 21").unwrap();
|
||||
test.write_file("lib/sub/bot/botmod.py", "botmod = 31")
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_module() {
|
||||
let mut test = cursor_test(
|
||||
@@ -450,6 +744,226 @@ mod tests {
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_string_annotation1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCla<CURSOR>ss" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> 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_type_string_annotation2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyCl<CURSOR>ass" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_string_annotation3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None |<CURSOR> MyClass" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:4:7
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| ^^^^^^^
|
||||
5 | """some docs"""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/types.pyi:950:11
|
||||
|
|
||||
948 | if sys.version_info >= (3, 10):
|
||||
949 | @final
|
||||
950 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
951 | """The type of the None singleton."""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_string_annotation4() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyClass<CURSOR>" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_string_annotation5() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyClass"<CURSOR> = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:4:7
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
| ^^^^^^^
|
||||
5 | """some docs"""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/types.pyi:950:11
|
||||
|
|
||||
948 | if sys.version_info >= (3, 10):
|
||||
949 | @final
|
||||
950 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
951 | """The type of the None singleton."""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_string_annotation_dangling1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCl<CURSOR>ass |" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
19 | # Types
|
||||
20 | Unknown = object()
|
||||
| ^^^^^^^
|
||||
21 | AlwaysTruthy = object()
|
||||
22 | AlwaysFalsy = object()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:4
|
||||
|
|
||||
2 | a: "MyClass |" = 1
|
||||
| ^^^^^^^^^^^
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_string_annotation_dangling2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCl<CURSOR>ass | No" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_string_annotation_dangling3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyClass | N<CURSOR>o" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_keyword_argument() {
|
||||
let test = cursor_test(
|
||||
@@ -548,6 +1062,118 @@ f(**kwargs<CURSOR>)
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_nonlocal_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def outer():
|
||||
x = "outer_value"
|
||||
|
||||
def inner():
|
||||
nonlocal x
|
||||
x = "modified"
|
||||
return x<CURSOR> # 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_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:915:7
|
||||
|
|
||||
914 | @disjoint_base
|
||||
915 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:8:16
|
||||
|
|
||||
6 | nonlocal x
|
||||
7 | x = "modified"
|
||||
8 | return x # Should find the nonlocal x declaration in outer scope
|
||||
| ^
|
||||
9 |
|
||||
10 | return inner
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_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_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_global_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
global_var = "global_value"
|
||||
|
||||
def function():
|
||||
global global_var
|
||||
global_var = "modified"
|
||||
return global_<CURSOR>var # Should find the global variable declaration
|
||||
"#,
|
||||
);
|
||||
|
||||
// Should find the global variable declaration, not the global statement
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:915:7
|
||||
|
|
||||
914 | @disjoint_base
|
||||
915 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:12
|
||||
|
|
||||
5 | global global_var
|
||||
6 | global_var = "modified"
|
||||
7 | return global_var # Should find the global variable declaration
|
||||
| ^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_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_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_builtin() {
|
||||
let test = cursor_test(
|
||||
|
||||
@@ -11,7 +11,8 @@ use ty_python_semantic::{DisplaySettings, SemanticModel};
|
||||
|
||||
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover<'_>>> {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
let goto_target = find_goto_target(&parsed, offset)?;
|
||||
let model = SemanticModel::new(db, file);
|
||||
let goto_target = find_goto_target(&model, &parsed, offset)?;
|
||||
|
||||
if let GotoTarget::Expression(expr) = goto_target {
|
||||
if expr.is_literal_expr() {
|
||||
@@ -19,11 +20,9 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
|
||||
}
|
||||
}
|
||||
|
||||
let model = SemanticModel::new(db, file);
|
||||
let docs = goto_target
|
||||
.get_definition_targets(
|
||||
file,
|
||||
db,
|
||||
&model,
|
||||
ty_python_semantic::ImportAliasResolution::ResolveAliases,
|
||||
)
|
||||
.and_then(|definitions| definitions.docstring(db))
|
||||
@@ -905,6 +904,191 @@ mod tests {
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_string_annotation1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCla<CURSOR>ss" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
MyClass
|
||||
---------------------------------------------
|
||||
some docs
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
MyClass
|
||||
```
|
||||
---
|
||||
some docs
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | a: "MyClass" = 1
|
||||
| ^^^^^-^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_string_annotation2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyCl<CURSOR>ass" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
some docs
|
||||
|
||||
---------------------------------------------
|
||||
some docs
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:12
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_string_annotation3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None |<CURSOR> MyClass" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_string_annotation4() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyClass<CURSOR>" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
some docs
|
||||
|
||||
---------------------------------------------
|
||||
some docs
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:12
|
||||
|
|
||||
2 | a: "None | MyClass" = 1
|
||||
| ^^^^^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_string_annotation5() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "None | MyClass"<CURSOR> = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_string_annotation_dangling1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCl<CURSOR>ass |" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_string_annotation_dangling2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyCl<CURSOR>ass | No" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
some docs
|
||||
|
||||
---------------------------------------------
|
||||
some docs
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | a: "MyClass | No" = 1
|
||||
| ^^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 |
|
||||
4 | class MyClass:
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_string_annotation_dangling3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: "MyClass | N<CURSOR>o" = 1
|
||||
|
||||
class MyClass:
|
||||
"""some docs"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_overload_type_disambiguated1() {
|
||||
let test = CursorTest::builder()
|
||||
@@ -1464,6 +1648,117 @@ def ab(a: int, *, c: int):
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_nonlocal_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def outer():
|
||||
x = "outer_value"
|
||||
|
||||
def inner():
|
||||
nonlocal x
|
||||
x = "modified"
|
||||
return x<CURSOR> # 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.hover(), @r#"
|
||||
Literal["modified"]
|
||||
---------------------------------------------
|
||||
```python
|
||||
Literal["modified"]
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:8:16
|
||||
|
|
||||
6 | nonlocal x
|
||||
7 | x = "modified"
|
||||
8 | return x # Should find the nonlocal x declaration in outer scope
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
9 |
|
||||
10 | return inner
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_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.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_global_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
global_var = "global_value"
|
||||
|
||||
def function():
|
||||
global global_var
|
||||
global_var = "modified"
|
||||
return global_<CURSOR>var # Should find the global variable declaration
|
||||
"#,
|
||||
);
|
||||
|
||||
// Should find the global variable declaration, not the global statement
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
Literal["modified"]
|
||||
---------------------------------------------
|
||||
```python
|
||||
Literal["modified"]
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:7:12
|
||||
|
|
||||
5 | global global_var
|
||||
6 | global_var = "modified"
|
||||
7 | return global_var # Should find the global variable declaration
|
||||
| ^^^^^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_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.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_module_import() {
|
||||
let mut test = cursor_test(
|
||||
@@ -1488,11 +1783,17 @@ def ab(a: int, *, c: int):
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'lib'>
|
||||
---------------------------------------------
|
||||
The cool lib_py module!
|
||||
|
||||
Wow this module rocks.
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
<module 'lib'>
|
||||
```
|
||||
---
|
||||
The cool lib/_py module!
|
||||
|
||||
Wow this module rocks.
|
||||
|
||||
@@ -6,8 +6,8 @@ use ruff_db::parsed::parsed_module;
|
||||
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword, Expr, ExprUnaryOp, Stmt, UnaryOp};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use ty_python_semantic::types::Type;
|
||||
use ty_python_semantic::types::ide_support::inlay_hint_call_argument_details;
|
||||
use ty_python_semantic::types::{Type, TypeDetail};
|
||||
use ty_python_semantic::{HasType, SemanticModel};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -15,10 +15,12 @@ pub struct InlayHint {
|
||||
pub position: TextSize,
|
||||
pub kind: InlayHintKind,
|
||||
pub label: InlayHintLabel,
|
||||
pub text_edits: Vec<InlayHintTextEdit>,
|
||||
}
|
||||
|
||||
impl InlayHint {
|
||||
fn variable_type(position: TextSize, ty: Type, db: &dyn Db) -> Self {
|
||||
fn variable_type(expr: &Expr, ty: Type, db: &dyn Db, allow_edits: bool) -> Self {
|
||||
let position = expr.range().end();
|
||||
// Render the type to a string, and get subspans for all the types that make it up
|
||||
let details = ty.display(db).to_string_parts();
|
||||
|
||||
@@ -34,7 +36,7 @@ impl InlayHint {
|
||||
let mut label_parts = vec![": ".into()];
|
||||
for (target, detail) in details.targets.iter().zip(&details.details) {
|
||||
match detail {
|
||||
ty_python_semantic::types::TypeDetail::Type(ty) => {
|
||||
TypeDetail::Type(ty) => {
|
||||
let start = target.start().to_usize();
|
||||
let end = target.end().to_usize();
|
||||
// If we skipped over some bytes, push them with no target
|
||||
@@ -50,9 +52,9 @@ impl InlayHint {
|
||||
offset = end;
|
||||
}
|
||||
}
|
||||
ty_python_semantic::types::TypeDetail::SignatureStart
|
||||
| ty_python_semantic::types::TypeDetail::SignatureEnd
|
||||
| ty_python_semantic::types::TypeDetail::Parameter(_) => {
|
||||
TypeDetail::SignatureStart
|
||||
| TypeDetail::SignatureEnd
|
||||
| TypeDetail::Parameter(_) => {
|
||||
// Don't care about these
|
||||
}
|
||||
}
|
||||
@@ -62,10 +64,20 @@ impl InlayHint {
|
||||
label_parts.push(details.label[offset..details.label.len()].into());
|
||||
}
|
||||
|
||||
let text_edits = if details.is_valid_syntax && allow_edits {
|
||||
vec![InlayHintTextEdit {
|
||||
range: TextRange::new(position, position),
|
||||
new_text: format!(": {}", details.label),
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Self {
|
||||
position,
|
||||
kind: InlayHintKind::Type,
|
||||
label: InlayHintLabel { parts: label_parts },
|
||||
text_edits,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +95,7 @@ impl InlayHint {
|
||||
position,
|
||||
kind: InlayHintKind::CallArgumentName,
|
||||
label: InlayHintLabel { parts: label_parts },
|
||||
text_edits: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +188,12 @@ impl From<&str> for InlayHintLabelPart {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InlayHintTextEdit {
|
||||
pub range: TextRange,
|
||||
pub new_text: String,
|
||||
}
|
||||
|
||||
pub fn inlay_hints(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
@@ -234,6 +253,7 @@ struct InlayHintVisitor<'a, 'db> {
|
||||
in_assignment: bool,
|
||||
range: TextRange,
|
||||
settings: &'a InlayHintSettings,
|
||||
in_no_edits_allowed: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'db> InlayHintVisitor<'a, 'db> {
|
||||
@@ -245,15 +265,16 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
|
||||
in_assignment: false,
|
||||
range,
|
||||
settings,
|
||||
in_no_edits_allowed: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) {
|
||||
fn add_type_hint(&mut self, expr: &Expr, ty: Type<'db>, allow_edits: bool) {
|
||||
if !self.settings.variable_types {
|
||||
return;
|
||||
}
|
||||
|
||||
let inlay_hint = InlayHint::variable_type(position, ty, self.db);
|
||||
let inlay_hint = InlayHint::variable_type(expr, ty, self.db, allow_edits);
|
||||
|
||||
self.hints.push(inlay_hint);
|
||||
}
|
||||
@@ -297,9 +318,13 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||
match stmt {
|
||||
Stmt::Assign(assign) => {
|
||||
self.in_assignment = !type_hint_is_excessive_for_expr(&assign.value);
|
||||
if !annotations_are_valid_syntax(assign) {
|
||||
self.in_no_edits_allowed = true;
|
||||
}
|
||||
for target in &assign.targets {
|
||||
self.visit_expr(target);
|
||||
}
|
||||
self.in_no_edits_allowed = false;
|
||||
self.in_assignment = false;
|
||||
|
||||
self.visit_expr(&assign.value);
|
||||
@@ -325,7 +350,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||
if self.in_assignment {
|
||||
if name.ctx.is_store() {
|
||||
let ty = expr.inferred_type(&self.model);
|
||||
self.add_type_hint(expr.range().end(), ty);
|
||||
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
|
||||
}
|
||||
}
|
||||
source_order::walk_expr(self, expr);
|
||||
@@ -334,7 +359,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||
if self.in_assignment {
|
||||
if attribute.ctx.is_store() {
|
||||
let ty = expr.inferred_type(&self.model);
|
||||
self.add_type_hint(expr.range().end(), ty);
|
||||
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
|
||||
}
|
||||
}
|
||||
source_order::walk_expr(self, expr);
|
||||
@@ -420,6 +445,22 @@ fn type_hint_is_excessive_for_expr(expr: &Expr) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn annotations_are_valid_syntax(stmt_assign: &ruff_python_ast::StmtAssign) -> bool {
|
||||
if stmt_assign.targets.len() > 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if stmt_assign
|
||||
.targets
|
||||
.iter()
|
||||
.any(|target| matches!(target, Expr::Tuple(_)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -427,6 +468,7 @@ mod tests {
|
||||
use crate::NavigationTarget;
|
||||
use crate::tests::IntoDiagnostic;
|
||||
use insta::{assert_snapshot, internals::SettingsBindDropGuard};
|
||||
use itertools::Itertools;
|
||||
use ruff_db::{
|
||||
diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig,
|
||||
@@ -517,12 +559,15 @@ mod tests {
|
||||
fn inlay_hints_with_settings(&mut self, settings: &InlayHintSettings) -> String {
|
||||
let hints = inlay_hints(&self.db, self.file, self.range, settings);
|
||||
|
||||
let mut buf = source_text(&self.db, self.file).as_str().to_string();
|
||||
let mut inlay_hint_buf = source_text(&self.db, self.file).as_str().to_string();
|
||||
let mut text_edit_buf = inlay_hint_buf.clone();
|
||||
|
||||
let mut tbd_diagnostics = Vec::new();
|
||||
|
||||
let mut offset = 0;
|
||||
|
||||
let mut edit_offset = 0;
|
||||
|
||||
for hint in hints {
|
||||
let end_position = hint.position.to_usize() + offset;
|
||||
let mut hint_str = "[".to_string();
|
||||
@@ -538,36 +583,65 @@ mod tests {
|
||||
hint_str.push_str(part.text());
|
||||
}
|
||||
|
||||
for edit in hint.text_edits {
|
||||
let start = edit.range.start().to_usize() + edit_offset;
|
||||
let end = edit.range.end().to_usize() + edit_offset;
|
||||
|
||||
text_edit_buf.replace_range(start..end, &edit.new_text);
|
||||
|
||||
if start == end {
|
||||
edit_offset += edit.new_text.len();
|
||||
} else {
|
||||
edit_offset += edit.new_text.len() - edit.range.len().to_usize();
|
||||
}
|
||||
}
|
||||
|
||||
hint_str.push(']');
|
||||
offset += hint_str.len();
|
||||
|
||||
buf.insert_str(end_position, &hint_str);
|
||||
inlay_hint_buf.insert_str(end_position, &hint_str);
|
||||
}
|
||||
|
||||
self.db.write_file("main2.py", &buf).unwrap();
|
||||
self.db.write_file("main2.py", &inlay_hint_buf).unwrap();
|
||||
let inlayed_file =
|
||||
system_path_to_file(&self.db, "main2.py").expect("newly written file to existing");
|
||||
|
||||
let diagnostics = tbd_diagnostics.into_iter().map(|(label_range, target)| {
|
||||
let location_diagnostics = tbd_diagnostics.into_iter().map(|(label_range, target)| {
|
||||
InlayHintLocationDiagnostic::new(FileRange::new(inlayed_file, label_range), &target)
|
||||
});
|
||||
|
||||
let mut rendered_diagnostics = self.render_diagnostics(diagnostics);
|
||||
let mut rendered_diagnostics = location_diagnostics
|
||||
.map(|diagnostic| self.render_diagnostic(diagnostic))
|
||||
.join("");
|
||||
|
||||
if !rendered_diagnostics.is_empty() {
|
||||
rendered_diagnostics = format!(
|
||||
"{}{}",
|
||||
crate::MarkupKind::PlainText.horizontal_line(),
|
||||
rendered_diagnostics
|
||||
.strip_suffix("\n")
|
||||
.unwrap_or(&rendered_diagnostics)
|
||||
);
|
||||
}
|
||||
|
||||
format!("{buf}{rendered_diagnostics}",)
|
||||
let rendered_edit_diagnostic = if edit_offset != 0 {
|
||||
let edit_diagnostic = InlayHintEditDiagnostic::new(text_edit_buf);
|
||||
let text_edit_buf = self.render_diagnostic(edit_diagnostic);
|
||||
|
||||
format!(
|
||||
"{}{}",
|
||||
crate::MarkupKind::PlainText.horizontal_line(),
|
||||
text_edit_buf
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
format!("{inlay_hint_buf}{rendered_diagnostics}{rendered_edit_diagnostic}",)
|
||||
}
|
||||
|
||||
fn render_diagnostics<I, D>(&self, diagnostics: I) -> String
|
||||
fn render_diagnostic<D>(&self, diagnostic: D) -> String
|
||||
where
|
||||
I: IntoIterator<Item = D>,
|
||||
D: IntoDiagnostic,
|
||||
{
|
||||
use std::fmt::Write;
|
||||
@@ -578,10 +652,8 @@ mod tests {
|
||||
.color(false)
|
||||
.format(DiagnosticFormat::Full);
|
||||
|
||||
for diagnostic in diagnostics {
|
||||
let diag = diagnostic.into_diagnostic();
|
||||
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
|
||||
}
|
||||
let diag = diagnostic.into_diagnostic();
|
||||
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
|
||||
|
||||
buf
|
||||
}
|
||||
@@ -728,6 +800,20 @@ mod tests {
|
||||
10 | bb[: Literal[b"foo"]] = aa
|
||||
| ^^^^^^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def i(x: int, /) -> int:
|
||||
return x
|
||||
|
||||
x = 1
|
||||
y: Literal[1] = x
|
||||
z: int = i(1)
|
||||
w: int = z
|
||||
aa = b'foo'
|
||||
bb: Literal[b"foo"] = aa
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -1321,6 +1407,20 @@ mod tests {
|
||||
10 | w[: tuple[int, str]] = z
|
||||
| ^^^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def i(x: int, /) -> int:
|
||||
return x
|
||||
def s(x: str, /) -> str:
|
||||
return x
|
||||
|
||||
x = (1, 'abc')
|
||||
y: tuple[Literal[1], Literal["abc"]] = x
|
||||
z: tuple[int, str] = (i(1), s('abc'))
|
||||
w: tuple[int, str] = z
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -1654,6 +1754,18 @@ mod tests {
|
||||
8 | w[: int] = z
|
||||
| ^^^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def i(x: int, /) -> int:
|
||||
return x
|
||||
|
||||
x: int = 1
|
||||
y: Literal[1] = x
|
||||
z: int = i(1)
|
||||
w: int = z
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -1691,6 +1803,15 @@ mod tests {
|
||||
| ^^^
|
||||
5 | z = x
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def i(x: int, /) -> int:
|
||||
return x
|
||||
x: int = i(1)
|
||||
z = x
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -1810,6 +1931,18 @@ mod tests {
|
||||
8 | a.y[: int] = int(3)
|
||||
| ^^^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
class A:
|
||||
def __init__(self, y):
|
||||
self.x: int = int(1)
|
||||
self.y: Unknown = y
|
||||
|
||||
a: A = A(2)
|
||||
a.y: int = int(3)
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -2640,6 +2773,22 @@ mod tests {
|
||||
12 | k[: list[Unknown | int | float]] = [-1, -2.0]
|
||||
| ^^^^^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
a: list[Unknown | int] = [1, 2]
|
||||
b: list[Unknown | float] = [1.0, 2.0]
|
||||
c: list[Unknown | bool] = [True, False]
|
||||
d: list[Unknown | None] = [None, None]
|
||||
e: list[Unknown | str] = ["hel", "lo"]
|
||||
f: list[Unknown | str] = ['the', 're']
|
||||
g: list[Unknown | str] = [f"{ft}", f"{ft}"]
|
||||
h: list[Unknown | Template] = [t"wow %d", t"wow %d"]
|
||||
i: list[Unknown | bytes] = [b'/x01', b'/x02']
|
||||
j: list[Unknown | int | float] = [+1, +2.0]
|
||||
k: list[Unknown | int | float] = [-1, -2.0]
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -2811,6 +2960,19 @@ mod tests {
|
||||
9 | c[: MyClass], d[: MyClass] = (MyClass(), MyClass())
|
||||
| ^^^^^^^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
|
||||
x: MyClass = MyClass()
|
||||
y: tuple[MyClass, MyClass] = (MyClass(), MyClass())
|
||||
a, b = MyClass(), MyClass()
|
||||
c, d = (MyClass(), MyClass())
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -3681,6 +3843,20 @@ mod tests {
|
||||
10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "…
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
class MyClass[T, U]:
|
||||
def __init__(self, x: list[T], y: tuple[U, U]):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
x: MyClass[Unknown | int, str] = MyClass([42], ("a", "b"))
|
||||
y: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]] = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b")))
|
||||
a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))
|
||||
c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b")))
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -3836,6 +4012,20 @@ mod tests {
|
||||
10 | foo([x=]val.y)
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def foo(x: int): pass
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
self.y: int = 2
|
||||
val: MyClass = MyClass()
|
||||
|
||||
foo(val.x)
|
||||
foo(val.y)
|
||||
");
|
||||
}
|
||||
|
||||
@@ -3901,6 +4091,20 @@ mod tests {
|
||||
10 | foo([x=]x.y)
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def foo(x: int): pass
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
self.y: int = 2
|
||||
x: MyClass = MyClass()
|
||||
|
||||
foo(x.x)
|
||||
foo(x.y)
|
||||
");
|
||||
}
|
||||
|
||||
@@ -3969,6 +4173,22 @@ mod tests {
|
||||
12 | foo([x=]val.y())
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def foo(x: int): pass
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
def x() -> int:
|
||||
return 1
|
||||
def y() -> int:
|
||||
return 2
|
||||
val: MyClass = MyClass()
|
||||
|
||||
foo(val.x())
|
||||
foo(val.y())
|
||||
");
|
||||
}
|
||||
|
||||
@@ -4043,6 +4263,24 @@ mod tests {
|
||||
14 | foo([x=]val.y()[1])
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
from typing import List
|
||||
|
||||
def foo(x: int): pass
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
def x() -> List[int]:
|
||||
return 1
|
||||
def y() -> List[int]:
|
||||
return 2
|
||||
val: MyClass = MyClass()
|
||||
|
||||
foo(val.x()[0])
|
||||
foo(val.y()[1])
|
||||
");
|
||||
}
|
||||
|
||||
@@ -4193,6 +4431,17 @@ mod tests {
|
||||
7 | foo([x=]y[0])
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def foo(x: int): pass
|
||||
x: list[Unknown | int] = [1]
|
||||
y: list[Unknown | int] = [2]
|
||||
|
||||
foo(x[0])
|
||||
foo(y[0])
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -4378,6 +4627,15 @@ mod tests {
|
||||
5 | f[: Foo] = Foo([x=]1)
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
class Foo:
|
||||
def __init__(self, x: int): pass
|
||||
Foo(1)
|
||||
f: Foo = Foo(1)
|
||||
");
|
||||
}
|
||||
|
||||
@@ -4450,6 +4708,15 @@ mod tests {
|
||||
5 | f[: Foo] = Foo([x=]1)
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
class Foo:
|
||||
def __new__(cls, x: int): pass
|
||||
Foo(1)
|
||||
f: Foo = Foo(1)
|
||||
");
|
||||
}
|
||||
|
||||
@@ -5197,6 +5464,15 @@ mod tests {
|
||||
| ^^^^^^^^^^^^^
|
||||
5 | my_func(x="hello")
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
from typing import LiteralString
|
||||
def my_func(x: LiteralString):
|
||||
y: LiteralString = x
|
||||
my_func(x="hello")
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -5339,6 +5615,23 @@ mod tests {
|
||||
13 | y[: Literal[1, 2, 3, "hello"] | None] = x
|
||||
| ^^^^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def branch(cond: int):
|
||||
if cond < 10:
|
||||
x = 1
|
||||
elif cond < 20:
|
||||
x = 2
|
||||
elif cond < 30:
|
||||
x = 3
|
||||
elif cond < 40:
|
||||
x = "hello"
|
||||
else:
|
||||
x = None
|
||||
y: Literal[1, 2, 3, "hello"] | None = x
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -5454,6 +5747,13 @@ mod tests {
|
||||
3 | y[: type[list[str]]] = type(x)
|
||||
| ^^^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def f(x: list[str]):
|
||||
y: type[list[str]] = type(x)
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -5493,6 +5793,16 @@ mod tests {
|
||||
6 | ab[: property] = F.whatever
|
||||
| ^^^^^^^^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
class F:
|
||||
@property
|
||||
def whatever(self): ...
|
||||
|
||||
ab: property = F.whatever
|
||||
");
|
||||
}
|
||||
|
||||
@@ -5810,6 +6120,196 @@ mod tests {
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_signature_inlay_hint() {
|
||||
let mut test = inlay_hint_test(
|
||||
"
|
||||
def foo(x: int, *y: bool, z: str | int | list[str]): ...
|
||||
|
||||
a = foo",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r#"
|
||||
def foo(x: int, *y: bool, z: str | int | list[str]): ...
|
||||
|
||||
a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/builtins.pyi:348:7
|
||||
|
|
||||
347 | @disjoint_base
|
||||
348 | class int:
|
||||
| ^^^
|
||||
349 | """int([x]) -> integer
|
||||
350 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:16
|
||||
|
|
||||
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
|
||||
3 |
|
||||
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
|
||||
| ^^^
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/builtins.pyi:2591:7
|
||||
|
|
||||
2590 | @final
|
||||
2591 | class bool(int):
|
||||
| ^^^^
|
||||
2592 | """Returns True when the argument is true, False otherwise.
|
||||
2593 | The builtins True and False are the only two instances of the class bool.
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:25
|
||||
|
|
||||
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
|
||||
3 |
|
||||
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
|
||||
| ^^^^
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/builtins.pyi:915:7
|
||||
|
|
||||
914 | @disjoint_base
|
||||
915 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:37
|
||||
|
|
||||
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
|
||||
3 |
|
||||
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
|
||||
| ^^^
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/builtins.pyi:348:7
|
||||
|
|
||||
347 | @disjoint_base
|
||||
348 | class int:
|
||||
| ^^^
|
||||
349 | """int([x]) -> integer
|
||||
350 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:43
|
||||
|
|
||||
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
|
||||
3 |
|
||||
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
|
||||
| ^^^
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/builtins.pyi:2802:7
|
||||
|
|
||||
2801 | @disjoint_base
|
||||
2802 | class list(MutableSequence[_T]):
|
||||
| ^^^^
|
||||
2803 | """Built-in mutable sequence.
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:49
|
||||
|
|
||||
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
|
||||
3 |
|
||||
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
|
||||
| ^^^^
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/builtins.pyi:915:7
|
||||
|
|
||||
914 | @disjoint_base
|
||||
915 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:54
|
||||
|
|
||||
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
|
||||
3 |
|
||||
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
|
||||
| ^^^
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
19 | # Types
|
||||
20 | Unknown = object()
|
||||
| ^^^^^^^
|
||||
21 | AlwaysTruthy = object()
|
||||
22 | AlwaysFalsy = object()
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:63
|
||||
|
|
||||
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
|
||||
3 |
|
||||
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
|
||||
| ^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_inlay_hint() {
|
||||
let mut test = inlay_hint_test(
|
||||
"
|
||||
import foo
|
||||
|
||||
a = foo",
|
||||
);
|
||||
|
||||
test.with_extra_file("foo.py", "'''Foo module'''");
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r"
|
||||
import foo
|
||||
|
||||
a[: <module 'foo'>] = foo
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> foo.py:1:1
|
||||
|
|
||||
1 | '''Foo module'''
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:5
|
||||
|
|
||||
2 | import foo
|
||||
3 |
|
||||
4 | a[: <module 'foo'>] = foo
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_literal_type_alias_inlay_hint() {
|
||||
let mut test = inlay_hint_test(
|
||||
"
|
||||
from typing import Literal
|
||||
|
||||
a = Literal['a', 'b', 'c']",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r"
|
||||
from typing import Literal
|
||||
|
||||
a[: <typing.Literal special form>] = Literal['a', 'b', 'c']
|
||||
");
|
||||
}
|
||||
|
||||
struct InlayHintLocationDiagnostic {
|
||||
source: FileRange,
|
||||
target: FileRange,
|
||||
@@ -5847,4 +6347,31 @@ mod tests {
|
||||
main
|
||||
}
|
||||
}
|
||||
|
||||
struct InlayHintEditDiagnostic {
|
||||
file_content: String,
|
||||
}
|
||||
|
||||
impl InlayHintEditDiagnostic {
|
||||
fn new(file_content: String) -> Self {
|
||||
Self { file_content }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for InlayHintEditDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut main = Diagnostic::new(
|
||||
DiagnosticId::Lint(LintName::of("inlay-hint-edit")),
|
||||
Severity::Info,
|
||||
"File after edits".to_string(),
|
||||
);
|
||||
|
||||
main.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
format!("{}\n{}", "Source", self.file_content),
|
||||
));
|
||||
|
||||
main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ pub use document_symbols::document_symbols;
|
||||
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>,
|
||||
@@ -226,6 +226,22 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> {
|
||||
self.check_identifier_reference(rest_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 +300,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 +315,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,162 @@ 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 test_cannot_rename_import_module_component() {
|
||||
// Test that we cannot rename parts of module names in import statements
|
||||
|
||||
@@ -187,9 +187,9 @@ impl Deref for SemanticTokens {
|
||||
/// Pass None to get tokens for the entire file.
|
||||
pub fn semantic_tokens(db: &dyn Db, file: File, range: Option<TextRange>) -> SemanticTokens {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
let semantic_model = SemanticModel::new(db, file);
|
||||
let model = SemanticModel::new(db, file);
|
||||
|
||||
let mut visitor = SemanticTokenVisitor::new(&semantic_model, file, range);
|
||||
let mut visitor = SemanticTokenVisitor::new(&model, range);
|
||||
visitor.visit_body(parsed.suite());
|
||||
|
||||
SemanticTokens::new(visitor.tokens)
|
||||
@@ -197,8 +197,7 @@ pub fn semantic_tokens(db: &dyn Db, file: File, range: Option<TextRange>) -> Sem
|
||||
|
||||
/// AST visitor that collects semantic tokens.
|
||||
struct SemanticTokenVisitor<'db> {
|
||||
semantic_model: &'db SemanticModel<'db>,
|
||||
file: File,
|
||||
model: &'db SemanticModel<'db>,
|
||||
tokens: Vec<SemanticToken>,
|
||||
in_class_scope: bool,
|
||||
in_type_annotation: bool,
|
||||
@@ -207,14 +206,9 @@ struct SemanticTokenVisitor<'db> {
|
||||
}
|
||||
|
||||
impl<'db> SemanticTokenVisitor<'db> {
|
||||
fn new(
|
||||
semantic_model: &'db SemanticModel<'db>,
|
||||
file: File,
|
||||
range_filter: Option<TextRange>,
|
||||
) -> Self {
|
||||
fn new(model: &'db SemanticModel<'db>, range_filter: Option<TextRange>) -> Self {
|
||||
Self {
|
||||
semantic_model,
|
||||
file,
|
||||
model,
|
||||
tokens: Vec::new(),
|
||||
in_class_scope: false,
|
||||
in_target_creating_definition: false,
|
||||
@@ -265,7 +259,7 @@ impl<'db> SemanticTokenVisitor<'db> {
|
||||
|
||||
fn classify_name(&self, name: &ast::ExprName) -> (SemanticTokenType, SemanticTokenModifier) {
|
||||
// First try to classify the token based on its definition kind.
|
||||
let definition = definition_for_name(self.semantic_model.db(), self.file, name);
|
||||
let definition = definition_for_name(self.model, name);
|
||||
|
||||
if let Some(definition) = definition {
|
||||
let name_str = name.id.as_str();
|
||||
@@ -275,7 +269,7 @@ impl<'db> SemanticTokenVisitor<'db> {
|
||||
}
|
||||
|
||||
// Fall back to type-based classification.
|
||||
let ty = name.inferred_type(self.semantic_model);
|
||||
let ty = name.inferred_type(self.model);
|
||||
let name_str = name.id.as_str();
|
||||
self.classify_from_type_and_name_str(ty, name_str)
|
||||
}
|
||||
@@ -286,7 +280,7 @@ impl<'db> SemanticTokenVisitor<'db> {
|
||||
name_str: &str,
|
||||
) -> Option<(SemanticTokenType, SemanticTokenModifier)> {
|
||||
let mut modifiers = SemanticTokenModifier::empty();
|
||||
let db = self.semantic_model.db();
|
||||
let db = self.model.db();
|
||||
let model = SemanticModel::new(db, definition.file(db));
|
||||
|
||||
match definition.kind(db) {
|
||||
@@ -712,12 +706,12 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
|
||||
for alias in &import.names {
|
||||
if let Some(asname) = &alias.asname {
|
||||
// For aliased imports (from X import Y as Z), classify Z based on what Y is
|
||||
let ty = alias.inferred_type(self.semantic_model);
|
||||
let ty = alias.inferred_type(self.model);
|
||||
let (token_type, modifiers) = self.classify_from_alias_type(ty, asname);
|
||||
self.add_token(asname, token_type, modifiers);
|
||||
} else {
|
||||
// For direct imports (from X import Y), use semantic classification
|
||||
let ty = alias.inferred_type(self.semantic_model);
|
||||
let ty = alias.inferred_type(self.model);
|
||||
let (token_type, modifiers) =
|
||||
self.classify_from_alias_type(ty, &alias.name);
|
||||
self.add_token(&alias.name, token_type, modifiers);
|
||||
@@ -837,7 +831,7 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
|
||||
self.visit_expr(&attr.value);
|
||||
|
||||
// Then add token for the attribute name (e.g., 'path' in 'os.path')
|
||||
let ty = expr.inferred_type(self.semantic_model);
|
||||
let ty = expr.inferred_type(self.model);
|
||||
let (token_type, modifiers) =
|
||||
Self::classify_from_type_for_attribute(ty, &attr.attr);
|
||||
self.add_token(&attr.attr, token_type, modifiers);
|
||||
@@ -881,6 +875,17 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
|
||||
|
||||
self.visit_expr(&named.value);
|
||||
}
|
||||
ast::Expr::StringLiteral(string_expr) => {
|
||||
// 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_visitor = SemanticTokenVisitor::new(&sub_model, None);
|
||||
sub_visitor.visit_expr(sub_ast.expr());
|
||||
self.tokens.extend(sub_visitor.tokens);
|
||||
} else {
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// For all other expression types, let the default visitor handle them
|
||||
walk_expr(self, expr);
|
||||
@@ -1061,6 +1066,17 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_comprehension(&mut self, comp: &ast::Comprehension) {
|
||||
self.in_target_creating_definition = true;
|
||||
self.visit_expr(&comp.target);
|
||||
self.in_target_creating_definition = false;
|
||||
|
||||
self.visit_expr(&comp.iter);
|
||||
for if_clause in &comp.ifs {
|
||||
self.visit_expr(if_clause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1564,6 +1580,50 @@ from mymodule import CONSTANT, my_function, MyClass
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_str_annotation() {
|
||||
let test = SemanticTokenTest::new(
|
||||
r#"
|
||||
x: int = 1
|
||||
y: "int" = 1
|
||||
z = "int"
|
||||
w1: "int | str" = "hello"
|
||||
w2: "int | sr" = "hello"
|
||||
w3: "int | " = "hello"
|
||||
w4: "float"
|
||||
w5: "float
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = test.highlight_file();
|
||||
|
||||
assert_snapshot!(test.to_snapshot(&tokens), @r#"
|
||||
"x" @ 1..2: Variable [definition]
|
||||
"int" @ 4..7: Class
|
||||
"1" @ 10..11: Number
|
||||
"y" @ 12..13: Variable [definition]
|
||||
"int" @ 16..19: Class
|
||||
"1" @ 23..24: Number
|
||||
"z" @ 25..26: Variable [definition]
|
||||
"\"int\"" @ 29..34: String
|
||||
"w1" @ 35..37: Variable [definition]
|
||||
"int" @ 40..43: Class
|
||||
"str" @ 46..49: Class
|
||||
"\"hello\"" @ 53..60: String
|
||||
"w2" @ 61..63: Variable [definition]
|
||||
"int" @ 66..69: Class
|
||||
"sr" @ 72..74: Variable
|
||||
"\"hello\"" @ 78..85: String
|
||||
"w3" @ 86..88: Variable [definition]
|
||||
"\"int | \"" @ 90..98: String
|
||||
"\"hello\"" @ 101..108: String
|
||||
"w4" @ 109..111: Variable [definition]
|
||||
"float" @ 114..119: Class
|
||||
"w5" @ 121..123: Variable [definition]
|
||||
"float" @ 126..131: Class
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attribute_classification() {
|
||||
let test = SemanticTokenTest::new(
|
||||
@@ -2595,6 +2655,50 @@ with open("file.txt") as f:
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comprehensions() {
|
||||
let test = SemanticTokenTest::new(
|
||||
r#"
|
||||
list_comp = [x for x in range(10) if x % 2 == 0]
|
||||
set_comp = {x for x in range(10)}
|
||||
dict_comp = {k: v for k, v in zip(["a", "b"], [1, 2])}
|
||||
generator = (x for x in range(10))
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = test.highlight_file();
|
||||
assert_snapshot!(test.to_snapshot(&tokens), @r#"
|
||||
"list_comp" @ 1..10: Variable [definition]
|
||||
"x" @ 14..15: Variable
|
||||
"x" @ 20..21: Variable [definition]
|
||||
"range" @ 25..30: Class
|
||||
"10" @ 31..33: Number
|
||||
"x" @ 38..39: Variable
|
||||
"2" @ 42..43: Number
|
||||
"0" @ 47..48: Number
|
||||
"set_comp" @ 50..58: Variable [definition]
|
||||
"x" @ 62..63: Variable
|
||||
"x" @ 68..69: Variable [definition]
|
||||
"range" @ 73..78: Class
|
||||
"10" @ 79..81: Number
|
||||
"dict_comp" @ 84..93: Variable [definition]
|
||||
"k" @ 97..98: Variable
|
||||
"v" @ 100..101: Variable
|
||||
"k" @ 106..107: Variable [definition]
|
||||
"v" @ 109..110: Variable [definition]
|
||||
"zip" @ 114..117: Class
|
||||
"\"a\"" @ 119..122: String
|
||||
"\"b\"" @ 124..127: String
|
||||
"1" @ 131..132: Number
|
||||
"2" @ 134..135: Number
|
||||
"generator" @ 139..148: Variable [definition]
|
||||
"x" @ 152..153: Variable
|
||||
"x" @ 158..159: Variable [definition]
|
||||
"range" @ 163..168: Class
|
||||
"10" @ 169..171: Number
|
||||
"#);
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/astral-sh/ty/issues/1406>
|
||||
#[test]
|
||||
fn test_invalid_kwargs() {
|
||||
|
||||
@@ -74,7 +74,7 @@ pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option<Signa
|
||||
|
||||
// Get signature details from the semantic analyzer.
|
||||
let signature_details: Vec<CallSignatureDetails<'_>> =
|
||||
call_signature_details(db, &model, call_expr);
|
||||
call_signature_details(&model, call_expr);
|
||||
|
||||
if signature_details.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -11,12 +11,12 @@ use crate::cached_vendored_root;
|
||||
/// other language server providers (like hover, completion, and signature help) to find
|
||||
/// docstrings for functions that resolve to stubs.
|
||||
pub(crate) struct StubMapper<'db> {
|
||||
db: &'db dyn crate::Db,
|
||||
db: &'db dyn ty_python_semantic::Db,
|
||||
cached_vendored_root: Option<SystemPathBuf>,
|
||||
}
|
||||
|
||||
impl<'db> StubMapper<'db> {
|
||||
pub(crate) fn new(db: &'db dyn crate::Db) -> Self {
|
||||
pub(crate) fn new(db: &'db dyn ty_python_semantic::Db) -> Self {
|
||||
let cached_vendored_root = cached_vendored_root(db);
|
||||
Self {
|
||||
db,
|
||||
|
||||
@@ -13,6 +13,7 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_annotate_snippets = { workspace = true }
|
||||
ruff_diagnostics = { workspace = true }
|
||||
ruff_index = { workspace = true, features = ["salsa"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_memory_usage = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
try:
|
||||
type name_4 = name_1
|
||||
finally:
|
||||
from .. import name_3
|
||||
|
||||
try:
|
||||
pass
|
||||
except* 0:
|
||||
pass
|
||||
else:
|
||||
def name_1() -> name_4:
|
||||
pass
|
||||
|
||||
@name_1
|
||||
def name_3():
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
pass
|
||||
except* 0:
|
||||
assert name_3
|
||||
finally:
|
||||
|
||||
@name_3
|
||||
class name_1:
|
||||
pass
|
||||
@@ -0,0 +1,5 @@
|
||||
class foo[_: foo](object): ...
|
||||
|
||||
[_] = (foo,) = foo
|
||||
|
||||
def foo(): ...
|
||||
@@ -0,0 +1,20 @@
|
||||
name_3: Foo = 0
|
||||
name_4 = 0
|
||||
|
||||
if _0:
|
||||
type name_3 = name_5
|
||||
type name_4 = name_3
|
||||
|
||||
_1: name_3
|
||||
|
||||
def name_1(_2: name_4):
|
||||
pass
|
||||
|
||||
match 0:
|
||||
case name_1._3:
|
||||
pass
|
||||
case 1:
|
||||
type name_5 = name_4
|
||||
case name_5:
|
||||
pass
|
||||
name_3 = name_5
|
||||
@@ -35,6 +35,8 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
|
||||
|
||||
`LiteralString` cannot be parameterized.
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
@@ -42,7 +44,6 @@ from typing_extensions import LiteralString
|
||||
a: LiteralString[str]
|
||||
|
||||
# error: [invalid-type-form]
|
||||
# error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
b: LiteralString["foo"]
|
||||
```
|
||||
|
||||
|
||||
@@ -222,10 +222,10 @@ reveal_type(r) # revealed: dict[int | str, int | str]
|
||||
## Incorrect collection literal assignments are complained about
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Object of type `list[Unknown | int]` is not assignable to `list[str]`"
|
||||
# error: [invalid-assignment] "Object of type `list[str | int]` is not assignable to `list[str]`"
|
||||
a: list[str] = [1, 2, 3]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `set[Unknown | int | str]` is not assignable to `set[int]`"
|
||||
# error: [invalid-assignment] "Object of type `set[int | str]` is not assignable to `set[int]`"
|
||||
b: set[int] = {1, 2, "3"}
|
||||
```
|
||||
|
||||
@@ -422,7 +422,7 @@ reveal_type(d) # revealed: list[int | tuple[int, int]]
|
||||
e: list[int] = f(True)
|
||||
reveal_type(e) # revealed: list[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `list[str]` is not assignable to `list[int]`"
|
||||
# error: [invalid-assignment] "Object of type `list[int | str]` is not assignable to `list[int]`"
|
||||
g: list[int] = f("a")
|
||||
|
||||
# error: [invalid-assignment] "Object of type `list[str]` is not assignable to `tuple[int]`"
|
||||
@@ -459,12 +459,12 @@ reveal_type(b) # revealed: TD
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
|
||||
# error: [invalid-key] "Unknown key "y" for TypedDict `TD`"
|
||||
# error: [invalid-assignment] "Object of type `Unknown | dict[Unknown | str, Unknown | int]` is not assignable to `TD`"
|
||||
# error: [invalid-assignment] "Object of type `TD | dict[Unknown | str, Unknown | int]` is not assignable to `TD`"
|
||||
c: TD = f([{"y": 0}, {"x": 1}])
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
|
||||
# error: [invalid-key] "Unknown key "y" for TypedDict `TD`"
|
||||
# error: [invalid-assignment] "Object of type `Unknown | dict[Unknown | str, Unknown | int]` is not assignable to `TD | None`"
|
||||
# error: [invalid-assignment] "Object of type `TD | None | dict[Unknown | str, Unknown | int]` is not assignable to `TD | None`"
|
||||
c: TD | None = f([{"y": 0}, {"x": 1}])
|
||||
```
|
||||
|
||||
@@ -715,3 +715,17 @@ def _(a: int, b: str, c: int | str):
|
||||
x9: int | str | None = f(lst(c))
|
||||
reveal_type(x9) # revealed: int | str | None
|
||||
```
|
||||
|
||||
## Forward annotation with unclosed string literal
|
||||
|
||||
Regression test for [#1611](https://github.com/astral-sh/ty/issues/1611).
|
||||
|
||||
<!-- blacken-docs:off -->
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
a:'
|
||||
```
|
||||
|
||||
<!-- blacken-docs:on -->
|
||||
|
||||
@@ -1904,6 +1904,7 @@ we only consider the attribute assignment to be valid if the assigned attribute
|
||||
from typing import Literal
|
||||
|
||||
class Date:
|
||||
# error: [invalid-method-override]
|
||||
def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None:
|
||||
pass
|
||||
|
||||
@@ -2382,6 +2383,34 @@ class B:
|
||||
|
||||
reveal_type(B().x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(A().x) # revealed: Unknown | Literal[1]
|
||||
|
||||
class Base:
|
||||
def flip(self) -> "Sub":
|
||||
return Sub()
|
||||
|
||||
class Sub(Base):
|
||||
# error: [invalid-method-override]
|
||||
def flip(self) -> "Base":
|
||||
return Base()
|
||||
|
||||
class C2:
|
||||
def __init__(self, x: Sub):
|
||||
self.x = x
|
||||
|
||||
def replace_with(self, other: "C2"):
|
||||
self.x = other.x.flip()
|
||||
|
||||
reveal_type(C2(Sub()).x) # revealed: Unknown | Base
|
||||
|
||||
class C3:
|
||||
def __init__(self, x: Sub):
|
||||
self.x = [x]
|
||||
|
||||
def replace_with(self, other: "C3"):
|
||||
self.x = [self.x[0].flip()]
|
||||
|
||||
# TODO: should be `Unknown | list[Unknown | Sub] | list[Unknown | Base]`
|
||||
reveal_type(C3(Sub()).x) # revealed: Unknown | list[Unknown | Sub] | list[Divergent]
|
||||
```
|
||||
|
||||
And cycles between many attributes:
|
||||
@@ -2431,6 +2460,30 @@ class ManyCycles:
|
||||
reveal_type(self.x5) # revealed: Unknown | int
|
||||
reveal_type(self.x6) # revealed: Unknown | int
|
||||
reveal_type(self.x7) # revealed: Unknown | int
|
||||
|
||||
class ManyCycles2:
|
||||
def __init__(self: "ManyCycles2"):
|
||||
self.x1 = [0]
|
||||
self.x2 = [1]
|
||||
self.x3 = [1]
|
||||
|
||||
def f1(self: "ManyCycles2"):
|
||||
# TODO: should be Unknown | list[Unknown | int] | list[Divergent]
|
||||
reveal_type(self.x3) # revealed: Unknown | list[Unknown | int] | list[Divergent] | list[Divergent]
|
||||
|
||||
self.x1 = [self.x2] + [self.x3]
|
||||
self.x2 = [self.x1] + [self.x3]
|
||||
self.x3 = [self.x1] + [self.x2]
|
||||
|
||||
def f2(self: "ManyCycles2"):
|
||||
self.x1 = self.x2 + self.x3
|
||||
self.x2 = self.x1 + self.x3
|
||||
self.x3 = self.x1 + self.x2
|
||||
|
||||
def f3(self: "ManyCycles2"):
|
||||
self.x1 = self.x2 + self.x3
|
||||
self.x2 = self.x1 + self.x3
|
||||
self.x3 = self.x1 + self.x2
|
||||
```
|
||||
|
||||
This case additionally tests our union/intersection simplification logic:
|
||||
@@ -2610,12 +2663,18 @@ reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown]
|
||||
|
||||
## Divergent inferred implicit instance attribute types
|
||||
|
||||
If an implicit attribute is defined recursively and type inference diverges, the divergent part is
|
||||
filled in with the dynamic type `Divergent`. Types containing `Divergent` can be seen as "cheap"
|
||||
recursive types: they are not true recursive types based on recursive type theory, so no unfolding
|
||||
is performed when you use them.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def f(self, other: "C"):
|
||||
self.x = (other.x, 1)
|
||||
|
||||
reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
|
||||
reveal_type(C().x[0]) # revealed: Unknown | Divergent
|
||||
```
|
||||
|
||||
This also works if the tuple is not constructed directly:
|
||||
@@ -2654,11 +2713,11 @@ And it also works for homogeneous tuples:
|
||||
def make_homogeneous_tuple(x: T) -> tuple[T, ...]:
|
||||
return (x, x)
|
||||
|
||||
class E:
|
||||
def f(self, other: "E"):
|
||||
class F:
|
||||
def f(self, other: "F"):
|
||||
self.x = make_homogeneous_tuple(other.x)
|
||||
|
||||
reveal_type(E().x) # revealed: Unknown | tuple[Divergent, ...]
|
||||
reveal_type(F().x) # revealed: Unknown | tuple[Divergent, ...]
|
||||
```
|
||||
|
||||
## Attributes of standard library modules that aren't yet defined
|
||||
@@ -2710,10 +2769,10 @@ We give special diagnostics for this common case too:
|
||||
import foo
|
||||
import baz
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(foo.bar) # revealed: Unknown
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(baz.bar) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute]
|
||||
reveal_type(foo.bar) # revealed: <module 'foo.bar'>
|
||||
# error: [possibly-missing-attribute]
|
||||
reveal_type(baz.bar) # revealed: <module 'baz.bar'>
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
@@ -268,8 +268,8 @@ class A:
|
||||
|
||||
A(f(1))
|
||||
|
||||
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `list[int | str]`, found `list[list[Unknown]]`"
|
||||
# error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `list[int | None]`, found `list[list[Unknown]]`"
|
||||
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `list[int | str]`, found `list[int | None | list[Unknown]] & list[int | str | list[Unknown]] & list[list[Unknown]]`"
|
||||
# error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `list[int | None]`, found `list[int | None | list[Unknown]] & list[int | str | list[Unknown]] & list[list[Unknown]]`"
|
||||
A(f([]))
|
||||
```
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ type("Foo", Base, {})
|
||||
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
|
||||
type("Foo", (1, 2), {})
|
||||
|
||||
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`"
|
||||
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
|
||||
type("Foo", (Base,), {b"attr": 1})
|
||||
```
|
||||
|
||||
|
||||
@@ -237,4 +237,26 @@ class Matrix:
|
||||
Matrix() < Matrix()
|
||||
```
|
||||
|
||||
## `self`-binding behaviour of function-like `Callable`s
|
||||
|
||||
Binding the `self` parameter of a function-like `Callable` creates a new `Callable` that is also
|
||||
function-like:
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def my_lossy_decorator(fn: Callable[..., int]) -> Callable[..., int]:
|
||||
return fn
|
||||
|
||||
class MyClass:
|
||||
@my_lossy_decorator
|
||||
def method(self) -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(MyClass().method) # revealed: (...) -> int
|
||||
reveal_type(MyClass().method.__name__) # revealed: str
|
||||
```
|
||||
|
||||
[`tensorbase`]: https://github.com/pytorch/pytorch/blob/f3913ea641d871f04fa2b6588a77f63efeeb9f10/torch/_tensor.py#L1084-L1092
|
||||
|
||||
@@ -277,6 +277,6 @@ def _(flag: bool):
|
||||
x = f({"x": 1})
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[Unknown | str, Unknown | int]`"
|
||||
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int] & dict[Unknown | str, Unknown | int]`"
|
||||
f({"y": 1})
|
||||
```
|
||||
|
||||
@@ -501,8 +501,8 @@ class A[T]:
|
||||
return a
|
||||
|
||||
class B[T](A[T]):
|
||||
def f(self, b: T) -> T:
|
||||
return super().f(b)
|
||||
def f(self, a: T) -> T:
|
||||
return super().f(a)
|
||||
```
|
||||
|
||||
## Invalid Usages
|
||||
|
||||
@@ -24,10 +24,10 @@ class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: A) -> EqReturnType:
|
||||
def __eq__(self, other: A) -> EqReturnType: # error: [invalid-method-override]
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: A) -> NeReturnType:
|
||||
def __ne__(self, other: A) -> NeReturnType: # error: [invalid-method-override]
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: A) -> LtReturnType:
|
||||
@@ -66,10 +66,10 @@ class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: B) -> EqReturnType:
|
||||
def __eq__(self, other: B) -> EqReturnType: # error: [invalid-method-override]
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: B) -> NeReturnType:
|
||||
def __ne__(self, other: B) -> NeReturnType: # error: [invalid-method-override]
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: B) -> LtReturnType:
|
||||
@@ -111,10 +111,10 @@ class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: B) -> EqReturnType:
|
||||
def __eq__(self, other: B) -> EqReturnType: # error: [invalid-method-override]
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: B) -> NeReturnType:
|
||||
def __ne__(self, other: B) -> NeReturnType: # error: [invalid-method-override]
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: B) -> LtReturnType:
|
||||
@@ -132,12 +132,10 @@ class A:
|
||||
class Unrelated: ...
|
||||
|
||||
class B:
|
||||
# To override builtins.object.__eq__ and builtins.object.__ne__
|
||||
# TODO these should emit an invalid override diagnostic
|
||||
def __eq__(self, other: Unrelated) -> B:
|
||||
def __eq__(self, other: Unrelated) -> B: # error: [invalid-method-override]
|
||||
return B()
|
||||
|
||||
def __ne__(self, other: Unrelated) -> B:
|
||||
def __ne__(self, other: Unrelated) -> B: # error: [invalid-method-override]
|
||||
return B()
|
||||
|
||||
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
|
||||
@@ -180,10 +178,10 @@ class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: A) -> A:
|
||||
def __eq__(self, other: A) -> A: # error: [invalid-method-override]
|
||||
return A()
|
||||
|
||||
def __ne__(self, other: A) -> A:
|
||||
def __ne__(self, other: A) -> A: # error: [invalid-method-override]
|
||||
return A()
|
||||
|
||||
def __lt__(self, other: A) -> A:
|
||||
@@ -199,22 +197,22 @@ class A:
|
||||
return A()
|
||||
|
||||
class B(A):
|
||||
def __eq__(self, other: A) -> EqReturnType:
|
||||
def __eq__(self, other: A) -> EqReturnType: # error: [invalid-method-override]
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: A) -> NeReturnType:
|
||||
def __ne__(self, other: A) -> NeReturnType: # error: [invalid-method-override]
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: A) -> LtReturnType:
|
||||
def __lt__(self, other: A) -> LtReturnType: # error: [invalid-method-override]
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, other: A) -> LeReturnType:
|
||||
def __le__(self, other: A) -> LeReturnType: # error: [invalid-method-override]
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, other: A) -> GtReturnType:
|
||||
def __gt__(self, other: A) -> GtReturnType: # error: [invalid-method-override]
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, other: A) -> GeReturnType:
|
||||
def __ge__(self, other: A) -> GeReturnType: # error: [invalid-method-override]
|
||||
return GeReturnType()
|
||||
|
||||
reveal_type(A() == B()) # revealed: EqReturnType
|
||||
@@ -243,10 +241,10 @@ class A:
|
||||
return A()
|
||||
|
||||
class B(A):
|
||||
def __lt__(self, other: int) -> B:
|
||||
def __lt__(self, other: int) -> B: # error: [invalid-method-override]
|
||||
return B()
|
||||
|
||||
def __gt__(self, other: int) -> B:
|
||||
def __gt__(self, other: int) -> B: # error: [invalid-method-override]
|
||||
return B()
|
||||
|
||||
reveal_type(A() < B()) # revealed: A
|
||||
@@ -291,11 +289,10 @@ Please refer to the [docs](https://docs.python.org/3/reference/datamodel.html#ob
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
# TODO both these overrides should emit invalid-override diagnostic
|
||||
def __eq__(self, other: int) -> A:
|
||||
def __eq__(self, other: int) -> A: # error: [invalid-method-override]
|
||||
return A()
|
||||
|
||||
def __ne__(self, other: int) -> A:
|
||||
def __ne__(self, other: int) -> A: # error: [invalid-method-override]
|
||||
return A()
|
||||
|
||||
reveal_type(A() == A()) # revealed: bool
|
||||
|
||||
@@ -155,10 +155,10 @@ class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, o: object) -> EqReturnType:
|
||||
def __eq__(self, o: object) -> EqReturnType: # error: [invalid-method-override]
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, o: object) -> NeReturnType:
|
||||
def __ne__(self, o: object) -> NeReturnType: # error: [invalid-method-override]
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, o: A) -> LtReturnType:
|
||||
@@ -386,6 +386,7 @@ class NotBoolable:
|
||||
__bool__: None = None
|
||||
|
||||
class A:
|
||||
# error: [invalid-method-override]
|
||||
def __eq__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
|
||||
@@ -32,6 +32,55 @@ reveal_type(p.x) # revealed: Unknown | int
|
||||
reveal_type(p.y) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Self-referential bare type alias
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12" # typing.TypeAliasType
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Union, TypeAliasType, Sequence, Mapping
|
||||
|
||||
A = list["A" | None]
|
||||
|
||||
def f(x: A):
|
||||
# TODO: should be `list[A | None]`?
|
||||
reveal_type(x) # revealed: list[Divergent]
|
||||
# TODO: should be `A | None`?
|
||||
reveal_type(x[0]) # revealed: Divergent
|
||||
|
||||
JSONPrimitive = Union[str, int, float, bool, None]
|
||||
JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]')
|
||||
```
|
||||
|
||||
## Self-referential legacy type variables
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
B = TypeVar("B", bound="Base")
|
||||
|
||||
class Base(Generic[B]):
|
||||
pass
|
||||
|
||||
T = TypeVar("T", bound="Foo[int]")
|
||||
|
||||
class Foo(Generic[T]): ...
|
||||
```
|
||||
|
||||
## Self-referential PEP-695 type variables
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class Node[T: "Node[int]"]:
|
||||
pass
|
||||
```
|
||||
|
||||
## Parameter default values
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ty/issues/1402>. When a parameter has a
|
||||
|
||||
@@ -171,7 +171,7 @@ class Config:
|
||||
import generic_a
|
||||
import generic_b
|
||||
|
||||
# TODO should be error: [invalid-assignment] "Object of type `<class 'generic_b.Container[int]'>` is not assignable to `type[generic_a.Container[int]]`"
|
||||
# error: [invalid-assignment] "Object of type `<class 'generic_b.Container[int]'>` is not assignable to `type[generic_a.Container[int]]`"
|
||||
container: type[generic_a.Container[int]] = generic_b.Container[int]
|
||||
```
|
||||
|
||||
|
||||
@@ -103,8 +103,7 @@ class UnknownLengthSubclassWithDunderLenOverridden(tuple[int, ...]):
|
||||
reveal_type(len(UnknownLengthSubclassWithDunderLenOverridden())) # revealed: Literal[42]
|
||||
|
||||
class FixedLengthSubclassWithDunderLenOverridden(tuple[int]):
|
||||
# TODO: we should complain about this as a Liskov violation (incompatible override)
|
||||
def __len__(self) -> Literal[42]:
|
||||
def __len__(self) -> Literal[42]: # error: [invalid-method-override]
|
||||
return 42
|
||||
|
||||
reveal_type(len(FixedLengthSubclassWithDunderLenOverridden((1,)))) # revealed: Literal[42]
|
||||
|
||||
@@ -273,7 +273,7 @@ class C(Generic[T]):
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
@@ -289,7 +289,7 @@ class C(Generic[T]):
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
@@ -308,7 +308,7 @@ class C(Generic[T]):
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
@@ -327,7 +327,7 @@ class C(Generic[T]):
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
|
||||
class D(Generic[T]):
|
||||
@@ -338,7 +338,7 @@ class D(Generic[T]):
|
||||
|
||||
reveal_type(D(1)) # revealed: D[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `D[str]` is not assignable to `D[int]`"
|
||||
# error: [invalid-assignment] "Object of type `D[int | str]` is not assignable to `D[int]`"
|
||||
wrong_innards: D[int] = D("five")
|
||||
```
|
||||
|
||||
@@ -454,7 +454,7 @@ reveal_type(C(1, 1)) # revealed: C[int]
|
||||
reveal_type(C(1, "string")) # revealed: C[int]
|
||||
reveal_type(C(1, True)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five", 1)
|
||||
```
|
||||
|
||||
@@ -732,6 +732,14 @@ class Base(Generic[T]): ...
|
||||
class Sub(Base["Sub"]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: <class 'Sub'>
|
||||
|
||||
U = TypeVar("U")
|
||||
|
||||
class Base2(Generic[T, U]): ...
|
||||
|
||||
# TODO: no error
|
||||
# error: [unsupported-base] "Unsupported class base with type `<class 'Base2[Sub2, U@Sub2]'> | <class 'Base2[Sub2[Unknown], U@Sub2]'>`"
|
||||
class Sub2(Base2["Sub2", U]): ...
|
||||
```
|
||||
|
||||
#### Without string forward references
|
||||
@@ -756,6 +764,8 @@ from typing_extensions import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# TODO: no error "Unsupported class base with type `<class 'list[Derived[T@Derived]]'> | <class 'list[@Todo]'>`"
|
||||
# error: [unsupported-base]
|
||||
class Derived(list[Derived[T]], Generic[T]): ...
|
||||
```
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ class C[T]:
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
@@ -263,7 +263,7 @@ class C[T]:
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
@@ -280,7 +280,7 @@ class C[T]:
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
@@ -297,7 +297,7 @@ class C[T]:
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
|
||||
class D[T]:
|
||||
@@ -310,7 +310,7 @@ class D[T]:
|
||||
|
||||
reveal_type(D(1)) # revealed: D[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `D[str]` is not assignable to `D[int]`"
|
||||
# error: [invalid-assignment] "Object of type `D[int | str]` is not assignable to `D[int]`"
|
||||
wrong_innards: D[int] = D("five")
|
||||
```
|
||||
|
||||
@@ -395,7 +395,7 @@ reveal_type(C(1, 1)) # revealed: C[int]
|
||||
reveal_type(C(1, "string")) # revealed: C[int]
|
||||
reveal_type(C(1, True)) # revealed: C[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
|
||||
# error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five", 1)
|
||||
```
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ from ty_extensions import ConstraintSet, generic_context
|
||||
# fmt: off
|
||||
|
||||
def unbounded[T]():
|
||||
# revealed: ty_extensions.Specialization[T@unbounded = object]
|
||||
# revealed: ty_extensions.Specialization[T@unbounded = Unknown]
|
||||
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.always()))
|
||||
# revealed: ty_extensions.Specialization[T@unbounded = object]
|
||||
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, object)))
|
||||
# revealed: None
|
||||
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.never()))
|
||||
|
||||
@@ -88,6 +90,7 @@ that makes the test succeed.
|
||||
from typing import Any
|
||||
|
||||
def bounded_by_gradual[T: Any]():
|
||||
# TODO: revealed: ty_extensions.Specialization[T@bounded_by_gradual = Any]
|
||||
# revealed: ty_extensions.Specialization[T@bounded_by_gradual = object]
|
||||
reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.always()))
|
||||
# revealed: None
|
||||
@@ -168,12 +171,16 @@ from typing import Any
|
||||
# fmt: off
|
||||
|
||||
def constrained_by_gradual[T: (Base, Any)]():
|
||||
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Unknown]
|
||||
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
|
||||
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.always()))
|
||||
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
|
||||
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = object]
|
||||
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.always()))
|
||||
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, object)))
|
||||
# revealed: None
|
||||
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.never()))
|
||||
|
||||
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
|
||||
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
|
||||
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Base)))
|
||||
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
|
||||
@@ -181,14 +188,14 @@ def constrained_by_gradual[T: (Base, Any)]():
|
||||
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Unrelated)))
|
||||
|
||||
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
|
||||
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Super]
|
||||
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
|
||||
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Super)))
|
||||
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
|
||||
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Super]
|
||||
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Super, T, Super)))
|
||||
|
||||
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
|
||||
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = object]
|
||||
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
|
||||
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Sub, T, object)))
|
||||
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
|
||||
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Sub]
|
||||
@@ -288,7 +295,7 @@ class Unrelated: ...
|
||||
# fmt: off
|
||||
|
||||
def mutually_bound[T: Base, U]():
|
||||
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = object]
|
||||
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Unknown]
|
||||
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.always()))
|
||||
# revealed: None
|
||||
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.never()))
|
||||
@@ -296,7 +303,7 @@ def mutually_bound[T: Base, U]():
|
||||
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Base]
|
||||
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, U, T)))
|
||||
|
||||
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = object]
|
||||
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = Unknown]
|
||||
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, T, Sub)))
|
||||
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = Sub]
|
||||
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, T, Sub) & ConstraintSet.range(Never, U, T)))
|
||||
|
||||
@@ -38,7 +38,7 @@ See: <https://github.com/astral-sh/ty/issues/113>
|
||||
from pkg.sub import A
|
||||
|
||||
# TODO: This should be `<class 'A'>`
|
||||
reveal_type(A) # revealed: Never
|
||||
reveal_type(A) # revealed: Divergent
|
||||
```
|
||||
|
||||
`pkg/outer.py`:
|
||||
|
||||
@@ -60,8 +60,8 @@ Y: int = 47
|
||||
import mypackage
|
||||
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
# error: [unresolved-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: int
|
||||
```
|
||||
|
||||
### In Non-Stub
|
||||
@@ -90,8 +90,8 @@ Y: int = 47
|
||||
import mypackage
|
||||
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
# error: [unresolved-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: int
|
||||
```
|
||||
|
||||
## Absolute `from` Import of Direct Submodule in `__init__`
|
||||
@@ -125,8 +125,8 @@ Y: int = 47
|
||||
import mypackage
|
||||
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
# error: [unresolved-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: int
|
||||
```
|
||||
|
||||
### In Non-Stub
|
||||
@@ -155,8 +155,8 @@ Y: int = 47
|
||||
import mypackage
|
||||
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
# error: [unresolved-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: int
|
||||
```
|
||||
|
||||
## Import of Direct Submodule in `__init__`
|
||||
@@ -184,8 +184,8 @@ X: int = 42
|
||||
import mypackage
|
||||
|
||||
# TODO: this could work and would be nice to have?
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
```
|
||||
|
||||
### In Non-Stub
|
||||
@@ -208,8 +208,8 @@ X: int = 42
|
||||
import mypackage
|
||||
|
||||
# TODO: this could work and would be nice to have
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
```
|
||||
|
||||
## Relative `from` Import of Nested Submodule in `__init__`
|
||||
@@ -242,10 +242,10 @@ X: int = 42
|
||||
import mypackage
|
||||
|
||||
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: int
|
||||
# error: [unresolved-attribute] "has no member `nested`"
|
||||
reveal_type(mypackage.nested) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "has no member `nested`"
|
||||
@@ -280,10 +280,10 @@ import mypackage
|
||||
|
||||
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
|
||||
# TODO: this would be nice to support
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: int
|
||||
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
|
||||
reveal_type(mypackage.nested.X) # revealed: int
|
||||
```
|
||||
@@ -318,10 +318,10 @@ X: int = 42
|
||||
import mypackage
|
||||
|
||||
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: int
|
||||
# error: [unresolved-attribute] "has no member `nested`"
|
||||
reveal_type(mypackage.nested) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "has no member `nested`"
|
||||
@@ -356,10 +356,10 @@ import mypackage
|
||||
|
||||
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
|
||||
# TODO: this would be nice to support
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: int
|
||||
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
|
||||
reveal_type(mypackage.nested.X) # revealed: int
|
||||
```
|
||||
@@ -393,12 +393,14 @@ X: int = 42
|
||||
```py
|
||||
import mypackage
|
||||
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
|
||||
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
|
||||
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: int
|
||||
```
|
||||
|
||||
### In Non-Stub
|
||||
@@ -429,12 +431,14 @@ X: int = 42
|
||||
import mypackage
|
||||
|
||||
# TODO: this would be nice to support
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
|
||||
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
|
||||
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested) # revealed: <module 'mypackage.submodule.nested'>
|
||||
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
|
||||
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
|
||||
reveal_type(mypackage.submodule.nested.X) # revealed: int
|
||||
```
|
||||
|
||||
## Relative `from` Import of Direct Submodule in `__init__`, Mismatched Alias
|
||||
@@ -460,8 +464,8 @@ X: int = 42
|
||||
```py
|
||||
import mypackage
|
||||
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
# error: [unresolved-attribute] "has no member `imported_m`"
|
||||
reveal_type(mypackage.imported_m.X) # revealed: Unknown
|
||||
```
|
||||
@@ -486,8 +490,8 @@ X: int = 42
|
||||
import mypackage
|
||||
|
||||
# TODO: this would be nice to support, as it works at runtime
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
reveal_type(mypackage.imported_m.X) # revealed: int
|
||||
```
|
||||
|
||||
@@ -647,8 +651,8 @@ reveal_type(mypackage.imported.X) # revealed: int
|
||||
|
||||
## `from` Import of Other Package's Submodule
|
||||
|
||||
`from mypackage import submodule` from outside the package is not modeled as a side-effect on
|
||||
`mypackage`, even in the importing file (this could be changed!).
|
||||
`from mypackage import submodule` and `from mypackage.submodule import not_a_submodule` from outside
|
||||
the package are both modeled as a side-effects on `mypackage`.
|
||||
|
||||
### In Stub
|
||||
|
||||
@@ -663,18 +667,28 @@ reveal_type(mypackage.imported.X) # revealed: int
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
`package2/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`package2/submodule.pyi`:
|
||||
|
||||
```pyi
|
||||
not_a_submodule: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import mypackage
|
||||
import package2
|
||||
from mypackage import imported
|
||||
from package2.submodule import not_a_submodule
|
||||
|
||||
reveal_type(imported.X) # revealed: int
|
||||
|
||||
# TODO: this would be nice to support, but it's dangerous with available_submodule_attributes
|
||||
# for details, see: https://github.com/astral-sh/ty/issues/1488
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
reveal_type(package2.submodule.not_a_submodule) # revealed: int
|
||||
```
|
||||
|
||||
### In Non-Stub
|
||||
@@ -690,17 +704,28 @@ reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
`package2/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`package2/submodule.py`:
|
||||
|
||||
```py
|
||||
not_a_submodule: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import mypackage
|
||||
import package2
|
||||
from mypackage import imported
|
||||
from package2.submodule import not_a_submodule
|
||||
|
||||
reveal_type(imported.X) # revealed: int
|
||||
|
||||
# TODO: this would be nice to support, as it works at runtime
|
||||
# error: [unresolved-attribute] "Submodule `imported` may not be available"
|
||||
reveal_type(mypackage.imported.X) # revealed: Unknown
|
||||
reveal_type(mypackage.imported.X) # revealed: int
|
||||
reveal_type(package2.submodule.not_a_submodule) # revealed: int
|
||||
```
|
||||
|
||||
## `from` Import of Sibling Module
|
||||
@@ -737,8 +762,8 @@ from mypackage import imported
|
||||
reveal_type(imported.X) # revealed: int
|
||||
# error: [unresolved-attribute] "has no member `fails`"
|
||||
reveal_type(imported.fails.Y) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
|
||||
reveal_type(mypackage.fails.Y) # revealed: int
|
||||
```
|
||||
|
||||
### In Non-Stub
|
||||
@@ -770,8 +795,8 @@ from mypackage import imported
|
||||
|
||||
reveal_type(imported.X) # revealed: int
|
||||
reveal_type(imported.fails.Y) # revealed: int
|
||||
# error: [unresolved-attribute] "Submodule `fails`"
|
||||
reveal_type(mypackage.fails.Y) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute] "Submodule `fails`"
|
||||
reveal_type(mypackage.fails.Y) # revealed: int
|
||||
```
|
||||
|
||||
## Fractal Re-export Nameclash Problems
|
||||
@@ -859,6 +884,48 @@ from mypackage import funcmod
|
||||
x = funcmod(1)
|
||||
```
|
||||
|
||||
## A Tale of Two Modules
|
||||
|
||||
`from typing import TYPE_CHECKING` has side-effects???
|
||||
|
||||
### In Stub
|
||||
|
||||
`mypackage/__init__.py`:
|
||||
|
||||
```py
|
||||
from .conflicted.b import x
|
||||
```
|
||||
|
||||
`mypackage/conflicted/__init__.py`:
|
||||
|
||||
`mypackage/conflicted/other1/__init__.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`mypackage/conflicted/b/__init__.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`mypackage/conflicted/b/c/__init__.py`:
|
||||
|
||||
```py
|
||||
y: int = 2
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from typing import TYPE_CHECKING
|
||||
from mypackage.conflicted.other1 import x as x1
|
||||
import mypackage.conflicted.b.c
|
||||
|
||||
reveal_type(mypackage.conflicted.b.c.y) # revealed: int
|
||||
```
|
||||
|
||||
## Re-export Nameclash Problems In Functions
|
||||
|
||||
`from` imports in an `__init__.py` at file scope should be visible to functions defined in the file:
|
||||
|
||||
@@ -247,8 +247,8 @@ X: int = 42
|
||||
from . import foo
|
||||
import package
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(package.foo.X) # revealed: Unknown
|
||||
# error: [possibly-missing-attribute]
|
||||
reveal_type(package.foo.X) # revealed: int
|
||||
```
|
||||
|
||||
## Relative imports at the top of a search path
|
||||
|
||||
@@ -116,3 +116,43 @@ b = 1
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
## Submodule is loaded in a non-global scope
|
||||
|
||||
We recognise submodules as being available as attributes even if they are loaded in a function
|
||||
scope. The function might never be executed, which means that the submodule might never be loaded;
|
||||
however, we prefer to prioritise avoiding false positives over catching all possible errors here.
|
||||
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`a/c.py`:
|
||||
|
||||
```py
|
||||
d = 42
|
||||
```
|
||||
|
||||
`a/e/f.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import a
|
||||
|
||||
def f():
|
||||
import a.b
|
||||
from a.c import d
|
||||
from a.e import f
|
||||
|
||||
f()
|
||||
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.c) # revealed: <module 'a.c'>
|
||||
reveal_type(a.e) # revealed: <module 'a.e'>
|
||||
reveal_type(a.e.f) # revealed: <module 'a.e.f'>
|
||||
```
|
||||
|
||||
585
crates/ty_python_semantic/resources/mdtest/liskov.md
Normal file
585
crates/ty_python_semantic/resources/mdtest/liskov.md
Normal file
@@ -0,0 +1,585 @@
|
||||
# The Liskov Substitution Principle
|
||||
|
||||
The Liskov Substitution Principle provides the basis for many of the assumptions a type checker
|
||||
generally makes about types in Python:
|
||||
|
||||
> Subtype Requirement: Let `ϕ(x)` be a property provable about objects `x` of type `T`. Then
|
||||
> `ϕ(y)` should be true for objects `y` of type `S` where `S` is a subtype of `T`.
|
||||
|
||||
In order for a type checker's assumptions to be sound, it is crucial for the type checker to enforce
|
||||
the Liskov Substitution Principle on code that it checks. In practice, this usually manifests as
|
||||
several checks for a type checker to perform when it checks a subclass `B` of a class `A`:
|
||||
|
||||
1. Read-only attributes should only ever be overridden covariantly: if a property `A.p` resolves to
|
||||
`int` when accessed, accessing `B.p` should either resolve to `int` or a subtype of `int`.
|
||||
1. Method return types should only ever be overridden covariantly: if a method `A.f` returns `int`
|
||||
when called, calling `B.f` should also resolve to `int or a subtype of`int\`.
|
||||
1. Method parameters should only ever be overridden contravariantly: if a method `A.f` can be called
|
||||
with an argument of type `bool`, then the method `B.f` must also be callable with type `bool`
|
||||
(though it is permitted for the override to also accept other types)
|
||||
1. Mutable attributes should only ever be overridden invariantly: if a mutable attribute `A.attr`
|
||||
resolves to type `str`, it can only be overridden on a subclass with exactly the same type.
|
||||
|
||||
## Method return types
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```pyi
|
||||
class Super:
|
||||
def method(self) -> int: ...
|
||||
|
||||
class Sub1(Super):
|
||||
def method(self) -> int: ... # fine
|
||||
|
||||
class Sub2(Super):
|
||||
def method(self) -> bool: ... # fine: `bool` is a subtype of `int`
|
||||
|
||||
class Sub3(Super):
|
||||
def method(self) -> object: ... # error: [invalid-method-override]
|
||||
|
||||
class Sub4(Super):
|
||||
def method(self) -> str: ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
## Method parameters
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```pyi
|
||||
class Super:
|
||||
def method(self, x: int, /): ...
|
||||
|
||||
class Sub1(Super):
|
||||
def method(self, x: int, /): ... # fine
|
||||
|
||||
class Sub2(Super):
|
||||
def method(self, x: object, /): ... # fine: `method` still accepts any argument of type `int`
|
||||
|
||||
class Sub4(Super):
|
||||
def method(self, x: int | str, /): ... # fine
|
||||
|
||||
class Sub5(Super):
|
||||
def method(self, x: int): ... # fine: `x` can still be passed positionally
|
||||
|
||||
class Sub6(Super):
|
||||
# fine: `method()` can still be called with just a single argument
|
||||
def method(self, x: int, *args): ...
|
||||
|
||||
class Sub7(Super):
|
||||
def method(self, x: int, **kwargs): ... # fine
|
||||
|
||||
class Sub8(Super):
|
||||
def method(self, x: int, *args, **kwargs): ... # fine
|
||||
|
||||
class Sub9(Super):
|
||||
def method(self, x: int, extra_positional_arg=42, /): ... # fine
|
||||
|
||||
class Sub10(Super):
|
||||
def method(self, x: int, extra_pos_or_kw_arg=42): ... # fine
|
||||
|
||||
class Sub11(Super):
|
||||
def method(self, x: int, *, extra_kw_only_arg=42): ... # fine
|
||||
|
||||
class Sub12(Super):
|
||||
# Some calls permitted by the superclass are now no longer allowed
|
||||
# (the method can no longer be passed any arguments!)
|
||||
def method(self, /): ... # error: [invalid-method-override]
|
||||
|
||||
class Sub13(Super):
|
||||
# Some calls permitted by the superclass are now no longer allowed
|
||||
# (the method can no longer be passed exactly one argument!)
|
||||
def method(self, x, y, /): ... # error: [invalid-method-override]
|
||||
|
||||
class Sub14(Super):
|
||||
# Some calls permitted by the superclass are now no longer allowed
|
||||
# (x can no longer be passed positionally!)
|
||||
def method(self, /, *, x): ... # error: [invalid-method-override]
|
||||
|
||||
class Sub15(Super):
|
||||
# Some calls permitted by the superclass are now no longer allowed
|
||||
# (x can no longer be passed any integer -- it now requires a bool!)
|
||||
def method(self, x: bool, /): ... # error: [invalid-method-override]
|
||||
|
||||
class Super2:
|
||||
def method2(self, x): ...
|
||||
|
||||
class Sub16(Super2):
|
||||
def method2(self, x, /): ... # error: [invalid-method-override]
|
||||
|
||||
class Sub17(Super2):
|
||||
def method2(self, *, x): ... # error: [invalid-method-override]
|
||||
|
||||
class Super3:
|
||||
def method3(self, *, x): ...
|
||||
|
||||
class Sub18(Super3):
|
||||
def method3(self, x): ... # fine: `x` can still be used as a keyword argument
|
||||
|
||||
class Sub19(Super3):
|
||||
def method3(self, x, /): ... # error: [invalid-method-override]
|
||||
|
||||
class Super4:
|
||||
def method(self, *args: int, **kwargs: str): ...
|
||||
|
||||
class Sub20(Super4):
|
||||
def method(self, *args: object, **kwargs: object): ... # fine
|
||||
|
||||
class Sub21(Super4):
|
||||
def method(self, *args): ... # error: [invalid-method-override]
|
||||
|
||||
class Sub22(Super4):
|
||||
def method(self, **kwargs): ... # error: [invalid-method-override]
|
||||
|
||||
class Sub23(Super4):
|
||||
def method(self, x, *args, y, **kwargs): ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
## The entire class hierarchy is checked
|
||||
|
||||
If a child class's method definition is Liskov-compatible with the method definition on its parent
|
||||
class, Liskov compatibility must also nonetheless be checked with respect to the method definition
|
||||
on its grandparent class. This is because type checkers will treat the child class as a subtype of
|
||||
the grandparent class just as much as they treat it as a subtype of the parent class, so
|
||||
substitutability with respect to the grandparent class is just as important:
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`stub.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import Any
|
||||
|
||||
class Grandparent:
|
||||
def method(self, x: int) -> None: ...
|
||||
|
||||
class Parent(Grandparent):
|
||||
def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
|
||||
class Child(Parent):
|
||||
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
|
||||
def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
|
||||
class OtherChild(Parent):
|
||||
# compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
|
||||
def method(self, x: int) -> None: ... # error: [invalid-method-override]
|
||||
|
||||
class GradualParent(Grandparent):
|
||||
def method(self, x: Any) -> None: ...
|
||||
|
||||
class ThirdChild(GradualParent):
|
||||
# `GradualParent.method` is compatible with the signature of `Grandparent.method`,
|
||||
# and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
|
||||
# but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
|
||||
def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
`other_stub.pyi`:
|
||||
|
||||
```pyi
|
||||
class A:
|
||||
def get(self, default): ...
|
||||
|
||||
class B(A):
|
||||
def get(self, default, /): ... # error: [invalid-method-override]
|
||||
|
||||
get = 56
|
||||
|
||||
class C(B):
|
||||
# `get` appears in the symbol table of `C`,
|
||||
# but that doesn't confuse our diagnostic...
|
||||
foo = get
|
||||
|
||||
class D(C):
|
||||
# compatible with `C.get` and `B.get`, but not with `A.get`
|
||||
def get(self, my_default): ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
## Non-generic methods on generic classes work as expected
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```pyi
|
||||
class A[T]:
|
||||
def method(self, x: T) -> None: ...
|
||||
|
||||
class B[T](A[T]):
|
||||
def method(self, x: T) -> None: ... # fine
|
||||
|
||||
class C(A[int]):
|
||||
def method(self, x: int) -> None: ... # fine
|
||||
|
||||
class D[T](A[T]):
|
||||
def method(self, x: object) -> None: ... # fine
|
||||
|
||||
class E(A[int]):
|
||||
def method(self, x: object) -> None: ... # fine
|
||||
|
||||
class F[T](A[T]):
|
||||
# TODO: we should emit `invalid-method-override` on this:
|
||||
# `str` is not necessarily a supertype of `T`!
|
||||
def method(self, x: str) -> None: ...
|
||||
|
||||
class G(A[int]):
|
||||
def method(self, x: bool) -> None: ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
## Generic methods on non-generic classes work as expected
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```pyi
|
||||
from typing import Never, Self
|
||||
|
||||
class A:
|
||||
def method[T](self, x: T) -> T: ...
|
||||
|
||||
class B(A):
|
||||
def method[T](self, x: T) -> T: ... # fine
|
||||
|
||||
class C(A):
|
||||
def method(self, x: object) -> Never: ... # fine
|
||||
|
||||
class D(A):
|
||||
# TODO: we should emit [invalid-method-override] here:
|
||||
# `A.method` accepts an argument of any type,
|
||||
# but `D.method` only accepts `int`s
|
||||
def method(self, x: int) -> int: ...
|
||||
|
||||
class A2:
|
||||
def method(self, x: int) -> int: ...
|
||||
|
||||
class B2(A2):
|
||||
# fine: although `B2.method()` will not always return an `int`,
|
||||
# an instance of `B2` can be substituted wherever an instance of `A2` is expected,
|
||||
# and it *will* always return an `int` if it is passed an `int`
|
||||
# (which is all that will be allowed if an instance of `A2` is expected)
|
||||
def method[T](self, x: T) -> T: ...
|
||||
|
||||
class C2(A2):
|
||||
def method[T: int](self, x: T) -> T: ...
|
||||
|
||||
class D2(A2):
|
||||
# The type variable is bound to a type disjoint from `int`,
|
||||
# so the method will not accept integers, and therefore this is an invalid override
|
||||
def method[T: str](self, x: T) -> T: ... # error: [invalid-method-override]
|
||||
|
||||
class A3:
|
||||
def method(self) -> Self: ...
|
||||
|
||||
class B3(A3):
|
||||
def method(self) -> Self: ... # fine
|
||||
|
||||
class C3(A3):
|
||||
# TODO: should this be allowed?
|
||||
# Mypy/pyright/pyrefly all allow it,
|
||||
# but conceptually it seems similar to `B4.method` below,
|
||||
# which mypy/pyrefly agree is a Liskov violation
|
||||
# (pyright disagrees as of 20/11/2025: https://github.com/microsoft/pyright/issues/11128)
|
||||
# when called on a subclass, `C3.method()` will not return an
|
||||
# instance of that subclass
|
||||
def method(self) -> C3: ...
|
||||
|
||||
class D3(A3):
|
||||
def method(self: Self) -> Self: ... # fine
|
||||
|
||||
class E3(A3):
|
||||
def method(self: E3) -> Self: ... # fine
|
||||
|
||||
class F3(A3):
|
||||
def method(self: A3) -> Self: ... # fine
|
||||
|
||||
class G3(A3):
|
||||
def method(self: object) -> Self: ... # fine
|
||||
|
||||
class H3(A3):
|
||||
# TODO: we should emit `invalid-method-override` here
|
||||
# (`A3.method()` can be called on any instance of `A3`,
|
||||
# but `H3.method()` can only be called on objects that are
|
||||
# instances of `str`)
|
||||
def method(self: str) -> Self: ...
|
||||
|
||||
class I3(A3):
|
||||
# TODO: we should emit `invalid-method-override` here
|
||||
# (`I3.method()` cannot be called with any inhabited type!)
|
||||
def method(self: Never) -> Self: ...
|
||||
|
||||
class A4:
|
||||
def method[T: int](self, x: T) -> T: ...
|
||||
|
||||
class B4(A4):
|
||||
# TODO: we should emit `invalid-method-override` here.
|
||||
# `A4.method` promises that if it is passed a `bool`, it will return a `bool`,
|
||||
# but this is not necessarily true for `B4.method`: if passed a `bool`,
|
||||
# it could return a non-`bool` `int`!
|
||||
def method(self, x: int) -> int: ...
|
||||
```
|
||||
|
||||
## Generic methods on generic classes work as expected
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```pyi
|
||||
from typing import Never
|
||||
|
||||
class A[T]:
|
||||
def method[S](self, x: T, y: S) -> S: ...
|
||||
|
||||
class B[T](A[T]):
|
||||
def method[S](self, x: T, y: S) -> S: ... # fine
|
||||
|
||||
class C(A[int]):
|
||||
def method[S](self, x: int, y: S) -> S: ... # fine
|
||||
|
||||
class D[T](A[T]):
|
||||
def method[S](self, x: object, y: S) -> S: ... # fine
|
||||
|
||||
class E(A[int]):
|
||||
def method[S](self, x: object, y: S) -> S: ... # fine
|
||||
|
||||
class F(A[int]):
|
||||
def method(self, x: object, y: object) -> Never: ... # fine
|
||||
|
||||
class A2[T]:
|
||||
def method(self, x: T, y: int) -> int: ...
|
||||
|
||||
class B2[T](A2[T]):
|
||||
def method[S](self, x: T, y: S) -> S: ... # fine
|
||||
```
|
||||
|
||||
## Fully qualified names are used in diagnostics where appropriate
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
class A:
|
||||
def foo(self, x): ...
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
import a
|
||||
|
||||
class A(a.A):
|
||||
def foo(self, y): ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
## Excluded methods
|
||||
|
||||
Certain special constructor methods are excluded from Liskov checks. None of the following classes
|
||||
cause us to emit any errors, therefore:
|
||||
|
||||
```toml
|
||||
# This is so that the dataclasses machinery will generate `__replace__` methods for us
|
||||
# (the synthesized `__replace__` methods should not be reported as invalid overrides!)
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```pyi
|
||||
from dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
class Grandparent: ...
|
||||
class Parent(Grandparent):
|
||||
def __new__(cls, x: int) -> Self: ...
|
||||
def __init__(self, x: int) -> None: ...
|
||||
|
||||
class Child(Parent):
|
||||
def __new__(cls, x: str, y: str) -> Self: ...
|
||||
def __init__(self, x: str, y: str) -> Self: ...
|
||||
|
||||
@dataclass(init=False)
|
||||
class DataSuper:
|
||||
x: int
|
||||
|
||||
def __post_init__(self, x: int) -> None:
|
||||
self.x = x
|
||||
|
||||
@dataclass(init=False)
|
||||
class DataSub(DataSuper):
|
||||
y: str
|
||||
|
||||
def __post_init__(self, x: int, y: str) -> None:
|
||||
self.y = y
|
||||
super().__post_init__(x)
|
||||
```
|
||||
|
||||
## Edge case: function defined in another module and then assigned in a class body
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`foo.pyi`:
|
||||
|
||||
```pyi
|
||||
def x(self, y: str): ...
|
||||
```
|
||||
|
||||
`bar.pyi`:
|
||||
|
||||
```pyi
|
||||
import foo
|
||||
|
||||
class A:
|
||||
def x(self, y: int): ...
|
||||
|
||||
class B(A):
|
||||
x = foo.x # error: [invalid-method-override]
|
||||
|
||||
class C:
|
||||
x = foo.x
|
||||
|
||||
class D(C):
|
||||
def x(self, y: int): ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
## Bad override of `__eq__`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class Bad:
|
||||
x: int
|
||||
def __eq__(self, other: "Bad") -> bool: # error: [invalid-method-override]
|
||||
return self.x == other.x
|
||||
```
|
||||
|
||||
## Synthesized methods
|
||||
|
||||
`NamedTuple` classes and dataclasses both have methods generated at runtime that do not have
|
||||
source-code definitions. There are several scenarios to consider here:
|
||||
|
||||
1. A synthesized method on a superclass is overridden by a "normal" (not synthesized) method on a
|
||||
subclass
|
||||
1. A "normal" method on a superclass is overridden by a synthesized method on a subclass
|
||||
1. A synthesized method on a superclass is overridden by a synthesized method on a subclass
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```pyi
|
||||
from dataclasses import dataclass
|
||||
from typing import NamedTuple
|
||||
|
||||
@dataclass(order=True)
|
||||
class Foo:
|
||||
x: int
|
||||
|
||||
class Bar(Foo):
|
||||
def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
|
||||
|
||||
# TODO: specifying `order=True` on the subclass means that a `__lt__` method is
|
||||
# generated that is incompatible with the generated `__lt__` method on the superclass.
|
||||
# We could consider detecting this and emitting a diagnostic, though maybe it shouldn't
|
||||
# be `invalid-method-override` since we'd emit it on the class definition rather than
|
||||
# on any method definition. Note also that no other type checker complains about this
|
||||
# as of 2025-11-21.
|
||||
@dataclass(order=True)
|
||||
class Bar2(Foo):
|
||||
y: str
|
||||
|
||||
# TODO: Although this class does not override any methods of `Foo`, the design of the
|
||||
# `order=True` stdlib dataclasses feature itself arguably violates the Liskov Substitution
|
||||
# Principle! Instances of `Bar3` cannot be substituted wherever an instance of `Foo` is
|
||||
# expected, because the generated `__lt__` method on `Foo` raises an error unless the r.h.s.
|
||||
# and `l.h.s.` have exactly the same `__class__` (it does not permit instances of `Foo` to
|
||||
# be compared with instances of subclasses of `Foo`).
|
||||
#
|
||||
# Many users would probably like their type checkers to alert them to cases where instances
|
||||
# of subclasses cannot be substituted for instances of superclasses, as this violates many
|
||||
# assumptions a type checker will make and makes it likely that a type checker will fail to
|
||||
# catch type errors elsewhere in the user's code. We could therefore consider treating all
|
||||
# `order=True` dataclasses as implicitly `@final` in order to enforce soundness. However,
|
||||
# this probably shouldn't be reported with the same error code as Liskov violations, since
|
||||
# the error does not stem from any method signatures written by the user. The example is
|
||||
# only included here for completeness.
|
||||
#
|
||||
# Note that no other type checker catches this error as of 2025-11-21.
|
||||
class Bar3(Foo): ...
|
||||
|
||||
class Eggs:
|
||||
def __lt__(self, other: Eggs) -> bool: ...
|
||||
|
||||
# TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
|
||||
# We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
|
||||
# diagnostic here but pyright and pyrefly do not.
|
||||
@dataclass(order=True)
|
||||
class Ham(Eggs):
|
||||
x: int
|
||||
|
||||
class Baz(NamedTuple):
|
||||
x: int
|
||||
|
||||
class Spam(Baz):
|
||||
def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
## Staticmethods and classmethods
|
||||
|
||||
Methods decorated with `@staticmethod` or `@classmethod` are checked in much the same way as other
|
||||
methods.
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```pyi
|
||||
class Parent:
|
||||
def instance_method(self, x: int) -> int: ...
|
||||
@classmethod
|
||||
def class_method(cls, x: int) -> int: ...
|
||||
@staticmethod
|
||||
def static_method(x: int) -> int: ...
|
||||
|
||||
class BadChild1(Parent):
|
||||
@staticmethod
|
||||
def instance_method(self, x: int) -> int: ... # error: [invalid-method-override]
|
||||
# TODO: we should emit `invalid-method-override` here.
|
||||
# Although the method has the same signature as `Parent.class_method`
|
||||
# when accessed on instances, it does not have the same signature as
|
||||
# `Parent.class_method` when accessed on the class object itself
|
||||
def class_method(cls, x: int) -> int: ...
|
||||
def static_method(x: int) -> int: ... # error: [invalid-method-override]
|
||||
|
||||
class BadChild2(Parent):
|
||||
# TODO: we should emit `invalid-method-override` here.
|
||||
# Although the method has the same signature as `Parent.class_method`
|
||||
# when accessed on instances, it does not have the same signature as
|
||||
# `Parent.class_method` when accessed on the class object itself.
|
||||
#
|
||||
# Note that whereas `BadChild1.class_method` is reported as a Liskov violation by
|
||||
# mypy, pyright and pyrefly, pyright is the only one of those three to report a
|
||||
# Liskov violation on this method as of 2025-11-23.
|
||||
@classmethod
|
||||
def instance_method(self, x: int) -> int: ...
|
||||
@staticmethod
|
||||
def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
|
||||
@classmethod
|
||||
def static_method(x: int) -> int: ... # error: [invalid-method-override]
|
||||
|
||||
class BadChild3(Parent):
|
||||
@classmethod
|
||||
def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
|
||||
@staticmethod
|
||||
def static_method(x: bool) -> object: ... # error: [invalid-method-override]
|
||||
|
||||
class GoodChild1(Parent):
|
||||
@classmethod
|
||||
def class_method(cls, x: int) -> int: ...
|
||||
@staticmethod
|
||||
def static_method(x: int) -> int: ...
|
||||
|
||||
class GoodChild2(Parent):
|
||||
@classmethod
|
||||
def class_method(cls, x: object) -> bool: ...
|
||||
@staticmethod
|
||||
def static_method(x: object) -> bool: ...
|
||||
```
|
||||
314
crates/ty_python_semantic/resources/mdtest/override.md
Normal file
314
crates/ty_python_semantic/resources/mdtest/override.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# `typing.override`
|
||||
|
||||
## Basics
|
||||
|
||||
Decorating a method with `typing.override` decorator is an explicit indication to a type checker
|
||||
that the method is intended to override a method on a superclass. If the decorated method does not
|
||||
in fact override anything, a type checker should report a diagnostic on that method.
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```pyi
|
||||
from typing_extensions import override, Callable, TypeVar
|
||||
|
||||
def lossy_decorator(fn: Callable) -> Callable: ...
|
||||
|
||||
class A:
|
||||
@override
|
||||
def __repr__(self): ... # fine: overrides `object.__repr__`
|
||||
|
||||
class Parent:
|
||||
def foo(self): ...
|
||||
@property
|
||||
def my_property1(self) -> int: ...
|
||||
@property
|
||||
def my_property2(self) -> int: ...
|
||||
baz = None
|
||||
@classmethod
|
||||
def class_method1(cls) -> int: ...
|
||||
@staticmethod
|
||||
def static_method1() -> int: ...
|
||||
@classmethod
|
||||
def class_method2(cls) -> int: ...
|
||||
@staticmethod
|
||||
def static_method2() -> int: ...
|
||||
@lossy_decorator
|
||||
def decorated_1(self): ...
|
||||
@lossy_decorator
|
||||
def decorated_2(self): ...
|
||||
@lossy_decorator
|
||||
def decorated_3(self): ...
|
||||
|
||||
class Child(Parent):
|
||||
@override
|
||||
def foo(self): ... # fine: overrides `Parent.foo`
|
||||
@property
|
||||
@override
|
||||
def my_property1(self) -> int: ... # fine: overrides `Parent.my_property1`
|
||||
@override
|
||||
@property
|
||||
def my_property2(self) -> int: ... # fine: overrides `Parent.my_property2`
|
||||
@override
|
||||
def baz(self): ... # fine: overrides `Parent.baz`
|
||||
@classmethod
|
||||
@override
|
||||
def class_method1(cls) -> int: ... # fine: overrides `Parent.class_method1`
|
||||
@staticmethod
|
||||
@override
|
||||
def static_method1() -> int: ... # fine: overrides `Parent.static_method1`
|
||||
@override
|
||||
@classmethod
|
||||
def class_method2(cls) -> int: ... # fine: overrides `Parent.class_method2`
|
||||
@override
|
||||
@staticmethod
|
||||
def static_method2() -> int: ... # fine: overrides `Parent.static_method2`
|
||||
@override
|
||||
def decorated_1(self): ... # fine: overrides `Parent.decorated_1`
|
||||
@override
|
||||
@lossy_decorator
|
||||
def decorated_2(self): ... # fine: overrides `Parent.decorated_2`
|
||||
@lossy_decorator
|
||||
@override
|
||||
def decorated_3(self): ... # fine: overrides `Parent.decorated_3`
|
||||
|
||||
class OtherChild(Parent): ...
|
||||
|
||||
class Grandchild(OtherChild):
|
||||
@override
|
||||
def foo(self): ... # fine: overrides `Parent.foo`
|
||||
@override
|
||||
@property
|
||||
def bar(self) -> int: ... # fine: overrides `Parent.bar`
|
||||
@override
|
||||
def baz(self): ... # fine: overrides `Parent.baz`
|
||||
@classmethod
|
||||
@override
|
||||
def class_method1(cls) -> int: ... # fine: overrides `Parent.class_method1`
|
||||
@staticmethod
|
||||
@override
|
||||
def static_method1() -> int: ... # fine: overrides `Parent.static_method1`
|
||||
@override
|
||||
@classmethod
|
||||
def class_method2(cls) -> int: ... # fine: overrides `Parent.class_method2`
|
||||
@override
|
||||
@staticmethod
|
||||
def static_method2() -> int: ... # fine: overrides `Parent.static_method2`
|
||||
@override
|
||||
def decorated_1(self): ... # fine: overrides `Parent.decorated_1`
|
||||
@override
|
||||
@lossy_decorator
|
||||
def decorated_2(self): ... # fine: overrides `Parent.decorated_2`
|
||||
@lossy_decorator
|
||||
@override
|
||||
def decorated_3(self): ... # fine: overrides `Parent.decorated_3`
|
||||
|
||||
class Invalid:
|
||||
@override
|
||||
def ___reprrr__(self): ... # error: [invalid-explicit-override]
|
||||
@override
|
||||
@classmethod
|
||||
def foo(self): ... # error: [invalid-explicit-override]
|
||||
@classmethod
|
||||
@override
|
||||
def bar(self): ... # error: [invalid-explicit-override]
|
||||
@staticmethod
|
||||
@override
|
||||
def baz(): ... # error: [invalid-explicit-override]
|
||||
@override
|
||||
@staticmethod
|
||||
def eggs(): ... # error: [invalid-explicit-override]
|
||||
@property
|
||||
@override
|
||||
def bad_property1(self) -> int: ... # TODO: should emit `invalid-explicit-override` here
|
||||
@override
|
||||
@property
|
||||
def bad_property2(self) -> int: ... # TODO: should emit `invalid-explicit-override` here
|
||||
@lossy_decorator
|
||||
@override
|
||||
def lossy(self): ... # TODO: should emit `invalid-explicit-override` here
|
||||
@override
|
||||
@lossy_decorator
|
||||
def lossy2(self): ... # TODO: should emit `invalid-explicit-override` here
|
||||
|
||||
# TODO: all overrides in this class should cause us to emit *Liskov* violations,
|
||||
# but not `@override` violations
|
||||
class LiskovViolatingButNotOverrideViolating(Parent):
|
||||
@override
|
||||
@property
|
||||
def foo(self) -> int: ...
|
||||
@override
|
||||
def my_property1(self) -> int: ...
|
||||
@staticmethod
|
||||
@override
|
||||
def class_method1() -> int: ...
|
||||
@classmethod
|
||||
@override
|
||||
def static_method1(cls) -> int: ...
|
||||
|
||||
# Diagnostic edge case: `override` is very far away from the method definition in the source code:
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def identity(x: T) -> T: ...
|
||||
|
||||
class Foo:
|
||||
@override
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
def bar(self): ... # error: [invalid-explicit-override]
|
||||
```
|
||||
|
||||
## Overloads
|
||||
|
||||
The typing spec states that for an overloaded method, `@override` should only be applied to the
|
||||
implementation function. However, we nonetheless respect the decorator in this situation, even
|
||||
though we also emit `invalid-overload` on these methods.
|
||||
|
||||
```py
|
||||
from typing_extensions import override, overload
|
||||
|
||||
class Spam:
|
||||
@overload
|
||||
def foo(self, x: str) -> str: ...
|
||||
@overload
|
||||
def foo(self, x: int) -> int: ...
|
||||
@override
|
||||
def foo(self, x: str | int) -> str | int: # error: [invalid-explicit-override]
|
||||
return x
|
||||
|
||||
@overload
|
||||
@override
|
||||
def bar(self, x: str) -> str: ...
|
||||
@overload
|
||||
@override
|
||||
def bar(self, x: int) -> int: ...
|
||||
@override
|
||||
# error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation"
|
||||
# error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation"
|
||||
# error: [invalid-explicit-override]
|
||||
def bar(self, x: str | int) -> str | int:
|
||||
return x
|
||||
|
||||
@overload
|
||||
@override
|
||||
def baz(self, x: str) -> str: ...
|
||||
@overload
|
||||
def baz(self, x: int) -> int: ...
|
||||
# error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation"
|
||||
# error: [invalid-explicit-override]
|
||||
def baz(self, x: str | int) -> str | int:
|
||||
return x
|
||||
```
|
||||
|
||||
In a stub file, `@override` should always be applied to the first overload. Even if it isn't, we
|
||||
always emit `invalid-explicit-override` diagnostics on the first overload.
|
||||
|
||||
`module.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing_extensions import override, overload
|
||||
|
||||
class Spam:
|
||||
@overload
|
||||
def foo(self, x: str) -> str: ... # error: [invalid-explicit-override]
|
||||
@overload
|
||||
@override
|
||||
# error: [invalid-overload] "`@override` decorator should be applied only to the first overload"
|
||||
def foo(self, x: int) -> int: ...
|
||||
|
||||
@overload
|
||||
@override
|
||||
def bar(self, x: str) -> str: ... # error: [invalid-explicit-override]
|
||||
@overload
|
||||
@override
|
||||
# error: [invalid-overload] "`@override` decorator should be applied only to the first overload"
|
||||
def bar(self, x: int) -> int: ...
|
||||
|
||||
@overload
|
||||
@override
|
||||
def baz(self, x: str) -> str: ... # error: [invalid-explicit-override]
|
||||
@overload
|
||||
def baz(self, x: int) -> int: ...
|
||||
```
|
||||
|
||||
## Classes inheriting from `Any`
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, override
|
||||
from does_not_exist import SomethingUnknown # error: [unresolved-import]
|
||||
|
||||
class Parent1(Any): ...
|
||||
class Parent2(SomethingUnknown): ...
|
||||
|
||||
class Child1(Parent1):
|
||||
@override
|
||||
def bar(self): ... # fine
|
||||
|
||||
class Child2(Parent2):
|
||||
@override
|
||||
def bar(self): ... # fine
|
||||
```
|
||||
|
||||
## Override of a synthesized method
|
||||
|
||||
```pyi
|
||||
from typing_extensions import NamedTuple, TypedDict, override, Any, Self
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(order=True)
|
||||
class ParentDataclass:
|
||||
x: int
|
||||
|
||||
class Child(ParentDataclass):
|
||||
@override
|
||||
def __lt__(self, other: ParentDataclass) -> bool: ... # fine
|
||||
|
||||
class MyNamedTuple(NamedTuple):
|
||||
x: int
|
||||
|
||||
@override
|
||||
# TODO: this raises an exception at runtime (which we should emit a diagnostic for).
|
||||
# It shouldn't be an `invalid-explicit-override` diagnostic, however.
|
||||
def _asdict(self, /) -> dict[str, Any]: ...
|
||||
|
||||
class MyNamedTupleParent(NamedTuple):
|
||||
x: int
|
||||
|
||||
class MyNamedTupleChild(MyNamedTupleParent):
|
||||
@override
|
||||
def _asdict(self, /) -> dict[str, Any]: ... # fine
|
||||
|
||||
class MyTypedDict(TypedDict):
|
||||
x: int
|
||||
|
||||
@override
|
||||
# TODO: it's invalid to define a method on a `TypedDict` class,
|
||||
# so we should emit a diagnostic here.
|
||||
# It shouldn't be an `invalid-explicit-override` diagnostic, however.
|
||||
def copy(self) -> Self: ...
|
||||
|
||||
class Grandparent(Any): ...
|
||||
|
||||
class Parent(Grandparent, NamedTuple): # error: [invalid-named-tuple]
|
||||
x: int
|
||||
|
||||
class Child(Parent):
|
||||
@override
|
||||
def foo(self): ... # fine because `Any` is in the MRO
|
||||
```
|
||||
@@ -144,17 +144,46 @@ def _(x: IntOrStr):
|
||||
## Cyclic
|
||||
|
||||
```py
|
||||
from typing import TypeAlias
|
||||
from typing import TypeAlias, TypeVar, Union
|
||||
from types import UnionType
|
||||
|
||||
RecursiveTuple: TypeAlias = tuple[int | "RecursiveTuple", str]
|
||||
|
||||
def _(rec: RecursiveTuple):
|
||||
# TODO should be `tuple[int | RecursiveTuple, str]`
|
||||
reveal_type(rec) # revealed: tuple[Divergent, str]
|
||||
|
||||
RecursiveHomogeneousTuple: TypeAlias = tuple[int | "RecursiveHomogeneousTuple", ...]
|
||||
|
||||
def _(rec: RecursiveHomogeneousTuple):
|
||||
# TODO should be `tuple[int | RecursiveHomogeneousTuple, ...]`
|
||||
reveal_type(rec) # revealed: tuple[Divergent, ...]
|
||||
|
||||
ClassInfo: TypeAlias = type | UnionType | tuple["ClassInfo", ...]
|
||||
reveal_type(ClassInfo) # revealed: types.UnionType
|
||||
|
||||
def my_isinstance(obj: object, classinfo: ClassInfo) -> bool:
|
||||
# TODO should be `type | UnionType | tuple[ClassInfo, ...]`
|
||||
reveal_type(classinfo) # revealed: type | UnionType | tuple[Divergent, ...]
|
||||
return isinstance(obj, classinfo)
|
||||
|
||||
K = TypeVar("K")
|
||||
V = TypeVar("V")
|
||||
NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]]
|
||||
|
||||
def _(nested: NestedDict[str, int]):
|
||||
# TODO should be `dict[str, int | NestedDict[str, int]]`
|
||||
reveal_type(nested) # revealed: @Todo(specialized generic alias in type expression)
|
||||
|
||||
my_isinstance(1, int)
|
||||
my_isinstance(1, int | str)
|
||||
my_isinstance(1, (int, str))
|
||||
my_isinstance(1, (int, (str, float)))
|
||||
my_isinstance(1, (int, (str | float)))
|
||||
# error: [invalid-argument-type]
|
||||
my_isinstance(1, 1)
|
||||
# TODO should be an invalid-argument-type error
|
||||
my_isinstance(1, (int, (str, 1)))
|
||||
```
|
||||
|
||||
## Conditionally imported on Python < 3.10
|
||||
|
||||
@@ -106,6 +106,29 @@ def _(flag: bool):
|
||||
```py
|
||||
type ListOrSet[T] = list[T] | set[T]
|
||||
reveal_type(ListOrSet.__type_params__) # revealed: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
type Tuple1[T] = tuple[T]
|
||||
|
||||
def _(cond: bool):
|
||||
Generic = ListOrSet if cond else Tuple1
|
||||
|
||||
def _(x: Generic[int]):
|
||||
reveal_type(x) # revealed: list[int] | set[int] | tuple[int]
|
||||
|
||||
try:
|
||||
class Foo[T]:
|
||||
x: T
|
||||
def foo(self) -> T:
|
||||
return self.x
|
||||
|
||||
...
|
||||
except Exception:
|
||||
class Foo[T]:
|
||||
x: T
|
||||
def foo(self) -> T:
|
||||
return self.x
|
||||
|
||||
def f(x: Foo[int]):
|
||||
reveal_type(x.foo()) # revealed: int
|
||||
```
|
||||
|
||||
## In unions and intersections
|
||||
@@ -244,6 +267,47 @@ def f(x: IntOr, y: OrInt):
|
||||
reveal_type(x) # revealed: Never
|
||||
if not isinstance(y, int):
|
||||
reveal_type(y) # revealed: Never
|
||||
|
||||
# error: [cyclic-type-alias-definition] "Cyclic definition of `Itself`"
|
||||
type Itself = Itself
|
||||
|
||||
def foo(
|
||||
# this is a very strange thing to do, but this is a regression test to ensure it doesn't panic
|
||||
Itself: Itself,
|
||||
):
|
||||
x: Itself
|
||||
reveal_type(Itself) # revealed: Divergent
|
||||
|
||||
# A type alias defined with invalid recursion behaves as a dynamic type.
|
||||
foo(42)
|
||||
foo("hello")
|
||||
|
||||
# error: [cyclic-type-alias-definition] "Cyclic definition of `A`"
|
||||
type A = B
|
||||
# error: [cyclic-type-alias-definition] "Cyclic definition of `B`"
|
||||
type B = A
|
||||
|
||||
def bar(B: B):
|
||||
x: B
|
||||
reveal_type(B) # revealed: Divergent
|
||||
|
||||
# error: [cyclic-type-alias-definition] "Cyclic definition of `G`"
|
||||
type G[T] = G[T]
|
||||
# error: [cyclic-type-alias-definition] "Cyclic definition of `H`"
|
||||
type H[T] = I[T]
|
||||
# error: [cyclic-type-alias-definition] "Cyclic definition of `I`"
|
||||
type I[T] = H[T]
|
||||
|
||||
# It's not possible to create an element of this type, but it's not an error for now
|
||||
type DirectRecursiveList[T] = list[DirectRecursiveList[T]]
|
||||
|
||||
# TODO: this should probably be a cyclic-type-alias-definition error
|
||||
type Foo[T] = list[T] | Bar[T]
|
||||
type Bar[T] = int | Foo[T]
|
||||
|
||||
def _(x: Bar[int]):
|
||||
# TODO: should be `int | list[int]`
|
||||
reveal_type(x) # revealed: int | list[int] | Any
|
||||
```
|
||||
|
||||
### With legacy generic
|
||||
@@ -327,7 +391,7 @@ class C(P[T]):
|
||||
pass
|
||||
|
||||
reveal_type(C[int]()) # revealed: C[int]
|
||||
reveal_type(C()) # revealed: C[Divergent]
|
||||
reveal_type(C()) # revealed: C[C[Divergent]]
|
||||
```
|
||||
|
||||
### Union inside generic
|
||||
|
||||
@@ -2003,6 +2003,7 @@ python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
from typing_extensions import TypeVar, Self, Protocol
|
||||
from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of
|
||||
|
||||
@@ -2094,6 +2095,13 @@ class NominalReturningSelfNotGeneric:
|
||||
def g(self) -> "NominalReturningSelfNotGeneric":
|
||||
return self
|
||||
|
||||
@final
|
||||
class Other: ...
|
||||
|
||||
class NominalReturningOtherClass:
|
||||
def g(self) -> Other:
|
||||
raise NotImplementedError
|
||||
|
||||
# TODO: should pass
|
||||
static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error]
|
||||
|
||||
@@ -2112,8 +2120,7 @@ static_assert(not is_assignable_to(NominalLegacy, UsesSelf))
|
||||
static_assert(not is_assignable_to(NominalWithSelf, NewStyleFunctionScoped))
|
||||
static_assert(not is_assignable_to(NominalWithSelf, LegacyFunctionScoped))
|
||||
static_assert(is_assignable_to(NominalWithSelf, UsesSelf))
|
||||
# TODO: should pass
|
||||
static_assert(is_subtype_of(NominalWithSelf, UsesSelf)) # error: [static-assert-error]
|
||||
static_assert(is_subtype_of(NominalWithSelf, UsesSelf))
|
||||
|
||||
# TODO: these should pass
|
||||
static_assert(not is_assignable_to(NominalNotGeneric, NewStyleFunctionScoped)) # error: [static-assert-error]
|
||||
@@ -2126,6 +2133,8 @@ static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, LegacyFunctio
|
||||
# TODO: should pass
|
||||
static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, UsesSelf)) # error: [static-assert-error]
|
||||
|
||||
static_assert(not is_assignable_to(NominalReturningOtherClass, UsesSelf))
|
||||
|
||||
# These test cases are taken from the typing conformance suite:
|
||||
class ShapeProtocolImplicitSelf(Protocol):
|
||||
def set_scale(self, scale: float) -> Self: ...
|
||||
@@ -3069,18 +3078,15 @@ from typing import Protocol
|
||||
from ty_extensions import static_assert, is_subtype_of, is_equivalent_to, is_disjoint_from
|
||||
|
||||
class HasRepr(Protocol):
|
||||
# TODO: we should emit a diagnostic here complaining about a Liskov violation
|
||||
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasRepr`)
|
||||
# error: [invalid-method-override]
|
||||
def __repr__(self) -> object: ...
|
||||
|
||||
class HasReprRecursive(Protocol):
|
||||
# TODO: we should emit a diagnostic here complaining about a Liskov violation
|
||||
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasReprRecursive`)
|
||||
# error: [invalid-method-override]
|
||||
def __repr__(self) -> "HasReprRecursive": ...
|
||||
|
||||
class HasReprRecursiveAndFoo(Protocol):
|
||||
# TODO: we should emit a diagnostic here complaining about a Liskov violation
|
||||
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasReprRecursiveAndFoo`)
|
||||
# error: [invalid-method-override]
|
||||
def __repr__(self) -> "HasReprRecursiveAndFoo": ...
|
||||
foo: int
|
||||
|
||||
@@ -3180,6 +3186,33 @@ from ty_extensions import reveal_protocol_interface
|
||||
reveal_protocol_interface(Foo)
|
||||
```
|
||||
|
||||
## Known panics
|
||||
|
||||
### Protocols generic over TypeVars bound to forward references
|
||||
|
||||
This test currently panics because the `ClassLiteral::explicit_bases` query fails to converge. See
|
||||
issue <https://github.com/astral-sh/ty/issues/1587>.
|
||||
|
||||
<!-- expect-panic: execute: too many cycle iterations -->
|
||||
|
||||
```py
|
||||
from typing import Any, Protocol, TypeVar
|
||||
|
||||
T1 = TypeVar("T1", bound="A2[Any]")
|
||||
T2 = TypeVar("T2", bound="A1[Any]")
|
||||
T3 = TypeVar("T3", bound="B2[Any]")
|
||||
T4 = TypeVar("T4", bound="B1[Any]")
|
||||
|
||||
class A1(Protocol[T1]):
|
||||
def get_x(self): ...
|
||||
|
||||
class A2(Protocol[T2]):
|
||||
def get_y(self): ...
|
||||
|
||||
class B1(A1[T3], Protocol[T3]): ...
|
||||
class B2(A2[T4], Protocol[T4]): ...
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
Add tests for:
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
Regression test for <https://github.com/astral-sh/ty/issues/1377>.
|
||||
|
||||
The code is an excerpt from <https://github.com/Gobot1234/steam.py> that is minimal enough to
|
||||
trigger the iteration count mismatch bug in Salsa.
|
||||
|
||||
<!-- expect-panic: execute: too many cycle iterations -->
|
||||
The code is an excerpt from <https://github.com/Gobot1234/steam.py>.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
|
||||
@@ -32,7 +32,8 @@ error[unresolved-attribute]: Module `datetime` has no member `UTC`
|
||||
5 | # error: [unresolved-attribute]
|
||||
6 | reveal_type(datetime.fakenotreal) # revealed: Unknown
|
||||
|
|
||||
info: Python 3.10 was assumed when accessing `UTC` because it was specified on the command line
|
||||
info: The member may be available on other Python versions or platforms
|
||||
info: Python 3.10 was assumed when resolving the `UTC` attribute because it was specified on the command line
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
@@ -30,39 +30,39 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
|
||||
1 | import foo
|
||||
2 | import baz
|
||||
3 |
|
||||
4 | # error: [unresolved-attribute]
|
||||
5 | reveal_type(foo.bar) # revealed: Unknown
|
||||
6 | # error: [unresolved-attribute]
|
||||
7 | reveal_type(baz.bar) # revealed: Unknown
|
||||
4 | # error: [possibly-missing-attribute]
|
||||
5 | reveal_type(foo.bar) # revealed: <module 'foo.bar'>
|
||||
6 | # error: [possibly-missing-attribute]
|
||||
7 | reveal_type(baz.bar) # revealed: <module 'baz.bar'>
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[unresolved-attribute]: Submodule `bar` may not be available as an attribute on module `foo`
|
||||
warning[possibly-missing-attribute]: Submodule `bar` may not be available as an attribute on module `foo`
|
||||
--> src/main.py:5:13
|
||||
|
|
||||
4 | # error: [unresolved-attribute]
|
||||
5 | reveal_type(foo.bar) # revealed: Unknown
|
||||
4 | # error: [possibly-missing-attribute]
|
||||
5 | reveal_type(foo.bar) # revealed: <module 'foo.bar'>
|
||||
| ^^^^^^^
|
||||
6 | # error: [unresolved-attribute]
|
||||
7 | reveal_type(baz.bar) # revealed: Unknown
|
||||
6 | # error: [possibly-missing-attribute]
|
||||
7 | reveal_type(baz.bar) # revealed: <module 'baz.bar'>
|
||||
|
|
||||
help: Consider explicitly importing `foo.bar`
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
info: rule `possibly-missing-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[unresolved-attribute]: Submodule `bar` may not be available as an attribute on module `baz`
|
||||
warning[possibly-missing-attribute]: Submodule `bar` may not be available as an attribute on module `baz`
|
||||
--> src/main.py:7:13
|
||||
|
|
||||
5 | reveal_type(foo.bar) # revealed: Unknown
|
||||
6 | # error: [unresolved-attribute]
|
||||
7 | reveal_type(baz.bar) # revealed: Unknown
|
||||
5 | reveal_type(foo.bar) # revealed: <module 'foo.bar'>
|
||||
6 | # error: [possibly-missing-attribute]
|
||||
7 | reveal_type(baz.bar) # revealed: <module 'baz.bar'>
|
||||
| ^^^^^^^
|
||||
|
|
||||
help: Consider explicitly importing `baz.bar`
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
info: rule `possibly-missing-attribute` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
@@ -34,29 +34,27 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-type-form]: Variable of type `<module 'datetime'>` is not allowed in a type expression
|
||||
error[invalid-type-form]: Module `datetime` is not valid in a type expression
|
||||
--> src/foo.py:3:10
|
||||
|
|
||||
1 | import datetime
|
||||
2 |
|
||||
3 | def f(x: datetime): ... # error: [invalid-type-form]
|
||||
| ^^^^^^^^
|
||||
| ^^^^^^^^ Did you mean to use the module's member `datetime.datetime`?
|
||||
|
|
||||
info: Did you mean to use the module's member `datetime.datetime` instead?
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-form]: Variable of type `<module 'PIL.Image'>` is not allowed in a type expression
|
||||
error[invalid-type-form]: Module `PIL.Image` is not valid in a type expression
|
||||
--> src/bar.py:3:10
|
||||
|
|
||||
1 | from PIL import Image
|
||||
2 |
|
||||
3 | def g(x: Image): ... # error: [invalid-type-form]
|
||||
| ^^^^^
|
||||
| ^^^^^ Did you mean to use the module's member `Image.Image`?
|
||||
|
|
||||
info: Did you mean to use the module's member `Image.Image` instead?
|
||||
info: rule `invalid-type-form` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
@@ -23,13 +23,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assi
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `tuple[Literal["a"], Literal["b"]]` is not assignable to `int`
|
||||
error[invalid-assignment]: Object of type `Literal["a"]` is not assignable to `int`
|
||||
--> src/mdtest_snippet.py:4:1
|
||||
|
|
||||
2 | y: str
|
||||
3 |
|
||||
4 | x, y = ("a", "b") # error: [invalid-assignment]
|
||||
| - ^^^^^^^^^^ Incompatible value of type `tuple[Literal["a"], Literal["b"]]`
|
||||
| - ^^^^^^^^^^ Incompatible value of type `Literal["a"]`
|
||||
| |
|
||||
| Declared type `int`
|
||||
5 |
|
||||
@@ -40,13 +40,13 @@ info: rule `invalid-assignment` is enabled by default
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `tuple[Literal[0], Literal[0]]` is not assignable to `str`
|
||||
error[invalid-assignment]: Object of type `Literal[0]` is not assignable to `str`
|
||||
--> src/mdtest_snippet.py:6:4
|
||||
|
|
||||
4 | x, y = ("a", "b") # error: [invalid-assignment]
|
||||
5 |
|
||||
6 | x, y = (0, 0) # error: [invalid-assignment]
|
||||
| - ^^^^^^ Incompatible value of type `tuple[Literal[0], Literal[0]]`
|
||||
| - ^^^^^^ Incompatible value of type `Literal[0]`
|
||||
| |
|
||||
| Declared type `str`
|
||||
|
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: liskov.md - The Liskov Substitution Principle - Bad override of `__eq__`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class Bad:
|
||||
2 | x: int
|
||||
3 | def __eq__(self, other: "Bad") -> bool: # error: [invalid-method-override]
|
||||
4 | return self.x == other.x
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `__eq__`
|
||||
--> src/mdtest_snippet.py:3:9
|
||||
|
|
||||
1 | class Bad:
|
||||
2 | x: int
|
||||
3 | def __eq__(self, other: "Bad") -> bool: # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `object.__eq__`
|
||||
4 | return self.x == other.x
|
||||
|
|
||||
::: stdlib/builtins.pyi:142:9
|
||||
|
|
||||
140 | def __setattr__(self, name: str, value: Any, /) -> None: ...
|
||||
141 | def __delattr__(self, name: str, /) -> None: ...
|
||||
142 | def __eq__(self, value: object, /) -> bool: ...
|
||||
| -------------------------------------- `object.__eq__` defined here
|
||||
143 | def __ne__(self, value: object, /) -> bool: ...
|
||||
144 | def __str__(self) -> str: ... # noqa: Y029
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
help: It is recommended for `__eq__` to work with arbitrary objects, for example:
|
||||
help
|
||||
help: def __eq__(self, other: object) -> bool:
|
||||
help: if not isinstance(other, Bad):
|
||||
help: return False
|
||||
help: return <logic to compare two `Bad` instances>
|
||||
help
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: liskov.md - The Liskov Substitution Principle - Edge case: function defined in another module and then assigned in a class body
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## foo.pyi
|
||||
|
||||
```
|
||||
1 | def x(self, y: str): ...
|
||||
```
|
||||
|
||||
## bar.pyi
|
||||
|
||||
```
|
||||
1 | import foo
|
||||
2 |
|
||||
3 | class A:
|
||||
4 | def x(self, y: int): ...
|
||||
5 |
|
||||
6 | class B(A):
|
||||
7 | x = foo.x # error: [invalid-method-override]
|
||||
8 |
|
||||
9 | class C:
|
||||
10 | x = foo.x
|
||||
11 |
|
||||
12 | class D(C):
|
||||
13 | def x(self, y: int): ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `x`
|
||||
--> src/bar.pyi:4:9
|
||||
|
|
||||
3 | class A:
|
||||
4 | def x(self, y: int): ...
|
||||
| --------------- `A.x` defined here
|
||||
5 |
|
||||
6 | class B(A):
|
||||
7 | x = foo.x # error: [invalid-method-override]
|
||||
| ^^^^^^^^^ Definition is incompatible with `A.x`
|
||||
8 |
|
||||
9 | class C:
|
||||
|
|
||||
::: src/foo.pyi:1:5
|
||||
|
|
||||
1 | def x(self, y: str): ...
|
||||
| --------------- Signature of `B.x`
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `x`
|
||||
--> src/bar.pyi:10:5
|
||||
|
|
||||
9 | class C:
|
||||
10 | x = foo.x
|
||||
| --------- `C.x` defined here
|
||||
11 |
|
||||
12 | class D(C):
|
||||
13 | def x(self, y: int): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^ Definition is incompatible with `C.x`
|
||||
|
|
||||
::: src/foo.pyi:1:5
|
||||
|
|
||||
1 | def x(self, y: str): ...
|
||||
| --------------- Signature of `C.x`
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: liskov.md - The Liskov Substitution Principle - Fully qualified names are used in diagnostics where appropriate
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## a.pyi
|
||||
|
||||
```
|
||||
1 | class A:
|
||||
2 | def foo(self, x): ...
|
||||
```
|
||||
|
||||
## b.pyi
|
||||
|
||||
```
|
||||
1 | import a
|
||||
2 |
|
||||
3 | class A(a.A):
|
||||
4 | def foo(self, y): ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `foo`
|
||||
--> src/b.pyi:4:9
|
||||
|
|
||||
3 | class A(a.A):
|
||||
4 | def foo(self, y): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^ Definition is incompatible with `a.A.foo`
|
||||
|
|
||||
::: src/a.pyi:2:9
|
||||
|
|
||||
1 | class A:
|
||||
2 | def foo(self, x): ...
|
||||
| ------------ `a.A.foo` defined here
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,331 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: liskov.md - The Liskov Substitution Principle - Method parameters
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.pyi
|
||||
|
||||
```
|
||||
1 | class Super:
|
||||
2 | def method(self, x: int, /): ...
|
||||
3 |
|
||||
4 | class Sub1(Super):
|
||||
5 | def method(self, x: int, /): ... # fine
|
||||
6 |
|
||||
7 | class Sub2(Super):
|
||||
8 | def method(self, x: object, /): ... # fine: `method` still accepts any argument of type `int`
|
||||
9 |
|
||||
10 | class Sub4(Super):
|
||||
11 | def method(self, x: int | str, /): ... # fine
|
||||
12 |
|
||||
13 | class Sub5(Super):
|
||||
14 | def method(self, x: int): ... # fine: `x` can still be passed positionally
|
||||
15 |
|
||||
16 | class Sub6(Super):
|
||||
17 | # fine: `method()` can still be called with just a single argument
|
||||
18 | def method(self, x: int, *args): ...
|
||||
19 |
|
||||
20 | class Sub7(Super):
|
||||
21 | def method(self, x: int, **kwargs): ... # fine
|
||||
22 |
|
||||
23 | class Sub8(Super):
|
||||
24 | def method(self, x: int, *args, **kwargs): ... # fine
|
||||
25 |
|
||||
26 | class Sub9(Super):
|
||||
27 | def method(self, x: int, extra_positional_arg=42, /): ... # fine
|
||||
28 |
|
||||
29 | class Sub10(Super):
|
||||
30 | def method(self, x: int, extra_pos_or_kw_arg=42): ... # fine
|
||||
31 |
|
||||
32 | class Sub11(Super):
|
||||
33 | def method(self, x: int, *, extra_kw_only_arg=42): ... # fine
|
||||
34 |
|
||||
35 | class Sub12(Super):
|
||||
36 | # Some calls permitted by the superclass are now no longer allowed
|
||||
37 | # (the method can no longer be passed any arguments!)
|
||||
38 | def method(self, /): ... # error: [invalid-method-override]
|
||||
39 |
|
||||
40 | class Sub13(Super):
|
||||
41 | # Some calls permitted by the superclass are now no longer allowed
|
||||
42 | # (the method can no longer be passed exactly one argument!)
|
||||
43 | def method(self, x, y, /): ... # error: [invalid-method-override]
|
||||
44 |
|
||||
45 | class Sub14(Super):
|
||||
46 | # Some calls permitted by the superclass are now no longer allowed
|
||||
47 | # (x can no longer be passed positionally!)
|
||||
48 | def method(self, /, *, x): ... # error: [invalid-method-override]
|
||||
49 |
|
||||
50 | class Sub15(Super):
|
||||
51 | # Some calls permitted by the superclass are now no longer allowed
|
||||
52 | # (x can no longer be passed any integer -- it now requires a bool!)
|
||||
53 | def method(self, x: bool, /): ... # error: [invalid-method-override]
|
||||
54 |
|
||||
55 | class Super2:
|
||||
56 | def method2(self, x): ...
|
||||
57 |
|
||||
58 | class Sub16(Super2):
|
||||
59 | def method2(self, x, /): ... # error: [invalid-method-override]
|
||||
60 |
|
||||
61 | class Sub17(Super2):
|
||||
62 | def method2(self, *, x): ... # error: [invalid-method-override]
|
||||
63 |
|
||||
64 | class Super3:
|
||||
65 | def method3(self, *, x): ...
|
||||
66 |
|
||||
67 | class Sub18(Super3):
|
||||
68 | def method3(self, x): ... # fine: `x` can still be used as a keyword argument
|
||||
69 |
|
||||
70 | class Sub19(Super3):
|
||||
71 | def method3(self, x, /): ... # error: [invalid-method-override]
|
||||
72 |
|
||||
73 | class Super4:
|
||||
74 | def method(self, *args: int, **kwargs: str): ...
|
||||
75 |
|
||||
76 | class Sub20(Super4):
|
||||
77 | def method(self, *args: object, **kwargs: object): ... # fine
|
||||
78 |
|
||||
79 | class Sub21(Super4):
|
||||
80 | def method(self, *args): ... # error: [invalid-method-override]
|
||||
81 |
|
||||
82 | class Sub22(Super4):
|
||||
83 | def method(self, **kwargs): ... # error: [invalid-method-override]
|
||||
84 |
|
||||
85 | class Sub23(Super4):
|
||||
86 | def method(self, x, *args, y, **kwargs): ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/mdtest_snippet.pyi:38:9
|
||||
|
|
||||
36 | # Some calls permitted by the superclass are now no longer allowed
|
||||
37 | # (the method can no longer be passed any arguments!)
|
||||
38 | def method(self, /): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
|
||||
39 |
|
||||
40 | class Sub13(Super):
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:2:9
|
||||
|
|
||||
1 | class Super:
|
||||
2 | def method(self, x: int, /): ...
|
||||
| ----------------------- `Super.method` defined here
|
||||
3 |
|
||||
4 | class Sub1(Super):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/mdtest_snippet.pyi:43:9
|
||||
|
|
||||
41 | # Some calls permitted by the superclass are now no longer allowed
|
||||
42 | # (the method can no longer be passed exactly one argument!)
|
||||
43 | def method(self, x, y, /): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
|
||||
44 |
|
||||
45 | class Sub14(Super):
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:2:9
|
||||
|
|
||||
1 | class Super:
|
||||
2 | def method(self, x: int, /): ...
|
||||
| ----------------------- `Super.method` defined here
|
||||
3 |
|
||||
4 | class Sub1(Super):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/mdtest_snippet.pyi:48:9
|
||||
|
|
||||
46 | # Some calls permitted by the superclass are now no longer allowed
|
||||
47 | # (x can no longer be passed positionally!)
|
||||
48 | def method(self, /, *, x): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
|
||||
49 |
|
||||
50 | class Sub15(Super):
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:2:9
|
||||
|
|
||||
1 | class Super:
|
||||
2 | def method(self, x: int, /): ...
|
||||
| ----------------------- `Super.method` defined here
|
||||
3 |
|
||||
4 | class Sub1(Super):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/mdtest_snippet.pyi:53:9
|
||||
|
|
||||
51 | # Some calls permitted by the superclass are now no longer allowed
|
||||
52 | # (x can no longer be passed any integer -- it now requires a bool!)
|
||||
53 | def method(self, x: bool, /): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
|
||||
54 |
|
||||
55 | class Super2:
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:2:9
|
||||
|
|
||||
1 | class Super:
|
||||
2 | def method(self, x: int, /): ...
|
||||
| ----------------------- `Super.method` defined here
|
||||
3 |
|
||||
4 | class Sub1(Super):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method2`
|
||||
--> src/mdtest_snippet.pyi:56:9
|
||||
|
|
||||
55 | class Super2:
|
||||
56 | def method2(self, x): ...
|
||||
| ---------------- `Super2.method2` defined here
|
||||
57 |
|
||||
58 | class Sub16(Super2):
|
||||
59 | def method2(self, x, /): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super2.method2`
|
||||
60 |
|
||||
61 | class Sub17(Super2):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method2`
|
||||
--> src/mdtest_snippet.pyi:62:9
|
||||
|
|
||||
61 | class Sub17(Super2):
|
||||
62 | def method2(self, *, x): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super2.method2`
|
||||
63 |
|
||||
64 | class Super3:
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:56:9
|
||||
|
|
||||
55 | class Super2:
|
||||
56 | def method2(self, x): ...
|
||||
| ---------------- `Super2.method2` defined here
|
||||
57 |
|
||||
58 | class Sub16(Super2):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method3`
|
||||
--> src/mdtest_snippet.pyi:71:9
|
||||
|
|
||||
70 | class Sub19(Super3):
|
||||
71 | def method3(self, x, /): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super3.method3`
|
||||
72 |
|
||||
73 | class Super4:
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:65:9
|
||||
|
|
||||
64 | class Super3:
|
||||
65 | def method3(self, *, x): ...
|
||||
| ------------------- `Super3.method3` defined here
|
||||
66 |
|
||||
67 | class Sub18(Super3):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/mdtest_snippet.pyi:80:9
|
||||
|
|
||||
79 | class Sub21(Super4):
|
||||
80 | def method(self, *args): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method`
|
||||
81 |
|
||||
82 | class Sub22(Super4):
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:74:9
|
||||
|
|
||||
73 | class Super4:
|
||||
74 | def method(self, *args: int, **kwargs: str): ...
|
||||
| --------------------------------------- `Super4.method` defined here
|
||||
75 |
|
||||
76 | class Sub20(Super4):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/mdtest_snippet.pyi:83:9
|
||||
|
|
||||
82 | class Sub22(Super4):
|
||||
83 | def method(self, **kwargs): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method`
|
||||
84 |
|
||||
85 | class Sub23(Super4):
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:74:9
|
||||
|
|
||||
73 | class Super4:
|
||||
74 | def method(self, *args: int, **kwargs: str): ...
|
||||
| --------------------------------------- `Super4.method` defined here
|
||||
75 |
|
||||
76 | class Sub20(Super4):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/mdtest_snippet.pyi:86:9
|
||||
|
|
||||
85 | class Sub23(Super4):
|
||||
86 | def method(self, x, *args, y, **kwargs): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method`
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:74:9
|
||||
|
|
||||
73 | class Super4:
|
||||
74 | def method(self, *args: int, **kwargs: str): ...
|
||||
| --------------------------------------- `Super4.method` defined here
|
||||
75 |
|
||||
76 | class Sub20(Super4):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: liskov.md - The Liskov Substitution Principle - Method return types
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.pyi
|
||||
|
||||
```
|
||||
1 | class Super:
|
||||
2 | def method(self) -> int: ...
|
||||
3 |
|
||||
4 | class Sub1(Super):
|
||||
5 | def method(self) -> int: ... # fine
|
||||
6 |
|
||||
7 | class Sub2(Super):
|
||||
8 | def method(self) -> bool: ... # fine: `bool` is a subtype of `int`
|
||||
9 |
|
||||
10 | class Sub3(Super):
|
||||
11 | def method(self) -> object: ... # error: [invalid-method-override]
|
||||
12 |
|
||||
13 | class Sub4(Super):
|
||||
14 | def method(self) -> str: ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/mdtest_snippet.pyi:11:9
|
||||
|
|
||||
10 | class Sub3(Super):
|
||||
11 | def method(self) -> object: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
|
||||
12 |
|
||||
13 | class Sub4(Super):
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:2:9
|
||||
|
|
||||
1 | class Super:
|
||||
2 | def method(self) -> int: ...
|
||||
| ------------------- `Super.method` defined here
|
||||
3 |
|
||||
4 | class Sub1(Super):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/mdtest_snippet.pyi:14:9
|
||||
|
|
||||
13 | class Sub4(Super):
|
||||
14 | def method(self) -> str: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:2:9
|
||||
|
|
||||
1 | class Super:
|
||||
2 | def method(self) -> int: ...
|
||||
| ------------------- `Super.method` defined here
|
||||
3 |
|
||||
4 | class Sub1(Super):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,220 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: liskov.md - The Liskov Substitution Principle - Staticmethods and classmethods
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.pyi
|
||||
|
||||
```
|
||||
1 | class Parent:
|
||||
2 | def instance_method(self, x: int) -> int: ...
|
||||
3 | @classmethod
|
||||
4 | def class_method(cls, x: int) -> int: ...
|
||||
5 | @staticmethod
|
||||
6 | def static_method(x: int) -> int: ...
|
||||
7 |
|
||||
8 | class BadChild1(Parent):
|
||||
9 | @staticmethod
|
||||
10 | def instance_method(self, x: int) -> int: ... # error: [invalid-method-override]
|
||||
11 | # TODO: we should emit `invalid-method-override` here.
|
||||
12 | # Although the method has the same signature as `Parent.class_method`
|
||||
13 | # when accessed on instances, it does not have the same signature as
|
||||
14 | # `Parent.class_method` when accessed on the class object itself
|
||||
15 | def class_method(cls, x: int) -> int: ...
|
||||
16 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
|
||||
17 |
|
||||
18 | class BadChild2(Parent):
|
||||
19 | # TODO: we should emit `invalid-method-override` here.
|
||||
20 | # Although the method has the same signature as `Parent.class_method`
|
||||
21 | # when accessed on instances, it does not have the same signature as
|
||||
22 | # `Parent.class_method` when accessed on the class object itself.
|
||||
23 | #
|
||||
24 | # Note that whereas `BadChild1.class_method` is reported as a Liskov violation by
|
||||
25 | # mypy, pyright and pyrefly, pyright is the only one of those three to report a
|
||||
26 | # Liskov violation on this method as of 2025-11-23.
|
||||
27 | @classmethod
|
||||
28 | def instance_method(self, x: int) -> int: ...
|
||||
29 | @staticmethod
|
||||
30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
|
||||
31 | @classmethod
|
||||
32 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
|
||||
33 |
|
||||
34 | class BadChild3(Parent):
|
||||
35 | @classmethod
|
||||
36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
|
||||
37 | @staticmethod
|
||||
38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override]
|
||||
39 |
|
||||
40 | class GoodChild1(Parent):
|
||||
41 | @classmethod
|
||||
42 | def class_method(cls, x: int) -> int: ...
|
||||
43 | @staticmethod
|
||||
44 | def static_method(x: int) -> int: ...
|
||||
45 |
|
||||
46 | class GoodChild2(Parent):
|
||||
47 | @classmethod
|
||||
48 | def class_method(cls, x: object) -> bool: ...
|
||||
49 | @staticmethod
|
||||
50 | def static_method(x: object) -> bool: ...
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `instance_method`
|
||||
--> src/mdtest_snippet.pyi:10:9
|
||||
|
|
||||
8 | class BadChild1(Parent):
|
||||
9 | @staticmethod
|
||||
10 | def instance_method(self, x: int) -> int: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.instance_method`
|
||||
11 | # TODO: we should emit `invalid-method-override` here.
|
||||
12 | # Although the method has the same signature as `Parent.class_method`
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:2:9
|
||||
|
|
||||
1 | class Parent:
|
||||
2 | def instance_method(self, x: int) -> int: ...
|
||||
| ------------------------------------ `Parent.instance_method` defined here
|
||||
3 | @classmethod
|
||||
4 | def class_method(cls, x: int) -> int: ...
|
||||
|
|
||||
info: `BadChild1.instance_method` is a staticmethod but `Parent.instance_method` is an instance method
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `static_method`
|
||||
--> src/mdtest_snippet.pyi:16:9
|
||||
|
|
||||
14 | # `Parent.class_method` when accessed on the class object itself
|
||||
15 | def class_method(cls, x: int) -> int: ...
|
||||
16 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method`
|
||||
17 |
|
||||
18 | class BadChild2(Parent):
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:6:9
|
||||
|
|
||||
4 | def class_method(cls, x: int) -> int: ...
|
||||
5 | @staticmethod
|
||||
6 | def static_method(x: int) -> int: ...
|
||||
| ---------------------------- `Parent.static_method` defined here
|
||||
7 |
|
||||
8 | class BadChild1(Parent):
|
||||
|
|
||||
info: `BadChild1.static_method` is an instance method but `Parent.static_method` is a staticmethod
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `class_method`
|
||||
--> src/mdtest_snippet.pyi:30:9
|
||||
|
|
||||
28 | def instance_method(self, x: int) -> int: ...
|
||||
29 | @staticmethod
|
||||
30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.class_method`
|
||||
31 | @classmethod
|
||||
32 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:4:9
|
||||
|
|
||||
2 | def instance_method(self, x: int) -> int: ...
|
||||
3 | @classmethod
|
||||
4 | def class_method(cls, x: int) -> int: ...
|
||||
| -------------------------------- `Parent.class_method` defined here
|
||||
5 | @staticmethod
|
||||
6 | def static_method(x: int) -> int: ...
|
||||
|
|
||||
info: `BadChild2.class_method` is a staticmethod but `Parent.class_method` is a classmethod
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `static_method`
|
||||
--> src/mdtest_snippet.pyi:32:9
|
||||
|
|
||||
30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
|
||||
31 | @classmethod
|
||||
32 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method`
|
||||
33 |
|
||||
34 | class BadChild3(Parent):
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:6:9
|
||||
|
|
||||
4 | def class_method(cls, x: int) -> int: ...
|
||||
5 | @staticmethod
|
||||
6 | def static_method(x: int) -> int: ...
|
||||
| ---------------------------- `Parent.static_method` defined here
|
||||
7 |
|
||||
8 | class BadChild1(Parent):
|
||||
|
|
||||
info: `BadChild2.static_method` is a classmethod but `Parent.static_method` is a staticmethod
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `class_method`
|
||||
--> src/mdtest_snippet.pyi:36:9
|
||||
|
|
||||
34 | class BadChild3(Parent):
|
||||
35 | @classmethod
|
||||
36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.class_method`
|
||||
37 | @staticmethod
|
||||
38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override]
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:4:9
|
||||
|
|
||||
2 | def instance_method(self, x: int) -> int: ...
|
||||
3 | @classmethod
|
||||
4 | def class_method(cls, x: int) -> int: ...
|
||||
| -------------------------------- `Parent.class_method` defined here
|
||||
5 | @staticmethod
|
||||
6 | def static_method(x: int) -> int: ...
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `static_method`
|
||||
--> src/mdtest_snippet.pyi:38:9
|
||||
|
|
||||
36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
|
||||
37 | @staticmethod
|
||||
38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method`
|
||||
39 |
|
||||
40 | class GoodChild1(Parent):
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:6:9
|
||||
|
|
||||
4 | def class_method(cls, x: int) -> int: ...
|
||||
5 | @staticmethod
|
||||
6 | def static_method(x: int) -> int: ...
|
||||
| ---------------------------- `Parent.static_method` defined here
|
||||
7 |
|
||||
8 | class BadChild1(Parent):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user