Compare commits
340 Commits
charlie/co
...
david/self
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ba9707ca | ||
|
|
4aba390838 | ||
|
|
82c01aa662 | ||
|
|
9f446faa6c | ||
|
|
b94d6cf567 | ||
|
|
cd0c97211c | ||
|
|
0e71c9e3bb | ||
|
|
24c90d6953 | ||
|
|
fbff4dec3a | ||
|
|
f3dac27e9a | ||
|
|
e4cefd9bf9 | ||
|
|
9e4ee98109 | ||
|
|
557d583e32 | ||
|
|
f98eebdbab | ||
|
|
c606bf014e | ||
|
|
e8fce20736 | ||
|
|
5a30ec0df6 | ||
|
|
fab1b0d546 | ||
|
|
66abef433b | ||
|
|
fa22bd604a | ||
|
|
0c9165fc3a | ||
|
|
9f6147490b | ||
|
|
b7571c3e24 | ||
|
|
d178d115f3 | ||
|
|
6501782678 | ||
|
|
bca4341dcc | ||
|
|
31ede11774 | ||
|
|
ba9f881687 | ||
|
|
4357a0a3c2 | ||
|
|
c18afa93b3 | ||
|
|
8f04202ee4 | ||
|
|
efe54081d6 | ||
|
|
ac23c99744 | ||
|
|
e5c7d87461 | ||
|
|
de62e39eba | ||
|
|
d285717da8 | ||
|
|
545e9deba3 | ||
|
|
e3d792605f | ||
|
|
1f303a5eb6 | ||
|
|
07d13c6b4a | ||
|
|
e1838aac29 | ||
|
|
4ba847f250 | ||
|
|
13e9fc9362 | ||
|
|
3fda2d17c7 | ||
|
|
931fa06d85 | ||
|
|
e53ac7985d | ||
|
|
e25e7044ba | ||
|
|
b80de52592 | ||
|
|
2917534279 | ||
|
|
f6b2cd5588 | ||
|
|
302fe76c2b | ||
|
|
a90e404c3f | ||
|
|
8358ad8d25 | ||
|
|
2b8b1ef178 | ||
|
|
2efa3fbb62 | ||
|
|
b9da4305e6 | ||
|
|
87043a2415 | ||
|
|
f684b6fff4 | ||
|
|
47f39ed1a0 | ||
|
|
aecdb8c144 | ||
|
|
3c52d2d1bd | ||
|
|
942d6eeb9f | ||
|
|
4ccacc80f9 | ||
|
|
b2bb119c6a | ||
|
|
cef12f4925 | ||
|
|
aa7ac2ce0f | ||
|
|
70d9c90827 | ||
|
|
adfa723464 | ||
|
|
844c07f1f0 | ||
|
|
11d20a1a51 | ||
|
|
e9079e7d95 | ||
|
|
c400725713 | ||
|
|
1081694140 | ||
|
|
52f526eb38 | ||
|
|
dc05b38165 | ||
|
|
8c3c5ee5e3 | ||
|
|
b46cc6ac0b | ||
|
|
8b925ea626 | ||
|
|
1b180c8342 | ||
|
|
afeb217452 | ||
|
|
c0b3dd3745 | ||
|
|
5f6607bf54 | ||
|
|
a6deca44b5 | ||
|
|
0dbceccbc1 | ||
|
|
48680e10b6 | ||
|
|
b0c88a2a42 | ||
|
|
b9c53a74f9 | ||
|
|
6a4d207db7 | ||
|
|
42c35b6f44 | ||
|
|
9e79d64d62 | ||
|
|
582857f292 | ||
|
|
9bbeb793e5 | ||
|
|
dbbe7a773c | ||
|
|
5f09d4a90a | ||
|
|
f8c20258ae | ||
|
|
d8538d8c98 | ||
|
|
3642381489 | ||
|
|
1f07880d5c | ||
|
|
d81b6cd334 | ||
|
|
d99210c049 | ||
|
|
577653551c | ||
|
|
38a385fb6f | ||
|
|
cd2ae5aa2d | ||
|
|
41694f21c6 | ||
|
|
fccbe56d23 | ||
|
|
c46555da41 | ||
|
|
0a27c9dabd | ||
|
|
3c9e76eb66 | ||
|
|
80f5cdcf66 | ||
|
|
35fe0e90da | ||
|
|
157b49a8ee | ||
|
|
8a6e223df5 | ||
|
|
5a48da53da | ||
|
|
58005b590c | ||
|
|
884835e386 | ||
|
|
efd4407f7f | ||
|
|
761588a60e | ||
|
|
e1eb188049 | ||
|
|
ff19629b11 | ||
|
|
cd80c9d907 | ||
|
|
abb34828bd | ||
|
|
cab7caf80b | ||
|
|
d470f29093 | ||
|
|
1fbed6c325 | ||
|
|
4dcb7ddafe | ||
|
|
5be90c3a67 | ||
|
|
d0dca7bfcf | ||
|
|
78210b198b | ||
|
|
4a2310b595 | ||
|
|
fc392c663a | ||
|
|
81d3c419e9 | ||
|
|
a6a3d3f656 | ||
|
|
c847cad389 | ||
|
|
81e5830585 | ||
|
|
2b58705cc1 | ||
|
|
9f3235a37f | ||
|
|
62d650226b | ||
|
|
5d8a391a3e | ||
|
|
ed7b98cf9b | ||
|
|
6591775cd9 | ||
|
|
1f82731856 | ||
|
|
874da9c400 | ||
|
|
375cead202 | ||
|
|
9ec690b8f8 | ||
|
|
a48d779c4e | ||
|
|
ba6c7f6897 | ||
|
|
8095ff0e55 | ||
|
|
24cd592a1d | ||
|
|
a40bc6a460 | ||
|
|
577de6c599 | ||
|
|
d8b1afbc6e | ||
|
|
9a3001b571 | ||
|
|
ec2c7cad0e | ||
|
|
924741cb11 | ||
|
|
77e8da7497 | ||
|
|
5e64863895 | ||
|
|
78e4753d74 | ||
|
|
eb55b9b5a0 | ||
|
|
0eb36e4345 | ||
|
|
5fcf0afff4 | ||
|
|
b946cfd1f7 | ||
|
|
95c8f5fd0f | ||
|
|
89aa804b2d | ||
|
|
f789b12705 | ||
|
|
3e36a7ab81 | ||
|
|
5c548dcc04 | ||
|
|
bd30701980 | ||
|
|
2b6d66b793 | ||
|
|
147ea399fd | ||
|
|
907047bf4b | ||
|
|
13a1483f1e | ||
|
|
be69f61b3e | ||
|
|
f1f3bd1cd3 | ||
|
|
3bef23669f | ||
|
|
f82ee8ea59 | ||
|
|
b8a65182dd | ||
|
|
fc15d8a3bd | ||
|
|
b3b5c19105 | ||
|
|
f8aae9b1d6 | ||
|
|
9180635171 | ||
|
|
3ef4b3bf32 | ||
|
|
5a3886c8b5 | ||
|
|
813ec23ecd | ||
|
|
13883414af | ||
|
|
84d4f114ef | ||
|
|
1c586b29e2 | ||
|
|
d76a8518c2 | ||
|
|
5f0ee2670a | ||
|
|
f8ca6c3316 | ||
|
|
ba7b023f26 | ||
|
|
e947d163b2 | ||
|
|
1cf4d2ff69 | ||
|
|
2308522f38 | ||
|
|
438f3d967b | ||
|
|
5bf4759cff | ||
|
|
2e9e96338e | ||
|
|
5fa7ace1f5 | ||
|
|
704868ca83 | ||
|
|
dc71c8a484 | ||
|
|
2499297392 | ||
|
|
7b9189bb2c | ||
|
|
d4cf61d98b | ||
|
|
5d91ba0b10 | ||
|
|
a7e9f0c4b9 | ||
|
|
c7d48e10e6 | ||
|
|
94dee2a36d | ||
|
|
555a5c9319 | ||
|
|
1279c20ee1 | ||
|
|
ce3af27f59 | ||
|
|
71da1d6df5 | ||
|
|
e598240f04 | ||
|
|
c9b84e2a85 | ||
|
|
d3f1c8e536 | ||
|
|
eea6b31980 | ||
|
|
b8dc780bdc | ||
|
|
93fdf7ed36 | ||
|
|
b19f388249 | ||
|
|
de947deee7 | ||
|
|
c0c4ae14ac | ||
|
|
645ce7e5ec | ||
|
|
1430f21283 | ||
|
|
953e862aca | ||
|
|
fbf140a665 | ||
|
|
670f958525 | ||
|
|
fed35a25e8 | ||
|
|
d1ef418bb0 | ||
|
|
272d24bf3e | ||
|
|
2624249219 | ||
|
|
4b08d17088 | ||
|
|
5b6169b02d | ||
|
|
2040e93add | ||
|
|
794eb886e4 | ||
|
|
57ba25caaf | ||
|
|
4f74db5630 | ||
|
|
adc4216afb | ||
|
|
fe8e49de9a | ||
|
|
574eb3f4bd | ||
|
|
311b0bdf9a | ||
|
|
f2546c562c | ||
|
|
59c0dacea0 | ||
|
|
b8188b2262 | ||
|
|
136721e608 | ||
|
|
5b500b838b | ||
|
|
cb003ebe22 | ||
|
|
03a5788aa1 | ||
|
|
626f716de6 | ||
|
|
46c5a13103 | ||
|
|
31681f66c9 | ||
|
|
a56ee9268e | ||
|
|
4ece8e5c1e | ||
|
|
34b6a9b909 | ||
|
|
eead549254 | ||
|
|
abafeb4bee | ||
|
|
2b76fa8fa1 | ||
|
|
239cbc6f33 | ||
|
|
2296627528 | ||
|
|
05687285fe | ||
|
|
05f97bae73 | ||
|
|
4323512a65 | ||
|
|
9dddd73c29 | ||
|
|
6c56a7a868 | ||
|
|
bb25bd9c6c | ||
|
|
b7e32b0a18 | ||
|
|
fb94b71e63 | ||
|
|
bc0586d922 | ||
|
|
a7a78f939c | ||
|
|
6dabf045c3 | ||
|
|
df45a0e3f9 | ||
|
|
88d9bb191b | ||
|
|
e302c2de7c | ||
|
|
012f385f5d | ||
|
|
a6f7f22b27 | ||
|
|
8d7dda9fb7 | ||
|
|
fb0881d836 | ||
|
|
ded2b15e05 | ||
|
|
3133964d8c | ||
|
|
f00039b6f2 | ||
|
|
6ccd0f187b | ||
|
|
de40f6a3ad | ||
|
|
dfbd27dc2f | ||
|
|
1531ca8a1b | ||
|
|
71702bbd48 | ||
|
|
8d9bdb5b92 | ||
|
|
2b73a1c039 | ||
|
|
2b0cdd2338 | ||
|
|
f09dc8b67c | ||
|
|
71a122f060 | ||
|
|
3ca24785ae | ||
|
|
1de36cfe4c | ||
|
|
66872a41fc | ||
|
|
e00594e8d2 | ||
|
|
443fd3b660 | ||
|
|
ae9f08d1e5 | ||
|
|
f69712c11d | ||
|
|
be485602de | ||
|
|
bc7615af0e | ||
|
|
4a3eeeff86 | ||
|
|
35c6dfe481 | ||
|
|
f8374280c0 | ||
|
|
0925513529 | ||
|
|
70bdde4085 | ||
|
|
34a5d7cb7f | ||
|
|
487941ea66 | ||
|
|
099f077311 | ||
|
|
8574751911 | ||
|
|
ddae741b72 | ||
|
|
5053d2c127 | ||
|
|
ef72fd79a7 | ||
|
|
658a51ea10 | ||
|
|
7c2da4f06e | ||
|
|
48fa839c80 | ||
|
|
cf0f5e1318 | ||
|
|
20b8a43017 | ||
|
|
b8acadd6a2 | ||
|
|
b372fe7198 | ||
|
|
53fa32a389 | ||
|
|
d1189c20df | ||
|
|
9a6b08b557 | ||
|
|
76e4277696 | ||
|
|
2d917d72f6 | ||
|
|
2629527559 | ||
|
|
bf20061268 | ||
|
|
eddc8d7644 | ||
|
|
b1ce8a3949 | ||
|
|
262c04f297 | ||
|
|
71536a43db | ||
|
|
e6dcdf3e49 | ||
|
|
f426349051 | ||
|
|
42c70697d8 | ||
|
|
1607d88c22 | ||
|
|
c6b82151dd | ||
|
|
39cf46ecd6 | ||
|
|
96b3c400fe | ||
|
|
60a2dc53e7 | ||
|
|
8d98aea6c4 | ||
|
|
d2c9f5e43c | ||
|
|
7dd0c7f4bd | ||
|
|
56c796acee | ||
|
|
2fe203292a | ||
|
|
b6847b371e |
@@ -17,4 +17,7 @@ indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = 100
|
||||
max_line_length = 100
|
||||
|
||||
[*.toml]
|
||||
indent_size = 4
|
||||
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -17,5 +17,5 @@
|
||||
/scripts/fuzz-parser/ @AlexWaygood
|
||||
|
||||
# red-knot
|
||||
/crates/red_knot* @carljm @MichaReiser @AlexWaygood
|
||||
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood
|
||||
/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp
|
||||
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp
|
||||
|
||||
56
.github/workflows/ci.yaml
vendored
56
.github/workflows/ci.yaml
vendored
@@ -16,7 +16,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
PACKAGE_NAME: ruff
|
||||
PYTHON_VERSION: "3.11"
|
||||
PYTHON_VERSION: "3.12"
|
||||
|
||||
jobs:
|
||||
determine_changes:
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
|
||||
cargo-test-linux:
|
||||
name: "cargo test (linux)"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
@@ -157,9 +157,36 @@ jobs:
|
||||
name: ruff
|
||||
path: target/debug/ruff
|
||||
|
||||
cargo-test-linux-release:
|
||||
name: "cargo test (linux, release)"
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
|
||||
|
||||
cargo-test-windows:
|
||||
name: "cargo test (windows)"
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-latest-xlarge
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
@@ -197,6 +224,8 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: playground/package-lock.json
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Test ruff_wasm"
|
||||
run: |
|
||||
@@ -210,8 +239,7 @@ jobs:
|
||||
cargo-build-release:
|
||||
name: "cargo build (release)"
|
||||
runs-on: macos-latest
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -255,11 +283,11 @@ jobs:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
run: cargo +${{ steps.msrv.outputs.value }} insta test --all-features --unreferenced reject --test-runner nextest
|
||||
|
||||
cargo-fuzz:
|
||||
name: "cargo fuzz"
|
||||
cargo-fuzz-build:
|
||||
name: "cargo fuzz build"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -278,7 +306,7 @@ jobs:
|
||||
- run: cargo fuzz build -s none
|
||||
|
||||
fuzz-parser:
|
||||
name: "Fuzz the parser"
|
||||
name: "fuzz parser"
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
@@ -307,7 +335,7 @@ jobs:
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
|
||||
python scripts/fuzz-parser/fuzz.py 0-500 --test-executable ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
python scripts/fuzz-parser/fuzz.py --bin ruff 0-500 --test-executable ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
|
||||
scripts:
|
||||
name: "test scripts"
|
||||
@@ -331,7 +359,7 @@ jobs:
|
||||
|
||||
ecosystem:
|
||||
name: "ecosystem"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-latest-8
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
@@ -561,12 +589,12 @@ jobs:
|
||||
run: rustup show
|
||||
- name: "Cache rust"
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: "Formatter progress"
|
||||
- name: "Run checks"
|
||||
run: scripts/formatter_ecosystem_checks.sh
|
||||
- name: "Github step summary"
|
||||
run: cat target/progress_projects_stats.txt > $GITHUB_STEP_SUMMARY
|
||||
run: cat target/formatter-ecosystem/stats.txt > $GITHUB_STEP_SUMMARY
|
||||
- name: "Remove checkouts from cache"
|
||||
run: rm -r target/progress_projects
|
||||
run: rm -r target/formatter-ecosystem
|
||||
|
||||
check-ruff-lsp:
|
||||
name: "test ruff-lsp"
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI
|
||||
run: cargo build --locked
|
||||
- name: Fuzz
|
||||
run: python scripts/fuzz-parser/fuzz.py $(shuf -i 0-9999999999999999999 -n 1000) --test-executable target/debug/ruff
|
||||
run: python scripts/fuzz-parser/fuzz.py --bin ruff $(shuf -i 0-9999999999999999999 -n 1000) --test-executable target/debug/ruff
|
||||
|
||||
create-issue-on-failure:
|
||||
name: Create an issue if the daily fuzz surfaced any bugs
|
||||
|
||||
2
.github/workflows/publish-playground.yml
vendored
2
.github/workflows/publish-playground.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.9.0
|
||||
uses: cloudflare/wrangler-action@v3.12.1
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
8
.github/workflows/publish-pypi.yml
vendored
8
.github/workflows/publish-pypi.yml
vendored
@@ -21,14 +21,12 @@ jobs:
|
||||
# For PyPI's trusted publishing.
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: wheels-*
|
||||
path: wheels
|
||||
merge-multiple: true
|
||||
- name: Publish to PyPi
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
skip-existing: true
|
||||
packages-dir: wheels
|
||||
verbose: true
|
||||
run: uv publish -v wheels/*
|
||||
|
||||
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/
|
||||
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
@@ -6,7 +6,7 @@
|
||||
# CI that:
|
||||
#
|
||||
# * checks for a Git Tag that looks like a release
|
||||
# * builds artifacts with cargo-dist (archives, installers, hashes)
|
||||
# * builds artifacts with dist (archives, installers, hashes)
|
||||
# * uploads those artifacts to temporary workflow zip
|
||||
# * on success, uploads the artifacts to a GitHub Release
|
||||
#
|
||||
@@ -24,10 +24,10 @@ permissions:
|
||||
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||
#
|
||||
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
|
||||
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||
#
|
||||
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||
# (cargo-dist-able) packages in the workspace with that version (this mode is
|
||||
# (dist-able) packages in the workspace with that version (this mode is
|
||||
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||
# packages versioned/released in lockstep).
|
||||
#
|
||||
@@ -48,7 +48,7 @@ on:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: "ubuntu-20.04"
|
||||
outputs:
|
||||
@@ -62,16 +62,16 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cargo-dist
|
||||
- name: Install dist
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.22.1/cargo-dist-installer.sh | sh"
|
||||
- name: Cache cargo-dist
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.25.2-prerelease.3/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/cargo-dist
|
||||
path: ~/.cargo/bin/dist
|
||||
# sure would be cool if github gave us proper conditionals...
|
||||
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||
@@ -79,8 +79,8 @@ jobs:
|
||||
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||
- id: plan
|
||||
run: |
|
||||
cargo dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "cargo dist ran successfully"
|
||||
dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
@@ -124,12 +124,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cached cargo-dist
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/cargo-dist
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -140,8 +140,8 @@ jobs:
|
||||
- id: cargo-dist
|
||||
shell: bash
|
||||
run: |
|
||||
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "cargo dist ran successfully"
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
@@ -174,12 +174,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cached cargo-dist
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/cargo-dist
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
- id: host
|
||||
shell: bash
|
||||
run: |
|
||||
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
echo "artifacts uploaded and released successfully"
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -17,7 +17,7 @@ exclude: |
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.21
|
||||
rev: v0.23
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
@@ -51,11 +51,15 @@ repos:
|
||||
- id: blacken-docs
|
||||
args: ["--pyi", "--line-length", "130"]
|
||||
files: '^crates/.*/resources/mdtest/.*\.md'
|
||||
exclude: |
|
||||
(?x)^(
|
||||
.*?invalid(_.+)*_syntax\.md
|
||||
)$
|
||||
additional_dependencies:
|
||||
- black==24.10.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.26.0
|
||||
rev: v1.27.3
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -69,7 +73,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.7.0
|
||||
rev: v0.7.4
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- **Default to Python 3.9**
|
||||
|
||||
Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896))
|
||||
|
||||
- **Changed location of `pydoclint` diagnostics**
|
||||
|
||||
[`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case.
|
||||
|
||||
If you've opted into these preview rules but have them suppressed using
|
||||
[`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in
|
||||
some places, this change may mean that you need to move the `noqa` suppression
|
||||
comments. Most users should be unaffected by this change.
|
||||
|
||||
- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer**
|
||||
|
||||
Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order).
|
||||
|
||||
This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected.
|
||||
|
||||
- **Changes to the line width calculation**
|
||||
|
||||
Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)).
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments
|
||||
|
||||
213
CHANGELOG.md
213
CHANGELOG.md
@@ -1,5 +1,216 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.0
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.8.0) for a migration guide and overview of the changes!
|
||||
|
||||
### Breaking changes
|
||||
|
||||
See also, the "Remapped rules" section which may result in disabled rules.
|
||||
|
||||
- **Default to Python 3.9**
|
||||
|
||||
Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896))
|
||||
|
||||
- **Changed location of `pydoclint` diagnostics**
|
||||
|
||||
[`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case.
|
||||
|
||||
If you've opted into these preview rules but have them suppressed using
|
||||
[`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in
|
||||
some places, this change may mean that you need to move the `noqa` suppression
|
||||
comments. Most users should be unaffected by this change.
|
||||
|
||||
- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer**
|
||||
|
||||
Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order).
|
||||
|
||||
This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected.
|
||||
|
||||
- **Changes to the line width calculation**
|
||||
|
||||
Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)).
|
||||
|
||||
### Removed Rules
|
||||
|
||||
The following deprecated rules have been removed:
|
||||
|
||||
- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`)
|
||||
- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`)
|
||||
- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`)
|
||||
- [`pytest-missing-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-missing-fixture-name-underscore/) (`PT004`)
|
||||
- [`pytest-incorrect-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-incorrect-fixture-name-underscore/) (`PT005`)
|
||||
- [`unpacked-list-comprehension`](https://docs.astral.sh/ruff/rules/unpacked-list-comprehension/) (`UP027`)
|
||||
|
||||
### Remapped rules
|
||||
|
||||
The following rules have been remapped to new rule codes:
|
||||
|
||||
- [`flake8-type-checking`](https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc): `TCH` to `TC`
|
||||
|
||||
### Stabilization
|
||||
|
||||
The following rules have been stabilized and are no longer in preview:
|
||||
|
||||
- [`builtin-import-shadowing`](https://docs.astral.sh/ruff/rules/builtin-import-shadowing/) (`A004`)
|
||||
- [`mutable-contextvar-default`](https://docs.astral.sh/ruff/rules/mutable-contextvar-default/) (`B039`)
|
||||
- [`fast-api-redundant-response-model`](https://docs.astral.sh/ruff/rules/fast-api-redundant-response-model/) (`FAST001`)
|
||||
- [`fast-api-non-annotated-dependency`](https://docs.astral.sh/ruff/rules/fast-api-non-annotated-dependency/) (`FAST002`)
|
||||
- [`dict-index-missing-items`](https://docs.astral.sh/ruff/rules/dict-index-missing-items/) (`PLC0206`)
|
||||
- [`pep484-style-positional-only-parameter`](https://docs.astral.sh/ruff/rules/pep484-style-positional-only-parameter/) (`PYI063`)
|
||||
- [`redundant-final-literal`](https://docs.astral.sh/ruff/rules/redundant-final-literal/) (`PYI064`)
|
||||
- [`bad-version-info-order`](https://docs.astral.sh/ruff/rules/bad-version-info-order/) (`PYI066`)
|
||||
- [`parenthesize-chained-operators`](https://docs.astral.sh/ruff/rules/parenthesize-chained-operators/) (`RUF021`)
|
||||
- [`unsorted-dunder-all`](https://docs.astral.sh/ruff/rules/unsorted-dunder-all/) (`RUF022`)
|
||||
- [`unsorted-dunder-slots`](https://docs.astral.sh/ruff/rules/unsorted-dunder-slots/) (`RUF023`)
|
||||
- [`assert-with-print-message`](https://docs.astral.sh/ruff/rules/assert-with-print-message/) (`RUF030`)
|
||||
- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`)
|
||||
|
||||
The following behaviors have been stabilized:
|
||||
|
||||
- [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name/) (`E741`): Violations in stub files are now ignored. Stub authors typically don't control variable names.
|
||||
- [`printf-string-formatting`](https://docs.astral.sh/ruff/rules/printf-string-formatting/) (`UP031`): Report all `printf`-like usages even if no autofix is available
|
||||
|
||||
The following fixes have been stabilized:
|
||||
|
||||
- [`zip-instead-of-pairwise`](https://docs.astral.sh/ruff/rules/zip-instead-of-pairwise/) (`RUF007`)
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-datetimez`\] Exempt `min.time()` and `max.time()` (`DTZ901`) ([#14394](https://github.com/astral-sh/ruff/pull/14394))
|
||||
- \[`flake8-pie`\] Mark fix as unsafe if the following statement is a string literal (`PIE790`) ([#14393](https://github.com/astral-sh/ruff/pull/14393))
|
||||
- \[`flake8-pyi`\] New rule `redundant-none-literal` (`PYI061`) ([#14316](https://github.com/astral-sh/ruff/pull/14316))
|
||||
- \[`flake8-pyi`\] Add autofix for `redundant-numeric-union` (`PYI041`) ([#14273](https://github.com/astral-sh/ruff/pull/14273))
|
||||
- \[`ruff`\] New rule `map-int-version-parsing` (`RUF048`) ([#14373](https://github.com/astral-sh/ruff/pull/14373))
|
||||
- \[`ruff`\] New rule `redundant-bool-literal` (`RUF038`) ([#14319](https://github.com/astral-sh/ruff/pull/14319))
|
||||
- \[`ruff`\] New rule `unraw-re-pattern` (`RUF039`) ([#14446](https://github.com/astral-sh/ruff/pull/14446))
|
||||
- \[`pycodestyle`\] Exempt `pytest.importorskip()` calls (`E402`) ([#14474](https://github.com/astral-sh/ruff/pull/14474))
|
||||
- \[`pylint`\] Autofix suggests using sets when possible (`PLR1714`) ([#14372](https://github.com/astral-sh/ruff/pull/14372))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- [`invalid-pyproject-toml`](https://docs.astral.sh/ruff/rules/invalid-pyproject-toml/) (`RUF200`): Updated to reflect the provisionally accepted [PEP 639](https://peps.python.org/pep-0639/).
|
||||
- \[`flake8-pyi`\] Avoid panic in unfixable case (`PYI041`) ([#14402](https://github.com/astral-sh/ruff/pull/14402))
|
||||
- \[`flake8-type-checking`\] Correctly handle quotes in subscript expression when generating an autofix ([#14371](https://github.com/astral-sh/ruff/pull/14371))
|
||||
- \[`pylint`\] Suggest correct autofix for `__contains__` (`PLC2801`) ([#14424](https://github.com/astral-sh/ruff/pull/14424))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Ruff now emits a warning instead of an error when a configuration [`ignore`](https://docs.astral.sh/ruff/settings/#lint_ignore)s a rule that has been removed ([#14435](https://github.com/astral-sh/ruff/pull/14435))
|
||||
- Ruff now validates that `lint.flake8-import-conventions.aliases` only uses valid module names and aliases ([#14477](https://github.com/astral-sh/ruff/pull/14477))
|
||||
|
||||
## 0.7.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-datetimez`\] Detect usages of `datetime.max`/`datetime.min` (`DTZ901`) ([#14288](https://github.com/astral-sh/ruff/pull/14288))
|
||||
- \[`flake8-logging`\] Implement `root-logger-calls` (`LOG015`) ([#14302](https://github.com/astral-sh/ruff/pull/14302))
|
||||
- \[`flake8-no-pep420`\] Detect empty implicit namespace packages (`INP001`) ([#14236](https://github.com/astral-sh/ruff/pull/14236))
|
||||
- \[`flake8-pyi`\] Add "replace with `Self`" fix (`PYI019`) ([#14238](https://github.com/astral-sh/ruff/pull/14238))
|
||||
- \[`perflint`\] Implement quick-fix for `manual-list-comprehension` (`PERF401`) ([#13919](https://github.com/astral-sh/ruff/pull/13919))
|
||||
- \[`pylint`\] Implement `shallow-copy-environ` (`W1507`) ([#14241](https://github.com/astral-sh/ruff/pull/14241))
|
||||
- \[`ruff`\] Implement `none-not-at-end-of-union` (`RUF036`) ([#14314](https://github.com/astral-sh/ruff/pull/14314))
|
||||
- \[`ruff`\] Implementation `unsafe-markup-call` from `flake8-markupsafe` plugin (`RUF035`) ([#14224](https://github.com/astral-sh/ruff/pull/14224))
|
||||
- \[`ruff`\] Report problems for `attrs` dataclasses (`RUF008`, `RUF009`) ([#14327](https://github.com/astral-sh/ruff/pull/14327))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-boolean-trap`\] Exclude dunder methods that define operators (`FBT001`) ([#14203](https://github.com/astral-sh/ruff/pull/14203))
|
||||
- \[`flake8-pyi`\] Add "replace with `Self`" fix (`PYI034`) ([#14217](https://github.com/astral-sh/ruff/pull/14217))
|
||||
- \[`flake8-pyi`\] Always autofix `duplicate-union-members` (`PYI016`) ([#14270](https://github.com/astral-sh/ruff/pull/14270))
|
||||
- \[`flake8-pyi`\] Improve autofix for nested and mixed type unions for `unnecessary-type-union` (`PYI055`) ([#14272](https://github.com/astral-sh/ruff/pull/14272))
|
||||
- \[`flake8-pyi`\] Mark fix as unsafe when type annotation contains comments for `duplicate-literal-member` (`PYI062`) ([#14268](https://github.com/astral-sh/ruff/pull/14268))
|
||||
|
||||
### Server
|
||||
|
||||
- Use the current working directory to resolve settings from `ruff.configuration` ([#14352](https://github.com/astral-sh/ruff/pull/14352))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid conflicts between `PLC014` (`useless-import-alias`) and `I002` (`missing-required-import`) by considering `lint.isort.required-imports` for `PLC014` ([#14287](https://github.com/astral-sh/ruff/pull/14287))
|
||||
- \[`flake8-type-checking`\] Skip quoting annotation if it becomes invalid syntax (`TCH001`)
|
||||
- \[`flake8-pyi`\] Avoid using `typing.Self` in stub files pre-Python 3.11 (`PYI034`) ([#14230](https://github.com/astral-sh/ruff/pull/14230))
|
||||
- \[`flake8-pytest-style`\] Flag `pytest.raises` call with keyword argument `expected_exception` (`PT011`) ([#14298](https://github.com/astral-sh/ruff/pull/14298))
|
||||
- \[`flake8-simplify`\] Infer "unknown" truthiness for literal iterables whose items are all unpacks (`SIM222`) ([#14263](https://github.com/astral-sh/ruff/pull/14263))
|
||||
- \[`flake8-type-checking`\] Fix false positives for `typing.Annotated` (`TCH001`) ([#14311](https://github.com/astral-sh/ruff/pull/14311))
|
||||
- \[`pylint`\] Allow `await` at the top-level scope of a notebook (`PLE1142`) ([#14225](https://github.com/astral-sh/ruff/pull/14225))
|
||||
- \[`pylint`\] Fix miscellaneous issues in `await-outside-async` detection (`PLE1142`) ([#14218](https://github.com/astral-sh/ruff/pull/14218))
|
||||
- \[`pyupgrade`\] Avoid applying PEP 646 rewrites in invalid contexts (`UP044`) ([#14234](https://github.com/astral-sh/ruff/pull/14234))
|
||||
- \[`pyupgrade`\] Detect permutations in redundant open modes (`UP015`) ([#14255](https://github.com/astral-sh/ruff/pull/14255))
|
||||
- \[`refurb`\] Avoid triggering `hardcoded-string-charset` for reordered sets (`FURB156`) ([#14233](https://github.com/astral-sh/ruff/pull/14233))
|
||||
- \[`refurb`\] Further special cases added to `verbose-decimal-constructor` (`FURB157`) ([#14216](https://github.com/astral-sh/ruff/pull/14216))
|
||||
- \[`refurb`\] Use `UserString` instead of non-existent `UserStr` (`FURB189`) ([#14209](https://github.com/astral-sh/ruff/pull/14209))
|
||||
- \[`ruff`\] Avoid treating lowercase letters as `# noqa` codes (`RUF100`) ([#14229](https://github.com/astral-sh/ruff/pull/14229))
|
||||
- \[`ruff`\] Do not report when `Optional` has no type arguments (`RUF013`) ([#14181](https://github.com/astral-sh/ruff/pull/14181))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add "Notebook behavior" section for `F704`, `PLE1142` ([#14266](https://github.com/astral-sh/ruff/pull/14266))
|
||||
- Document comment policy around fix safety ([#14300](https://github.com/astral-sh/ruff/pull/14300))
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- Formatter: Disallow single-line implicit concatenated strings ([#13928](https://github.com/astral-sh/ruff/pull/13928))
|
||||
- \[`flake8-pyi`\] Include all Python file types for `PYI006` and `PYI066` ([#14059](https://github.com/astral-sh/ruff/pull/14059))
|
||||
- \[`flake8-simplify`\] Implement `split-of-static-string` (`SIM905`) ([#14008](https://github.com/astral-sh/ruff/pull/14008))
|
||||
- \[`refurb`\] Implement `subclass-builtin` (`FURB189`) ([#14105](https://github.com/astral-sh/ruff/pull/14105))
|
||||
- \[`ruff`\] Improve diagnostic messages and docs (`RUF031`, `RUF032`, `RUF034`) ([#14068](https://github.com/astral-sh/ruff/pull/14068))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Detect items that hash to same value in duplicate sets (`B033`, `PLC0208`) ([#14064](https://github.com/astral-sh/ruff/pull/14064))
|
||||
- \[`eradicate`\] Better detection of IntelliJ language injection comments (`ERA001`) ([#14094](https://github.com/astral-sh/ruff/pull/14094))
|
||||
- \[`flake8-pyi`\] Add autofix for `docstring-in-stub` (`PYI021`) ([#14150](https://github.com/astral-sh/ruff/pull/14150))
|
||||
- \[`flake8-pyi`\] Update `duplicate-literal-member` (`PYI062`) to alawys provide an autofix ([#14188](https://github.com/astral-sh/ruff/pull/14188))
|
||||
- \[`pyflakes`\] Detect items that hash to same value in duplicate dictionaries (`F601`) ([#14065](https://github.com/astral-sh/ruff/pull/14065))
|
||||
- \[`ruff`\] Fix false positive for decorators (`RUF028`) ([#14061](https://github.com/astral-sh/ruff/pull/14061))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid parsing joint rule codes as distinct codes in `# noqa` ([#12809](https://github.com/astral-sh/ruff/pull/12809))
|
||||
- \[`eradicate`\] ignore `# language=` in commented-out-code rule (ERA001) ([#14069](https://github.com/astral-sh/ruff/pull/14069))
|
||||
- \[`flake8-bugbear`\] - do not run `mutable-argument-default` on stubs (`B006`) ([#14058](https://github.com/astral-sh/ruff/pull/14058))
|
||||
- \[`flake8-builtins`\] Skip lambda expressions in `builtin-argument-shadowing (A002)` ([#14144](https://github.com/astral-sh/ruff/pull/14144))
|
||||
- \[`flake8-comprehension`\] Also remove trailing comma while fixing `C409` and `C419` ([#14097](https://github.com/astral-sh/ruff/pull/14097))
|
||||
- \[`flake8-simplify`\] Allow `open` without context manager in `return` statement (`SIM115`) ([#14066](https://github.com/astral-sh/ruff/pull/14066))
|
||||
- \[`pylint`\] Respect hash-equivalent literals in `iteration-over-set` (`PLC0208`) ([#14063](https://github.com/astral-sh/ruff/pull/14063))
|
||||
- \[`pylint`\] Update known dunder methods for Python 3.13 (`PLW3201`) ([#14146](https://github.com/astral-sh/ruff/pull/14146))
|
||||
- \[`pyupgrade`\] - ignore kwarg unpacking for `UP044` ([#14053](https://github.com/astral-sh/ruff/pull/14053))
|
||||
- \[`refurb`\] Parse more exotic decimal strings in `verbose-decimal-constructor` (`FURB157`) ([#14098](https://github.com/astral-sh/ruff/pull/14098))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add links to missing related options within rule documentations ([#13971](https://github.com/astral-sh/ruff/pull/13971))
|
||||
- Add rule short code to mkdocs tags to allow searching via rule codes ([#14040](https://github.com/astral-sh/ruff/pull/14040))
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### Preview features
|
||||
|
||||
- Fix formatting of single with-item with trailing comment ([#14005](https://github.com/astral-sh/ruff/pull/14005))
|
||||
- \[`pyupgrade`\] Add PEP 646 `Unpack` conversion to `*` with fix (`UP044`) ([#13988](https://github.com/astral-sh/ruff/pull/13988))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Regenerate `known_stdlibs.rs` with stdlibs 2024.10.25 ([#13963](https://github.com/astral-sh/ruff/pull/13963))
|
||||
- \[`flake8-no-pep420`\] Skip namespace package enforcement for PEP 723 scripts (`INP001`) ([#13974](https://github.com/astral-sh/ruff/pull/13974))
|
||||
|
||||
### Server
|
||||
|
||||
- Fix server panic when undoing an edit ([#14010](https://github.com/astral-sh/ruff/pull/14010))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix issues in discovering ruff in pip build environments ([#13881](https://github.com/astral-sh/ruff/pull/13881))
|
||||
- \[`flake8-type-checking`\] Fix false positive for `singledispatchmethod` (`TCH003`) ([#13941](https://github.com/astral-sh/ruff/pull/13941))
|
||||
- \[`flake8-type-checking`\] Treat return type of `singledispatch` as runtime-required (`TCH003`) ([#13957](https://github.com/astral-sh/ruff/pull/13957))
|
||||
|
||||
### Documentation
|
||||
|
||||
- \[`flake8-simplify`\] Include caveats of enabling `if-else-block-instead-of-if-exp` (`SIM108`) ([#14019](https://github.com/astral-sh/ruff/pull/14019))
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Preview features
|
||||
@@ -866,7 +1077,7 @@ The following deprecated CLI commands have been removed:
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-bugbear`\] Implement `return-in-generator` (`B901`) ([#11644](https://github.com/astral-sh/ruff/pull/11644))
|
||||
- \[`flake8-pyi`\] Implement `PYI063` ([#11699](https://github.com/astral-sh/ruff/pull/11699))
|
||||
- \[`flake8-pyi`\] Implement `pep484-style-positional-only-parameter` (`PYI063`) ([#11699](https://github.com/astral-sh/ruff/pull/11699))
|
||||
- \[`pygrep_hooks`\] Check blanket ignores via file-level pragmas (`PGH004`) ([#11540](https://github.com/astral-sh/ruff/pull/11540))
|
||||
|
||||
### Rule changes
|
||||
|
||||
649
Cargo.lock
generated
649
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
33
Cargo.toml
33
Cargo.toml
@@ -65,7 +65,8 @@ compact_str = "0.8.0"
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
crossbeam = { version = "0.8.4" }
|
||||
dashmap = { version = "6.0.1" }
|
||||
dir-test = { version = "0.3.0" }
|
||||
dir-test = { version = "0.4.0" }
|
||||
dunce = { version = "1.0.5" }
|
||||
drop_bomb = { version = "0.1.5" }
|
||||
env_logger = { version = "0.11.0" }
|
||||
etcetera = { version = "0.8.0" }
|
||||
@@ -81,6 +82,7 @@ hashbrown = { version = "0.15.0", default-features = false, features = [
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff = { version = "0.1.5" }
|
||||
imperative = { version = "1.0.4" }
|
||||
indexmap = { version = "2.6.0" }
|
||||
indicatif = { version = "0.17.8" }
|
||||
indoc = { version = "2.0.4" }
|
||||
insta = { version = "1.35.1" }
|
||||
@@ -101,7 +103,7 @@ matchit = { version = "0.8.1" }
|
||||
memchr = { version = "2.7.1" }
|
||||
mimalloc = { version = "0.1.39" }
|
||||
natord = { version = "1.0.9" }
|
||||
notify = { version = "6.1.1" }
|
||||
notify = { version = "7.0.0" }
|
||||
ordermap = { version = "0.5.0" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
path-slash = { version = "0.2.1" }
|
||||
@@ -109,7 +111,7 @@ pathdiff = { version = "0.2.1" }
|
||||
pep440_rs = { version = "0.7.1" }
|
||||
pretty_assertions = "1.3.0"
|
||||
proc-macro2 = { version = "1.0.79" }
|
||||
pyproject-toml = { version = "0.9.0" }
|
||||
pyproject-toml = { version = "0.13.4" }
|
||||
quick-junit = { version = "0.5.0" }
|
||||
quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
@@ -135,7 +137,7 @@ strum_macros = { version = "0.26.0" }
|
||||
syn = { version = "2.0.55" }
|
||||
tempfile = { version = "3.9.0" }
|
||||
test-case = { version = "3.3.1" }
|
||||
thiserror = { version = "1.0.58" }
|
||||
thiserror = { version = "2.0.0" }
|
||||
tikv-jemallocator = { version = "0.6.0" }
|
||||
toml = { version = "0.8.11" }
|
||||
tracing = { version = "0.1.40" }
|
||||
@@ -149,7 +151,7 @@ tracing-tree = { version = "0.4.0" }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unic-ucd-category = { version = "0.9" }
|
||||
unicode-ident = { version = "1.0.12" }
|
||||
unicode-width = { version = "0.1.11" }
|
||||
unicode-width = { version = "0.2.0" }
|
||||
unicode_names2 = { version = "1.2.2" }
|
||||
unicode-normalization = { version = "0.1.23" }
|
||||
ureq = { version = "2.9.6" }
|
||||
@@ -188,8 +190,9 @@ missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
similar_names = "allow"
|
||||
single_match_else = "allow"
|
||||
too_many_lines = "allow"
|
||||
# To allow `#[allow(clippy::all)]` in `crates/ruff_python_parser/src/python.rs`.
|
||||
# Without the hashes we run into a `rustfmt` bug in some snapshot tests, see #13250
|
||||
needless_raw_string_hashes = "allow"
|
||||
# Disallowed restriction lints
|
||||
print_stdout = "warn"
|
||||
@@ -245,10 +248,10 @@ debug = 1
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
# Config for 'cargo dist'
|
||||
# Config for 'dist'
|
||||
[workspace.metadata.dist]
|
||||
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.22.1"
|
||||
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.25.2-prerelease.3"
|
||||
# CI backends to support
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
@@ -279,13 +282,13 @@ targets = [
|
||||
]
|
||||
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
|
||||
auto-includes = false
|
||||
# Whether cargo-dist should create a GitHub Release or use an existing draft
|
||||
# Whether dist should create a Github Release or use an existing draft
|
||||
create-release = true
|
||||
# Which actions to run on pull requests
|
||||
pr-run-mode = "skip"
|
||||
# Whether CI should trigger releases with dispatches instead of tag pushes
|
||||
dispatch-releases = true
|
||||
# Which phase cargo-dist should use to create the GitHub release
|
||||
# Which phase dist should use to create the GitHub release
|
||||
github-release = "announce"
|
||||
# Whether CI should include auto-generated code to build local artifacts
|
||||
build-local-artifacts = false
|
||||
@@ -294,14 +297,10 @@ local-artifacts-jobs = ["./build-binaries", "./build-docker"]
|
||||
# Publish jobs to run in CI
|
||||
publish-jobs = ["./publish-pypi", "./publish-wasm"]
|
||||
# Post-announce jobs to run in CI
|
||||
post-announce-jobs = [
|
||||
"./notify-dependents",
|
||||
"./publish-docs",
|
||||
"./publish-playground",
|
||||
]
|
||||
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
|
||||
# Custom permissions for GitHub Jobs
|
||||
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
|
||||
# Whether to install an updater program
|
||||
install-updater = false
|
||||
# Path that installers should place binaries in
|
||||
install-path = "CARGO_HOME"
|
||||
install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
|
||||
|
||||
10
README.md
10
README.md
@@ -136,8 +136,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.7.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.7.1/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.8.0/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.0/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -170,7 +170,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.7.1
|
||||
rev: v0.8.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -238,8 +238,8 @@ exclude = [
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
|
||||
# Assume Python 3.8
|
||||
target-version = "py38"
|
||||
# Assume Python 3.9
|
||||
target-version = "py39"
|
||||
|
||||
[lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
[files]
|
||||
# https://github.com/crate-ci/typos/issues/868
|
||||
extend-exclude = ["crates/red_knot_vendored/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*"]
|
||||
extend-exclude = [
|
||||
"crates/red_knot_vendored/vendor/**/*",
|
||||
"**/resources/**/*",
|
||||
"**/snapshots/**/*",
|
||||
"crates/red_knot_workspace/src/workspace/pyproject/package_name.rs"
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
"arange" = "arange" # e.g. `numpy.arange`
|
||||
|
||||
@@ -34,6 +34,7 @@ tracing-tree = { workspace = true }
|
||||
[dev-dependencies]
|
||||
filetime = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["testing"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -5,8 +5,6 @@ use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
use red_knot_python_semantic::SitePackages;
|
||||
use red_knot_server::run_server;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
@@ -14,7 +12,9 @@ use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::WorkspaceWatcher;
|
||||
use red_knot_workspace::workspace::settings::Configuration;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use target_version::TargetVersion;
|
||||
|
||||
use crate::logging::{setup_tracing, Verbosity};
|
||||
@@ -183,10 +183,10 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
|
||||
let system = OsSystem::new(cwd.clone());
|
||||
let cli_configuration = args.to_configuration(&cwd);
|
||||
let workspace_metadata = WorkspaceMetadata::from_path(
|
||||
let workspace_metadata = WorkspaceMetadata::discover(
|
||||
system.current_directory(),
|
||||
&system,
|
||||
Some(cli_configuration.clone()),
|
||||
Some(&cli_configuration),
|
||||
)?;
|
||||
|
||||
// TODO: Use the `program_settings` to compute the key for the database's persistent
|
||||
@@ -318,8 +318,9 @@ impl MainLoop {
|
||||
} => {
|
||||
let has_diagnostics = !result.is_empty();
|
||||
if check_revision == revision {
|
||||
#[allow(clippy::print_stdout)]
|
||||
for diagnostic in result {
|
||||
tracing::error!("{}", diagnostic);
|
||||
println!("{}", diagnostic.display(db));
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
@@ -378,7 +379,10 @@ impl MainLoopCancellationToken {
|
||||
#[derive(Debug)]
|
||||
enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted { result: Vec<String>, revision: u64 },
|
||||
CheckCompleted {
|
||||
result: Vec<Box<dyn Diagnostic>>,
|
||||
revision: u64,
|
||||
},
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub enum TargetVersion {
|
||||
Py37,
|
||||
#[default]
|
||||
Py38,
|
||||
#[default]
|
||||
Py39,
|
||||
Py310,
|
||||
Py311,
|
||||
@@ -46,3 +46,17 @@ impl From<TargetVersion> for red_knot_python_semantic::PythonVersion {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::target_version::TargetVersion;
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
|
||||
#[test]
|
||||
fn same_default_as_python_version() {
|
||||
assert_eq!(
|
||||
PythonVersion::from(TargetVersion::default()),
|
||||
PythonVersion::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
|
||||
use red_knot_python_semantic::{resolve_module, ModuleName, Program, PythonVersion, SitePackages};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::db::{Db, RootDatabase};
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::{directory_watcher, WorkspaceWatcher};
|
||||
use red_knot_workspace::workspace::settings::{Configuration, SearchPathConfiguration};
|
||||
@@ -14,6 +13,7 @@ use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
|
||||
use ruff_db::Upcast;
|
||||
|
||||
struct TestCase {
|
||||
@@ -46,6 +46,8 @@ impl TestCase {
|
||||
}
|
||||
|
||||
fn try_stop_watch(&mut self, timeout: Duration) -> Option<Vec<watch::ChangeEvent>> {
|
||||
tracing::debug!("Try stopping watch with timeout {:?}", timeout);
|
||||
|
||||
let watcher = self
|
||||
.watcher
|
||||
.take()
|
||||
@@ -55,8 +57,11 @@ impl TestCase {
|
||||
.changes_receiver
|
||||
.recv_timeout(timeout)
|
||||
.unwrap_or_default();
|
||||
|
||||
watcher.flush();
|
||||
tracing::debug!("Flushed file watcher");
|
||||
watcher.stop();
|
||||
tracing::debug!("Stopping file watcher");
|
||||
|
||||
for event in &self.changes_receiver {
|
||||
all_events.extend(event);
|
||||
@@ -69,7 +74,6 @@ impl TestCase {
|
||||
Some(all_events)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn take_watch_changes(&self) -> Vec<watch::ChangeEvent> {
|
||||
self.try_take_watch_changes(Duration::from_secs(10))
|
||||
.expect("Expected watch changes but observed none")
|
||||
@@ -110,8 +114,8 @@ impl TestCase {
|
||||
) -> anyhow::Result<()> {
|
||||
let program = Program::get(self.db());
|
||||
|
||||
self.configuration.search_paths = configuration.clone();
|
||||
let new_settings = configuration.into_settings(self.db.workspace().root(&self.db));
|
||||
let new_settings = configuration.to_settings(self.db.workspace().root(&self.db));
|
||||
self.configuration.search_paths = configuration;
|
||||
|
||||
program.update_search_paths(&mut self.db, &new_settings)?;
|
||||
|
||||
@@ -204,7 +208,9 @@ where
|
||||
.as_utf8_path()
|
||||
.canonicalize_utf8()
|
||||
.with_context(|| "Failed to canonicalize root path.")?,
|
||||
);
|
||||
)
|
||||
.simplified()
|
||||
.to_path_buf();
|
||||
|
||||
let workspace_path = root_path.join("workspace");
|
||||
|
||||
@@ -241,8 +247,7 @@ where
|
||||
search_paths,
|
||||
};
|
||||
|
||||
let workspace =
|
||||
WorkspaceMetadata::from_path(&workspace_path, &system, Some(configuration.clone()))?;
|
||||
let workspace = WorkspaceMetadata::discover(&workspace_path, &system, Some(&configuration))?;
|
||||
|
||||
let db = RootDatabase::new(workspace, system)?;
|
||||
|
||||
@@ -599,6 +604,8 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
||||
|
||||
#[test]
|
||||
fn directory_renamed() -> anyhow::Result<()> {
|
||||
let _tracing = setup_logging_with_filter("file_watching=TRACE,red_knot=TRACE");
|
||||
|
||||
let mut case = setup([
|
||||
("bar.py", "import sub.a"),
|
||||
("sub/__init__.py", ""),
|
||||
@@ -639,6 +646,10 @@ fn directory_renamed() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
for event in &changes {
|
||||
tracing::debug!("Event: {:?}", event);
|
||||
}
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
@@ -1311,3 +1322,138 @@ mod unix {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_packages_delete_root() -> anyhow::Result<()> {
|
||||
let mut case = setup(|root: &SystemPath, workspace_root: &SystemPath| {
|
||||
std::fs::write(
|
||||
workspace_root.join("pyproject.toml").as_std_path(),
|
||||
r#"
|
||||
[project]
|
||||
name = "inner"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
std::fs::write(
|
||||
root.join("pyproject.toml").as_std_path(),
|
||||
r#"
|
||||
[project]
|
||||
name = "outer"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
assert_eq!(
|
||||
case.db().workspace().root(case.db()),
|
||||
&*case.workspace_path("")
|
||||
);
|
||||
|
||||
std::fs::remove_file(case.workspace_path("pyproject.toml").as_std_path())?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
// It should now pick up the outer workspace.
|
||||
assert_eq!(case.db().workspace().root(case.db()), case.root_path());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn added_package() -> anyhow::Result<()> {
|
||||
let _ = setup_logging();
|
||||
let mut case = setup([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "inner"
|
||||
|
||||
[tool.knot.workspace]
|
||||
members = ["packages/*"]
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"packages/a/pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "a"
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_eq!(case.db().workspace().packages(case.db()).len(), 2);
|
||||
|
||||
std::fs::create_dir(case.workspace_path("packages/b").as_std_path())
|
||||
.context("failed to create folder for package 'b'")?;
|
||||
|
||||
// It seems that the file watcher won't pick up on file changes shortly after the folder
|
||||
// was created... I suspect this is because most file watchers don't support recursive
|
||||
// file watching. Instead, file-watching libraries manually implement recursive file watching
|
||||
// by setting a watcher for each directory. But doing this obviously "lags" behind.
|
||||
case.take_watch_changes();
|
||||
|
||||
std::fs::write(
|
||||
case.workspace_path("packages/b/pyproject.toml")
|
||||
.as_std_path(),
|
||||
r#"
|
||||
[project]
|
||||
name = "b"
|
||||
"#,
|
||||
)
|
||||
.context("failed to write pyproject.toml for package b")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(case.db().workspace().packages(case.db()).len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_package() -> anyhow::Result<()> {
|
||||
let mut case = setup([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "inner"
|
||||
|
||||
[tool.knot.workspace]
|
||||
members = ["packages/*"]
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"packages/a/pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "a"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"packages/b/pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "b"
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_eq!(case.db().workspace().packages(case.db()).len(), 3);
|
||||
|
||||
std::fs::remove_dir_all(case.workspace_path("packages/b").as_std_path())
|
||||
.context("failed to remove package 'b'")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(case.db().workspace().packages(case.db()).len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ license = { workspace = true }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
@@ -24,13 +25,15 @@ bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
itertools = { workspace = true}
|
||||
indexmap = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
ordermap = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
smallvec = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
@@ -43,10 +46,9 @@ red_knot_test = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
dir-test = {workspace = true}
|
||||
dir-test = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
wrap = 100
|
||||
@@ -0,0 +1,62 @@
|
||||
# NoReturn & Never
|
||||
|
||||
`NoReturn` is used to annotate the return type for functions that never return. `Never` is the
|
||||
bottom type, representing the empty set of Python objects. These two annotations can be used
|
||||
interchangeably.
|
||||
|
||||
## Function Return Type Annotation
|
||||
|
||||
```py
|
||||
from typing import NoReturn
|
||||
|
||||
def stop() -> NoReturn:
|
||||
raise RuntimeError("no way")
|
||||
|
||||
# revealed: Never
|
||||
reveal_type(stop())
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing import NoReturn, Never, Any
|
||||
|
||||
# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
|
||||
x: Never[int]
|
||||
a1: NoReturn
|
||||
# TODO: Test `Never` is only available in python >= 3.11
|
||||
a2: Never
|
||||
b1: Any
|
||||
b2: int
|
||||
|
||||
def f():
|
||||
# revealed: Never
|
||||
reveal_type(a1)
|
||||
# revealed: Never
|
||||
reveal_type(a2)
|
||||
|
||||
# Never is assignable to all types.
|
||||
v1: int = a1
|
||||
v2: str = a1
|
||||
# Other types are not assignable to Never except for Never (and Any).
|
||||
v3: Never = b1
|
||||
v4: Never = a2
|
||||
v5: Any = b2
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Never`"
|
||||
v6: Never = 1
|
||||
```
|
||||
|
||||
## Typing Extensions
|
||||
|
||||
```py
|
||||
from typing_extensions import NoReturn, Never
|
||||
|
||||
x: NoReturn
|
||||
y: Never
|
||||
|
||||
def f():
|
||||
# revealed: Never
|
||||
reveal_type(x)
|
||||
# revealed: Never
|
||||
reveal_type(y)
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# Optional
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Optional` is equivalent to using the type with a None in a Union.
|
||||
|
||||
```py
|
||||
from typing import Optional
|
||||
|
||||
a: Optional[int]
|
||||
a1: Optional[bool]
|
||||
a2: Optional[Optional[bool]]
|
||||
a3: Optional[None]
|
||||
|
||||
def f():
|
||||
# revealed: int | None
|
||||
reveal_type(a)
|
||||
# revealed: bool | None
|
||||
reveal_type(a1)
|
||||
# revealed: bool | None
|
||||
reveal_type(a2)
|
||||
# revealed: None
|
||||
reveal_type(a3)
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing import Optional
|
||||
|
||||
a: Optional[int] = 1
|
||||
a = None
|
||||
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int | None`"
|
||||
a = ""
|
||||
```
|
||||
|
||||
## Typing Extensions
|
||||
|
||||
```py
|
||||
from typing_extensions import Optional
|
||||
|
||||
a: Optional[int]
|
||||
|
||||
def f():
|
||||
# revealed: int | None
|
||||
reveal_type(a)
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# Starred expression annotations
|
||||
|
||||
Type annotations for `*args` can be starred expressions themselves:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVarTuple
|
||||
|
||||
Ts = TypeVarTuple("Ts")
|
||||
|
||||
def append_int(*args: *Ts) -> tuple[*Ts, int]:
|
||||
# TODO: should show some representation of the variadic generic type
|
||||
reveal_type(args) # revealed: @Todo(function parameter type)
|
||||
|
||||
return (*args, 1)
|
||||
|
||||
# TODO should be tuple[Literal[True], Literal["a"], int]
|
||||
reveal_type(append_int(True, "a")) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
@@ -0,0 +1,218 @@
|
||||
# String annotations
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
def f() -> "int":
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
```py
|
||||
def f() -> "'int'":
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
## Type expression
|
||||
|
||||
```py
|
||||
def f1() -> "int | str":
|
||||
return 1
|
||||
|
||||
def f2() -> "tuple[int, str]":
|
||||
return 1
|
||||
|
||||
reveal_type(f1()) # revealed: int | str
|
||||
reveal_type(f2()) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Partial
|
||||
|
||||
```py
|
||||
def f() -> tuple[int, "str"]:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Deferred
|
||||
|
||||
```py
|
||||
def f() -> "Foo":
|
||||
return Foo()
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Foo
|
||||
```
|
||||
|
||||
## Deferred (undefined)
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
def f() -> "Foo":
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Partial deferred
|
||||
|
||||
```py
|
||||
def f() -> int | "Foo":
|
||||
return 1
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: int | Foo
|
||||
```
|
||||
|
||||
## `typing.Literal`
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f1() -> Literal["Foo", "Bar"]:
|
||||
return "Foo"
|
||||
|
||||
def f2() -> 'Literal["Foo", "Bar"]':
|
||||
return "Foo"
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f1()) # revealed: Literal["Foo", "Bar"]
|
||||
reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
|
||||
```
|
||||
|
||||
## Various string kinds
|
||||
|
||||
```py
|
||||
# error: [annotation-raw-string] "Type expressions cannot use raw string literal"
|
||||
def f1() -> r"int":
|
||||
return 1
|
||||
|
||||
# error: [annotation-f-string] "Type expressions cannot use f-strings"
|
||||
def f2() -> f"int":
|
||||
return 1
|
||||
|
||||
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
|
||||
def f3() -> b"int":
|
||||
return 1
|
||||
|
||||
def f4() -> "int":
|
||||
return 1
|
||||
|
||||
# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals"
|
||||
def f5() -> "in" "t":
|
||||
return 1
|
||||
|
||||
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
|
||||
def f6() -> "\N{LATIN SMALL LETTER I}nt":
|
||||
return 1
|
||||
|
||||
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
|
||||
def f7() -> "\x69nt":
|
||||
return 1
|
||||
|
||||
def f8() -> """int""":
|
||||
return 1
|
||||
|
||||
def f9() -> "b'int'":
|
||||
return 1
|
||||
|
||||
reveal_type(f1()) # revealed: Unknown
|
||||
reveal_type(f2()) # revealed: Unknown
|
||||
reveal_type(f3()) # revealed: Unknown
|
||||
reveal_type(f4()) # revealed: int
|
||||
reveal_type(f5()) # revealed: Unknown
|
||||
reveal_type(f6()) # revealed: Unknown
|
||||
reveal_type(f7()) # revealed: Unknown
|
||||
reveal_type(f8()) # revealed: int
|
||||
reveal_type(f9()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Various string kinds in `typing.Literal`
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f() -> Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]:
|
||||
return "normal"
|
||||
|
||||
reveal_type(f()) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
|
||||
```
|
||||
|
||||
## Class variables
|
||||
|
||||
```py
|
||||
MyType = int
|
||||
|
||||
class Aliases:
|
||||
MyType = str
|
||||
|
||||
forward: "MyType"
|
||||
not_forward: MyType
|
||||
|
||||
reveal_type(Aliases.forward) # revealed: str
|
||||
reveal_type(Aliases.not_forward) # revealed: str
|
||||
```
|
||||
|
||||
## Annotated assignment
|
||||
|
||||
```py
|
||||
a: "int" = 1
|
||||
b: "'int'" = 1
|
||||
c: "Foo"
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
|
||||
d: "Foo" = 1
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
c = Foo()
|
||||
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[1]
|
||||
reveal_type(c) # revealed: Foo
|
||||
reveal_type(d) # revealed: Foo
|
||||
```
|
||||
|
||||
## Parameter
|
||||
|
||||
TODO: Add tests once parameter inference is supported
|
||||
|
||||
## Invalid expressions
|
||||
|
||||
The expressions in these string annotations aren't valid expressions in this context but we
|
||||
shouldn't panic.
|
||||
|
||||
```py
|
||||
a: "1 or 2"
|
||||
b: "(x := 1)"
|
||||
c: "1 + 2"
|
||||
d: "lambda x: x"
|
||||
e: "x if True else y"
|
||||
f: "{'a': 1, 'b': 2}"
|
||||
g: "{1, 2}"
|
||||
h: "[i for i in range(5)]"
|
||||
i: "{i for i in range(5)}"
|
||||
j: "{i: i for i in range(5)}"
|
||||
k: "(i for i in range(5))"
|
||||
l: "await 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
m: "yield 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
n: "yield from 1"
|
||||
o: "1 < 2"
|
||||
p: "call()"
|
||||
r: "[1, 2]"
|
||||
s: "(1, 2)"
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# Union
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Union` can be used to construct union types same as `|` operator.
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
a: Union[int, str]
|
||||
a1: Union[int, bool]
|
||||
a2: Union[int, Union[float, str]]
|
||||
a3: Union[int, None]
|
||||
a4: Union[Union[float, str]]
|
||||
a5: Union[int]
|
||||
a6: Union[()]
|
||||
|
||||
def f():
|
||||
# revealed: int | str
|
||||
reveal_type(a)
|
||||
# Since bool is a subtype of int we simplify to int here. But we do allow assigning boolean values (see below).
|
||||
# revealed: int
|
||||
reveal_type(a1)
|
||||
# revealed: int | float | str
|
||||
reveal_type(a2)
|
||||
# revealed: int | None
|
||||
reveal_type(a3)
|
||||
# revealed: float | str
|
||||
reveal_type(a4)
|
||||
# revealed: int
|
||||
reveal_type(a5)
|
||||
# revealed: Never
|
||||
reveal_type(a6)
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
a: Union[int, str]
|
||||
a = 1
|
||||
a = ""
|
||||
a1: Union[int, bool]
|
||||
a1 = 1
|
||||
a1 = True
|
||||
# error: [invalid-assignment] "Object of type `Literal[b""]` is not assignable to `int | str`"
|
||||
a = b""
|
||||
```
|
||||
|
||||
## Typing Extensions
|
||||
|
||||
```py
|
||||
from typing_extensions import Union
|
||||
|
||||
a: Union[int, str]
|
||||
|
||||
def f():
|
||||
# revealed: int | str
|
||||
reveal_type(a)
|
||||
```
|
||||
@@ -23,6 +23,58 @@ x: int
|
||||
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## Tuple annotations are understood
|
||||
|
||||
```py path=module.py
|
||||
from typing_extensions import Unpack
|
||||
|
||||
a: tuple[()] = ()
|
||||
b: tuple[int] = (42,)
|
||||
c: tuple[str, int] = ("42", 42)
|
||||
d: tuple[tuple[str, str], tuple[int, int]] = (("foo", "foo"), (42, 42))
|
||||
e: tuple[str, ...] = ()
|
||||
# TODO: we should not emit this error
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[tuple]` is possibly unbound"
|
||||
f: tuple[str, *tuple[int, ...], bytes] = ("42", b"42")
|
||||
g: tuple[str, Unpack[tuple[int, ...]], bytes] = ("42", b"42")
|
||||
h: tuple[list[int], list[int]] = ([], [])
|
||||
i: tuple[str | int, str | int] = (42, 42)
|
||||
j: tuple[str | int] = (42,)
|
||||
```
|
||||
|
||||
```py path=script.py
|
||||
from module import a, b, c, d, e, f, g, h, i, j
|
||||
|
||||
reveal_type(a) # revealed: tuple[()]
|
||||
reveal_type(b) # revealed: tuple[int]
|
||||
reveal_type(c) # revealed: tuple[str, int]
|
||||
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
|
||||
|
||||
# TODO: homogenous tuples, PEP-646 tuples
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(f) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
|
||||
# TODO: support more kinds of type expressions in annotations
|
||||
reveal_type(h) # revealed: @Todo(full tuple[...] support)
|
||||
|
||||
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||
reveal_type(j) # revealed: tuple[str | int]
|
||||
```
|
||||
|
||||
## Incorrect tuple assignments are complained about
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Object of type `tuple[Literal[1], Literal[2]]` is not assignable to `tuple[()]`"
|
||||
a: tuple[()] = (1, 2)
|
||||
|
||||
# error: [invalid-assignment] "Object of type `tuple[Literal["foo"]]` is not assignable to `tuple[int]`"
|
||||
b: tuple[int] = ("foo",)
|
||||
|
||||
# error: [invalid-assignment] "Object of type `tuple[list, Literal["foo"]]` is not assignable to `tuple[str | int, str]`"
|
||||
c: tuple[str | int, str] = ([], "foo")
|
||||
```
|
||||
|
||||
## PEP-604 annotations are supported
|
||||
|
||||
```py
|
||||
@@ -41,3 +93,46 @@ def baz() -> str | str:
|
||||
|
||||
reveal_type(baz()) # revealed: str
|
||||
```
|
||||
|
||||
## Attribute expressions in type annotations are understood
|
||||
|
||||
```py
|
||||
import builtins
|
||||
|
||||
int = "foo"
|
||||
a: builtins.int = 42
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal["bar"]` is not assignable to `int`"
|
||||
b: builtins.int = "bar"
|
||||
|
||||
c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = ((42, 42), 42)
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `tuple[tuple[int, int], int]`"
|
||||
c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = "foo"
|
||||
```
|
||||
|
||||
## Future annotations are deferred
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
x: Foo
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
|
||||
## Annotations in stub files are deferred
|
||||
|
||||
```pyi path=main.pyi
|
||||
x: Foo
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# Augmented assignment
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
x = 3
|
||||
x -= 1
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
|
||||
x = 1.0
|
||||
x /= 2
|
||||
reveal_type(x) # revealed: float
|
||||
```
|
||||
|
||||
## Dunder methods
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __isub__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
|
||||
x = C()
|
||||
x -= 1
|
||||
reveal_type(x) # revealed: str
|
||||
|
||||
class C:
|
||||
def __iadd__(self, other: str) -> float:
|
||||
return 1.0
|
||||
|
||||
x = C()
|
||||
x += "Hello"
|
||||
reveal_type(x) # revealed: float
|
||||
```
|
||||
|
||||
## Unsupported types
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __isub__(self, other: str) -> int:
|
||||
return 42
|
||||
|
||||
x = C()
|
||||
x -= 1
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Method union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class Foo:
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
else:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
f = Foo()
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: str | int
|
||||
```
|
||||
|
||||
## Partially bound `__iadd__`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class Foo:
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
|
||||
f = Foo()
|
||||
|
||||
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
|
||||
# that `Foo.__iadd__` may be unbound as additional context.
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Partially bound with `__add__`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class Foo:
|
||||
def __add__(self, other: str) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
|
||||
f = Foo()
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | str
|
||||
```
|
||||
|
||||
## Partially bound target union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
if bool_instance():
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
|
||||
## Target union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class Foo:
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: str | float
|
||||
```
|
||||
|
||||
## Partially bound target union with `__add__`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
class Bar:
|
||||
def __add__(self, other: int) -> bytes:
|
||||
return b"Hello, world!"
|
||||
|
||||
def __iadd__(self, other: int) -> float:
|
||||
return 42.0
|
||||
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = Bar()
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
@@ -6,11 +6,19 @@
|
||||
x = foo # error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
foo = 1
|
||||
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
# No error `unresolved-reference` diagnostic is reported for `x`. This is
|
||||
# desirable because we would get a lot of cascading errors even though there
|
||||
# is only one root cause (the unbound variable `foo`).
|
||||
|
||||
# revealed: Unknown
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
Note: in this particular example, one could argue that the most likely error would be a wrong order
|
||||
of the `x`/`foo` definitions, and so it could be desirable to infer `Literal[1]` for the type of
|
||||
`x`. On the other hand, there might be a variable `fob` a little higher up in this file, and the
|
||||
actual error might have been just a typo. Inferring `Unknown` thus seems like the safest option.
|
||||
|
||||
## Unbound class variable
|
||||
|
||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
||||
@@ -27,6 +35,26 @@ class C:
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Possibly unbound in class and global scope
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance():
|
||||
x = "abc"
|
||||
|
||||
class C:
|
||||
if bool_instance():
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
y = x
|
||||
|
||||
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
|
||||
```
|
||||
|
||||
@@ -9,12 +9,128 @@ def bool_instance() -> bool:
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
class C:
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
else:
|
||||
class C:
|
||||
class C1:
|
||||
x = 2
|
||||
|
||||
reveal_type(C.x) # revealed: Literal[1, 2]
|
||||
class C2:
|
||||
if flag:
|
||||
x = 3
|
||||
else:
|
||||
x = 4
|
||||
|
||||
reveal_type(C1.x) # revealed: Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Literal[3, 4]
|
||||
```
|
||||
|
||||
## Inherited attributes
|
||||
|
||||
```py
|
||||
class A:
|
||||
X = "foo"
|
||||
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
|
||||
reveal_type(C.X) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
## Inherited attributes (multiple inheritance)
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
|
||||
class F(O):
|
||||
X = 56
|
||||
|
||||
class E(O):
|
||||
X = 42
|
||||
|
||||
class D(O): ...
|
||||
class C(D, F): ...
|
||||
class B(E, D): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__)
|
||||
|
||||
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
|
||||
reveal_type(A.X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
## Unions with possibly unbound paths
|
||||
|
||||
### Definite boundness within a class
|
||||
|
||||
In this example, the `x` attribute is not defined in the `C2` element of the union:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
class C2: ...
|
||||
|
||||
class C3:
|
||||
x = 3
|
||||
|
||||
flag1 = bool_instance()
|
||||
flag2 = bool_instance()
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
### Possibly-unbound within a class
|
||||
|
||||
We raise the same diagnostic if the attribute is possibly-unbound in at least one element of the
|
||||
union:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
class C2:
|
||||
if bool_instance():
|
||||
x = 2
|
||||
|
||||
class C3:
|
||||
x = 3
|
||||
|
||||
flag1 = bool_instance()
|
||||
flag2 = bool_instance()
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## Unions with all paths unbound
|
||||
|
||||
If the symbol is unbound in all elements of the union, we detect that:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class C1: ...
|
||||
class C2: ...
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
C = C1 if flag else C2
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -9,8 +9,8 @@ For references, see:
|
||||
|
||||
## Operations
|
||||
|
||||
We support inference for all Python's binary operators:
|
||||
`+`, `-`, `*`, `@`, `/`, `//`, `%`, `**`, `<<`, `>>`, `&`, `^`, and `|`.
|
||||
We support inference for all Python's binary operators: `+`, `-`, `*`, `@`, `/`, `//`, `%`, `**`,
|
||||
`<<`, `>>`, `&`, `^`, and `|`.
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -152,9 +152,8 @@ reveal_type(B() - A()) # revealed: int
|
||||
|
||||
## Non-reflected precedence in general
|
||||
|
||||
In general, if the left-hand side defines `__add__` and the right-hand side
|
||||
defines `__radd__` and the right-hand side is not a subtype of the left-hand
|
||||
side, `lhs.__add__` will take precedence:
|
||||
In general, if the left-hand side defines `__add__` and the right-hand side defines `__radd__` and
|
||||
the right-hand side is not a subtype of the left-hand side, `lhs.__add__` will take precedence:
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -181,9 +180,8 @@ reveal_type(C() + C()) # revealed: int
|
||||
|
||||
## Reflected precedence for subtypes (in some cases)
|
||||
|
||||
If the right-hand operand is a subtype of the left-hand operand and has a
|
||||
different implementation of the reflected method, the reflected method on the
|
||||
right-hand operand takes precedence.
|
||||
If the right-hand operand is a subtype of the left-hand operand and has a different implementation
|
||||
of the reflected method, the reflected method on the right-hand operand takes precedence.
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -204,18 +202,13 @@ reveal_type(A() + B()) # revealed: MyString
|
||||
# N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__`
|
||||
class C(B): ...
|
||||
|
||||
# TODO: we currently only understand direct subclasses as subtypes of the superclass.
|
||||
# We need to iterate through the full MRO rather than just the class's bases;
|
||||
# if we do, we'll understand `C` as a subtype of `A`, and correctly understand this as being
|
||||
# `MyString` rather than `str`
|
||||
reveal_type(A() + C()) # revealed: str
|
||||
reveal_type(A() + C()) # revealed: MyString
|
||||
```
|
||||
|
||||
## Reflected precedence 2
|
||||
|
||||
If the right-hand operand is a subtype of the left-hand operand, but does not
|
||||
override the reflected method, the left-hand operand's non-reflected method
|
||||
still takes precedence:
|
||||
If the right-hand operand is a subtype of the left-hand operand, but does not override the reflected
|
||||
method, the left-hand operand's non-reflected method still takes precedence:
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -232,17 +225,15 @@ reveal_type(A() + B()) # revealed: str
|
||||
|
||||
## Only reflected supported
|
||||
|
||||
For example, at runtime, `(1).__add__(1.2)` is `NotImplemented`, but
|
||||
`(1.2).__radd__(1) == 2.2`, meaning that `1 + 1.2` succeeds at runtime
|
||||
(producing `2.2`). The runtime tries the second one only if the first one
|
||||
returns `NotImplemented` to signal failure.
|
||||
For example, at runtime, `(1).__add__(1.2)` is `NotImplemented`, but `(1.2).__radd__(1) == 2.2`,
|
||||
meaning that `1 + 1.2` succeeds at runtime (producing `2.2`). The runtime tries the second one only
|
||||
if the first one returns `NotImplemented` to signal failure.
|
||||
|
||||
Typeshed and other stubs annotate dunder-method calls that would return
|
||||
`NotImplemented` as being "illegal" calls. `int.__add__` is annotated as only
|
||||
"accepting" `int`s, even though it strictly-speaking "accepts" any other object
|
||||
without raising an exception -- it will simply return `NotImplemented`,
|
||||
allowing the runtime to try the `__radd__` method of the right-hand operand
|
||||
as well.
|
||||
Typeshed and other stubs annotate dunder-method calls that would return `NotImplemented` as being
|
||||
"illegal" calls. `int.__add__` is annotated as only "accepting" `int`s, even though it
|
||||
strictly-speaking "accepts" any other object without raising an exception -- it will simply return
|
||||
`NotImplemented`, allowing the runtime to try the `__radd__` method of the right-hand operand as
|
||||
well.
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -308,8 +299,8 @@ reveal_type(y + 4.12) # revealed: int
|
||||
|
||||
## With literal types
|
||||
|
||||
When we have a literal type for one operand, we're able to fall back to the
|
||||
instance handling for its instance super-type.
|
||||
When we have a literal type for one operand, we're able to fall back to the instance handling for
|
||||
its instance super-type.
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -326,7 +317,7 @@ reveal_type(1 + A()) # revealed: int
|
||||
reveal_type(A() + "foo") # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type("foo" + A()) # revealed: @Todo
|
||||
reveal_type("foo" + A()) # revealed: @Todo(return type)
|
||||
|
||||
reveal_type(A() + b"foo") # revealed: A
|
||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||
@@ -334,7 +325,7 @@ reveal_type(b"foo" + A()) # revealed: bytes
|
||||
|
||||
reveal_type(A() + ()) # revealed: A
|
||||
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
||||
reveal_type(() + A()) # revealed: @Todo
|
||||
reveal_type(() + A()) # revealed: @Todo(return type)
|
||||
|
||||
literal_string_instance = "foo" * 1_000_000_000
|
||||
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
||||
@@ -343,20 +334,18 @@ reveal_type(literal_string_instance) # revealed: LiteralString
|
||||
reveal_type(A() + literal_string_instance) # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
|
||||
`Any` and `Unknown` represent a set of possible runtime objects, wherein the
|
||||
bounds of the set are unknown. Whether the left-hand operand's dunder or the
|
||||
right-hand operand's reflected dunder depends on whether the right-hand operand
|
||||
is an instance of a class that is a subclass of the left-hand operand's class
|
||||
and overrides the reflected dunder. In the following example, because of the
|
||||
unknowable nature of `Any`/`Unknown`, we must consider both possibilities:
|
||||
`Any`/`Unknown` might resolve to an unknown third class that inherits from `X`
|
||||
and overrides `__radd__`; but it also might not. Thus, the correct answer here
|
||||
for the `reveal_type` is `int | Unknown`.
|
||||
`Any` and `Unknown` represent a set of possible runtime objects, wherein the bounds of the set are
|
||||
unknown. Whether the left-hand operand's dunder or the right-hand operand's reflected dunder depends
|
||||
on whether the right-hand operand is an instance of a class that is a subclass of the left-hand
|
||||
operand's class and overrides the reflected dunder. In the following example, because of the
|
||||
unknowable nature of `Any`/`Unknown`, we must consider both possibilities: `Any`/`Unknown` might
|
||||
resolve to an unknown third class that inherits from `X` and overrides `__radd__`; but it also might
|
||||
not. Thus, the correct answer here for the `reveal_type` is `int | Unknown`.
|
||||
|
||||
```py
|
||||
from does_not_exist import Foo # error: [unresolved-import]
|
||||
@@ -426,10 +415,9 @@ reveal_type(B() + C())
|
||||
|
||||
### Reflected dunder is not tried between two objects of the same type
|
||||
|
||||
For the specific case where the left-hand operand is the exact same type as the
|
||||
right-hand operand, the reflected dunder of the right-hand operand is not
|
||||
tried; the runtime short-circuits after trying the unreflected dunder of the
|
||||
left-hand operand. For context, see
|
||||
For the specific case where the left-hand operand is the exact same type as the right-hand operand,
|
||||
the reflected dunder of the right-hand operand is not tried; the runtime short-circuits after trying
|
||||
the unreflected dunder of the left-hand operand. For context, see
|
||||
[this mailing list discussion](https://mail.python.org/archives/list/python-dev@python.org/thread/7NZUCODEAPQFMRFXYRMGJXDSIS3WJYIV/).
|
||||
|
||||
```py
|
||||
|
||||
@@ -12,11 +12,11 @@ def bool_instance() -> bool:
|
||||
|
||||
if bool_instance() or (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if bool_instance() and (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## First expression is always evaluated
|
||||
@@ -36,14 +36,14 @@ if (x := 1) and bool_instance():
|
||||
|
||||
```py
|
||||
if True or (x := 1):
|
||||
# TODO: infer that the second arm is never executed so type should be just "Unbound".
|
||||
# TODO: infer that the second arm is never executed, and raise `unresolved-reference`.
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if True and (x := 1):
|
||||
# TODO: infer that the second arm is always executed so type should be just "Literal[1]".
|
||||
# TODO: infer that the second arm is always executed, do not raise a diagnostic
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Later expressions can always use variables from earlier expressions
|
||||
@@ -55,7 +55,7 @@ def bool_instance() -> bool:
|
||||
bool_instance() or (x := 1) or reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
# error: [unresolved-reference]
|
||||
bool_instance() or reveal_type(y) or (y := 1) # revealed: Unbound
|
||||
bool_instance() or reveal_type(y) or (y := 1) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Nested expressions
|
||||
@@ -65,14 +65,14 @@ def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance() or ((x := 1) and bool_instance()):
|
||||
# error: "Name `x` used when possibly not defined"
|
||||
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if ((y := 1) and bool_instance()) or bool_instance():
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
if (bool_instance() and (z := 1)) or reveal_type(z): # revealed: Unbound | Literal[1]
|
||||
if (bool_instance() and (z := 1)) or reveal_type(z): # revealed: Literal[1]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(z) # revealed: Unbound | Literal[1]
|
||||
reveal_type(z) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -18,3 +18,58 @@ class Unit: ...
|
||||
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly unbound `__call__` method
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
class PossiblyNotCallable:
|
||||
if flag():
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = PossiblyNotCallable()
|
||||
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(result) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly unbound callable
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
class PossiblyUnbound:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
a = PossiblyUnbound()
|
||||
reveal_type(a()) # revealed: int
|
||||
```
|
||||
|
||||
## Non-callable `__call__`
|
||||
|
||||
```py
|
||||
class NonCallable:
|
||||
__call__ = 1
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `NonCallable` is not callable"
|
||||
reveal_type(a()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly non-callable `__call__`
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
class NonCallable:
|
||||
if flag():
|
||||
__call__ = 1
|
||||
else:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
@@ -16,7 +16,16 @@ async def get_int_async() -> int:
|
||||
return 42
|
||||
|
||||
# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]`
|
||||
reveal_type(get_int_async()) # revealed: @Todo
|
||||
reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
|
||||
```
|
||||
|
||||
## Generic
|
||||
|
||||
```py
|
||||
def get_int[T]() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(get_int()) # revealed: int
|
||||
```
|
||||
|
||||
## Decorated
|
||||
@@ -27,6 +36,8 @@ from typing import Callable
|
||||
def foo() -> int:
|
||||
return 42
|
||||
|
||||
# TODO: This should be resolved once we understand `Callable` annotations
|
||||
# error: [annotation-with-invalid-expression]
|
||||
def decorator(func) -> Callable[[], int]:
|
||||
return foo
|
||||
|
||||
@@ -35,7 +46,7 @@ def bar() -> str:
|
||||
return "bar"
|
||||
|
||||
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
||||
reveal_type(bar()) # revealed: @Todo
|
||||
reveal_type(bar()) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Invalid callable
|
||||
@@ -44,3 +55,16 @@ reveal_type(bar()) # revealed: @Todo
|
||||
nonsense = 123
|
||||
x = nonsense() # error: "Object of type `Literal[123]` is not callable"
|
||||
```
|
||||
|
||||
## Potentially unbound function
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
def foo() -> int:
|
||||
return 42
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Comparison: Byte literals
|
||||
|
||||
These tests assert that we infer precise `Literal` types for comparisons between objects
|
||||
inferred as having `Literal` bytes types:
|
||||
These tests assert that we infer precise `Literal` types for comparisons between objects inferred as
|
||||
having `Literal` bytes types:
|
||||
|
||||
```py
|
||||
reveal_type(b"abc" == b"abc") # revealed: Literal[True]
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Identity tests
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_a() -> A: ...
|
||||
def get_object() -> object: ...
|
||||
|
||||
a1 = get_a()
|
||||
a2 = get_a()
|
||||
|
||||
n1 = None
|
||||
n2 = None
|
||||
|
||||
o = get_object()
|
||||
|
||||
reveal_type(a1 is a1) # revealed: bool
|
||||
reveal_type(a1 is a2) # revealed: bool
|
||||
|
||||
reveal_type(n1 is n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is n2) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is a1) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is o) # revealed: bool
|
||||
reveal_type(n1 is o) # revealed: bool
|
||||
|
||||
reveal_type(a1 is not a1) # revealed: bool
|
||||
reveal_type(a1 is not a2) # revealed: bool
|
||||
|
||||
reveal_type(n1 is not n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is not n2) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is not n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is not a1) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is not o) # revealed: bool
|
||||
reveal_type(n1 is not o) # revealed: bool
|
||||
```
|
||||
@@ -1,8 +1,8 @@
|
||||
# Comparison: Membership Test
|
||||
|
||||
In Python, the term "membership test operators" refers to the operators
|
||||
`in` and `not in`. To customize their behavior, classes can implement one of
|
||||
the special methods `__contains__`, `__iter__`, or `__getitem__`.
|
||||
In Python, the term "membership test operators" refers to the operators `in` and `not in`. To
|
||||
customize their behavior, classes can implement one of the special methods `__contains__`,
|
||||
`__iter__`, or `__getitem__`.
|
||||
|
||||
For references, see:
|
||||
|
||||
@@ -28,8 +28,8 @@ reveal_type(42 not in A()) # revealed: bool
|
||||
|
||||
## Implements `__iter__`
|
||||
|
||||
Classes that don't implement `__contains__`, but do implement `__iter__`, also
|
||||
support containment checks; the needle will be sought in their iterated items:
|
||||
Classes that don't implement `__contains__`, but do implement `__iter__`, also support containment
|
||||
checks; the needle will be sought in their iterated items:
|
||||
|
||||
```py
|
||||
class StringIterator:
|
||||
@@ -48,11 +48,10 @@ reveal_type(42 not in A()) # revealed: bool
|
||||
|
||||
## Implements `__getitems__`
|
||||
|
||||
The final fallback is to implement `__getitem__` for integer keys. Python will
|
||||
call `__getitem__` with `0`, `1`, `2`... until either the needle is found
|
||||
(leading the membership test to evaluate to `True`) or `__getitem__` raises
|
||||
`IndexError` (the raised exception is swallowed, but results in the membership
|
||||
test evaluating to `False`).
|
||||
The final fallback is to implement `__getitem__` for integer keys. Python will call `__getitem__`
|
||||
with `0`, `1`, `2`... until either the needle is found (leading the membership test to evaluate to
|
||||
`True`) or `__getitem__` raises `IndexError` (the raised exception is swallowed, but results in the
|
||||
membership test evaluating to `False`).
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -67,8 +66,8 @@ reveal_type(42 not in A()) # revealed: bool
|
||||
|
||||
## Wrong Return Type
|
||||
|
||||
Python coerces the results of containment checks to `bool`, even if `__contains__`
|
||||
returns a non-bool:
|
||||
Python coerces the results of containment checks to `bool`, even if `__contains__` returns a
|
||||
non-bool:
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -81,8 +80,7 @@ reveal_type("hello" not in A()) # revealed: bool
|
||||
|
||||
## Literal Result for `in` and `not in`
|
||||
|
||||
`__contains__` with a literal return type may result in a `BooleanLiteral`
|
||||
outcome.
|
||||
`__contains__` with a literal return type may result in a `BooleanLiteral` outcome.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
@@ -95,20 +93,17 @@ class AlwaysFalse:
|
||||
def __contains__(self, item: int) -> Literal[""]:
|
||||
return ""
|
||||
|
||||
# TODO: it should be Literal[True] and Literal[False]
|
||||
reveal_type(42 in AlwaysTrue()) # revealed: @Todo
|
||||
reveal_type(42 not in AlwaysTrue()) # revealed: @Todo
|
||||
reveal_type(42 in AlwaysTrue()) # revealed: Literal[True]
|
||||
reveal_type(42 not in AlwaysTrue()) # revealed: Literal[False]
|
||||
|
||||
# TODO: it should be Literal[False] and Literal[True]
|
||||
reveal_type(42 in AlwaysFalse()) # revealed: @Todo
|
||||
reveal_type(42 not in AlwaysFalse()) # revealed: @Todo
|
||||
reveal_type(42 in AlwaysFalse()) # revealed: Literal[False]
|
||||
reveal_type(42 not in AlwaysFalse()) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## No Fallback for `__contains__`
|
||||
|
||||
If `__contains__` is implemented, checking membership of a type it doesn't
|
||||
accept is an error; it doesn't result in a fallback to `__iter__` or
|
||||
`__getitem__`:
|
||||
If `__contains__` is implemented, checking membership of a type it doesn't accept is an error; it
|
||||
doesn't result in a fallback to `__iter__` or `__getitem__`:
|
||||
|
||||
```py
|
||||
class CheckContains: ...
|
||||
@@ -151,8 +146,8 @@ reveal_type(CheckGetItem() in B()) # revealed: bool
|
||||
|
||||
## Invalid Old-Style Iteration
|
||||
|
||||
If `__getitem__` is implemented but does not accept integer arguments, then
|
||||
the membership test is not supported and should trigger a diagnostic.
|
||||
If `__getitem__` is implemented but does not accept integer arguments, then the membership test is
|
||||
not supported and should trigger a diagnostic.
|
||||
|
||||
```py
|
||||
class A:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# Comparison: Rich Comparison
|
||||
|
||||
Rich comparison operations (`==`, `!=`, `<`, `<=`, `>`, `>=`) in Python are
|
||||
implemented through double-underscore methods that allow customization of
|
||||
comparison behavior.
|
||||
Rich comparison operations (`==`, `!=`, `<`, `<=`, `>`, `>=`) in Python are implemented through
|
||||
double-underscore methods that allow customization of comparison behavior.
|
||||
|
||||
For references, see:
|
||||
|
||||
@@ -11,9 +10,8 @@ For references, see:
|
||||
|
||||
## Rich Comparison Dunder Implementations For Same Class
|
||||
|
||||
Classes can support rich comparison by implementing dunder methods like
|
||||
`__eq__`, `__ne__`, etc. The most common case involves implementing these
|
||||
methods for the same type:
|
||||
Classes can support rich comparison by implementing dunder methods like `__eq__`, `__ne__`, etc. The
|
||||
most common case involves implementing these methods for the same type:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
@@ -47,8 +45,8 @@ reveal_type(A() >= A()) # revealed: set
|
||||
|
||||
## Rich Comparison Dunder Implementations for Other Class
|
||||
|
||||
In some cases, classes may implement rich comparison dunder methods for
|
||||
comparisons with a different type:
|
||||
In some cases, classes may implement rich comparison dunder methods for comparisons with a different
|
||||
type:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
@@ -84,10 +82,9 @@ reveal_type(A() >= B()) # revealed: set
|
||||
|
||||
## Reflected Comparisons
|
||||
|
||||
Fallback to the right-hand side’s comparison methods occurs when the left-hand
|
||||
side does not define them. Note: class `B` has its own `__eq__` and `__ne__`
|
||||
methods to override those of `object`, but these methods will be ignored here
|
||||
because they require a mismatched operand type.
|
||||
Fallback to the right-hand side’s comparison methods occurs when the left-hand side does not define
|
||||
them. Note: class `B` has its own `__eq__` and `__ne__` methods to override those of `object`, but
|
||||
these methods will be ignored here because they require a mismatched operand type.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
@@ -148,9 +145,9 @@ reveal_type(C() <= C()) # revealed: float
|
||||
|
||||
## Reflected Comparisons with Subclasses
|
||||
|
||||
When subclasses override comparison methods, these overridden methods take
|
||||
precedence over those in the parent class. Class `B` inherits from `A` and
|
||||
redefines comparison methods to return types other than `A`.
|
||||
When subclasses override comparison methods, these overridden methods take precedence over those in
|
||||
the parent class. Class `B` inherits from `A` and redefines comparison methods to return types other
|
||||
than `A`.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
@@ -205,9 +202,8 @@ reveal_type(A() >= B()) # revealed: bytes
|
||||
|
||||
## Reflected Comparisons with Subclass But Falls Back to LHS
|
||||
|
||||
In the case of a subclass, the right-hand side has priority. However, if the
|
||||
overridden dunder method has an mismatched type to operand, the comparison will
|
||||
fall back to the left-hand side.
|
||||
In the case of a subclass, the right-hand side has priority. However, if the overridden dunder
|
||||
method has an mismatched type to operand, the comparison will fall back to the left-hand side.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
@@ -233,15 +229,13 @@ reveal_type(A() > B()) # revealed: B
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
|
||||
`Any` and `Unknown` represent a set of possible runtime objects, wherein the
|
||||
bounds of the set are unknown. Whether the left-hand operand's dunder or the
|
||||
right-hand operand's reflected dunder depends on whether the right-hand operand
|
||||
is an instance of a class that is a subclass of the left-hand operand's class
|
||||
and overrides the reflected dunder. In the following example, because of the
|
||||
unknowable nature of `Any`/`Unknown`, we must consider both possibilities:
|
||||
`Any`/`Unknown` might resolve to an unknown third class that inherits from `X`
|
||||
and overrides `__gt__`; but it also might not. Thus, the correct answer here for
|
||||
the `reveal_type` is `int | Unknown`.
|
||||
`Any` and `Unknown` represent a set of possible runtime objects, wherein the bounds of the set are
|
||||
unknown. Whether the left-hand operand's dunder or the right-hand operand's reflected dunder depends
|
||||
on whether the right-hand operand is an instance of a class that is a subclass of the left-hand
|
||||
operand's class and overrides the reflected dunder. In the following example, because of the
|
||||
unknowable nature of `Any`/`Unknown`, we must consider both possibilities: `Any`/`Unknown` might
|
||||
resolve to an unknown third class that inherits from `X` and overrides `__gt__`; but it also might
|
||||
not. Thus, the correct answer here for the `reveal_type` is `int | Unknown`.
|
||||
|
||||
(This test is referenced from `mdtest/binary/instances.md`)
|
||||
|
||||
@@ -262,12 +256,10 @@ reveal_type(X() < Y()) # revealed: int
|
||||
|
||||
## Equality and Inequality Fallback
|
||||
|
||||
This test confirms that `==` and `!=` comparisons default to identity
|
||||
comparisons (`is`, `is not`) when argument types do not match the method
|
||||
signature.
|
||||
This test confirms that `==` and `!=` comparisons default to identity comparisons (`is`, `is not`)
|
||||
when argument types do not match the method signature.
|
||||
|
||||
Please refer to the
|
||||
[docs](https://docs.python.org/3/reference/datamodel.html#object.__eq__)
|
||||
Please refer to the [docs](https://docs.python.org/3/reference/datamodel.html#object.__eq__)
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# Comparison: Intersections
|
||||
|
||||
## Positive contributions
|
||||
|
||||
If we have an intersection type `A & B` and we get a definitive true/false answer for one of the
|
||||
types, we can infer that the result for the intersection type is also true/false:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
class Child1(Base):
|
||||
def __eq__(self, other) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class Child2(Base): ...
|
||||
|
||||
def get_base() -> Base: ...
|
||||
|
||||
x = get_base()
|
||||
c1 = Child1()
|
||||
|
||||
# Create an intersection type through narrowing:
|
||||
if isinstance(x, Child1):
|
||||
if isinstance(x, Child2):
|
||||
reveal_type(x) # revealed: Child1 & Child2
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[True]
|
||||
|
||||
# Other comparison operators fall back to the base type:
|
||||
reveal_type(x > 1) # revealed: bool
|
||||
reveal_type(x is c1) # revealed: bool
|
||||
```
|
||||
|
||||
## Negative contributions
|
||||
|
||||
Negative contributions to the intersection type only allow simplifications in a few special cases
|
||||
(equality and identity comparisons).
|
||||
|
||||
### Equality comparisons
|
||||
|
||||
#### Literal strings
|
||||
|
||||
```py
|
||||
x = "x" * 1_000_000_000
|
||||
y = "y" * 1_000_000_000
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
|
||||
if x != "abc":
|
||||
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
|
||||
|
||||
reveal_type(x == "abc") # revealed: Literal[False]
|
||||
reveal_type("abc" == x) # revealed: Literal[False]
|
||||
reveal_type(x == "something else") # revealed: bool
|
||||
reveal_type("something else" == x) # revealed: bool
|
||||
|
||||
reveal_type(x != "abc") # revealed: Literal[True]
|
||||
reveal_type("abc" != x) # revealed: Literal[True]
|
||||
reveal_type(x != "something else") # revealed: bool
|
||||
reveal_type("something else" != x) # revealed: bool
|
||||
|
||||
reveal_type(x == y) # revealed: bool
|
||||
reveal_type(y == x) # revealed: bool
|
||||
reveal_type(x != y) # revealed: bool
|
||||
reveal_type(y != x) # revealed: bool
|
||||
|
||||
reveal_type(x >= "abc") # revealed: bool
|
||||
reveal_type("abc" >= x) # revealed: bool
|
||||
|
||||
reveal_type(x in "abc") # revealed: bool
|
||||
reveal_type("abc" in x) # revealed: bool
|
||||
```
|
||||
|
||||
#### Integers
|
||||
|
||||
```py
|
||||
def get_int() -> int: ...
|
||||
|
||||
x = get_int()
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
|
||||
reveal_type(x != 1) # revealed: Literal[True]
|
||||
reveal_type(x != 2) # revealed: bool
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[False]
|
||||
reveal_type(x == 2) # revealed: bool
|
||||
```
|
||||
|
||||
### Identity comparisons
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
o = object()
|
||||
|
||||
a = A()
|
||||
n = None
|
||||
|
||||
if o is not None:
|
||||
reveal_type(o) # revealed: object & ~None
|
||||
|
||||
reveal_type(o is n) # revealed: Literal[False]
|
||||
reveal_type(o is not n) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Diagnostics
|
||||
|
||||
### Unsupported operators for positive contributions
|
||||
|
||||
Raise an error if any of the positive contributions to the intersection type are unsupported for the
|
||||
given operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool: ...
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & NonContainer
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
### Unsupported operators for negative contributions
|
||||
|
||||
Do *not* raise an error if any of the negative contributions to the intersection type are
|
||||
unsupported for the given operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool: ...
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if not isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & ~NonContainer
|
||||
|
||||
# No error here!
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
@@ -5,9 +5,9 @@ Walking through examples:
|
||||
- `a = A() < B() < C()`
|
||||
|
||||
1. `A() < B() and B() < C()` - split in N comparison
|
||||
1. `A()` and `B()` - evaluate outcome types
|
||||
1. `bool` and `bool` - evaluate truthiness
|
||||
1. `A | B` - union of "first true" types
|
||||
1. `A()` and `B()` - evaluate outcome types
|
||||
1. `bool` and `bool` - evaluate truthiness
|
||||
1. `A | B` - union of "first true" types
|
||||
|
||||
- `b = 0 < 1 < A() < 3`
|
||||
|
||||
@@ -58,7 +58,9 @@ reveal_type(c >= d) # revealed: Literal[True]
|
||||
#### Results with Ambiguity
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool: ...
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
@@ -82,8 +84,8 @@ reveal_type(a >= b) # revealed: bool
|
||||
|
||||
#### Comparison Unsupported
|
||||
|
||||
If two tuples contain types that do not support comparison, the result may be `Unknown`.
|
||||
However, `==` and `!=` are exceptions and can still provide definite results.
|
||||
If two tuples contain types that do not support comparison, the result may be `Unknown`. However,
|
||||
`==` and `!=` are exceptions and can still provide definite results.
|
||||
|
||||
```py
|
||||
a = (1, 2)
|
||||
@@ -102,8 +104,8 @@ reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
```
|
||||
|
||||
However, if the lexicographic comparison completes without reaching a point where str and int are compared,
|
||||
Python will still produce a result based on the prior elements.
|
||||
However, if the lexicographic comparison completes without reaching a point where str and int are
|
||||
compared, Python will still produce a result based on the prior elements.
|
||||
|
||||
```py path=short_circuit.py
|
||||
a = (1, 2)
|
||||
@@ -134,23 +136,158 @@ reveal_type(c >= c) # revealed: Literal[True]
|
||||
|
||||
#### Non Boolean Rich Comparisons
|
||||
|
||||
Rich comparison methods defined in a class affect tuple comparisons as well. Proper type inference
|
||||
should be possible even in cases where these methods return non-boolean types.
|
||||
|
||||
Note: Tuples use lexicographic comparisons. If the `==` result for all paired elements in the tuple
|
||||
is True, the comparison then considers the tuple’s length. Regardless of the return type of the
|
||||
dunder methods, the final result can still be a boolean value.
|
||||
|
||||
(+cpython: For tuples, `==` and `!=` always produce boolean results, regardless of the return type
|
||||
of the dunder methods.)
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __eq__(self, o) -> str: ...
|
||||
def __ne__(self, o) -> int: ...
|
||||
def __lt__(self, o) -> float: ...
|
||||
def __le__(self, o) -> object: ...
|
||||
def __gt__(self, o) -> tuple: ...
|
||||
def __ge__(self, o) -> list: ...
|
||||
def __eq__(self, o: object) -> str:
|
||||
return "hello"
|
||||
|
||||
def __ne__(self, o: object) -> bytes:
|
||||
return b"world"
|
||||
|
||||
def __lt__(self, o: A) -> float:
|
||||
return 3.14
|
||||
|
||||
def __le__(self, o: A) -> complex:
|
||||
return complex(0.5, -0.5)
|
||||
|
||||
def __gt__(self, o: A) -> tuple:
|
||||
return (1, 2, 3)
|
||||
|
||||
def __ge__(self, o: A) -> list:
|
||||
return [1, 2, 3]
|
||||
|
||||
a = (A(), A())
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: float | Literal[False]
|
||||
reveal_type(a <= a) # revealed: complex | Literal[True]
|
||||
reveal_type(a > a) # revealed: tuple | Literal[False]
|
||||
reveal_type(a >= a) # revealed: list | Literal[True]
|
||||
|
||||
# If lexicographic comparison is finished before comparing A()
|
||||
b = ("1_foo", A())
|
||||
c = ("2_bar", A())
|
||||
|
||||
reveal_type(b == c) # revealed: Literal[False]
|
||||
reveal_type(b != c) # revealed: Literal[True]
|
||||
reveal_type(b < c) # revealed: Literal[True]
|
||||
reveal_type(b <= c) # revealed: Literal[True]
|
||||
reveal_type(b > c) # revealed: Literal[False]
|
||||
reveal_type(b >= c) # revealed: Literal[False]
|
||||
|
||||
class B:
|
||||
def __lt__(self, o: B) -> set:
|
||||
return set()
|
||||
|
||||
reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
|
||||
```
|
||||
|
||||
#### Special Handling of Eq and NotEq in Lexicographic Comparisons
|
||||
|
||||
> Example: `(int_instance(), "foo") == (int_instance(), "bar")`
|
||||
|
||||
`Eq` and `NotEq` have unique behavior compared to other operators in lexicographic comparisons.
|
||||
Specifically, for `Eq`, if any non-equal pair exists within the tuples being compared, we can
|
||||
immediately conclude that the tuples are not equal. Conversely, for `NotEq`, if any non-equal pair
|
||||
exists, we can determine that the tuples are unequal.
|
||||
|
||||
In contrast, with operators like `<` and `>`, the comparison must consider each pair of elements
|
||||
sequentially, and the final outcome might remain ambiguous until all pairs are compared.
|
||||
|
||||
```py
|
||||
def str_instance() -> str:
|
||||
return "hello"
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type("foo" == "bar") # revealed: Literal[False]
|
||||
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
|
||||
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
|
||||
reveal_type((int_instance(), "foo") == (int_instance(), "bar")) # revealed: Literal[False]
|
||||
|
||||
a = (str_instance(), int_instance(), "foo")
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
|
||||
b = (str_instance(), int_instance(), "bar")
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
|
||||
c = (str_instance(), int_instance(), "foo", "different_length")
|
||||
reveal_type(a == c) # revealed: Literal[False]
|
||||
reveal_type(a != c) # revealed: Literal[True]
|
||||
reveal_type(a < c) # revealed: bool
|
||||
reveal_type(a <= c) # revealed: bool
|
||||
reveal_type(a > c) # revealed: bool
|
||||
reveal_type(a >= c) # revealed: bool
|
||||
```
|
||||
|
||||
#### Error Propagation
|
||||
|
||||
Errors occurring within a tuple comparison should propagate outward. However, if the tuple
|
||||
comparison can clearly conclude before encountering an error, the error should not be raised.
|
||||
|
||||
```py
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
def str_instance() -> str:
|
||||
return "hello"
|
||||
|
||||
class A: ...
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
|
||||
A() < A()
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
|
||||
A() <= A()
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
|
||||
A() > A()
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
|
||||
A() >= A()
|
||||
|
||||
a = (0, int_instance(), A())
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a < a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a <= a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a > a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a >= a) # revealed: Unknown
|
||||
|
||||
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
|
||||
# and should terminate immediately.
|
||||
b = (99999, int_instance(), A())
|
||||
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
### Membership Test Comparisons
|
||||
|
||||
@@ -52,8 +52,8 @@ reveal_type(one_or_none is not None) # revealed: bool
|
||||
|
||||
## Union on both sides of the comparison
|
||||
|
||||
With unions on both sides, we need to consider the full cross product of
|
||||
options when building the resulting (union) type:
|
||||
With unions on both sides, we need to consider the full cross product of options when building the
|
||||
resulting (union) type:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
@@ -72,9 +72,9 @@ reveal_type(small > large) # revealed: Literal[False]
|
||||
|
||||
## Unsupported operations
|
||||
|
||||
Make sure we emit a diagnostic if *any* of the possible comparisons is
|
||||
unsupported. For now, we fall back to `bool` for the result type instead of
|
||||
trying to infer something more precise from the other (supported) variants:
|
||||
Make sure we emit a diagnostic if *any* of the possible comparisons is unsupported. For now, we fall
|
||||
back to `bool` for the result type instead of trying to infer something more precise from the other
|
||||
(supported) variants:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class A: ...
|
||||
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
reveal_type(a) # revealed: bool
|
||||
|
||||
@@ -33,4 +35,8 @@ reveal_type(e) # revealed: bool
|
||||
f = (1, 2) < (1, "hello")
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(f) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
|
||||
g = (bool_instance(), A()) < (bool_instance(), A())
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -37,11 +37,11 @@ x = y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
|
||||
# revealed: Unbound | Literal[2]
|
||||
# revealed: Literal[2]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(r)
|
||||
|
||||
# revealed: Unbound | Literal[5]
|
||||
# revealed: Literal[5]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(s)
|
||||
```
|
||||
|
||||
@@ -21,7 +21,7 @@ match 0:
|
||||
case 2:
|
||||
y = 3
|
||||
|
||||
# revealed: Unbound | Literal[2, 3]
|
||||
# revealed: Literal[2, 3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
@@ -41,16 +41,20 @@ except EXCEPTIONS as f:
|
||||
## Dynamic exception types
|
||||
|
||||
```py
|
||||
def foo(x: type[AttributeError], y: tuple[type[OSError], type[RuntimeError]], z: tuple[type[BaseException], ...]):
|
||||
def foo(
|
||||
x: type[AttributeError],
|
||||
y: tuple[type[OSError], type[RuntimeError]],
|
||||
z: tuple[type[BaseException], ...],
|
||||
):
|
||||
try:
|
||||
help()
|
||||
except x as e:
|
||||
# TODO: should be `AttributeError`
|
||||
reveal_type(e) # revealed: @Todo
|
||||
reveal_type(e) # revealed: @Todo(exception type)
|
||||
except y as f:
|
||||
# TODO: should be `OSError | RuntimeError`
|
||||
reveal_type(f) # revealed: @Todo
|
||||
reveal_type(f) # revealed: @Todo(exception type)
|
||||
except z as g:
|
||||
# TODO: should be `BaseException`
|
||||
reveal_type(g) # revealed: @Todo
|
||||
reveal_type(g) # revealed: @Todo(exception type)
|
||||
```
|
||||
|
||||
@@ -1,40 +1,33 @@
|
||||
# Control flow for exception handlers
|
||||
|
||||
These tests assert that we understand the possible "definition states" (which
|
||||
symbols might or might not be defined) in the various branches of a
|
||||
`try`/`except`/`else`/`finally` block.
|
||||
These tests assert that we understand the possible "definition states" (which symbols might or might
|
||||
not be defined) in the various branches of a `try`/`except`/`else`/`finally` block.
|
||||
|
||||
For a full writeup on the semantics of exception handlers,
|
||||
see [this document][1].
|
||||
For a full writeup on the semantics of exception handlers, see [this document][1].
|
||||
|
||||
The tests throughout this Markdown document use functions with names starting
|
||||
with `could_raise_*` to mark definitions that might or might not succeed
|
||||
(as the function could raise an exception). A type checker must assume that any
|
||||
arbitrary function call could raise an exception in Python; this is just a
|
||||
naming convention used in these tests for clarity, and to future-proof the
|
||||
tests against possible future improvements whereby certain statements or
|
||||
expressions could potentially be inferred as being incapable of causing an
|
||||
exception to be raised.
|
||||
The tests throughout this Markdown document use functions with names starting with `could_raise_*`
|
||||
to mark definitions that might or might not succeed (as the function could raise an exception). A
|
||||
type checker must assume that any arbitrary function call could raise an exception in Python; this
|
||||
is just a naming convention used in these tests for clarity, and to future-proof the tests against
|
||||
possible future improvements whereby certain statements or expressions could potentially be inferred
|
||||
as being incapable of causing an exception to be raised.
|
||||
|
||||
## A single bare `except`
|
||||
|
||||
Consider the following `try`/`except` block, with a single bare `except:`.
|
||||
There are different types for the variable `x` in the two branches of this
|
||||
block, and we can't determine which branch might have been taken from the
|
||||
perspective of code following this block. The inferred type after the block's
|
||||
conclusion is therefore the union of the type at the end of the `try` suite
|
||||
(`str`) and the type at the end of the `except` suite (`Literal[2]`).
|
||||
Consider the following `try`/`except` block, with a single bare `except:`. There are different types
|
||||
for the variable `x` in the two branches of this block, and we can't determine which branch might
|
||||
have been taken from the perspective of code following this block. The inferred type after the
|
||||
block's conclusion is therefore the union of the type at the end of the `try` suite (`str`) and the
|
||||
type at the end of the `except` suite (`Literal[2]`).
|
||||
|
||||
*Within* the `except` suite, we must infer a union of all possible "definition
|
||||
states" we could have been in at any point during the `try` suite. This is
|
||||
because control flow could have jumped to the `except` suite without any of the
|
||||
`try`-suite definitions successfully completing, with only *some* of the
|
||||
`try`-suite definitions successfully completing, or indeed with *all* of them
|
||||
successfully completing. The type of `x` at the beginning of the `except` suite
|
||||
in this example is therefore `Literal[1] | str`, taking into account that we
|
||||
might have jumped to the `except` suite before the
|
||||
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped
|
||||
to the `except` suite *after* that redefinition.
|
||||
*Within* the `except` suite, we must infer a union of all possible "definition states" we could have
|
||||
been in at any point during the `try` suite. This is because control flow could have jumped to the
|
||||
`except` suite without any of the `try`-suite definitions successfully completing, with only *some*
|
||||
of the `try`-suite definitions successfully completing, or indeed with *all* of them successfully
|
||||
completing. The type of `x` at the beginning of the `except` suite in this example is therefore
|
||||
`Literal[1] | str`, taking into account that we might have jumped to the `except` suite before the
|
||||
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
|
||||
*after* that redefinition.
|
||||
|
||||
```py path=union_type_inferred.py
|
||||
def could_raise_returns_str() -> str:
|
||||
@@ -54,9 +47,8 @@ except:
|
||||
reveal_type(x) # revealed: str | Literal[2]
|
||||
```
|
||||
|
||||
If `x` has the same type at the end of both branches, however, the branches
|
||||
unify and `x` is not inferred as having a union type following the
|
||||
`try`/`except` block:
|
||||
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
|
||||
inferred as having a union type following the `try`/`except` block:
|
||||
|
||||
```py path=branches_unify_to_non_union_type.py
|
||||
def could_raise_returns_str() -> str:
|
||||
@@ -74,13 +66,12 @@ reveal_type(x) # revealed: str
|
||||
|
||||
## A non-bare `except`
|
||||
|
||||
For simple `try`/`except` blocks, an `except TypeError:` handler has the same
|
||||
control flow semantics as an `except:` handler. An `except TypeError:` handler
|
||||
will not catch *all* exceptions: if this is the only handler, it opens up the
|
||||
possibility that an exception might occur that would not be handled. However,
|
||||
as described in [the document on exception-handling semantics][1], that would
|
||||
lead to termination of the scope. It's therefore irrelevant to consider this
|
||||
possibility when it comes to control-flow analysis.
|
||||
For simple `try`/`except` blocks, an `except TypeError:` handler has the same control flow semantics
|
||||
as an `except:` handler. An `except TypeError:` handler will not catch *all* exceptions: if this is
|
||||
the only handler, it opens up the possibility that an exception might occur that would not be
|
||||
handled. However, as described in [the document on exception-handling semantics][1], that would lead
|
||||
to termination of the scope. It's therefore irrelevant to consider this possibility when it comes to
|
||||
control-flow analysis.
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
@@ -102,11 +93,9 @@ reveal_type(x) # revealed: str | Literal[2]
|
||||
|
||||
## Multiple `except` branches
|
||||
|
||||
If the scope reaches the final `reveal_type` call in this example,
|
||||
either the `try`-block suite of statements was executed in its entirety,
|
||||
or exactly one `except` suite was executed in its entirety.
|
||||
The inferred type of `x` at this point is the union of the types at the end of
|
||||
the three suites:
|
||||
If the scope reaches the final `reveal_type` call in this example, either the `try`-block suite of
|
||||
statements was executed in its entirety, or exactly one `except` suite was executed in its entirety.
|
||||
The inferred type of `x` at this point is the union of the types at the end of the three suites:
|
||||
|
||||
- At the end of `try`, `type(x) == str`
|
||||
- At the end of `except TypeError`, `x == 2`
|
||||
@@ -136,11 +125,10 @@ reveal_type(x) # revealed: str | Literal[2, 3]
|
||||
|
||||
## Exception handlers with `else` branches (but no `finally`)
|
||||
|
||||
If we reach the `reveal_type` call at the end of this scope,
|
||||
either the `try` and `else` suites were both executed in their entireties,
|
||||
or the `except` suite was executed in its entirety. The type of `x` at this
|
||||
point is the union of the type at the end of the `else` suite and the type at
|
||||
the end of the `except` suite:
|
||||
If we reach the `reveal_type` call at the end of this scope, either the `try` and `else` suites were
|
||||
both executed in their entireties, or the `except` suite was executed in its entirety. The type of
|
||||
`x` at this point is the union of the type at the end of the `else` suite and the type at the end of
|
||||
the `except` suite:
|
||||
|
||||
- At the end of `else`, `x == 3`
|
||||
- At the end of `except`, `x == 2`
|
||||
@@ -167,10 +155,9 @@ else:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
For a block that has multiple `except` branches and an `else` branch, the same
|
||||
principle applies. In order to reach the final `reveal_type` call,
|
||||
either exactly one of the `except` suites must have been executed in its
|
||||
entirety, or the `try` suite and the `else` suite must both have been executed
|
||||
For a block that has multiple `except` branches and an `else` branch, the same principle applies. In
|
||||
order to reach the final `reveal_type` call, either exactly one of the `except` suites must have
|
||||
been executed in its entirety, or the `try` suite and the `else` suite must both have been executed
|
||||
in their entireties:
|
||||
|
||||
```py
|
||||
@@ -201,10 +188,9 @@ reveal_type(x) # revealed: Literal[2, 3, 4]
|
||||
|
||||
## Exception handlers with `finally` branches (but no `except` branches)
|
||||
|
||||
A `finally` suite is *always* executed. As such, if we reach the `reveal_type`
|
||||
call at the end of this example, we know that `x` *must* have been reassigned
|
||||
to `2` during the `finally` suite. The type of `x` at the end of the example is
|
||||
therefore `Literal[2]`:
|
||||
A `finally` suite is *always* executed. As such, if we reach the `reveal_type` call at the end of
|
||||
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
|
||||
type of `x` at the end of the example is therefore `Literal[2]`:
|
||||
|
||||
```py path=redef_in_finally.py
|
||||
def could_raise_returns_str() -> str:
|
||||
@@ -223,15 +209,13 @@ finally:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
If `x` was *not* redefined in the `finally` suite, however, things are somewhat
|
||||
more complicated. If we reach the final `reveal_type` call,
|
||||
unlike the state when we're visiting the `finally` suite,
|
||||
we know that the `try`-block suite ran to completion.
|
||||
This means that there are fewer possible states at this point than there were
|
||||
when we were inside the `finally` block.
|
||||
If `x` was *not* redefined in the `finally` suite, however, things are somewhat more complicated. If
|
||||
we reach the final `reveal_type` call, unlike the state when we're visiting the `finally` suite, we
|
||||
know that the `try`-block suite ran to completion. This means that there are fewer possible states
|
||||
at this point than there were when we were inside the `finally` block.
|
||||
|
||||
(Our current model does *not* correctly infer the types *inside* `finally`
|
||||
suites, however; this is still a TODO item for us.)
|
||||
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
|
||||
still a TODO item for us.)
|
||||
|
||||
```py path=no_redef_in_finally.py
|
||||
def could_raise_returns_str() -> str:
|
||||
@@ -252,18 +236,18 @@ reveal_type(x) # revealed: str
|
||||
|
||||
## Combining an `except` branch with a `finally` branch
|
||||
|
||||
As previously stated, we do not yet have accurate inference for types *inside*
|
||||
`finally` suites. When we do, however, we will have to take account of the
|
||||
following possibilities inside `finally` suites:
|
||||
As previously stated, we do not yet have accurate inference for types *inside* `finally` suites.
|
||||
When we do, however, we will have to take account of the following possibilities inside `finally`
|
||||
suites:
|
||||
|
||||
- The `try` suite could have run to completion
|
||||
- Or we could have jumped from halfway through the `try` suite to an `except`
|
||||
suite, and the `except` suite ran to completion
|
||||
- Or we could have jumped from halfway through the `try` suite straight to the
|
||||
`finally` suite due to an unhandled exception
|
||||
- Or we could have jumped from halfway through the `try` suite to an
|
||||
`except` suite, only for an exception raised in the `except` suite to cause
|
||||
us to jump to the `finally` suite before the `except` suite ran to completion
|
||||
- Or we could have jumped from halfway through the `try` suite to an `except` suite, and the
|
||||
`except` suite ran to completion
|
||||
- Or we could have jumped from halfway through the `try` suite straight to the `finally` suite due
|
||||
to an unhandled exception
|
||||
- Or we could have jumped from halfway through the `try` suite to an `except` suite, only for an
|
||||
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
|
||||
`except` suite ran to completion
|
||||
|
||||
```py path=redef_in_finally.py
|
||||
def could_raise_returns_str() -> str:
|
||||
@@ -296,12 +280,11 @@ finally:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
Now for an example without a redefinition in the `finally` suite.
|
||||
As before, there *should* be fewer possibilities after completion of the
|
||||
`finally` suite than there were during the `finally` suite itself.
|
||||
(In some control-flow possibilities, some exceptions were merely *suspended*
|
||||
during the `finally` suite; these lead to the scope's termination following the
|
||||
conclusion of the `finally` suite.)
|
||||
Now for an example without a redefinition in the `finally` suite. As before, there *should* be fewer
|
||||
possibilities after completion of the `finally` suite than there were during the `finally` suite
|
||||
itself. (In some control-flow possibilities, some exceptions were merely *suspended* during the
|
||||
`finally` suite; these lead to the scope's termination following the conclusion of the `finally`
|
||||
suite.)
|
||||
|
||||
```py path=no_redef_in_finally.py
|
||||
def could_raise_returns_str() -> str:
|
||||
@@ -377,9 +360,9 @@ reveal_type(x) # revealed: str | bool | float
|
||||
|
||||
## Combining `except`, `else` and `finally` branches
|
||||
|
||||
If the exception handler has an `else` branch, we must also take into account
|
||||
the possibility that control flow could have jumped to the `finally` suite from
|
||||
partway through the `else` suite due to an exception raised *there*.
|
||||
If the exception handler has an `else` branch, we must also take into account the possibility that
|
||||
control flow could have jumped to the `finally` suite from partway through the `else` suite due to
|
||||
an exception raised *there*.
|
||||
|
||||
```py path=single_except_branch.py
|
||||
def could_raise_returns_str() -> str:
|
||||
@@ -479,15 +462,13 @@ reveal_type(x) # revealed: bool | float | slice
|
||||
|
||||
## Nested `try`/`except` blocks
|
||||
|
||||
It would take advanced analysis, which we are not yet capable of, to be able
|
||||
to determine that an exception handler always suppresses all exceptions. This
|
||||
is partly because it is possible for statements in `except`, `else` and
|
||||
`finally` suites to raise exceptions as well as statements in `try` suites.
|
||||
This means that if an exception handler is nested inside the `try` statement of
|
||||
an enclosing exception handler, it should (at least for now) be treated the
|
||||
same as any other node: as a suite containing statements that could possibly
|
||||
raise exceptions, which would lead to control flow jumping out of that suite
|
||||
prior to the suite running to completion.
|
||||
It would take advanced analysis, which we are not yet capable of, to be able to determine that an
|
||||
exception handler always suppresses all exceptions. This is partly because it is possible for
|
||||
statements in `except`, `else` and `finally` suites to raise exceptions as well as statements in
|
||||
`try` suites. This means that if an exception handler is nested inside the `try` statement of an
|
||||
enclosing exception handler, it should (at least for now) be treated the same as any other node: as
|
||||
a suite containing statements that could possibly raise exceptions, which would lead to control flow
|
||||
jumping out of that suite prior to the suite running to completion.
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
@@ -580,8 +561,8 @@ reveal_type(x) # revealed: bytearray | Bar
|
||||
|
||||
## Nested scopes inside `try` blocks
|
||||
|
||||
Shadowing a variable in an inner scope has no effect on type inference of the
|
||||
variable by that name in the outer scope:
|
||||
Shadowing a variable in an inner scope has no effect on type inference of the variable by that name
|
||||
in the outer scope:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Exception Handling
|
||||
|
||||
## Invalid syntax
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
try:
|
||||
print
|
||||
except as e: # error: [invalid-syntax]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
# Attribute access
|
||||
|
||||
## Boundness
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
class A:
|
||||
always_bound = 1
|
||||
|
||||
if flag():
|
||||
union = 1
|
||||
else:
|
||||
union = "abc"
|
||||
|
||||
if flag():
|
||||
possibly_unbound = "abc"
|
||||
|
||||
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||
|
||||
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||
reveal_type(A.non_existent) # revealed: Unknown
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# If expression
|
||||
|
||||
## Union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(1 if bool_instance() else 2) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## Statically known branches
|
||||
|
||||
```py
|
||||
reveal_type(1 if True else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if "not empty" else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if (1,) else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if 1 else 2) # revealed: Literal[1]
|
||||
|
||||
reveal_type(1 if False else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if None else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if "" else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if 0 else 2) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## Leaked Narrowing Constraint
|
||||
|
||||
(issue #14588)
|
||||
|
||||
The test inside an if expression should not affect code outside of the expression.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x: Literal[42, "hello"] = 42 if bool_instance() else "hello"
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
|
||||
_ = ... if isinstance(x, str) else ...
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
```
|
||||
@@ -6,22 +6,19 @@ Basic PEP 695 generics
|
||||
|
||||
```py
|
||||
class MyBox[T]:
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
data: T
|
||||
box_model_number = 695
|
||||
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
def __init__(self, data: T):
|
||||
self.data = data
|
||||
|
||||
# TODO not error (should be subscriptable)
|
||||
box: MyBox[int] = MyBox(5) # error: [non-subscriptable]
|
||||
# TODO error differently (str and int don't unify)
|
||||
wrong_innards: MyBox[int] = MyBox("five") # error: [non-subscriptable]
|
||||
box: MyBox[int] = MyBox(5)
|
||||
|
||||
# TODO should emit a diagnostic here (str is not assignable to int)
|
||||
wrong_innards: MyBox[int] = MyBox("five")
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(box.data) # revealed: @Todo
|
||||
reveal_type(box.data) # revealed: @Todo(instance attributes)
|
||||
|
||||
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||
```
|
||||
@@ -30,29 +27,25 @@ reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||
|
||||
```py
|
||||
class MyBox[T]:
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
data: T
|
||||
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
def __init__(self, data: T):
|
||||
self.data = data
|
||||
|
||||
# TODO not error on the subscripting or the use of type param
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
# TODO not error on the subscripting
|
||||
# error: [non-subscriptable]
|
||||
class MySecureBox[T](MyBox[T]): ...
|
||||
|
||||
secure_box: MySecureBox[int] = MySecureBox(5)
|
||||
reveal_type(secure_box) # revealed: MySecureBox
|
||||
# TODO reveal int
|
||||
reveal_type(secure_box.data) # revealed: @Todo
|
||||
reveal_type(secure_box.data) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Cyclical class definition
|
||||
|
||||
In type stubs, classes can reference themselves in their base class definitions. For example, in `typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
In type stubs, classes can reference themselves in their base class definitions. For example, in
|
||||
`typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
|
||||
This should hold true even with generics at play.
|
||||
|
||||
@@ -64,3 +57,23 @@ class S[T](Seq[S]): ... # error: [non-subscriptable]
|
||||
|
||||
reveal_type(S) # revealed: Literal[S]
|
||||
```
|
||||
|
||||
## Type params
|
||||
|
||||
A PEP695 type variable defines a value of type `typing.TypeVar`.
|
||||
|
||||
```py
|
||||
def f[T]():
|
||||
reveal_type(T) # revealed: T
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
```
|
||||
|
||||
## Minimum two constraints
|
||||
|
||||
A typevar with less than two constraints emits a diagnostic:
|
||||
|
||||
```py
|
||||
# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types"
|
||||
def f[T: (int,)]():
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -12,16 +12,16 @@ if flag:
|
||||
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
# revealed: Literal[3]
|
||||
reveal_type(x)
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# revealed: Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound` is possibly unbound"
|
||||
from maybe_unbound import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
@@ -40,11 +40,10 @@ if flag:
|
||||
y: int = 3
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
# revealed: Literal[3]
|
||||
reveal_type(x)
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# revealed: Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
@@ -52,12 +51,31 @@ reveal_type(y)
|
||||
Importing an annotated name prefers the declared type over the inferred type:
|
||||
|
||||
```py
|
||||
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound_annotated` is possibly unbound"
|
||||
from maybe_unbound_annotated import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
## Maybe undeclared
|
||||
|
||||
Importing a possibly undeclared name still gives us its declared type:
|
||||
|
||||
```py path=maybe_undeclared.py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance():
|
||||
x: int
|
||||
```
|
||||
|
||||
```py
|
||||
from maybe_undeclared import x
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Reimport
|
||||
|
||||
```py path=c.py
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Literal
|
||||
|
||||
<https://typing.readthedocs.io/en/latest/spec/literal.html#literals>
|
||||
|
||||
## Parameterization
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from enum import Enum
|
||||
|
||||
mode: Literal["w", "r"]
|
||||
mode2: Literal["w"] | Literal["r"]
|
||||
union_var: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
|
||||
a1: Literal[26]
|
||||
a2: Literal[0x1A]
|
||||
a3: Literal[-4]
|
||||
a4: Literal["hello world"]
|
||||
a5: Literal[b"hello world"]
|
||||
a6: Literal[True]
|
||||
a7: Literal[None]
|
||||
a8: Literal[Literal[1]]
|
||||
a9: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]]
|
||||
|
||||
class Color(Enum):
|
||||
RED = 0
|
||||
GREEN = 1
|
||||
BLUE = 2
|
||||
|
||||
b1: Literal[Color.RED]
|
||||
|
||||
def f():
|
||||
reveal_type(mode) # revealed: Literal["w", "r"]
|
||||
reveal_type(mode2) # revealed: Literal["w", "r"]
|
||||
# TODO: should be revealed: Literal[1, 2, 3, "foo", 5] | None
|
||||
reveal_type(union_var) # revealed: Literal[1, 2, 3, 5] | Literal["foo"] | None
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
reveal_type(a2) # revealed: Literal[26]
|
||||
reveal_type(a3) # revealed: Literal[-4]
|
||||
reveal_type(a4) # revealed: Literal["hello world"]
|
||||
reveal_type(a5) # revealed: Literal[b"hello world"]
|
||||
reveal_type(a6) # revealed: Literal[True]
|
||||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
reveal_type(a9) # revealed: Literal["w", "r", "w+"]
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: Literal[0]
|
||||
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid1: Literal[3 + 4]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid2: Literal[4 + 3j]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid3: Literal[(3, 4)]
|
||||
|
||||
hello = "hello"
|
||||
invalid4: Literal[
|
||||
1 + 2, # error: [invalid-literal-parameter]
|
||||
"foo",
|
||||
hello, # error: [invalid-literal-parameter]
|
||||
(1, 2, 3), # error: [invalid-literal-parameter]
|
||||
]
|
||||
```
|
||||
|
||||
## Detecting Literal outside typing and typing_extensions
|
||||
|
||||
Only Literal that is defined in typing and typing_extension modules is detected as the special
|
||||
Literal.
|
||||
|
||||
```pyi path=other.pyi
|
||||
from typing import _SpecialForm
|
||||
|
||||
Literal: _SpecialForm
|
||||
```
|
||||
|
||||
```py
|
||||
from other import Literal
|
||||
|
||||
# error: [annotation-with-invalid-expression]
|
||||
a1: Literal[26]
|
||||
|
||||
def f():
|
||||
reveal_type(a1) # revealed: @Todo(generics)
|
||||
```
|
||||
|
||||
## Detecting typing_extensions.Literal
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
|
||||
a1: Literal[26]
|
||||
|
||||
def f():
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
```
|
||||
@@ -17,8 +17,8 @@ async def foo():
|
||||
async for x in Iterator():
|
||||
pass
|
||||
|
||||
# TODO: should reveal `Unbound | Unknown` because `__aiter__` is not defined
|
||||
# revealed: Unbound | @Todo
|
||||
# TODO: should reveal `Unknown` because `__aiter__` is not defined
|
||||
# revealed: @Todo(async iterables/iterators)
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
@@ -40,6 +40,6 @@ async def foo():
|
||||
pass
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
# revealed: Unbound | @Todo
|
||||
# revealed: @Todo(async iterables/iterators)
|
||||
reveal_type(x)
|
||||
```
|
||||
283
crates/red_knot_python_semantic/resources/mdtest/loops/for.md
Normal file
283
crates/red_knot_python_semantic/resources/mdtest/loops/for.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# For loops
|
||||
|
||||
## Basic `for` loop
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
for x in IntIterable():
|
||||
pass
|
||||
|
||||
# revealed: int
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With previous definition
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
x = "foo"
|
||||
|
||||
for x in IntIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Literal["foo"] | int
|
||||
```
|
||||
|
||||
## With `else` (no break)
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
for x in IntIterable():
|
||||
pass
|
||||
else:
|
||||
x = "foo"
|
||||
|
||||
reveal_type(x) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
## May `break`
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
for x in IntIterable():
|
||||
if x > 5:
|
||||
break
|
||||
else:
|
||||
x = "foo"
|
||||
|
||||
reveal_type(x) # revealed: int | Literal["foo"]
|
||||
```
|
||||
|
||||
## With old-style iteration protocol
|
||||
|
||||
```py
|
||||
class OldStyleIterable:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
|
||||
for x in OldStyleIterable():
|
||||
pass
|
||||
|
||||
# revealed: int
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With heterogeneous tuple
|
||||
|
||||
```py
|
||||
for x in (1, "a", b"foo"):
|
||||
pass
|
||||
|
||||
# revealed: Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With non-callable iterator
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
else:
|
||||
__iter__ = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
||||
# revealed: Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## Invalid iterable
|
||||
|
||||
```py
|
||||
nonsense = 123
|
||||
for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
|
||||
pass
|
||||
```
|
||||
|
||||
## New over old style iteration protocol
|
||||
|
||||
```py
|
||||
class NotIterable:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
__iter__ = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
```
|
||||
|
||||
## Union type as iterable
|
||||
|
||||
```py
|
||||
class TestIter:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Test:
|
||||
def __iter__(self) -> TestIter:
|
||||
return TestIter()
|
||||
|
||||
class Test2:
|
||||
def __iter__(self) -> TestIter:
|
||||
return TestIter()
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterator
|
||||
|
||||
```py
|
||||
class TestIter:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class TestIter2:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Test:
|
||||
def __iter__(self) -> TestIter | TestIter2:
|
||||
return TestIter()
|
||||
|
||||
for x in Test():
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterable and union type as iterator
|
||||
|
||||
```py
|
||||
class TestIter:
|
||||
def __next__(self) -> int | Exception:
|
||||
return 42
|
||||
|
||||
class TestIter2:
|
||||
def __next__(self) -> str | tuple[int, int]:
|
||||
return "42"
|
||||
|
||||
class TestIter3:
|
||||
def __next__(self) -> bytes:
|
||||
return b"42"
|
||||
|
||||
class TestIter4:
|
||||
def __next__(self) -> memoryview:
|
||||
return memoryview(b"42")
|
||||
|
||||
class Test:
|
||||
def __iter__(self) -> TestIter | TestIter2:
|
||||
return TestIter()
|
||||
|
||||
class Test2:
|
||||
def __iter__(self) -> TestIter3 | TestIter4:
|
||||
return TestIter3()
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
|
||||
```
|
||||
|
||||
## Union type as iterable where one union element has no `__iter__` method
|
||||
|
||||
```py
|
||||
class TestIter:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Test:
|
||||
def __iter__(self) -> TestIter:
|
||||
return TestIter()
|
||||
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
|
||||
for x in Test() if coinflip() else 42:
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterable where one union element has invalid `__iter__` method
|
||||
|
||||
```py
|
||||
class TestIter:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Test:
|
||||
def __iter__(self) -> TestIter:
|
||||
return TestIter()
|
||||
|
||||
class Test2:
|
||||
def __iter__(self) -> int:
|
||||
return 42
|
||||
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
# error: "Object of type `Test | Test2` is not iterable"
|
||||
for x in Test() if coinflip() else Test2():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Union type as iterator where one union element has no `__next__` method
|
||||
|
||||
```py
|
||||
class TestIter:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Test:
|
||||
def __iter__(self) -> TestIter | int:
|
||||
return TestIter()
|
||||
|
||||
# error: [not-iterable] "Object of type `Test` is not iterable"
|
||||
for x in Test():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
@@ -1,146 +0,0 @@
|
||||
# For loops
|
||||
|
||||
## Basic `for` loop
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
for x in IntIterable():
|
||||
pass
|
||||
|
||||
# revealed: Unbound | int
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With previous definition
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
x = "foo"
|
||||
|
||||
for x in IntIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Literal["foo"] | int
|
||||
```
|
||||
|
||||
## With `else` (no break)
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
for x in IntIterable():
|
||||
pass
|
||||
else:
|
||||
x = "foo"
|
||||
|
||||
reveal_type(x) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
## May `break`
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
for x in IntIterable():
|
||||
if x > 5:
|
||||
break
|
||||
else:
|
||||
x = "foo"
|
||||
|
||||
reveal_type(x) # revealed: int | Literal["foo"]
|
||||
```
|
||||
|
||||
## With old-style iteration protocol
|
||||
|
||||
```py
|
||||
class OldStyleIterable:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
|
||||
for x in OldStyleIterable():
|
||||
pass
|
||||
|
||||
# revealed: Unbound | int
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With heterogeneous tuple
|
||||
|
||||
```py
|
||||
for x in (1, "a", b"foo"):
|
||||
pass
|
||||
|
||||
# revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With non-callable iterator
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
else:
|
||||
__iter__ = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
||||
# revealed: Unbound | Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## Invalid iterable
|
||||
|
||||
```py
|
||||
nonsense = 123
|
||||
for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
|
||||
pass
|
||||
```
|
||||
|
||||
## New over old style iteration protocol
|
||||
|
||||
```py
|
||||
class NotIterable:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
__iter__ = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
```
|
||||
@@ -52,3 +52,29 @@ else:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
```
|
||||
|
||||
## Nested while loops
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
x = 1
|
||||
|
||||
while flag():
|
||||
x = 2
|
||||
|
||||
while flag():
|
||||
x = 3
|
||||
if flag():
|
||||
break
|
||||
else:
|
||||
x = 4
|
||||
|
||||
if flag():
|
||||
break
|
||||
else:
|
||||
x = 5
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
```
|
||||
|
||||
196
crates/red_knot_python_semantic/resources/mdtest/metaclass.md
Normal file
196
crates/red_knot_python_semantic/resources/mdtest/metaclass.md
Normal file
@@ -0,0 +1,196 @@
|
||||
## Default
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
|
||||
reveal_type(M.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## `object`
|
||||
|
||||
```py
|
||||
reveal_type(object.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## `type`
|
||||
|
||||
```py
|
||||
reveal_type(type.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class B(metaclass=M): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Invalid metaclass
|
||||
|
||||
A class which doesn't inherit `type` (and/or doesn't implement a custom `__new__` accepting the same
|
||||
arguments as `type.__new__`) isn't a valid metaclass.
|
||||
|
||||
```py
|
||||
class M: ...
|
||||
class A(metaclass=M): ...
|
||||
|
||||
# TODO: emit a diagnostic for the invalid metaclass
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Linear inheritance
|
||||
|
||||
If a class is a subclass of a class with a custom metaclass, then the subclass will also have that
|
||||
metaclass.
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Conflict (1)
|
||||
|
||||
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
|
||||
bases. ("Strict subclass" is a synonym for "proper subclass"; a non-strict subclass can be a
|
||||
subclass or the class itself.)
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type): ...
|
||||
class A(metaclass=M1): ...
|
||||
class B(metaclass=M2): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Conflict (2)
|
||||
|
||||
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
|
||||
bases. ("Strict subclass" is a synonym for "proper subclass"; a non-strict subclass can be a
|
||||
subclass or the class itself.)
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type): ...
|
||||
class A(metaclass=M1): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` (metaclass of `B`) and `M1` (metaclass of base class `A`) have no subclass relationship"
|
||||
class B(A, metaclass=M2): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Common metaclass
|
||||
|
||||
A class has two explicit bases, both of which have the same metaclass.
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(metaclass=M): ...
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Metaclass metaclass
|
||||
|
||||
A class has an explicit base with a custom metaclass. That metaclass itself has a custom metaclass.
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type, metaclass=M1): ...
|
||||
class M3(M2): ...
|
||||
class A(metaclass=M3): ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M3]
|
||||
```
|
||||
|
||||
## Diamond inheritance
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class M1(M): ...
|
||||
class M2(M): ...
|
||||
class M12(M1, M2): ...
|
||||
class A(metaclass=M1): ...
|
||||
class B(metaclass=M2): ...
|
||||
class C(metaclass=M12): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
|
||||
class D(A, B, C): ...
|
||||
|
||||
reveal_type(D.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unknown
|
||||
|
||||
```py
|
||||
from nonexistent_module import UnknownClass # error: [unresolved-import]
|
||||
|
||||
class C(UnknownClass): ...
|
||||
|
||||
# TODO: should be `type[type] & Unknown`
|
||||
reveal_type(C.__class__) # revealed: Literal[type]
|
||||
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A, UnknownClass): ...
|
||||
|
||||
# TODO: should be `type[M] & Unknown`
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Duplicate
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A, A): ... # error: [duplicate-base] "Duplicate base class `A`"
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Non-class
|
||||
|
||||
When a class has an explicit `metaclass` that is not a class, but is a callable that accepts
|
||||
`type.__new__` arguments, we should return the meta type of its return type.
|
||||
|
||||
```py
|
||||
def f(*args, **kwargs) -> int: ...
|
||||
|
||||
class A(metaclass=f): ...
|
||||
|
||||
# TODO should be `type[int]`
|
||||
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
|
||||
```
|
||||
|
||||
## Cyclic
|
||||
|
||||
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
|
||||
|
||||
```py path=a.pyi
|
||||
class A(B): ... # error: [cyclic-class-def]
|
||||
class B(C): ... # error: [cyclic-class-def]
|
||||
class C(A): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(A.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## PEP 695 generic
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A[T: str](metaclass=M): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
```
|
||||
409
crates/red_knot_python_semantic/resources/mdtest/mro.md
Normal file
409
crates/red_knot_python_semantic/resources/mdtest/mro.md
Normal file
@@ -0,0 +1,409 @@
|
||||
# Method Resolution Order tests
|
||||
|
||||
Tests that assert that we can infer the correct type for a class's `__mro__` attribute.
|
||||
|
||||
This attribute is rarely accessed directly at runtime. However, it's extremely important for *us* to
|
||||
know the precise possible values of a class's Method Resolution Order, or we won't be able to infer
|
||||
the correct type of attributes accessed from instances.
|
||||
|
||||
For documentation on method resolution orders, see:
|
||||
|
||||
- <https://docs.python.org/3/glossary.html#term-method-resolution-order>
|
||||
- <https://docs.python.org/3/howto/mro.html#python-2-3-mro>
|
||||
|
||||
## No bases
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
|
||||
```
|
||||
|
||||
## The special case: `object` itself
|
||||
|
||||
```py
|
||||
reveal_type(object.__mro__) # revealed: tuple[Literal[object]]
|
||||
```
|
||||
|
||||
## Explicit inheritance from `object`
|
||||
|
||||
```py
|
||||
class C(object): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
|
||||
```
|
||||
|
||||
## Explicit inheritance from non-`object` single base
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
|
||||
```
|
||||
|
||||
## Linearization of multiple bases
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (1)
|
||||
|
||||
This is "ex_2" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class X(O): ...
|
||||
class Y(O): ...
|
||||
class A(X, Y): ...
|
||||
class B(Y, X): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (2)
|
||||
|
||||
This is "ex_5" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class F(O): ...
|
||||
class E(O): ...
|
||||
class D(O): ...
|
||||
class C(D, F): ...
|
||||
class B(D, E): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(C.__mro__)
|
||||
# revealed: tuple[Literal[B], Literal[D], Literal[E], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__)
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[C], Literal[D], Literal[E], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__)
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (3)
|
||||
|
||||
This is "ex_6" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class F(O): ...
|
||||
class E(O): ...
|
||||
class D(O): ...
|
||||
class C(D, F): ...
|
||||
class B(E, D): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(C.__mro__)
|
||||
# revealed: tuple[Literal[B], Literal[E], Literal[D], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__)
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__)
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (4)
|
||||
|
||||
This is "ex_9" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class A(O): ...
|
||||
class B(O): ...
|
||||
class C(O): ...
|
||||
class D(O): ...
|
||||
class E(O): ...
|
||||
class K1(A, B, C): ...
|
||||
class K2(D, B, E): ...
|
||||
class K3(D, A): ...
|
||||
class Z(K1, K2, K3): ...
|
||||
|
||||
# revealed: tuple[Literal[K1], Literal[A], Literal[B], Literal[C], Literal[O], Literal[object]]
|
||||
reveal_type(K1.__mro__)
|
||||
# revealed: tuple[Literal[K2], Literal[D], Literal[B], Literal[E], Literal[O], Literal[object]]
|
||||
reveal_type(K2.__mro__)
|
||||
# revealed: tuple[Literal[K3], Literal[D], Literal[A], Literal[O], Literal[object]]
|
||||
reveal_type(K3.__mro__)
|
||||
# revealed: tuple[Literal[Z], Literal[K1], Literal[K2], Literal[K3], Literal[D], Literal[A], Literal[B], Literal[C], Literal[E], Literal[O], Literal[object]]
|
||||
reveal_type(Z.__mro__)
|
||||
```
|
||||
|
||||
## Inheritance from `Unknown`
|
||||
|
||||
```py
|
||||
from does_not_exist import DoesNotExist # error: [unresolved-import]
|
||||
|
||||
class A(DoesNotExist): ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D(A, B, C): ...
|
||||
class E(B, C): ...
|
||||
class F(E, A): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
|
||||
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[A], Unknown, Literal[B], Literal[C], Literal[object]]
|
||||
reveal_type(E.__mro__) # revealed: tuple[Literal[E], Literal[B], Literal[C], Literal[object]]
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], Literal[E], Literal[B], Literal[C], Literal[A], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists that cause errors at runtime
|
||||
|
||||
If the class's `__bases__` cause an exception to be raised at runtime and therefore the class
|
||||
creation to fail, we infer the class's `__mro__` as being `[<class>, Unknown, object]`:
|
||||
|
||||
```py
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[<class 'object'>, <class 'int'>]`"
|
||||
class Foo(object, int): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Bar(Foo): ...
|
||||
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
# This is the `TypeError` at the bottom of "ex_2"
|
||||
# in the examples at <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
class O: ...
|
||||
class X(O): ...
|
||||
class Y(O): ...
|
||||
class A(X, Y): ...
|
||||
class B(Y, X): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Z` with bases list `[<class 'A'>, <class 'B'>]`"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
|
||||
|
||||
class AA(Z): ...
|
||||
|
||||
reveal_type(AA.__mro__) # revealed: tuple[Literal[AA], Literal[Z], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` includes a `Union`
|
||||
|
||||
We don't support union types in a class's bases; a base must resolve to a single `ClassLiteralType`.
|
||||
If we find a union type in a class's bases, we infer the class's `__mro__` as being
|
||||
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
if returns_bool():
|
||||
x = A
|
||||
else:
|
||||
x = B
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
|
||||
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Foo(x): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` includes multiple `Union`s
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
|
||||
if returns_bool():
|
||||
x = A
|
||||
else:
|
||||
x = B
|
||||
|
||||
if returns_bool():
|
||||
y = C
|
||||
else:
|
||||
y = D
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(y) # revealed: Literal[C, D]
|
||||
|
||||
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
# error: 14 [invalid-base] "Invalid class base with type `Literal[C, D]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Foo(x, y): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists that cause errors... now with `Union`s
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
class O: ...
|
||||
class X(O): ...
|
||||
class Y(O): ...
|
||||
|
||||
if bool():
|
||||
foo = Y
|
||||
else:
|
||||
foo = object
|
||||
|
||||
# error: 21 [invalid-base] "Invalid class base with type `Literal[Y, object]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class PossibleError(foo, X): ...
|
||||
|
||||
reveal_type(PossibleError.__mro__) # revealed: tuple[Literal[PossibleError], Unknown, Literal[object]]
|
||||
|
||||
class A(X, Y): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
|
||||
if returns_bool():
|
||||
class B(X, Y): ...
|
||||
|
||||
else:
|
||||
class B(Y, X): ...
|
||||
|
||||
# revealed: tuple[Literal[B], Literal[X], Literal[Y], Literal[O], Literal[object]] | tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__)
|
||||
|
||||
# error: 12 [invalid-base] "Invalid class base with type `Literal[B, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists with duplicate bases
|
||||
|
||||
```py
|
||||
class Foo(str, str): ... # error: 16 [duplicate-base] "Duplicate base class `str`"
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Spam: ...
|
||||
class Eggs: ...
|
||||
class Ham(
|
||||
Spam,
|
||||
Eggs,
|
||||
Spam, # error: [duplicate-base] "Duplicate base class `Spam`"
|
||||
Eggs, # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
): ...
|
||||
|
||||
reveal_type(Ham.__mro__) # revealed: tuple[Literal[Ham], Unknown, Literal[object]]
|
||||
|
||||
class Mushrooms: ...
|
||||
class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
|
||||
|
||||
reveal_type(Omelette.__mro__) # revealed: tuple[Literal[Omelette], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists with duplicate `Unknown` bases
|
||||
|
||||
```py
|
||||
# error: [unresolved-import]
|
||||
# error: [unresolved-import]
|
||||
from does_not_exist import unknown_object_1, unknown_object_2
|
||||
|
||||
reveal_type(unknown_object_1) # revealed: Unknown
|
||||
reveal_type(unknown_object_2) # revealed: Unknown
|
||||
|
||||
# We *should* emit an error here to warn the user that we have no idea
|
||||
# what the MRO of this class should really be.
|
||||
# However, we don't complain about "duplicate base classes" here,
|
||||
# even though two classes are both inferred as being `Unknown`.
|
||||
#
|
||||
# (TODO: should we revisit this? Does it violate the gradual guarantee?
|
||||
# Should we just silently infer `[Foo, Unknown, object]` as the MRO here
|
||||
# without emitting any error at all? Not sure...)
|
||||
#
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[Unknown, Unknown]`"
|
||||
class Foo(unknown_object_1, unknown_object_2): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Unrelated objects inferred as `Any`/`Unknown` do not have special `__mro__` attributes
|
||||
|
||||
```py
|
||||
from does_not_exist import unknown_object # error: [unresolved-import]
|
||||
|
||||
reveal_type(unknown_object) # revealed: Unknown
|
||||
reveal_type(unknown_object.__mro__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Classes that inherit from themselves
|
||||
|
||||
These are invalid, but we need to be able to handle them gracefully without panicking.
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Foo): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Bar: ...
|
||||
class Baz: ...
|
||||
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(Boz) # revealed: Literal[Boz]
|
||||
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Classes with indirect cycles in their MROs
|
||||
|
||||
These are similarly unlikely, but we still shouldn't crash:
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Classes with cycles in their MROs, and multiple inheritance
|
||||
|
||||
```py path=a.pyi
|
||||
class Spam: ...
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Classes with cycles in their MRO, and a sub-graph
|
||||
|
||||
```py path=a.pyi
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-def]
|
||||
class Foo: ...
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-def]
|
||||
class Bar(Foo): ...
|
||||
|
||||
# TODO: can we avoid emitting the errors for these?
|
||||
# The classes have cyclic superclasses,
|
||||
# but are not themselves cyclic...
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-def]
|
||||
class Spam(Baz): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
||||
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
reveal_type(Spam.__mro__) # revealed: tuple[Literal[Spam], Unknown, Literal[object]]
|
||||
```
|
||||
@@ -1,10 +1,10 @@
|
||||
# Narrowing in boolean expressions
|
||||
|
||||
In `or` expressions, the right-hand side is evaluated only if the left-hand side is **falsy**.
|
||||
So when the right-hand side is evaluated, we know the left side has failed.
|
||||
In `or` expressions, the right-hand side is evaluated only if the left-hand side is **falsy**. So
|
||||
when the right-hand side is evaluated, we know the left side has failed.
|
||||
|
||||
Similarly, in `and` expressions, the right-hand side is evaluated only if the left-hand side is **truthy**.
|
||||
So when the right-hand side is evaluated, we know the left side has succeeded.
|
||||
Similarly, in `and` expressions, the right-hand side is evaluated only if the left-hand side is
|
||||
**truthy**. So when the right-hand side is evaluated, we know the left side has succeeded.
|
||||
|
||||
## Narrowing in `or`
|
||||
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
# Narrowing for conditionals with boolean expressions
|
||||
|
||||
## Narrowing in `and` conditional
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & B
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | A & ~B
|
||||
```
|
||||
|
||||
## Arms might not add narrowing constraints
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and bool_instance():
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if bool_instance() and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## Statically known arms
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and True:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if True and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if False and isinstance(x, A):
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if False or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if True or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## The type of multiple symbols can be narrowed down
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
y = instance()
|
||||
|
||||
if isinstance(x, A) and isinstance(y, B):
|
||||
reveal_type(x) # revealed: A
|
||||
reveal_type(y) # revealed: B
|
||||
else:
|
||||
# No narrowing: Only-one or both checks might have failed
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
```
|
||||
|
||||
## Narrowing in `or` conditional
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(x, B):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
```
|
||||
|
||||
## In `or`, all arms should add constraint in order to narrow
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(x, B) or bool_instance():
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
```
|
||||
|
||||
## in `or`, all arms should narrow the same set of symbols
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
y = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(y, A):
|
||||
# The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here.
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
# The same for `y`
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A
|
||||
reveal_type(y) # revealed: B & ~A | C & ~A
|
||||
|
||||
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
|
||||
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
```
|
||||
|
||||
## mixing `and` and `not`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, B) and not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B & ~C
|
||||
else:
|
||||
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
|
||||
reveal_type(x) # revealed: A & ~B | C
|
||||
```
|
||||
|
||||
## mixing `or` and `not`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, B) or not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B | A & ~C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~B
|
||||
```
|
||||
|
||||
## `or` with nested `and`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
|
||||
reveal_type(x) # revealed: A | B & ~C
|
||||
else:
|
||||
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
|
||||
reveal_type(x) # revealed: C & ~A
|
||||
```
|
||||
|
||||
## `and` with nested `or`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
|
||||
# A & (B | ~C) -> (A & B) | (A & ~C)
|
||||
reveal_type(x) # revealed: A & B | A & ~C
|
||||
else:
|
||||
# ~((A & B) | (A & ~C)) ->
|
||||
# ~(A & B) & ~(A & ~C) ->
|
||||
# (~A | ~B) & (~A | C) ->
|
||||
# [(~A | ~B) & ~A] | [(~A | ~B) & C] ->
|
||||
# ~A | (~A & C) | (~B & C) ->
|
||||
# ~A | (C & ~B) ->
|
||||
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
|
||||
```
|
||||
|
||||
## Boolean expression internal narrowing
|
||||
|
||||
```py
|
||||
def optional_string() -> str | None:
|
||||
return None
|
||||
|
||||
x = optional_string()
|
||||
y = optional_string()
|
||||
|
||||
if x is None and y is not x:
|
||||
reveal_type(y) # revealed: str
|
||||
|
||||
# Neither of the conditions alone is sufficient for narrowing y's type:
|
||||
if x is None:
|
||||
reveal_type(y) # revealed: str | None
|
||||
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: str | None
|
||||
```
|
||||
@@ -37,9 +37,8 @@ else:
|
||||
|
||||
## `is not` for non-singleton types
|
||||
|
||||
Non-singleton types should *not* narrow the type: two instances of a
|
||||
non-singleton class may occupy different addresses in memory even if
|
||||
they compare equal.
|
||||
Non-singleton types should *not* narrow the type: two instances of a non-singleton class may occupy
|
||||
different addresses in memory even if they compare equal.
|
||||
|
||||
```py
|
||||
x = 345
|
||||
@@ -26,9 +26,8 @@ if isinstance(x, (int, object)):
|
||||
|
||||
## `classinfo` is a tuple of types
|
||||
|
||||
Note: `isinstance(x, (int, str))` should not be confused with
|
||||
`isinstance(x, tuple[(int, str)])`. The former is equivalent to
|
||||
`isinstance(x, int | str)`:
|
||||
Note: `isinstance(x, (int, str))` should not be confused with `isinstance(x, tuple[(int, str)])`.
|
||||
The former is equivalent to `isinstance(x, int | str)`:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
# Narrowing for `issubclass` checks
|
||||
|
||||
Narrowing for `issubclass(class, classinfo)` expressions.
|
||||
|
||||
## `classinfo` is a single type
|
||||
|
||||
### Basic example
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
if issubclass(t, bytes):
|
||||
reveal_type(t) # revealed: Never
|
||||
|
||||
if issubclass(t, object):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
|
||||
if issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
### Proper narrowing in `elif` and `else` branches
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str, bytes]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
elif issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[bytes]
|
||||
```
|
||||
|
||||
### Multiple derived classes
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
class Derived1(Base): ...
|
||||
class Derived2(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t1 = Derived1 if flag() else Derived2
|
||||
|
||||
if issubclass(t1, Base):
|
||||
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
|
||||
|
||||
if issubclass(t1, Derived1):
|
||||
reveal_type(t1) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t1) # revealed: Literal[Derived2]
|
||||
|
||||
t2 = Derived1 if flag() else Base
|
||||
|
||||
if issubclass(t2, Base):
|
||||
reveal_type(t2) # revealed: Literal[Derived1, Base]
|
||||
|
||||
t3 = Derived1 if flag() else Unrelated
|
||||
|
||||
if issubclass(t3, Base):
|
||||
reveal_type(t3) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t3) # revealed: Literal[Unrelated]
|
||||
```
|
||||
|
||||
### Narrowing for non-literals
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_class() -> type[object]: ...
|
||||
|
||||
t = get_class()
|
||||
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
if issubclass(t, B):
|
||||
reveal_type(t) # revealed: type[A] & type[B]
|
||||
else:
|
||||
reveal_type(t) # revealed: type[object] & ~type[A]
|
||||
```
|
||||
|
||||
### Handling of `None`
|
||||
|
||||
```py
|
||||
# TODO: this error should ideally go away once we (1) understand `sys.version_info` branches,
|
||||
# and (2) set the target Python version for this test to 3.10.
|
||||
# error: [possibly-unbound-import] "Member `NoneType` of module `types` is possibly unbound"
|
||||
from types import NoneType
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else NoneType
|
||||
|
||||
if issubclass(t, NoneType):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
|
||||
if issubclass(t, type(None)):
|
||||
# TODO: this should be just `Literal[NoneType]`
|
||||
reveal_type(t) # revealed: Literal[int, NoneType]
|
||||
```
|
||||
|
||||
## `classinfo` contains multiple types
|
||||
|
||||
### (Nested) tuples of types
|
||||
|
||||
```py
|
||||
class Unrelated: ...
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, (int, (Unrelated, (bytes,)))):
|
||||
reveal_type(t) # revealed: Literal[int, bytes]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
```
|
||||
|
||||
## Special cases
|
||||
|
||||
### Emit a diagnostic if the first argument is of wrong type
|
||||
|
||||
#### Too wide
|
||||
|
||||
`type[object]` is a subtype of `object`, but not every `object` can be passed as the first argument
|
||||
to `issubclass`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
t = get_object()
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
```
|
||||
|
||||
#### Wrong
|
||||
|
||||
`Literal[1]` and `type` are entirely disjoint, so the inferred type of `Literal[1] & type[int]` is
|
||||
eagerly simplified to `Never` as a result of the type narrowing in the `if issubclass(t, int)`
|
||||
branch:
|
||||
|
||||
```py
|
||||
t = 1
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
### Do not use custom `issubclass` for narrowing
|
||||
|
||||
```py
|
||||
def issubclass(c, ci):
|
||||
return True
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
|
||||
### Do support narrowing if `issubclass` is aliased
|
||||
|
||||
```py
|
||||
issubclass_alias = issubclass
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if issubclass_alias(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
### Do support narrowing if `issubclass` is imported
|
||||
|
||||
```py
|
||||
from builtins import issubclass as imported_issubclass
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if imported_issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
### Do not narrow if second argument is not a proper `classinfo` argument
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, "str"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, (bytes, "str")):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, Any):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
|
||||
### Do not narrow if there are keyword arguments
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`issubclass` has no `foo` parameter)
|
||||
if issubclass(t, int, foo="bar"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
152
crates/red_knot_python_semantic/resources/mdtest/narrow/type.md
Normal file
152
crates/red_knot_python_semantic/resources/mdtest/narrow/type.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Narrowing for checks involving `type(x)`
|
||||
|
||||
## `type(x) is C`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
# It would be wrong to infer `B` here. The type
|
||||
# of `x` could be a subclass of `A`, so we need
|
||||
# to infer the full union type:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## `type(x) is not C`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is not A:
|
||||
# Same reasoning as above: no narrowing should occur here.
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
||||
## `type(x) == C`, `type(x) != C`
|
||||
|
||||
No narrowing can occur for equality comparisons, since there might be a custom `__eq__`
|
||||
implementation on the metaclass.
|
||||
|
||||
TODO: Narrowing might be possible in some cases where the classes themselves are `@final` or their
|
||||
metaclass is `@final`.
|
||||
|
||||
```py
|
||||
class IsEqualToEverything(type):
|
||||
def __eq__(cls, other):
|
||||
return True
|
||||
|
||||
class A(metaclass=IsEqualToEverything): ...
|
||||
class B(metaclass=IsEqualToEverything): ...
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return B()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) == A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if type(x) != A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## No narrowing for custom `type` callable
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def type(x):
|
||||
return int
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## No narrowing for multiple arguments
|
||||
|
||||
No narrowing should occur if `type` is used to dynamically create a class:
|
||||
|
||||
```py
|
||||
def get_str_or_int() -> str | int:
|
||||
return "test"
|
||||
|
||||
x = get_str_or_int()
|
||||
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
else:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
## No narrowing for keyword arguments
|
||||
|
||||
`type` can't be used with a keyword argument:
|
||||
|
||||
```py
|
||||
def get_str_or_int() -> str | int:
|
||||
return "test"
|
||||
|
||||
x = get_str_or_int()
|
||||
|
||||
# TODO: we could issue a diagnostic here
|
||||
if type(object=x) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
## Narrowing if `type` is aliased
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
alias_for_type = type
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if alias_for_type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
|
||||
def get_base() -> Base:
|
||||
return Base()
|
||||
|
||||
x = get_base()
|
||||
|
||||
if type(x) is Base:
|
||||
# Ideally, this could be narrower, but there is now way to
|
||||
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
|
||||
reveal_type(x) # revealed: Base
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
# Regression test for #14334
|
||||
|
||||
Regression test for [this issue](https://github.com/astral-sh/ruff/issues/14334).
|
||||
|
||||
```py path=base.py
|
||||
# error: [invalid-base]
|
||||
class Base(2): ...
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
# No error here
|
||||
from base import Base
|
||||
```
|
||||
@@ -0,0 +1,136 @@
|
||||
# Implicit globals from `types.ModuleType`
|
||||
|
||||
## Implicit `ModuleType` globals
|
||||
|
||||
All modules are instances of `types.ModuleType`. If a name can't be found in any local or global
|
||||
scope, we look it up as an attribute on `types.ModuleType` in typeshed before deciding that the name
|
||||
is unbound.
|
||||
|
||||
```py
|
||||
reveal_type(__name__) # revealed: str
|
||||
reveal_type(__file__) # revealed: str | None
|
||||
reveal_type(__loader__) # revealed: LoaderProtocol | None
|
||||
reveal_type(__package__) # revealed: str | None
|
||||
reveal_type(__doc__) # revealed: str | None
|
||||
|
||||
# TODO: Should be `ModuleSpec | None`
|
||||
# (needs support for `*` imports)
|
||||
reveal_type(__spec__) # revealed: Unknown | None
|
||||
|
||||
reveal_type(__path__) # revealed: @Todo(generics)
|
||||
|
||||
class X:
|
||||
reveal_type(__name__) # revealed: str
|
||||
|
||||
def foo():
|
||||
reveal_type(__name__) # revealed: str
|
||||
```
|
||||
|
||||
However, three attributes on `types.ModuleType` are not present as implicit module globals; these
|
||||
are excluded:
|
||||
|
||||
```py path=unbound_dunders.py
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unknown
|
||||
reveal_type(__getattr__)
|
||||
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unknown
|
||||
reveal_type(__dict__)
|
||||
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unknown
|
||||
reveal_type(__init__)
|
||||
```
|
||||
|
||||
## Accessed as attributes
|
||||
|
||||
`ModuleType` attributes can also be accessed as attributes on module-literal types. The special
|
||||
attributes `__dict__` and `__init__`, and all attributes on `builtins.object`, can also be accessed
|
||||
as attributes on module-literal types, despite the fact that these are inaccessible as globals from
|
||||
inside the module:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: Literal[__init__]
|
||||
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
reveal_type(typing.__eq__) # revealed: Literal[__eq__]
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[type]
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
reveal_type(typing.__dict__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
|
||||
dynamic imports; but we ignore that for module-literal types where we know exactly which module
|
||||
we're dealing with:
|
||||
|
||||
```py path=__getattr__.py
|
||||
import typing
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(typing.__getattr__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## `types.ModuleType.__dict__` takes precedence over global variable `__dict__`
|
||||
|
||||
It's impossible to override the `__dict__` attribute of `types.ModuleType` instances from inside the
|
||||
module; we should prioritise the attribute in the `types.ModuleType` stub over a variable named
|
||||
`__dict__` in the module's global namespace:
|
||||
|
||||
```py path=foo.py
|
||||
__dict__ = "foo"
|
||||
|
||||
reveal_type(__dict__) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
```py path=bar.py
|
||||
import foo
|
||||
from foo import __dict__ as foo_dict
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties, and generics;
|
||||
# should be `dict[str, Any]` for both of these:
|
||||
reveal_type(foo.__dict__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(foo_dict) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute
|
||||
|
||||
Attributes overridden in the module namespace take priority. If a builtin name is conditionally
|
||||
defined as a global, however, a name lookup should union the `ModuleType` type with the
|
||||
conditionally defined type:
|
||||
|
||||
```py
|
||||
__file__ = 42
|
||||
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
__name__ = 1
|
||||
|
||||
reveal_type(__file__) # revealed: Literal[42]
|
||||
reveal_type(__name__) # revealed: Literal[1] | str
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute, with annotation
|
||||
|
||||
The same is true if the name is annotated:
|
||||
|
||||
```py
|
||||
__file__: int = 42
|
||||
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
__name__: int = 1
|
||||
|
||||
reveal_type(__file__) # revealed: Literal[42]
|
||||
reveal_type(__name__) # revealed: Literal[1] | str
|
||||
```
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
## Parameter
|
||||
|
||||
Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function. No diagnostics should be generated.
|
||||
Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function.
|
||||
No diagnostics should be generated.
|
||||
|
||||
```py path=a.py
|
||||
def f(x: str):
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
|
||||
## Cyclical class definition
|
||||
|
||||
In type stubs, classes can reference themselves in their base class definitions. For example, in `typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
In type stubs, classes can reference themselves in their base class definitions. For example, in
|
||||
`typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
|
||||
```py path=a.pyi
|
||||
class C(C): ...
|
||||
class Foo[T]: ...
|
||||
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
# TODO: actually is subscriptable
|
||||
# error: [non-subscriptable]
|
||||
class Bar(Foo[Bar]): ...
|
||||
|
||||
reveal_type(Bar) # revealed: Literal[Bar]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bytes subscript
|
||||
# Bytes subscripts
|
||||
|
||||
## Simple
|
||||
## Indexing
|
||||
|
||||
```py
|
||||
b = b"\x00abc\xff"
|
||||
@@ -21,15 +21,37 @@ reveal_type(x) # revealed: Unknown
|
||||
|
||||
y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5"
|
||||
reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Function return
|
||||
|
||||
```py
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
a = b"abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Slices
|
||||
|
||||
```py
|
||||
b = b"\x00abc\xff"
|
||||
|
||||
reveal_type(b[0:2]) # revealed: Literal[b"\x00a"]
|
||||
reveal_type(b[-3:]) # revealed: Literal[b"bc\xff"]
|
||||
|
||||
b[0:4:0] # error: [zero-stepsize-in-slice]
|
||||
b[:4:0] # error: [zero-stepsize-in-slice]
|
||||
b[0::0] # error: [zero-stepsize-in-slice]
|
||||
b[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def int_instance() -> int: ...
|
||||
|
||||
byte_slice1 = b[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type)
|
||||
|
||||
def bytes_instance() -> bytes: ...
|
||||
|
||||
byte_slice2 = bytes_instance()[0:5]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice2) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
@@ -39,7 +39,8 @@ reveal_type(UnionClassGetItem[0]) # revealed: str | int
|
||||
## Class getitem with class union
|
||||
|
||||
```py
|
||||
flag = True
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class A:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
@@ -49,7 +50,7 @@ class B:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
x = A if flag else B
|
||||
x = A if bool_instance() else B
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(x[0]) # revealed: str | int
|
||||
@@ -68,8 +69,8 @@ if flag:
|
||||
else:
|
||||
class Spam: ...
|
||||
|
||||
# error: [call-non-callable] "Method `__class_getitem__` of type `Literal[__class_getitem__] | Unbound` is not callable on object of type `Literal[Spam, Spam]`"
|
||||
# revealed: str | Unknown
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound"
|
||||
# revealed: str
|
||||
reveal_type(Spam[42])
|
||||
```
|
||||
|
||||
|
||||
@@ -12,19 +12,18 @@ x = [1, 2, 3]
|
||||
reveal_type(x) # revealed: list
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(x[0]) # revealed: @Todo
|
||||
reveal_type(x[0]) # revealed: @Todo(return type)
|
||||
|
||||
# TODO reveal list
|
||||
reveal_type(x[0:1]) # revealed: @Todo
|
||||
reveal_type(x[0:1]) # revealed: @Todo(return type)
|
||||
|
||||
# TODO error
|
||||
reveal_type(x["a"]) # revealed: @Todo
|
||||
reveal_type(x["a"]) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Assignments within list assignment
|
||||
|
||||
In assignment, we might also have a named assignment.
|
||||
This should also get type checked.
|
||||
In assignment, we might also have a named assignment. This should also get type checked.
|
||||
|
||||
```py
|
||||
x = [1, 2, 3]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Stepsize zero in slices
|
||||
|
||||
We raise a `zero-stepsize-in-slice` diagnostic when trying to slice a literal string, bytes, or
|
||||
tuple with a step size of zero (see tests in `string.md`, `bytes.md` and `tuple.md`). But we don't
|
||||
want to raise this diagnostic when slicing a custom type:
|
||||
|
||||
```py
|
||||
class MySequence:
|
||||
def __getitem__(self, s: slice) -> int:
|
||||
return 0
|
||||
|
||||
MySequence()[0:1:0] # No error
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# Subscript on strings
|
||||
# String subscripts
|
||||
|
||||
## Simple
|
||||
## Indexing
|
||||
|
||||
```py
|
||||
s = "abcde"
|
||||
@@ -18,15 +18,82 @@ reveal_type(a) # revealed: Unknown
|
||||
|
||||
b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5"
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Function return
|
||||
|
||||
```py
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
def int_instance() -> int: ...
|
||||
|
||||
a = "abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Slices
|
||||
|
||||
```py
|
||||
s = "abcde"
|
||||
|
||||
reveal_type(s[0:0]) # revealed: Literal[""]
|
||||
reveal_type(s[0:1]) # revealed: Literal["a"]
|
||||
reveal_type(s[0:2]) # revealed: Literal["ab"]
|
||||
reveal_type(s[0:5]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[0:6]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[1:3]) # revealed: Literal["bc"]
|
||||
|
||||
reveal_type(s[-3:5]) # revealed: Literal["cde"]
|
||||
reveal_type(s[-4:-2]) # revealed: Literal["bc"]
|
||||
reveal_type(s[-10:10]) # revealed: Literal["abcde"]
|
||||
|
||||
reveal_type(s[0:]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[2:]) # revealed: Literal["cde"]
|
||||
reveal_type(s[5:]) # revealed: Literal[""]
|
||||
reveal_type(s[:2]) # revealed: Literal["ab"]
|
||||
reveal_type(s[:0]) # revealed: Literal[""]
|
||||
reveal_type(s[:2]) # revealed: Literal["ab"]
|
||||
reveal_type(s[:10]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[:]) # revealed: Literal["abcde"]
|
||||
|
||||
reveal_type(s[::-1]) # revealed: Literal["edcba"]
|
||||
reveal_type(s[::2]) # revealed: Literal["ace"]
|
||||
reveal_type(s[-2:-5:-1]) # revealed: Literal["dcb"]
|
||||
reveal_type(s[::-2]) # revealed: Literal["eca"]
|
||||
reveal_type(s[-1::-3]) # revealed: Literal["eb"]
|
||||
|
||||
reveal_type(s[None:2:None]) # revealed: Literal["ab"]
|
||||
reveal_type(s[1:None:1]) # revealed: Literal["bcde"]
|
||||
reveal_type(s[None:None:None]) # revealed: Literal["abcde"]
|
||||
|
||||
start = 1
|
||||
stop = None
|
||||
step = 2
|
||||
reveal_type(s[start:stop:step]) # revealed: Literal["bd"]
|
||||
|
||||
reveal_type(s[False:True]) # revealed: Literal["a"]
|
||||
reveal_type(s[True:3]) # revealed: Literal["bc"]
|
||||
|
||||
s[0:4:0] # error: [zero-stepsize-in-slice]
|
||||
s[:4:0] # error: [zero-stepsize-in-slice]
|
||||
s[0::0] # error: [zero-stepsize-in-slice]
|
||||
s[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def int_instance() -> int: ...
|
||||
|
||||
substring1 = s[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo(return type)
|
||||
|
||||
def str_instance() -> str: ...
|
||||
|
||||
substring2 = str_instance()[0:5]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(substring2) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Unsupported slice types
|
||||
|
||||
```py
|
||||
# TODO: It would be great if we raised an error here. This can be done once
|
||||
# we have support for overloads and generics, and once typeshed has a more
|
||||
# precise annotation for `str.__getitem__`, that makes use of the generic
|
||||
# `slice[..]` type. We could then infer `slice[str, str]` here and see that
|
||||
# it doesn't match the signature of `str.__getitem__`.
|
||||
"foo"["bar":"baz"]
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Tuple subscripts
|
||||
|
||||
## Basic
|
||||
## Indexing
|
||||
|
||||
```py
|
||||
t = (1, "a", "b")
|
||||
@@ -10,9 +10,66 @@ reveal_type(t[1]) # revealed: Literal["a"]
|
||||
reveal_type(t[-1]) # revealed: Literal["b"]
|
||||
reveal_type(t[-2]) # revealed: Literal["a"]
|
||||
|
||||
reveal_type(t[False]) # revealed: Literal[1]
|
||||
reveal_type(t[True]) # revealed: Literal["a"]
|
||||
|
||||
a = t[4] # error: [index-out-of-bounds]
|
||||
reveal_type(a) # revealed: Unknown
|
||||
|
||||
b = t[-4] # error: [index-out-of-bounds]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Slices
|
||||
|
||||
```py
|
||||
t = (1, "a", None, b"b")
|
||||
|
||||
reveal_type(t[0:0]) # revealed: tuple[()]
|
||||
reveal_type(t[0:1]) # revealed: tuple[Literal[1]]
|
||||
reveal_type(t[0:2]) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(t[0:4]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[0:5]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[1:3]) # revealed: tuple[Literal["a"], None]
|
||||
|
||||
reveal_type(t[-2:4]) # revealed: tuple[None, Literal[b"b"]]
|
||||
reveal_type(t[-3:-1]) # revealed: tuple[Literal["a"], None]
|
||||
reveal_type(t[-10:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
|
||||
reveal_type(t[0:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[2:]) # revealed: tuple[None, Literal[b"b"]]
|
||||
reveal_type(t[4:]) # revealed: tuple[()]
|
||||
reveal_type(t[:0]) # revealed: tuple[()]
|
||||
reveal_type(t[:2]) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(t[:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
|
||||
reveal_type(t[::-1]) # revealed: tuple[Literal[b"b"], None, Literal["a"], Literal[1]]
|
||||
reveal_type(t[::2]) # revealed: tuple[Literal[1], None]
|
||||
reveal_type(t[-2:-5:-1]) # revealed: tuple[None, Literal["a"], Literal[1]]
|
||||
reveal_type(t[::-2]) # revealed: tuple[Literal[b"b"], Literal["a"]]
|
||||
reveal_type(t[-1::-3]) # revealed: tuple[Literal[b"b"], Literal[1]]
|
||||
|
||||
reveal_type(t[None:2:None]) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(t[1:None:1]) # revealed: tuple[Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[None:None:None]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
|
||||
start = 1
|
||||
stop = None
|
||||
step = 2
|
||||
reveal_type(t[start:stop:step]) # revealed: tuple[Literal["a"], Literal[b"b"]]
|
||||
|
||||
reveal_type(t[False:True]) # revealed: tuple[Literal[1]]
|
||||
reveal_type(t[True:3]) # revealed: tuple[Literal["a"], None]
|
||||
|
||||
t[0:4:0] # error: [zero-stepsize-in-slice]
|
||||
t[:4:0] # error: [zero-stepsize-in-slice]
|
||||
t[0::0] # error: [zero-stepsize-in-slice]
|
||||
t[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def int_instance() -> int: ...
|
||||
|
||||
tuple_slice = t[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# `sys.version_info`
|
||||
|
||||
## The type of `sys.version_info`
|
||||
|
||||
The type of `sys.version_info` is `sys._version_info`, at least according to typeshed's stubs (which
|
||||
we treat as the single source of truth for the standard library). This is quite a complicated type
|
||||
in typeshed, so there are many things we don't fully understand about the type yet; this is the
|
||||
source of several TODOs in this test file. Many of these TODOs should be naturally fixed as we
|
||||
implement more type-system features in the future.
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info) # revealed: _version_info
|
||||
```
|
||||
|
||||
## Literal types from comparisons
|
||||
|
||||
Comparing `sys.version_info` with a 2-element tuple of literal integers always produces a `Literal`
|
||||
type:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info >= (3, 9)) # revealed: Literal[True]
|
||||
reveal_type((3, 9) <= sys.version_info) # revealed: Literal[True]
|
||||
|
||||
reveal_type(sys.version_info > (3, 9)) # revealed: Literal[True]
|
||||
reveal_type((3, 9) < sys.version_info) # revealed: Literal[True]
|
||||
|
||||
reveal_type(sys.version_info < (3, 9)) # revealed: Literal[False]
|
||||
reveal_type((3, 9) > sys.version_info) # revealed: Literal[False]
|
||||
|
||||
reveal_type(sys.version_info <= (3, 9)) # revealed: Literal[False]
|
||||
reveal_type((3, 9) >= sys.version_info) # revealed: Literal[False]
|
||||
|
||||
reveal_type(sys.version_info == (3, 9)) # revealed: Literal[False]
|
||||
reveal_type((3, 9) == sys.version_info) # revealed: Literal[False]
|
||||
|
||||
reveal_type(sys.version_info != (3, 9)) # revealed: Literal[True]
|
||||
reveal_type((3, 9) != sys.version_info) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Non-literal types from comparisons
|
||||
|
||||
Comparing `sys.version_info` with tuples of other lengths will sometimes produce `Literal` types,
|
||||
sometimes not:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info >= (3, 9, 1)) # revealed: bool
|
||||
reveal_type(sys.version_info >= (3, 9, 1, "final", 0)) # revealed: bool
|
||||
|
||||
# TODO: While this won't fail at runtime, the user has probably made a mistake
|
||||
# if they're comparing a tuple of length >5 with `sys.version_info`
|
||||
# (`sys.version_info` is a tuple of length 5). It might be worth
|
||||
# emitting a lint diagnostic of some kind warning them about the probable error?
|
||||
reveal_type(sys.version_info >= (3, 9, 1, "final", 0, 5)) # revealed: bool
|
||||
|
||||
reveal_type(sys.version_info == (3, 8, 1, "finallllll", 0)) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Imports and aliases
|
||||
|
||||
Comparisons with `sys.version_info` still produce literal types, even if the symbol is aliased to
|
||||
another name:
|
||||
|
||||
```py
|
||||
from sys import version_info
|
||||
from sys import version_info as foo
|
||||
|
||||
reveal_type(version_info >= (3, 9)) # revealed: Literal[True]
|
||||
reveal_type(foo >= (3, 9)) # revealed: Literal[True]
|
||||
|
||||
bar = version_info
|
||||
reveal_type(bar >= (3, 9)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Non-stdlib modules named `sys`
|
||||
|
||||
Only comparisons with the symbol `version_info` from the `sys` module produce literal types:
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/sys.py
|
||||
version_info: tuple[int, int] = (4, 2)
|
||||
```
|
||||
|
||||
```py path=package/script.py
|
||||
from .sys import version_info
|
||||
|
||||
reveal_type(version_info >= (3, 9)) # revealed: bool
|
||||
```
|
||||
|
||||
## Accessing fields by name
|
||||
|
||||
The fields of `sys.version_info` can be accessed by name:
|
||||
|
||||
```py path=a.py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info.major >= 3) # revealed: Literal[True]
|
||||
reveal_type(sys.version_info.minor >= 9) # revealed: Literal[True]
|
||||
reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
|
||||
properties on instance types:
|
||||
|
||||
```py path=b.py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(instance attributes)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(instance attributes)
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Accessing fields by index/slice
|
||||
|
||||
The fields of `sys.version_info` can be accessed by index or by slice:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info[0] < 3) # revealed: Literal[False]
|
||||
reveal_type(sys.version_info[1] > 9) # revealed: Literal[False]
|
||||
|
||||
# revealed: tuple[Literal[3], Literal[9], int, Literal["alpha", "beta", "candidate", "final"], int]
|
||||
reveal_type(sys.version_info[:5])
|
||||
|
||||
reveal_type(sys.version_info[:2] >= (3, 9)) # revealed: Literal[True]
|
||||
reveal_type(sys.version_info[0:2] >= (3, 10)) # revealed: Literal[False]
|
||||
reveal_type(sys.version_info[:3] >= (3, 10, 1)) # revealed: Literal[False]
|
||||
reveal_type(sys.version_info[3] == "final") # revealed: bool
|
||||
reveal_type(sys.version_info[3] == "finalllllll") # revealed: Literal[False]
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
# Type aliases
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr) # revealed: typing.TypeAliasType
|
||||
reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]
|
||||
|
||||
x: IntOrStr = 1
|
||||
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
def f() -> None:
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
## `__value__` attribute
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
# TODO: This should either fall back to the specified type from typeshed,
|
||||
# which is `Any`, or be the actual type of the runtime value expression
|
||||
# `int | str`, i.e. `types.UnionType`.
|
||||
reveal_type(IntOrStr.__value__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Invalid assignment
|
||||
|
||||
```py
|
||||
type OptionalInt = int | None
|
||||
|
||||
# error: [invalid-assignment]
|
||||
x: OptionalInt = "1"
|
||||
```
|
||||
|
||||
## Type aliases in type aliases
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
type IntOrStrOrBytes = IntOrStr | bytes
|
||||
|
||||
x: IntOrStrOrBytes = 1
|
||||
|
||||
def f() -> None:
|
||||
reveal_type(x) # revealed: int | str | bytes
|
||||
```
|
||||
|
||||
## Aliased type aliases
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
MyIntOrStr = IntOrStr
|
||||
|
||||
x: MyIntOrStr = 1
|
||||
|
||||
# error: [invalid-assignment]
|
||||
y: MyIntOrStr = None
|
||||
```
|
||||
|
||||
## Generic type aliases
|
||||
|
||||
```py
|
||||
type ListOrSet[T] = list[T] | set[T]
|
||||
|
||||
# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`,
|
||||
# as specified in the `typeshed` stubs.
|
||||
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
@@ -1,6 +1,10 @@
|
||||
# Unary Operations
|
||||
# Invert, UAdd, USub
|
||||
|
||||
## Instance
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Number:
|
||||
def __init__(self, value: int):
|
||||
self.value = 1
|
||||
@@ -18,7 +22,7 @@ a = Number()
|
||||
|
||||
reveal_type(+a) # revealed: int
|
||||
reveal_type(-a) # revealed: int
|
||||
reveal_type(~a) # revealed: @Todo
|
||||
reveal_type(~a) # revealed: Literal[True]
|
||||
|
||||
class NoDunder: ...
|
||||
|
||||
@@ -10,8 +10,6 @@ reveal_type(not not None) # revealed: Literal[False]
|
||||
## Function
|
||||
|
||||
```py
|
||||
from typing import reveal_type
|
||||
|
||||
def f():
|
||||
return 1
|
||||
|
||||
@@ -115,3 +113,101 @@ reveal_type(not ()) # revealed: Literal[True]
|
||||
reveal_type(not ("hello",)) # revealed: Literal[False]
|
||||
reveal_type(not (1, "hello")) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Instance
|
||||
|
||||
Not operator is inferred based on
|
||||
<https://docs.python.org/3/library/stdtypes.html#truth-value-testing>. An instance is True or False
|
||||
if the `__bool__` method says so.
|
||||
|
||||
At runtime, the `__len__` method is a fallback for `__bool__`, but we can't make use of that. If we
|
||||
have a class that defines `__len__` but not `__bool__`, it is possible that any subclass could add a
|
||||
`__bool__` method that would invalidate whatever conclusion we drew from `__len__`. So instances of
|
||||
classes without a `__bool__` method, with or without `__len__`, must be inferred as unknown
|
||||
truthiness.
|
||||
|
||||
```py
|
||||
class AlwaysTrue:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
# revealed: Literal[False]
|
||||
reveal_type(not AlwaysTrue())
|
||||
|
||||
class AlwaysFalse:
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
# revealed: Literal[True]
|
||||
reveal_type(not AlwaysFalse())
|
||||
|
||||
# We don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
|
||||
class BoolIsBool:
|
||||
__bool__ = bool
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not BoolIsBool())
|
||||
|
||||
# At runtime, no `__bool__` and no `__len__` means truthy, but we can't rely on that, because
|
||||
# a subclass could add a `__bool__` method.
|
||||
class NoBoolMethod: ...
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not NoBoolMethod())
|
||||
|
||||
# And we can't rely on `__len__` for the same reason: a subclass could add `__bool__`.
|
||||
class LenZero:
|
||||
def __len__(self) -> Literal[0]:
|
||||
return 0
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not LenZero())
|
||||
|
||||
class LenNonZero:
|
||||
def __len__(self) -> Literal[1]:
|
||||
return 1
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not LenNonZero())
|
||||
|
||||
class WithBothLenAndBool1:
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
def __len__(self) -> Literal[2]:
|
||||
return 2
|
||||
|
||||
# revealed: Literal[True]
|
||||
reveal_type(not WithBothLenAndBool1())
|
||||
|
||||
class WithBothLenAndBool2:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
def __len__(self) -> Literal[0]:
|
||||
return 0
|
||||
|
||||
# revealed: Literal[False]
|
||||
reveal_type(not WithBothLenAndBool2())
|
||||
|
||||
# TODO: raise diagnostic when __bool__ method is not valid: [unsupported-operator] "Method __bool__ for type `MethodBoolInvalid` should return `bool`, returned type `int`"
|
||||
# https://docs.python.org/3/reference/datamodel.html#object.__bool__
|
||||
class MethodBoolInvalid:
|
||||
def __bool__(self) -> int:
|
||||
return 0
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not MethodBoolInvalid())
|
||||
|
||||
# Don't trust a possibly-unbound `__bool__` method:
|
||||
def get_flag() -> bool:
|
||||
return True
|
||||
|
||||
class PossiblyUnboundBool:
|
||||
if get_flag():
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not PossiblyUnboundBool())
|
||||
```
|
||||
|
||||
@@ -81,11 +81,10 @@ reveal_type(b) # revealed: Literal[2]
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
[a, *b, c, d] = (1, 2) # error: "Object of type `None` is not iterable"
|
||||
[a, *b, c, d] = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: Literal[2]
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
@@ -93,32 +92,30 @@ reveal_type(d) # revealed: Unknown
|
||||
### Starred expression (2)
|
||||
|
||||
```py
|
||||
[a, *b, c] = (1, 2) # error: "Object of type `None` is not iterable"
|
||||
[a, *b, c] = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
### Starred expression (3)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
[a, *b, c] = (1, 2, 3) # error: "Object of type `None` is not iterable"
|
||||
[a, *b, c] = (1, 2, 3)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
### Starred expression (4)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
[a, *b, c, d] = (1, 2, 3, 4, 5, 6) # error: "Object of type `None` is not iterable"
|
||||
[a, *b, c, d] = (1, 2, 3, 4, 5, 6)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: Literal[5]
|
||||
reveal_type(d) # revealed: Literal[6]
|
||||
```
|
||||
@@ -126,23 +123,30 @@ reveal_type(d) # revealed: Literal[6]
|
||||
### Starred expression (5)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
[a, b, *c] = (1, 2, 3, 4) # error: "Object of type `None` is not iterable"
|
||||
[a, b, *c] = (1, 2, 3, 4)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(c) # revealed: @Todo
|
||||
reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
```
|
||||
|
||||
### Starred expression (6)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
(a, b, c, *d, e, f) = (1,)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Non-iterable unpacking
|
||||
|
||||
TODO: Remove duplicate diagnostics. This is happening because for a sequence-like
|
||||
assignment target, multiple definitions are created and the inference engine runs
|
||||
on each of them which results in duplicate diagnostics.
|
||||
|
||||
```py
|
||||
# error: "Object of type `Literal[1]` is not iterable"
|
||||
# error: "Object of type `Literal[1]` is not iterable"
|
||||
a, b = 1
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
@@ -215,11 +219,10 @@ reveal_type(b) # revealed: LiteralString
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
(a, *b, c, d) = "ab" # error: "Object of type `None` is not iterable"
|
||||
(a, *b, c, d) = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
@@ -227,32 +230,30 @@ reveal_type(d) # revealed: Unknown
|
||||
### Starred expression (2)
|
||||
|
||||
```py
|
||||
(a, *b, c) = "ab" # error: "Object of type `None` is not iterable"
|
||||
(a, *b, c) = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Starred expression (3)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
(a, *b, c) = "abc" # error: "Object of type `None` is not iterable"
|
||||
(a, *b, c) = "abc"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Starred expression (4)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
(a, *b, c, d) = "abcdef" # error: "Object of type `None` is not iterable"
|
||||
(a, *b, c, d) = "abcdef"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
reveal_type(d) # revealed: LiteralString
|
||||
```
|
||||
@@ -260,10 +261,9 @@ reveal_type(d) # revealed: LiteralString
|
||||
### Starred expression (5)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
(a, b, *c) = "abcd" # error: "Object of type `None` is not iterable"
|
||||
(a, b, *c) = "abcd"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(c) # revealed: @Todo
|
||||
reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Async with statements
|
||||
|
||||
## Basic `async with` statement
|
||||
|
||||
The type of the target variable in a `with` statement should be the return type from the context
|
||||
manager's `__aenter__` method. However, `async with` statements aren't supported yet. This test
|
||||
asserts that it doesn't emit any context manager-related errors.
|
||||
|
||||
```py
|
||||
class Target: ...
|
||||
|
||||
class Manager:
|
||||
async def __aenter__(self) -> Target:
|
||||
return Target()
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback): ...
|
||||
|
||||
async def test():
|
||||
async with Manager() as f:
|
||||
reveal_type(f) # revealed: @Todo(async with statement)
|
||||
```
|
||||
141
crates/red_knot_python_semantic/resources/mdtest/with/sync.md
Normal file
141
crates/red_knot_python_semantic/resources/mdtest/with/sync.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# With statements
|
||||
|
||||
## Basic `with` statement
|
||||
|
||||
The type of the target variable in a `with` statement is the return type from the context manager's
|
||||
`__enter__` method.
|
||||
|
||||
```py
|
||||
class Target: ...
|
||||
|
||||
class Manager:
|
||||
def __enter__(self) -> Target:
|
||||
return Target()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
|
||||
with Manager() as f:
|
||||
reveal_type(f) # revealed: Target
|
||||
```
|
||||
|
||||
## Union context manager
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
class Manager1:
|
||||
def __enter__(self) -> str:
|
||||
return "foo"
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
|
||||
class Manager2:
|
||||
def __enter__(self) -> int:
|
||||
return 42
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
|
||||
context_expr = Manager1() if coinflip() else Manager2()
|
||||
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str | int
|
||||
```
|
||||
|
||||
## Context manager without an `__enter__` or `__exit__` method
|
||||
|
||||
```py
|
||||
class Manager: ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
## Context manager without an `__enter__` method
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
def __exit__(self, exc_tpe, exc_value, traceback): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__enter__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
## Context manager without an `__exit__` method
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
def __enter__(self): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
## Context manager with non-callable `__enter__` attribute
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
__enter__ = 42
|
||||
|
||||
def __exit__(self, exc_tpe, exc_value, traceback): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` of type `Literal[42]` is not callable"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
## Context manager with non-callable `__exit__` attribute
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
def __enter__(self) -> Self: ...
|
||||
|
||||
__exit__ = 32
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `Literal[32]` is not callable"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
## Context expression with possibly-unbound union variants
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
class Manager1:
|
||||
def __enter__(self) -> str:
|
||||
return "foo"
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
|
||||
class NotAContextManager: ...
|
||||
|
||||
context_expr = Manager1() if coinflip() else NotAContextManager()
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__enter__` is possibly unbound"
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__exit__` is possibly unbound"
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Context expression with "sometimes" callable `__enter__` method
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
class Manager:
|
||||
if coinflip():
|
||||
def __enter__(self) -> str:
|
||||
return "abcd"
|
||||
|
||||
def __exit__(self, *args): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound"
|
||||
with Manager() as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
@@ -20,7 +20,9 @@ pub mod semantic_index;
|
||||
mod semantic_model;
|
||||
pub(crate) mod site_packages;
|
||||
mod stdlib;
|
||||
pub(crate) mod symbol;
|
||||
pub mod types;
|
||||
mod unpack;
|
||||
mod util;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
@@ -459,11 +459,11 @@ foo: 3.8- # trailing comment
|
||||
";
|
||||
let parsed_versions = TypeshedVersions::from_str(VERSIONS).unwrap();
|
||||
assert_eq!(parsed_versions.len(), 3);
|
||||
assert_snapshot!(parsed_versions.to_string(), @r###"
|
||||
assert_snapshot!(parsed_versions.to_string(), @r"
|
||||
bar: 2.7-3.10
|
||||
bar.baz: 3.1-3.9
|
||||
foo: 3.8-
|
||||
"###
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use ruff_python_ast::{AnyNodeRef, NodeKind};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
|
||||
/// Compact key for a node for use in a hash map.
|
||||
///
|
||||
/// Compares two nodes by their kind and text range.
|
||||
/// Stores the memory address of the node, because using the range and the kind
|
||||
/// of the node is not enough to uniquely identify them in ASTs resulting from
|
||||
/// invalid syntax. For example, parsing the input `for` results in a `StmtFor`
|
||||
/// AST node where both the `target` and the `iter` field are `ExprName` nodes
|
||||
/// with the same (empty) range `3..3`.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub(super) struct NodeKey {
|
||||
kind: NodeKind,
|
||||
range: TextRange,
|
||||
}
|
||||
pub(super) struct NodeKey(usize);
|
||||
|
||||
impl NodeKey {
|
||||
pub(super) fn from_node<'a, N>(node: N) -> Self
|
||||
@@ -16,9 +16,6 @@ impl NodeKey {
|
||||
N: Into<AnyNodeRef<'a>>,
|
||||
{
|
||||
let node = node.into();
|
||||
NodeKey {
|
||||
kind: node.kind(),
|
||||
range: node.range(),
|
||||
}
|
||||
NodeKey(node.as_ptr().as_ptr() as usize)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ impl Program {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct ProgramSettings {
|
||||
pub target_version: PythonVersion,
|
||||
pub search_paths: SearchPathSettings,
|
||||
@@ -61,6 +62,7 @@ pub struct ProgramSettings {
|
||||
|
||||
/// Configures the search paths for module resolution.
|
||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct SearchPathSettings {
|
||||
/// List of user-provided paths that should take first priority in the module resolution.
|
||||
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
|
||||
@@ -91,6 +93,7 @@ impl SearchPathSettings {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub enum SitePackages {
|
||||
Derived {
|
||||
venv_path: SystemPathBuf,
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::fmt;
|
||||
/// Unlike the `TargetVersion` enums in the CLI crates,
|
||||
/// this does not necessarily represent a Python version that we actually support.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct PythonVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
@@ -38,7 +39,7 @@ impl PythonVersion {
|
||||
|
||||
impl Default for PythonVersion {
|
||||
fn default() -> Self {
|
||||
Self::PY38
|
||||
Self::PY39
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ impl<'db> SemanticIndex<'db> {
|
||||
///
|
||||
/// Use the Salsa cached [`symbol_table()`] query if you only need the
|
||||
/// symbol table for a single scope.
|
||||
#[track_caller]
|
||||
pub(super) fn symbol_table(&self, scope_id: FileScopeId) -> Arc<SymbolTable> {
|
||||
self.symbol_tables[scope_id].clone()
|
||||
}
|
||||
@@ -133,15 +134,18 @@ impl<'db> SemanticIndex<'db> {
|
||||
///
|
||||
/// Use the Salsa cached [`use_def_map()`] query if you only need the
|
||||
/// use-def map for a single scope.
|
||||
#[track_caller]
|
||||
pub(super) fn use_def_map(&self, scope_id: FileScopeId) -> Arc<UseDefMap> {
|
||||
self.use_def_maps[scope_id].clone()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn ast_ids(&self, scope_id: FileScopeId) -> &AstIds {
|
||||
&self.ast_ids[scope_id]
|
||||
}
|
||||
|
||||
/// Returns the ID of the `expression`'s enclosing scope.
|
||||
#[track_caller]
|
||||
pub(crate) fn expression_scope_id(
|
||||
&self,
|
||||
expression: impl Into<ExpressionNodeKey>,
|
||||
@@ -151,11 +155,13 @@ impl<'db> SemanticIndex<'db> {
|
||||
|
||||
/// Returns the [`Scope`] of the `expression`'s enclosing scope.
|
||||
#[allow(unused)]
|
||||
#[track_caller]
|
||||
pub(crate) fn expression_scope(&self, expression: impl Into<ExpressionNodeKey>) -> &Scope {
|
||||
&self.scopes[self.expression_scope_id(expression)]
|
||||
}
|
||||
|
||||
/// Returns the [`Scope`] with the given id.
|
||||
#[track_caller]
|
||||
pub(crate) fn scope(&self, id: FileScopeId) -> &Scope {
|
||||
&self.scopes[id]
|
||||
}
|
||||
@@ -172,6 +178,7 @@ impl<'db> SemanticIndex<'db> {
|
||||
|
||||
/// Returns the parent scope of `scope_id`.
|
||||
#[allow(unused)]
|
||||
#[track_caller]
|
||||
pub(crate) fn parent_scope(&self, scope_id: FileScopeId) -> Option<&Scope> {
|
||||
Some(&self.scopes[self.parent_scope_id(scope_id)?])
|
||||
}
|
||||
@@ -195,6 +202,7 @@ impl<'db> SemanticIndex<'db> {
|
||||
}
|
||||
|
||||
/// Returns the [`Definition`] salsa ingredient for `definition_key`.
|
||||
#[track_caller]
|
||||
pub(crate) fn definition(
|
||||
&self,
|
||||
definition_key: impl Into<DefinitionNodeKey>,
|
||||
@@ -206,6 +214,7 @@ impl<'db> SemanticIndex<'db> {
|
||||
/// Panics if we have no expression ingredient for that node. We can only call this method for
|
||||
/// standalone-inferable expressions, which we call `add_standalone_expression` for in
|
||||
/// [`SemanticIndexBuilder`].
|
||||
#[track_caller]
|
||||
pub(crate) fn expression(
|
||||
&self,
|
||||
expression_key: impl Into<ExpressionNodeKey>,
|
||||
@@ -213,8 +222,18 @@ impl<'db> SemanticIndex<'db> {
|
||||
self.expressions_by_node[&expression_key.into()]
|
||||
}
|
||||
|
||||
pub(crate) fn try_expression(
|
||||
&self,
|
||||
expression_key: impl Into<ExpressionNodeKey>,
|
||||
) -> Option<Expression<'db>> {
|
||||
self.expressions_by_node
|
||||
.get(&expression_key.into())
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Returns the id of the scope that `node` creates. This is different from [`Definition::scope`] which
|
||||
/// returns the scope in which that definition is defined in.
|
||||
#[track_caller]
|
||||
pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId {
|
||||
self.scopes_by_node[&node.node_key()]
|
||||
}
|
||||
|
||||
@@ -49,56 +49,50 @@ fn ast_ids<'db>(db: &'db dyn Db, scope: ScopeId) -> &'db AstIds {
|
||||
semantic_index(db, scope.file(db)).ast_ids(scope.file_scope_id(db))
|
||||
}
|
||||
|
||||
pub trait HasScopedUseId {
|
||||
/// The type of the ID uniquely identifying the use.
|
||||
type Id: Copy;
|
||||
|
||||
/// Returns the ID that uniquely identifies the use in `scope`.
|
||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id;
|
||||
}
|
||||
|
||||
/// Uniquely identifies a use of a name in a [`crate::semantic_index::symbol::FileScopeId`].
|
||||
#[newtype_index]
|
||||
pub struct ScopedUseId;
|
||||
|
||||
impl HasScopedUseId for ast::ExprName {
|
||||
type Id = ScopedUseId;
|
||||
pub trait HasScopedUseId {
|
||||
/// Returns the ID that uniquely identifies the use in `scope`.
|
||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId;
|
||||
}
|
||||
|
||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
|
||||
impl HasScopedUseId for ast::ExprName {
|
||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
|
||||
let expression_ref = ExpressionRef::from(self);
|
||||
expression_ref.scoped_use_id(db, scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasScopedUseId for ast::ExpressionRef<'_> {
|
||||
type Id = ScopedUseId;
|
||||
|
||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
|
||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
|
||||
let ast_ids = ast_ids(db, scope);
|
||||
ast_ids.use_id(*self)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasScopedAstId {
|
||||
/// The type of the ID uniquely identifying the node.
|
||||
type Id: Copy;
|
||||
|
||||
/// Returns the ID that uniquely identifies the node in `scope`.
|
||||
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id;
|
||||
}
|
||||
|
||||
/// Uniquely identifies an [`ast::Expr`] in a [`crate::semantic_index::symbol::FileScopeId`].
|
||||
#[newtype_index]
|
||||
pub struct ScopedExpressionId;
|
||||
|
||||
pub trait HasScopedExpressionId {
|
||||
/// Returns the ID that uniquely identifies the node in `scope`.
|
||||
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId;
|
||||
}
|
||||
|
||||
impl<T: HasScopedExpressionId> HasScopedExpressionId for Box<T> {
|
||||
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
|
||||
self.as_ref().scoped_expression_id(db, scope)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_has_scoped_expression_id {
|
||||
($ty: ty) => {
|
||||
impl HasScopedAstId for $ty {
|
||||
type Id = ScopedExpressionId;
|
||||
|
||||
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
|
||||
impl HasScopedExpressionId for $ty {
|
||||
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
|
||||
let expression_ref = ExpressionRef::from(self);
|
||||
expression_ref.scoped_ast_id(db, scope)
|
||||
expression_ref.scoped_expression_id(db, scope)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -138,29 +132,20 @@ impl_has_scoped_expression_id!(ast::ExprSlice);
|
||||
impl_has_scoped_expression_id!(ast::ExprIpyEscapeCommand);
|
||||
impl_has_scoped_expression_id!(ast::Expr);
|
||||
|
||||
impl HasScopedAstId for ast::ExpressionRef<'_> {
|
||||
type Id = ScopedExpressionId;
|
||||
|
||||
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
|
||||
impl HasScopedExpressionId for ast::ExpressionRef<'_> {
|
||||
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
|
||||
let ast_ids = ast_ids(db, scope);
|
||||
ast_ids.expression_id(*self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct AstIdsBuilder {
|
||||
expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>,
|
||||
uses_map: FxHashMap<ExpressionNodeKey, ScopedUseId>,
|
||||
}
|
||||
|
||||
impl AstIdsBuilder {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
expressions_map: FxHashMap::default(),
|
||||
uses_map: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds `expr` to the expression ids map and returns its id.
|
||||
pub(super) fn record_expression(&mut self, expr: &ast::Expr) -> ScopedExpressionId {
|
||||
let expression_id = self.expressions_map.len().into();
|
||||
|
||||
@@ -25,27 +25,42 @@ use crate::semantic_index::symbol::{
|
||||
};
|
||||
use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::unpack::Unpack;
|
||||
use crate::Db;
|
||||
|
||||
use super::constraint::{Constraint, ConstraintNode, PatternConstraint};
|
||||
use super::definition::{
|
||||
AssignmentKind, DefinitionCategory, ExceptHandlerDefinitionNodeRef,
|
||||
MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
|
||||
DefinitionCategory, ExceptHandlerDefinitionNodeRef, MatchPatternDefinitionNodeRef,
|
||||
WithItemDefinitionNodeRef,
|
||||
};
|
||||
|
||||
mod except_handlers;
|
||||
|
||||
/// Are we in a state where a `break` statement is allowed?
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum LoopState {
|
||||
InLoop,
|
||||
NotInLoop,
|
||||
}
|
||||
|
||||
impl LoopState {
|
||||
fn is_inside(self) -> bool {
|
||||
matches!(self, LoopState::InLoop)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SemanticIndexBuilder<'db> {
|
||||
// Builder state
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
module: &'db ParsedModule,
|
||||
scope_stack: Vec<FileScopeId>,
|
||||
scope_stack: Vec<(FileScopeId, LoopState)>,
|
||||
/// The assignments we're currently visiting, with
|
||||
/// the most recent visit at the end of the Vec
|
||||
current_assignments: Vec<CurrentAssignment<'db>>,
|
||||
/// The match case we're currently visiting.
|
||||
current_match_case: Option<CurrentMatchCase<'db>>,
|
||||
|
||||
/// Flow states at each `break` in the current loop.
|
||||
loop_break_states: Vec<FlowSnapshot>,
|
||||
/// Per-scope contexts regarding nested `try`/`except` statements
|
||||
@@ -101,9 +116,24 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
*self
|
||||
.scope_stack
|
||||
.last()
|
||||
.map(|(scope, _)| scope)
|
||||
.expect("Always to have a root scope")
|
||||
}
|
||||
|
||||
fn loop_state(&self) -> LoopState {
|
||||
self.scope_stack
|
||||
.last()
|
||||
.expect("Always to have a root scope")
|
||||
.1
|
||||
}
|
||||
|
||||
fn set_inside_loop(&mut self, state: LoopState) {
|
||||
self.scope_stack
|
||||
.last_mut()
|
||||
.expect("Always to have a root scope")
|
||||
.1 = state;
|
||||
}
|
||||
|
||||
fn push_scope(&mut self, node: NodeWithScopeRef) {
|
||||
let parent = self.current_scope();
|
||||
self.push_scope_with_parent(node, Some(parent));
|
||||
@@ -112,38 +142,33 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
fn push_scope_with_parent(&mut self, node: NodeWithScopeRef, parent: Option<FileScopeId>) {
|
||||
let children_start = self.scopes.next_index() + 1;
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
let scope = Scope {
|
||||
parent,
|
||||
kind: node.scope_kind(),
|
||||
// SAFETY: `node` is guaranteed to be a child of `self.module`
|
||||
node: unsafe { node.to_kind(self.module.clone()) },
|
||||
descendents: children_start..children_start,
|
||||
};
|
||||
self.try_node_context_stack_manager.enter_nested_scope();
|
||||
|
||||
let file_scope_id = self.scopes.push(scope);
|
||||
self.symbol_tables.push(SymbolTableBuilder::new());
|
||||
self.use_def_maps.push(UseDefMapBuilder::new());
|
||||
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::new());
|
||||
self.symbol_tables.push(SymbolTableBuilder::default());
|
||||
self.use_def_maps.push(UseDefMapBuilder::default());
|
||||
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::default());
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
// SAFETY: `node` is guaranteed to be a child of `self.module`
|
||||
let scope_id = ScopeId::new(
|
||||
self.db,
|
||||
self.file,
|
||||
file_scope_id,
|
||||
unsafe { node.to_kind(self.module.clone()) },
|
||||
countme::Count::default(),
|
||||
);
|
||||
let scope_id = ScopeId::new(self.db, self.file, file_scope_id, countme::Count::default());
|
||||
|
||||
self.scope_ids_by_scope.push(scope_id);
|
||||
self.scopes_by_node.insert(node.node_key(), file_scope_id);
|
||||
let previous = self.scopes_by_node.insert(node.node_key(), file_scope_id);
|
||||
debug_assert_eq!(previous, None);
|
||||
|
||||
debug_assert_eq!(ast_id_scope, file_scope_id);
|
||||
|
||||
self.scope_stack.push(file_scope_id);
|
||||
self.scope_stack.push((file_scope_id, LoopState::NotInLoop));
|
||||
}
|
||||
|
||||
fn pop_scope(&mut self) -> FileScopeId {
|
||||
let id = self.scope_stack.pop().expect("Root scope to be present");
|
||||
let (id, _) = self.scope_stack.pop().expect("Root scope to be present");
|
||||
let children_end = self.scopes.next_index();
|
||||
let scope = &mut self.scopes[id];
|
||||
scope.descendents = scope.descendents.start..children_end;
|
||||
@@ -195,14 +220,18 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.current_symbol_table().mark_symbol_bound(id);
|
||||
}
|
||||
|
||||
fn mark_symbol_declared(&mut self, id: ScopedSymbolId) {
|
||||
self.current_symbol_table().mark_symbol_declared(id);
|
||||
}
|
||||
|
||||
fn mark_symbol_used(&mut self, id: ScopedSymbolId) {
|
||||
self.current_symbol_table().mark_symbol_used(id);
|
||||
}
|
||||
|
||||
fn add_definition<'a>(
|
||||
fn add_definition(
|
||||
&mut self,
|
||||
symbol: ScopedSymbolId,
|
||||
definition_node: impl Into<DefinitionNodeRef<'a>>,
|
||||
definition_node: impl Into<DefinitionNodeRef<'db>>,
|
||||
) -> Definition<'db> {
|
||||
let definition_node: DefinitionNodeRef<'_> = definition_node.into();
|
||||
#[allow(unsafe_code)]
|
||||
@@ -226,6 +255,9 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
if category.is_binding() {
|
||||
self.mark_symbol_bound(symbol);
|
||||
}
|
||||
if category.is_declaration() {
|
||||
self.mark_symbol_declared(symbol);
|
||||
}
|
||||
|
||||
let use_def = self.current_use_def_map_mut();
|
||||
match category {
|
||||
@@ -278,8 +310,12 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
debug_assert!(popped_assignment.is_some());
|
||||
}
|
||||
|
||||
fn current_assignment(&self) -> Option<&CurrentAssignment<'db>> {
|
||||
self.current_assignments.last()
|
||||
fn current_assignment(&self) -> Option<CurrentAssignment<'db>> {
|
||||
self.current_assignments.last().copied()
|
||||
}
|
||||
|
||||
fn current_assignment_mut(&mut self) -> Option<&mut CurrentAssignment<'db>> {
|
||||
self.current_assignments.last_mut()
|
||||
}
|
||||
|
||||
fn add_pattern_constraint(
|
||||
@@ -359,12 +395,18 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
// note that the "bound" on the typevar is a totally different thing than whether
|
||||
// or not a name is "bound" by a typevar declaration; the latter is always true.
|
||||
self.mark_symbol_bound(symbol);
|
||||
self.mark_symbol_declared(symbol);
|
||||
if let Some(bounds) = bound {
|
||||
self.visit_expr(bounds);
|
||||
}
|
||||
if let Some(default) = default {
|
||||
self.visit_expr(default);
|
||||
}
|
||||
match type_param {
|
||||
ast::TypeParam::TypeVar(node) => self.add_definition(symbol, node),
|
||||
ast::TypeParam::ParamSpec(node) => self.add_definition(symbol, node),
|
||||
ast::TypeParam::TypeVarTuple(node) => self.add_definition(symbol, node),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,7 +479,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.pop_scope();
|
||||
}
|
||||
|
||||
fn declare_parameter(&mut self, parameter: AnyParameterRef) {
|
||||
fn declare_parameter(&mut self, parameter: AnyParameterRef<'db>) {
|
||||
let symbol = self.add_symbol(parameter.name().id().clone());
|
||||
|
||||
let definition = self.add_definition(symbol, parameter);
|
||||
@@ -576,6 +618,27 @@ where
|
||||
},
|
||||
);
|
||||
}
|
||||
ast::Stmt::TypeAlias(type_alias) => {
|
||||
let symbol = self.add_symbol(
|
||||
type_alias
|
||||
.name
|
||||
.as_name_expr()
|
||||
.map(|name| name.id.clone())
|
||||
.unwrap_or("<unknown>".into()),
|
||||
);
|
||||
self.add_definition(symbol, type_alias);
|
||||
self.visit_expr(&type_alias.name);
|
||||
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::TypeAliasTypeParameters(type_alias),
|
||||
type_alias.type_params.as_ref(),
|
||||
|builder| {
|
||||
builder.push_scope(NodeWithScopeRef::TypeAlias(type_alias));
|
||||
builder.visit_expr(&type_alias.value);
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
}
|
||||
ast::Stmt::Import(node) => {
|
||||
for alias in &node.names {
|
||||
let symbol_name = if let Some(asname) = &alias.asname {
|
||||
@@ -611,24 +674,48 @@ where
|
||||
}
|
||||
ast::Stmt::Assign(node) => {
|
||||
debug_assert_eq!(&self.current_assignments, &[]);
|
||||
|
||||
self.visit_expr(&node.value);
|
||||
self.add_standalone_expression(&node.value);
|
||||
for (target_index, target) in node.targets.iter().enumerate() {
|
||||
let kind = match target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => Some(AssignmentKind::Sequence),
|
||||
ast::Expr::Name(_) => Some(AssignmentKind::Name),
|
||||
let value = self.add_standalone_expression(&node.value);
|
||||
|
||||
for target in &node.targets {
|
||||
// We only handle assignments to names and unpackings here, other targets like
|
||||
// attribute and subscript are handled separately as they don't create a new
|
||||
// definition.
|
||||
let current_assignment = match target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
|
||||
Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
first: true,
|
||||
unpack: Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
},
|
||||
value,
|
||||
countme::Count::default(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
ast::Expr::Name(_) => Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
unpack: None,
|
||||
first: false,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(kind) = kind {
|
||||
self.push_assignment(CurrentAssignment::Assign {
|
||||
assignment: node,
|
||||
target_index,
|
||||
kind,
|
||||
});
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
|
||||
self.visit_expr(target);
|
||||
if kind.is_some() {
|
||||
// only need to pop in the case where we pushed something
|
||||
|
||||
if current_assignment.is_some() {
|
||||
// Only need to pop in the case where we pushed something
|
||||
self.pop_assignment();
|
||||
}
|
||||
}
|
||||
@@ -639,9 +726,18 @@ where
|
||||
if let Some(value) = &node.value {
|
||||
self.visit_expr(value);
|
||||
}
|
||||
self.push_assignment(node.into());
|
||||
self.visit_expr(&node.target);
|
||||
self.pop_assignment();
|
||||
|
||||
// See https://docs.python.org/3/library/ast.html#ast.AnnAssign
|
||||
if matches!(
|
||||
*node.target,
|
||||
ast::Expr::Attribute(_) | ast::Expr::Subscript(_) | ast::Expr::Name(_)
|
||||
) {
|
||||
self.push_assignment(node.into());
|
||||
self.visit_expr(&node.target);
|
||||
self.pop_assignment();
|
||||
} else {
|
||||
self.visit_expr(&node.target);
|
||||
}
|
||||
}
|
||||
ast::Stmt::AugAssign(
|
||||
aug_assign @ ast::StmtAugAssign {
|
||||
@@ -653,9 +749,18 @@ where
|
||||
) => {
|
||||
debug_assert_eq!(&self.current_assignments, &[]);
|
||||
self.visit_expr(value);
|
||||
self.push_assignment(aug_assign.into());
|
||||
self.visit_expr(target);
|
||||
self.pop_assignment();
|
||||
|
||||
// See https://docs.python.org/3/library/ast.html#ast.AugAssign
|
||||
if matches!(
|
||||
**target,
|
||||
ast::Expr::Attribute(_) | ast::Expr::Subscript(_) | ast::Expr::Name(_)
|
||||
) {
|
||||
self.push_assignment(aug_assign.into());
|
||||
self.visit_expr(target);
|
||||
self.pop_assignment();
|
||||
} else {
|
||||
self.visit_expr(target);
|
||||
}
|
||||
}
|
||||
ast::Stmt::If(node) => {
|
||||
self.visit_expr(&node.test);
|
||||
@@ -708,7 +813,10 @@ where
|
||||
|
||||
// TODO: definitions created inside the body should be fully visible
|
||||
// to other statements/expressions inside the body --Alex/Carl
|
||||
let outer_loop_state = self.loop_state();
|
||||
self.set_inside_loop(LoopState::InLoop);
|
||||
self.visit_body(body);
|
||||
self.set_inside_loop(outer_loop_state);
|
||||
|
||||
// Get the break states from the body of this loop, and restore the saved outer
|
||||
// ones.
|
||||
@@ -726,12 +834,20 @@ where
|
||||
self.flow_merge(break_state);
|
||||
}
|
||||
}
|
||||
ast::Stmt::With(ast::StmtWith { items, body, .. }) => {
|
||||
ast::Stmt::With(ast::StmtWith {
|
||||
items,
|
||||
body,
|
||||
is_async,
|
||||
..
|
||||
}) => {
|
||||
for item in items {
|
||||
self.visit_expr(&item.context_expr);
|
||||
if let Some(optional_vars) = item.optional_vars.as_deref() {
|
||||
self.add_standalone_expression(&item.context_expr);
|
||||
self.push_assignment(item.into());
|
||||
self.push_assignment(CurrentAssignment::WithItem {
|
||||
item,
|
||||
is_async: *is_async,
|
||||
});
|
||||
self.visit_expr(optional_vars);
|
||||
self.pop_assignment();
|
||||
}
|
||||
@@ -739,7 +855,9 @@ where
|
||||
self.visit_body(body);
|
||||
}
|
||||
ast::Stmt::Break(_) => {
|
||||
self.loop_break_states.push(self.flow_snapshot());
|
||||
if self.loop_state().is_inside() {
|
||||
self.loop_break_states.push(self.flow_snapshot());
|
||||
}
|
||||
}
|
||||
|
||||
ast::Stmt::For(
|
||||
@@ -766,7 +884,10 @@ where
|
||||
// TODO: Definitions created by loop variables
|
||||
// (and definitions created inside the body)
|
||||
// are fully visible to other statements/expressions inside the body --Alex/Carl
|
||||
let outer_loop_state = self.loop_state();
|
||||
self.set_inside_loop(LoopState::InLoop);
|
||||
self.visit_body(body);
|
||||
self.set_inside_loop(outer_loop_state);
|
||||
|
||||
let break_states =
|
||||
std::mem::replace(&mut self.loop_break_states, saved_break_states);
|
||||
@@ -947,20 +1068,26 @@ where
|
||||
};
|
||||
let symbol = self.add_symbol(id.clone());
|
||||
|
||||
if is_use {
|
||||
self.mark_symbol_used(symbol);
|
||||
let use_id = self.current_ast_ids().record_use(expr);
|
||||
self.current_use_def_map_mut().record_use(symbol, use_id);
|
||||
}
|
||||
|
||||
if is_definition {
|
||||
match self.current_assignment().copied() {
|
||||
match self.current_assignment() {
|
||||
Some(CurrentAssignment::Assign {
|
||||
assignment,
|
||||
target_index,
|
||||
kind,
|
||||
node,
|
||||
first,
|
||||
unpack,
|
||||
}) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
AssignmentDefinitionNodeRef {
|
||||
assignment,
|
||||
target_index,
|
||||
unpack,
|
||||
value: &node.value,
|
||||
name: name_node,
|
||||
kind,
|
||||
first,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -997,12 +1124,13 @@ where
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(CurrentAssignment::WithItem(with_item)) => {
|
||||
Some(CurrentAssignment::WithItem { item, is_async }) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
WithItemDefinitionNodeRef {
|
||||
node: with_item,
|
||||
node: item,
|
||||
target: name_node,
|
||||
is_async,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1010,10 +1138,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if is_use {
|
||||
self.mark_symbol_used(symbol);
|
||||
let use_id = self.current_ast_ids().record_use(expr);
|
||||
self.current_use_def_map_mut().record_use(symbol, use_id);
|
||||
if let Some(CurrentAssignment::Assign { first, .. }) = self.current_assignment_mut()
|
||||
{
|
||||
*first = false;
|
||||
}
|
||||
|
||||
walk_expr(self, expr);
|
||||
@@ -1021,9 +1148,15 @@ where
|
||||
ast::Expr::Named(node) => {
|
||||
// TODO walrus in comprehensions is implicitly nonlocal
|
||||
self.visit_expr(&node.value);
|
||||
self.push_assignment(node.into());
|
||||
self.visit_expr(&node.target);
|
||||
self.pop_assignment();
|
||||
|
||||
// See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements
|
||||
if node.target.is_name_expr() {
|
||||
self.push_assignment(node.into());
|
||||
self.visit_expr(&node.target);
|
||||
self.pop_assignment();
|
||||
} else {
|
||||
self.visit_expr(&node.target);
|
||||
}
|
||||
}
|
||||
ast::Expr::Lambda(lambda) => {
|
||||
if let Some(parameters) = &lambda.parameters {
|
||||
@@ -1057,9 +1190,12 @@ where
|
||||
// later checking)
|
||||
self.visit_expr(test);
|
||||
let pre_if = self.flow_snapshot();
|
||||
let constraint = self.record_expression_constraint(test);
|
||||
self.visit_expr(body);
|
||||
let post_body = self.flow_snapshot();
|
||||
self.flow_restore(pre_if);
|
||||
|
||||
self.record_negated_constraint(constraint);
|
||||
self.visit_expr(orelse);
|
||||
self.flow_merge(post_body);
|
||||
}
|
||||
@@ -1212,9 +1348,9 @@ where
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
enum CurrentAssignment<'a> {
|
||||
Assign {
|
||||
assignment: &'a ast::StmtAssign,
|
||||
target_index: usize,
|
||||
kind: AssignmentKind,
|
||||
node: &'a ast::StmtAssign,
|
||||
first: bool,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
AnnAssign(&'a ast::StmtAnnAssign),
|
||||
AugAssign(&'a ast::StmtAugAssign),
|
||||
@@ -1224,7 +1360,10 @@ enum CurrentAssignment<'a> {
|
||||
node: &'a ast::Comprehension,
|
||||
first: bool,
|
||||
},
|
||||
WithItem(&'a ast::WithItem),
|
||||
WithItem {
|
||||
item: &'a ast::WithItem,
|
||||
is_async: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtAnnAssign> for CurrentAssignment<'a> {
|
||||
@@ -1251,12 +1390,6 @@ impl<'a> From<&'a ast::ExprNamed> for CurrentAssignment<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::WithItem> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::WithItem) -> Self {
|
||||
Self::WithItem(value)
|
||||
}
|
||||
}
|
||||
|
||||
struct CurrentMatchCase<'a> {
|
||||
/// The pattern that's part of the current match case.
|
||||
pattern: &'a ast::Pattern,
|
||||
|
||||
@@ -6,8 +6,22 @@ use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_resolver::file_to_module;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::Db;
|
||||
|
||||
/// A definition of a symbol.
|
||||
///
|
||||
/// ## Module-local type
|
||||
/// This type should not be used as part of any cross-module API because
|
||||
/// it holds a reference to the AST node. Range-offset changes
|
||||
/// then propagate through all usages, and deserialization requires
|
||||
/// reparsing the entire module.
|
||||
///
|
||||
/// E.g. don't use this type in:
|
||||
///
|
||||
/// * a return type of a cross-module query
|
||||
/// * a field of a type that is a return type of a cross-module query
|
||||
/// * an argument of a cross-module query
|
||||
#[salsa::tracked]
|
||||
pub struct Definition<'db> {
|
||||
/// The file in which the definition occurs.
|
||||
@@ -24,7 +38,7 @@ pub struct Definition<'db> {
|
||||
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) kind: DefinitionKind,
|
||||
pub(crate) kind: DefinitionKind<'db>,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<Definition<'static>>,
|
||||
@@ -69,6 +83,7 @@ pub(crate) enum DefinitionNodeRef<'a> {
|
||||
For(ForStmtDefinitionNodeRef<'a>),
|
||||
Function(&'a ast::StmtFunctionDef),
|
||||
Class(&'a ast::StmtClassDef),
|
||||
TypeAlias(&'a ast::StmtTypeAlias),
|
||||
NamedExpression(&'a ast::ExprNamed),
|
||||
Assignment(AssignmentDefinitionNodeRef<'a>),
|
||||
AnnotatedAssignment(&'a ast::StmtAnnAssign),
|
||||
@@ -78,6 +93,9 @@ pub(crate) enum DefinitionNodeRef<'a> {
|
||||
WithItem(WithItemDefinitionNodeRef<'a>),
|
||||
MatchPattern(MatchPatternDefinitionNodeRef<'a>),
|
||||
ExceptHandler(ExceptHandlerDefinitionNodeRef<'a>),
|
||||
TypeVar(&'a ast::TypeParamTypeVar),
|
||||
ParamSpec(&'a ast::TypeParamParamSpec),
|
||||
TypeVarTuple(&'a ast::TypeParamTypeVarTuple),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> {
|
||||
@@ -92,6 +110,12 @@ impl<'a> From<&'a ast::StmtClassDef> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtTypeAlias> for DefinitionNodeRef<'a> {
|
||||
fn from(node: &'a ast::StmtTypeAlias) -> Self {
|
||||
Self::TypeAlias(node)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprNamed> for DefinitionNodeRef<'a> {
|
||||
fn from(node: &'a ast::ExprNamed) -> Self {
|
||||
Self::NamedExpression(node)
|
||||
@@ -116,6 +140,24 @@ impl<'a> From<&'a ast::Alias> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::TypeParamTypeVar> for DefinitionNodeRef<'a> {
|
||||
fn from(value: &'a ast::TypeParamTypeVar) -> Self {
|
||||
Self::TypeVar(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::TypeParamParamSpec> for DefinitionNodeRef<'a> {
|
||||
fn from(value: &'a ast::TypeParamParamSpec) -> Self {
|
||||
Self::ParamSpec(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::TypeParamTypeVarTuple> for DefinitionNodeRef<'a> {
|
||||
fn from(value: &'a ast::TypeParamTypeVarTuple) -> Self {
|
||||
Self::TypeVarTuple(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ImportFromDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node_ref: ImportFromDefinitionNodeRef<'a>) -> Self {
|
||||
Self::ImportFrom(node_ref)
|
||||
@@ -166,16 +208,17 @@ pub(crate) struct ImportFromDefinitionNodeRef<'a> {
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct AssignmentDefinitionNodeRef<'a> {
|
||||
pub(crate) assignment: &'a ast::StmtAssign,
|
||||
pub(crate) target_index: usize,
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) value: &'a ast::Expr,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) kind: AssignmentKind,
|
||||
pub(crate) first: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct WithItemDefinitionNodeRef<'a> {
|
||||
pub(crate) node: &'a ast::WithItem,
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -210,9 +253,9 @@ pub(crate) struct MatchPatternDefinitionNodeRef<'a> {
|
||||
pub(crate) index: u32,
|
||||
}
|
||||
|
||||
impl DefinitionNodeRef<'_> {
|
||||
impl<'db> DefinitionNodeRef<'db> {
|
||||
#[allow(unsafe_code)]
|
||||
pub(super) unsafe fn into_owned(self, parsed: ParsedModule) -> DefinitionKind {
|
||||
pub(super) unsafe fn into_owned(self, parsed: ParsedModule) -> DefinitionKind<'db> {
|
||||
match self {
|
||||
DefinitionNodeRef::Import(alias) => {
|
||||
DefinitionKind::Import(AstNodeRef::new(parsed, alias))
|
||||
@@ -229,19 +272,22 @@ impl DefinitionNodeRef<'_> {
|
||||
DefinitionNodeRef::Class(class) => {
|
||||
DefinitionKind::Class(AstNodeRef::new(parsed, class))
|
||||
}
|
||||
DefinitionNodeRef::TypeAlias(type_alias) => {
|
||||
DefinitionKind::TypeAlias(AstNodeRef::new(parsed, type_alias))
|
||||
}
|
||||
DefinitionNodeRef::NamedExpression(named) => {
|
||||
DefinitionKind::NamedExpression(AstNodeRef::new(parsed, named))
|
||||
}
|
||||
DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef {
|
||||
assignment,
|
||||
target_index,
|
||||
unpack,
|
||||
value,
|
||||
name,
|
||||
kind,
|
||||
first,
|
||||
}) => DefinitionKind::Assignment(AssignmentDefinitionKind {
|
||||
assignment: AstNodeRef::new(parsed.clone(), assignment),
|
||||
target_index,
|
||||
target: TargetKind::from(unpack),
|
||||
value: AstNodeRef::new(parsed.clone(), value),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
kind,
|
||||
first,
|
||||
}),
|
||||
DefinitionNodeRef::AnnotatedAssignment(assign) => {
|
||||
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
|
||||
@@ -277,12 +323,15 @@ impl DefinitionNodeRef<'_> {
|
||||
DefinitionKind::ParameterWithDefault(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
},
|
||||
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef { node, target }) => {
|
||||
DefinitionKind::WithItem(WithItemDefinitionKind {
|
||||
node: AstNodeRef::new(parsed.clone(), node),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
})
|
||||
}
|
||||
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef {
|
||||
node,
|
||||
target,
|
||||
is_async,
|
||||
}) => DefinitionKind::WithItem(WithItemDefinitionKind {
|
||||
node: AstNodeRef::new(parsed.clone(), node),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
|
||||
pattern,
|
||||
identifier,
|
||||
@@ -299,6 +348,15 @@ impl DefinitionNodeRef<'_> {
|
||||
handler: AstNodeRef::new(parsed, handler),
|
||||
is_star,
|
||||
}),
|
||||
DefinitionNodeRef::TypeVar(node) => {
|
||||
DefinitionKind::TypeVar(AstNodeRef::new(parsed, node))
|
||||
}
|
||||
DefinitionNodeRef::ParamSpec(node) => {
|
||||
DefinitionKind::ParamSpec(AstNodeRef::new(parsed, node))
|
||||
}
|
||||
DefinitionNodeRef::TypeVarTuple(node) => {
|
||||
DefinitionKind::TypeVarTuple(AstNodeRef::new(parsed, node))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,12 +368,13 @@ impl DefinitionNodeRef<'_> {
|
||||
}
|
||||
Self::Function(node) => node.into(),
|
||||
Self::Class(node) => node.into(),
|
||||
Self::TypeAlias(node) => node.into(),
|
||||
Self::NamedExpression(node) => node.into(),
|
||||
Self::Assignment(AssignmentDefinitionNodeRef {
|
||||
assignment: _,
|
||||
target_index: _,
|
||||
value: _,
|
||||
unpack: _,
|
||||
name,
|
||||
kind: _,
|
||||
first: _,
|
||||
}) => name.into(),
|
||||
Self::AnnotatedAssignment(node) => node.into(),
|
||||
Self::AugmentedAssignment(node) => node.into(),
|
||||
@@ -329,11 +388,18 @@ impl DefinitionNodeRef<'_> {
|
||||
ast::AnyParameterRef::Variadic(parameter) => parameter.into(),
|
||||
ast::AnyParameterRef::NonVariadic(parameter) => parameter.into(),
|
||||
},
|
||||
Self::WithItem(WithItemDefinitionNodeRef { node: _, target }) => target.into(),
|
||||
Self::WithItem(WithItemDefinitionNodeRef {
|
||||
node: _,
|
||||
target,
|
||||
is_async: _,
|
||||
}) => target.into(),
|
||||
Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => {
|
||||
identifier.into()
|
||||
}
|
||||
Self::ExceptHandler(ExceptHandlerDefinitionNodeRef { handler, .. }) => handler.into(),
|
||||
Self::TypeVar(node) => node.into(),
|
||||
Self::ParamSpec(node) => node.into(),
|
||||
Self::TypeVarTuple(node) => node.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,13 +440,14 @@ impl DefinitionCategory {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DefinitionKind {
|
||||
pub enum DefinitionKind<'db> {
|
||||
Import(AstNodeRef<ast::Alias>),
|
||||
ImportFrom(ImportFromDefinitionKind),
|
||||
Function(AstNodeRef<ast::StmtFunctionDef>),
|
||||
Class(AstNodeRef<ast::StmtClassDef>),
|
||||
TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
|
||||
NamedExpression(AstNodeRef<ast::ExprNamed>),
|
||||
Assignment(AssignmentDefinitionKind),
|
||||
Assignment(AssignmentDefinitionKind<'db>),
|
||||
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
|
||||
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
|
||||
For(ForStmtDefinitionKind),
|
||||
@@ -390,16 +457,23 @@ pub enum DefinitionKind {
|
||||
WithItem(WithItemDefinitionKind),
|
||||
MatchPattern(MatchPatternDefinitionKind),
|
||||
ExceptHandler(ExceptHandlerDefinitionKind),
|
||||
TypeVar(AstNodeRef<ast::TypeParamTypeVar>),
|
||||
ParamSpec(AstNodeRef<ast::TypeParamParamSpec>),
|
||||
TypeVarTuple(AstNodeRef<ast::TypeParamTypeVarTuple>),
|
||||
}
|
||||
|
||||
impl DefinitionKind {
|
||||
impl DefinitionKind<'_> {
|
||||
pub(crate) fn category(&self) -> DefinitionCategory {
|
||||
match self {
|
||||
// functions, classes, and imports always bind, and we consider them declarations
|
||||
DefinitionKind::Function(_)
|
||||
| DefinitionKind::Class(_)
|
||||
| DefinitionKind::TypeAlias(_)
|
||||
| DefinitionKind::Import(_)
|
||||
| DefinitionKind::ImportFrom(_) => DefinitionCategory::DeclarationAndBinding,
|
||||
| DefinitionKind::ImportFrom(_)
|
||||
| DefinitionKind::TypeVar(_)
|
||||
| DefinitionKind::ParamSpec(_)
|
||||
| DefinitionKind::TypeVarTuple(_) => DefinitionCategory::DeclarationAndBinding,
|
||||
// a parameter always binds a value, but is only a declaration if annotated
|
||||
DefinitionKind::Parameter(parameter) => {
|
||||
if parameter.annotation.is_some() {
|
||||
@@ -437,6 +511,21 @@ impl DefinitionKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub(crate) enum TargetKind<'db> {
|
||||
Sequence(Unpack<'db>),
|
||||
Name,
|
||||
}
|
||||
|
||||
impl<'db> From<Option<Unpack<'db>>> for TargetKind<'db> {
|
||||
fn from(value: Option<Unpack<'db>>) -> Self {
|
||||
match value {
|
||||
Some(unpack) => TargetKind::Sequence(unpack),
|
||||
None => TargetKind::Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct MatchPatternDefinitionKind {
|
||||
@@ -498,42 +587,36 @@ impl ImportFromDefinitionKind {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AssignmentDefinitionKind {
|
||||
assignment: AstNodeRef<ast::StmtAssign>,
|
||||
target_index: usize,
|
||||
pub struct AssignmentDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
value: AstNodeRef<ast::Expr>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
kind: AssignmentKind,
|
||||
first: bool,
|
||||
}
|
||||
|
||||
impl AssignmentDefinitionKind {
|
||||
pub(crate) fn value(&self) -> &ast::Expr {
|
||||
&self.assignment.node().value
|
||||
impl<'db> AssignmentDefinitionKind<'db> {
|
||||
pub(crate) fn target(&self) -> TargetKind<'db> {
|
||||
self.target
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ast::Expr {
|
||||
&self.assignment.node().targets[self.target_index]
|
||||
pub(crate) fn value(&self) -> &ast::Expr {
|
||||
self.value.node()
|
||||
}
|
||||
|
||||
pub(crate) fn name(&self) -> &ast::ExprName {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) fn kind(&self) -> AssignmentKind {
|
||||
self.kind
|
||||
pub(crate) fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of assignment target expression.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum AssignmentKind {
|
||||
Sequence,
|
||||
Name,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WithItemDefinitionKind {
|
||||
node: AstNodeRef<ast::WithItem>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
impl WithItemDefinitionKind {
|
||||
@@ -544,6 +627,10 @@ impl WithItemDefinitionKind {
|
||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||
self.target.node()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_async(&self) -> bool {
|
||||
self.is_async
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -608,6 +695,12 @@ impl From<&ast::StmtClassDef> for DefinitionNodeKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::StmtTypeAlias> for DefinitionNodeKey {
|
||||
fn from(node: &ast::StmtTypeAlias) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::ExprName> for DefinitionNodeKey {
|
||||
fn from(node: &ast::ExprName) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
@@ -661,3 +754,21 @@ impl From<&ast::ExceptHandlerExceptHandler> for DefinitionNodeKey {
|
||||
Self(NodeKey::from_node(handler))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::TypeParamTypeVar> for DefinitionNodeKey {
|
||||
fn from(value: &ast::TypeParamTypeVar) -> Self {
|
||||
Self(NodeKey::from_node(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::TypeParamParamSpec> for DefinitionNodeKey {
|
||||
fn from(value: &ast::TypeParamParamSpec) -> Self {
|
||||
Self(NodeKey::from_node(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::TypeParamTypeVarTuple> for DefinitionNodeKey {
|
||||
fn from(value: &ast::TypeParamTypeVarTuple) -> Self {
|
||||
Self(NodeKey::from_node(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,18 @@ use salsa;
|
||||
/// An independently type-inferable expression.
|
||||
///
|
||||
/// Includes constraint expressions (e.g. if tests) and the RHS of an unpacking assignment.
|
||||
///
|
||||
/// ## Module-local type
|
||||
/// This type should not be used as part of any cross-module API because
|
||||
/// it holds a reference to the AST node. Range-offset changes
|
||||
/// then propagate through all usages, and deserialization requires
|
||||
/// reparsing the entire module.
|
||||
///
|
||||
/// E.g. don't use this type in:
|
||||
///
|
||||
/// * a return type of a cross-module query
|
||||
/// * a field of a type that is a return type of a cross-module query
|
||||
/// * an argument of a cross-module query
|
||||
#[salsa::tracked]
|
||||
pub(crate) struct Expression<'db> {
|
||||
/// The file in which the expression occurs.
|
||||
|
||||
@@ -47,17 +47,27 @@ impl Symbol {
|
||||
pub fn is_bound(&self) -> bool {
|
||||
self.flags.contains(SymbolFlags::IS_BOUND)
|
||||
}
|
||||
|
||||
/// Is the symbol declared in its containing scope?
|
||||
pub fn is_declared(&self) -> bool {
|
||||
self.flags.contains(SymbolFlags::IS_DECLARED)
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Flags that can be queried to obtain information about a symbol in a given scope.
|
||||
///
|
||||
/// See the doc-comment at the top of [`super::use_def`] for explanations of what it
|
||||
/// means for a symbol to be *bound* as opposed to *declared*.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
struct SymbolFlags: u8 {
|
||||
const IS_USED = 1 << 0;
|
||||
const IS_BOUND = 1 << 1;
|
||||
const IS_BOUND = 1 << 1;
|
||||
const IS_DECLARED = 1 << 2;
|
||||
/// TODO: This flag is not yet set by anything
|
||||
const MARKED_GLOBAL = 1 << 2;
|
||||
const MARKED_GLOBAL = 1 << 3;
|
||||
/// TODO: This flag is not yet set by anything
|
||||
const MARKED_NONLOCAL = 1 << 3;
|
||||
const MARKED_NONLOCAL = 1 << 4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,14 +103,10 @@ pub struct ScopedSymbolId;
|
||||
pub struct ScopeId<'db> {
|
||||
#[id]
|
||||
pub file: File,
|
||||
|
||||
#[id]
|
||||
pub file_scope_id: FileScopeId,
|
||||
|
||||
/// The node that introduces this scope.
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub node: NodeWithScopeKind,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<ScopeId<'static>>,
|
||||
}
|
||||
@@ -110,17 +116,22 @@ impl<'db> ScopeId<'db> {
|
||||
// Type parameter scopes behave like function scopes in terms of name resolution; CPython
|
||||
// symbol table also uses the term "function-like" for these scopes.
|
||||
matches!(
|
||||
self.node(db),
|
||||
NodeWithScopeKind::ClassTypeParameters(_)
|
||||
| NodeWithScopeKind::FunctionTypeParameters(_)
|
||||
| NodeWithScopeKind::Function(_)
|
||||
| NodeWithScopeKind::ListComprehension(_)
|
||||
| NodeWithScopeKind::SetComprehension(_)
|
||||
| NodeWithScopeKind::DictComprehension(_)
|
||||
| NodeWithScopeKind::GeneratorExpression(_)
|
||||
self.node(db).scope_kind(),
|
||||
ScopeKind::Annotation
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::TypeAlias
|
||||
| ScopeKind::Comprehension
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind {
|
||||
self.scope(db).node()
|
||||
}
|
||||
|
||||
pub(crate) fn scope(self, db: &dyn Db) -> &Scope {
|
||||
semantic_index(db, self.file(db)).scope(self.file_scope_id(db))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn name(self, db: &'db dyn Db) -> &'db str {
|
||||
match self.node(db) {
|
||||
@@ -130,6 +141,12 @@ impl<'db> ScopeId<'db> {
|
||||
}
|
||||
NodeWithScopeKind::Function(function)
|
||||
| NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(),
|
||||
NodeWithScopeKind::TypeAlias(type_alias)
|
||||
| NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => type_alias
|
||||
.name
|
||||
.as_name_expr()
|
||||
.map(|name| name.id.as_str())
|
||||
.unwrap_or("<type alias>"),
|
||||
NodeWithScopeKind::Lambda(_) => "<lambda>",
|
||||
NodeWithScopeKind::ListComprehension(_) => "<listcomp>",
|
||||
NodeWithScopeKind::SetComprehension(_) => "<setcomp>",
|
||||
@@ -159,10 +176,10 @@ impl FileScopeId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub struct Scope {
|
||||
pub(super) parent: Option<FileScopeId>,
|
||||
pub(super) kind: ScopeKind,
|
||||
pub(super) node: NodeWithScopeKind,
|
||||
pub(super) descendents: Range<FileScopeId>,
|
||||
}
|
||||
|
||||
@@ -171,8 +188,12 @@ impl Scope {
|
||||
self.parent
|
||||
}
|
||||
|
||||
pub fn node(&self) -> &NodeWithScopeKind {
|
||||
&self.node
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> ScopeKind {
|
||||
self.kind
|
||||
self.node().scope_kind()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +204,7 @@ pub enum ScopeKind {
|
||||
Class,
|
||||
Function,
|
||||
Comprehension,
|
||||
TypeAlias,
|
||||
}
|
||||
|
||||
impl ScopeKind {
|
||||
@@ -192,7 +214,7 @@ impl ScopeKind {
|
||||
}
|
||||
|
||||
/// Symbol table for a specific [`Scope`].
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SymbolTable {
|
||||
/// The symbols in this scope.
|
||||
symbols: IndexVec<ScopedSymbolId, Symbol>,
|
||||
@@ -202,13 +224,6 @@ pub struct SymbolTable {
|
||||
}
|
||||
|
||||
impl SymbolTable {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
symbols: IndexVec::new(),
|
||||
symbols_by_name: SymbolMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn shrink_to_fit(&mut self) {
|
||||
self.symbols.shrink_to_fit();
|
||||
}
|
||||
@@ -260,18 +275,12 @@ impl PartialEq for SymbolTable {
|
||||
|
||||
impl Eq for SymbolTable {}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct SymbolTableBuilder {
|
||||
table: SymbolTable,
|
||||
}
|
||||
|
||||
impl SymbolTableBuilder {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
table: SymbolTable::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) {
|
||||
let hash = SymbolTable::hash_name(&name);
|
||||
let entry = self
|
||||
@@ -298,6 +307,10 @@ impl SymbolTableBuilder {
|
||||
self.table.symbols[id].insert_flags(SymbolFlags::IS_BOUND);
|
||||
}
|
||||
|
||||
pub(super) fn mark_symbol_declared(&mut self, id: ScopedSymbolId) {
|
||||
self.table.symbols[id].insert_flags(SymbolFlags::IS_DECLARED);
|
||||
}
|
||||
|
||||
pub(super) fn mark_symbol_used(&mut self, id: ScopedSymbolId) {
|
||||
self.table.symbols[id].insert_flags(SymbolFlags::IS_USED);
|
||||
}
|
||||
@@ -317,6 +330,8 @@ pub(crate) enum NodeWithScopeRef<'a> {
|
||||
Lambda(&'a ast::ExprLambda),
|
||||
FunctionTypeParameters(&'a ast::StmtFunctionDef),
|
||||
ClassTypeParameters(&'a ast::StmtClassDef),
|
||||
TypeAlias(&'a ast::StmtTypeAlias),
|
||||
TypeAliasTypeParameters(&'a ast::StmtTypeAlias),
|
||||
ListComprehension(&'a ast::ExprListComp),
|
||||
SetComprehension(&'a ast::ExprSetComp),
|
||||
DictComprehension(&'a ast::ExprDictComp),
|
||||
@@ -338,6 +353,12 @@ impl NodeWithScopeRef<'_> {
|
||||
NodeWithScopeRef::Function(function) => {
|
||||
NodeWithScopeKind::Function(AstNodeRef::new(module, function))
|
||||
}
|
||||
NodeWithScopeRef::TypeAlias(type_alias) => {
|
||||
NodeWithScopeKind::TypeAlias(AstNodeRef::new(module, type_alias))
|
||||
}
|
||||
NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => {
|
||||
NodeWithScopeKind::TypeAliasTypeParameters(AstNodeRef::new(module, type_alias))
|
||||
}
|
||||
NodeWithScopeRef::Lambda(lambda) => {
|
||||
NodeWithScopeKind::Lambda(AstNodeRef::new(module, lambda))
|
||||
}
|
||||
@@ -362,21 +383,6 @@ impl NodeWithScopeRef<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn scope_kind(self) -> ScopeKind {
|
||||
match self {
|
||||
NodeWithScopeRef::Module => ScopeKind::Module,
|
||||
NodeWithScopeRef::Class(_) => ScopeKind::Class,
|
||||
NodeWithScopeRef::Function(_) => ScopeKind::Function,
|
||||
NodeWithScopeRef::Lambda(_) => ScopeKind::Function,
|
||||
NodeWithScopeRef::FunctionTypeParameters(_)
|
||||
| NodeWithScopeRef::ClassTypeParameters(_) => ScopeKind::Annotation,
|
||||
NodeWithScopeRef::ListComprehension(_)
|
||||
| NodeWithScopeRef::SetComprehension(_)
|
||||
| NodeWithScopeRef::DictComprehension(_)
|
||||
| NodeWithScopeRef::GeneratorExpression(_) => ScopeKind::Comprehension,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn node_key(self) -> NodeWithScopeKey {
|
||||
match self {
|
||||
NodeWithScopeRef::Module => NodeWithScopeKey::Module,
|
||||
@@ -393,6 +399,12 @@ impl NodeWithScopeRef<'_> {
|
||||
NodeWithScopeRef::ClassTypeParameters(class) => {
|
||||
NodeWithScopeKey::ClassTypeParameters(NodeKey::from_node(class))
|
||||
}
|
||||
NodeWithScopeRef::TypeAlias(type_alias) => {
|
||||
NodeWithScopeKey::TypeAlias(NodeKey::from_node(type_alias))
|
||||
}
|
||||
NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => {
|
||||
NodeWithScopeKey::TypeAliasTypeParameters(NodeKey::from_node(type_alias))
|
||||
}
|
||||
NodeWithScopeRef::ListComprehension(comprehension) => {
|
||||
NodeWithScopeKey::ListComprehension(NodeKey::from_node(comprehension))
|
||||
}
|
||||
@@ -417,6 +429,8 @@ pub enum NodeWithScopeKind {
|
||||
ClassTypeParameters(AstNodeRef<ast::StmtClassDef>),
|
||||
Function(AstNodeRef<ast::StmtFunctionDef>),
|
||||
FunctionTypeParameters(AstNodeRef<ast::StmtFunctionDef>),
|
||||
TypeAliasTypeParameters(AstNodeRef<ast::StmtTypeAlias>),
|
||||
TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
|
||||
Lambda(AstNodeRef<ast::ExprLambda>),
|
||||
ListComprehension(AstNodeRef<ast::ExprListComp>),
|
||||
SetComprehension(AstNodeRef<ast::ExprSetComp>),
|
||||
@@ -424,6 +438,45 @@ pub enum NodeWithScopeKind {
|
||||
GeneratorExpression(AstNodeRef<ast::ExprGenerator>),
|
||||
}
|
||||
|
||||
impl NodeWithScopeKind {
|
||||
pub(super) const fn scope_kind(&self) -> ScopeKind {
|
||||
match self {
|
||||
Self::Module => ScopeKind::Module,
|
||||
Self::Class(_) => ScopeKind::Class,
|
||||
Self::Function(_) | Self::Lambda(_) => ScopeKind::Function,
|
||||
Self::FunctionTypeParameters(_)
|
||||
| Self::ClassTypeParameters(_)
|
||||
| Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation,
|
||||
Self::TypeAlias(_) => ScopeKind::TypeAlias,
|
||||
Self::ListComprehension(_)
|
||||
| Self::SetComprehension(_)
|
||||
| Self::DictComprehension(_)
|
||||
| Self::GeneratorExpression(_) => ScopeKind::Comprehension,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_class(&self) -> &ast::StmtClassDef {
|
||||
match self {
|
||||
Self::Class(class) => class.node(),
|
||||
_ => panic!("expected class"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_function(&self) -> &ast::StmtFunctionDef {
|
||||
match self {
|
||||
Self::Function(function) => function.node(),
|
||||
_ => panic!("expected function"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias {
|
||||
match self {
|
||||
Self::TypeAlias(type_alias) => type_alias.node(),
|
||||
_ => panic!("expected type alias"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum NodeWithScopeKey {
|
||||
Module,
|
||||
@@ -431,6 +484,8 @@ pub(crate) enum NodeWithScopeKey {
|
||||
ClassTypeParameters(NodeKey),
|
||||
Function(NodeKey),
|
||||
FunctionTypeParameters(NodeKey),
|
||||
TypeAlias(NodeKey),
|
||||
TypeAliasTypeParameters(NodeKey),
|
||||
Lambda(NodeKey),
|
||||
ListComprehension(NodeKey),
|
||||
SetComprehension(NodeKey),
|
||||
|
||||
@@ -228,6 +228,7 @@ use self::symbol_state::{
|
||||
use crate::semantic_index::ast_ids::ScopedUseId;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
use crate::symbol::Boundness;
|
||||
use ruff_index::IndexVec;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -274,8 +275,12 @@ impl<'db> UseDefMap<'db> {
|
||||
self.bindings_iterator(&self.bindings_by_use[use_id])
|
||||
}
|
||||
|
||||
pub(crate) fn use_may_be_unbound(&self, use_id: ScopedUseId) -> bool {
|
||||
self.bindings_by_use[use_id].may_be_unbound()
|
||||
pub(crate) fn use_boundness(&self, use_id: ScopedUseId) -> Boundness {
|
||||
if self.bindings_by_use[use_id].may_be_unbound() {
|
||||
Boundness::PossiblyUnbound
|
||||
} else {
|
||||
Boundness::Bound
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn public_bindings(
|
||||
@@ -285,8 +290,12 @@ impl<'db> UseDefMap<'db> {
|
||||
self.bindings_iterator(self.public_symbols[symbol].bindings())
|
||||
}
|
||||
|
||||
pub(crate) fn public_may_be_unbound(&self, symbol: ScopedSymbolId) -> bool {
|
||||
self.public_symbols[symbol].may_be_unbound()
|
||||
pub(crate) fn public_boundness(&self, symbol: ScopedSymbolId) -> Boundness {
|
||||
if self.public_symbols[symbol].may_be_unbound() {
|
||||
Boundness::PossiblyUnbound
|
||||
} else {
|
||||
Boundness::Bound
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bindings_at_declaration(
|
||||
@@ -450,10 +459,6 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
}
|
||||
|
||||
impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
||||
let new_symbol = self.symbol_states.push(SymbolState::undefined());
|
||||
debug_assert_eq!(symbol, new_symbol);
|
||||
|
||||
@@ -6,9 +6,9 @@ use ruff_source_file::LineIndex;
|
||||
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{resolve_module, Module};
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::ast_ids::HasScopedExpressionId;
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::types::{binding_ty, global_symbol_ty, infer_scope_types, Type};
|
||||
use crate::types::{binding_ty, infer_scope_types, Type};
|
||||
use crate::Db;
|
||||
|
||||
pub struct SemanticModel<'db> {
|
||||
@@ -38,10 +38,6 @@ impl<'db> SemanticModel<'db> {
|
||||
pub fn resolve_module(&self, module_name: &ModuleName) -> Option<Module> {
|
||||
resolve_module(self.db, module_name)
|
||||
}
|
||||
|
||||
pub fn global_symbol_ty(&self, module: &Module, symbol_name: &str) -> Type<'db> {
|
||||
global_symbol_ty(self.db, module.file(), symbol_name)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasTy {
|
||||
@@ -58,7 +54,7 @@ impl HasTy for ast::ExpressionRef<'_> {
|
||||
let file_scope = index.expression_scope_id(*self);
|
||||
let scope = file_scope.to_scope_id(model.db, model.file);
|
||||
|
||||
let expression_id = self.scoped_ast_id(model.db, scope);
|
||||
let expression_id = self.scoped_expression_id(model.db, scope);
|
||||
infer_scope_types(model.db, scope).expression_ty(expression_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,7 +732,20 @@ mod tests {
|
||||
let system = TestSystem::default();
|
||||
assert!(matches!(
|
||||
VirtualEnvironment::new("/.venv", &system),
|
||||
Err(SitePackagesDiscoveryError::VenvDirIsNotADirectory(_))
|
||||
Err(SitePackagesDiscoveryError::VenvDirCanonicalizationError(..))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_venv_that_is_not_a_directory() {
|
||||
let system = TestSystem::default();
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_file("/.venv", "")
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
VirtualEnvironment::new("/.venv", &system),
|
||||
Err(SitePackagesDiscoveryError::VenvDirIsNotADirectory(..))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user