Compare commits
275 Commits
micha/fix-
...
zb/fuzz-ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48eb23b488 | ||
|
|
7f624cd0bb | ||
|
|
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 | ||
|
|
b19862c64a | ||
|
|
9a0dade925 | ||
|
|
ec6208e51b | ||
|
|
74cf66e4c2 | ||
|
|
1f19aca632 | ||
|
|
6f52d573ef | ||
|
|
c593ccb529 | ||
|
|
9f3a38d408 | ||
|
|
f8eb547fb4 | ||
|
|
b77de359bc | ||
|
|
41f74512df | ||
|
|
387dc664bd | ||
|
|
41c9bdbd37 | ||
|
|
222a646437 | ||
|
|
5b411fe606 | ||
|
|
47dd83e56f | ||
|
|
08e23d78aa | ||
|
|
5af0966057 | ||
|
|
faf9dfaa9d | ||
|
|
9d131c8c45 | ||
|
|
5a56886414 | ||
|
|
66c3aaa307 | ||
|
|
b6ffa51c16 | ||
|
|
35f007f17f | ||
|
|
3006d6da23 |
@@ -17,4 +17,7 @@ indent_size = 4
|
|||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.md]
|
[*.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
|
/scripts/fuzz-parser/ @AlexWaygood
|
||||||
|
|
||||||
# red-knot
|
# red-knot
|
||||||
/crates/red_knot* @carljm @MichaReiser @AlexWaygood
|
/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp
|
||||||
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood
|
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp
|
||||||
|
|||||||
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@@ -16,7 +16,7 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUSTUP_MAX_RETRIES: 10
|
RUSTUP_MAX_RETRIES: 10
|
||||||
PACKAGE_NAME: ruff
|
PACKAGE_NAME: ruff
|
||||||
PYTHON_VERSION: "3.11"
|
PYTHON_VERSION: "3.12"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
determine_changes:
|
determine_changes:
|
||||||
@@ -268,6 +268,7 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: "fuzz -> target"
|
workspaces: "fuzz -> target"
|
||||||
|
cache-all-crates: "true"
|
||||||
- name: "Install cargo-binstall"
|
- name: "Install cargo-binstall"
|
||||||
uses: cargo-bins/cargo-binstall@main
|
uses: cargo-bins/cargo-binstall@main
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/publish-playground.yml
vendored
2
.github/workflows/publish-playground.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
working-directory: playground
|
working-directory: playground
|
||||||
- name: "Deploy to Cloudflare Pages"
|
- name: "Deploy to Cloudflare Pages"
|
||||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||||
uses: cloudflare/wrangler-action@v3.9.0
|
uses: cloudflare/wrangler-action@v3.12.1
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
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.
|
# For PyPI's trusted publishing.
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
|
- name: "Install uv"
|
||||||
|
uses: astral-sh/setup-uv@v3
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: wheels-*
|
pattern: wheels-*
|
||||||
path: wheels
|
path: wheels
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Publish to PyPi
|
- name: Publish to PyPi
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
run: uv publish -v wheels/*
|
||||||
with:
|
|
||||||
skip-existing: true
|
|
||||||
packages-dir: wheels
|
|
||||||
verbose: true
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ exclude: |
|
|||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/abravalheri/validate-pyproject
|
- repo: https://github.com/abravalheri/validate-pyproject
|
||||||
rev: v0.21
|
rev: v0.23
|
||||||
hooks:
|
hooks:
|
||||||
- id: validate-pyproject
|
- id: validate-pyproject
|
||||||
|
|
||||||
@@ -51,11 +51,15 @@ repos:
|
|||||||
- id: blacken-docs
|
- id: blacken-docs
|
||||||
args: ["--pyi", "--line-length", "130"]
|
args: ["--pyi", "--line-length", "130"]
|
||||||
files: '^crates/.*/resources/mdtest/.*\.md'
|
files: '^crates/.*/resources/mdtest/.*\.md'
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
.*?invalid(_.+)*_syntax\.md
|
||||||
|
)$
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- black==24.10.0
|
- black==24.10.0
|
||||||
|
|
||||||
- repo: https://github.com/crate-ci/typos
|
- repo: https://github.com/crate-ci/typos
|
||||||
rev: v1.26.0
|
rev: v1.27.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: typos
|
- id: typos
|
||||||
|
|
||||||
@@ -69,7 +73,7 @@ repos:
|
|||||||
pass_filenames: false # This makes it a lot faster
|
pass_filenames: false # This makes it a lot faster
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.7.0
|
rev: v0.7.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|||||||
112
CHANGELOG.md
112
CHANGELOG.md
@@ -1,5 +1,117 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
## 0.7.1
|
||||||
|
|
||||||
### Preview features
|
### Preview features
|
||||||
|
|||||||
550
Cargo.lock
generated
550
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -66,6 +66,7 @@ criterion = { version = "0.5.1", default-features = false }
|
|||||||
crossbeam = { version = "0.8.4" }
|
crossbeam = { version = "0.8.4" }
|
||||||
dashmap = { version = "6.0.1" }
|
dashmap = { version = "6.0.1" }
|
||||||
dir-test = { version = "0.3.0" }
|
dir-test = { version = "0.3.0" }
|
||||||
|
dunce = { version = "1.0.5" }
|
||||||
drop_bomb = { version = "0.1.5" }
|
drop_bomb = { version = "0.1.5" }
|
||||||
env_logger = { version = "0.11.0" }
|
env_logger = { version = "0.11.0" }
|
||||||
etcetera = { version = "0.8.0" }
|
etcetera = { version = "0.8.0" }
|
||||||
@@ -81,6 +82,7 @@ hashbrown = { version = "0.15.0", default-features = false, features = [
|
|||||||
ignore = { version = "0.4.22" }
|
ignore = { version = "0.4.22" }
|
||||||
imara-diff = { version = "0.1.5" }
|
imara-diff = { version = "0.1.5" }
|
||||||
imperative = { version = "1.0.4" }
|
imperative = { version = "1.0.4" }
|
||||||
|
indexmap = { version = "2.6.0" }
|
||||||
indicatif = { version = "0.17.8" }
|
indicatif = { version = "0.17.8" }
|
||||||
indoc = { version = "2.0.4" }
|
indoc = { version = "2.0.4" }
|
||||||
insta = { version = "1.35.1" }
|
insta = { version = "1.35.1" }
|
||||||
@@ -101,7 +103,7 @@ matchit = { version = "0.8.1" }
|
|||||||
memchr = { version = "2.7.1" }
|
memchr = { version = "2.7.1" }
|
||||||
mimalloc = { version = "0.1.39" }
|
mimalloc = { version = "0.1.39" }
|
||||||
natord = { version = "1.0.9" }
|
natord = { version = "1.0.9" }
|
||||||
notify = { version = "6.1.1" }
|
notify = { version = "7.0.0" }
|
||||||
ordermap = { version = "0.5.0" }
|
ordermap = { version = "0.5.0" }
|
||||||
path-absolutize = { version = "3.1.1" }
|
path-absolutize = { version = "3.1.1" }
|
||||||
path-slash = { version = "0.2.1" }
|
path-slash = { version = "0.2.1" }
|
||||||
@@ -135,7 +137,7 @@ strum_macros = { version = "0.26.0" }
|
|||||||
syn = { version = "2.0.55" }
|
syn = { version = "2.0.55" }
|
||||||
tempfile = { version = "3.9.0" }
|
tempfile = { version = "3.9.0" }
|
||||||
test-case = { version = "3.3.1" }
|
test-case = { version = "3.3.1" }
|
||||||
thiserror = { version = "1.0.58" }
|
thiserror = { version = "2.0.0" }
|
||||||
tikv-jemallocator = { version = "0.6.0" }
|
tikv-jemallocator = { version = "0.6.0" }
|
||||||
toml = { version = "0.8.11" }
|
toml = { version = "0.8.11" }
|
||||||
tracing = { version = "0.1.40" }
|
tracing = { version = "0.1.40" }
|
||||||
@@ -188,8 +190,9 @@ missing_panics_doc = "allow"
|
|||||||
module_name_repetitions = "allow"
|
module_name_repetitions = "allow"
|
||||||
must_use_candidate = "allow"
|
must_use_candidate = "allow"
|
||||||
similar_names = "allow"
|
similar_names = "allow"
|
||||||
|
single_match_else = "allow"
|
||||||
too_many_lines = "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"
|
needless_raw_string_hashes = "allow"
|
||||||
# Disallowed restriction lints
|
# Disallowed restriction lints
|
||||||
print_stdout = "warn"
|
print_stdout = "warn"
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
|||||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||||
|
|
||||||
# For a specific version.
|
# For a specific version.
|
||||||
curl -LsSf https://astral.sh/ruff/0.7.1/install.sh | sh
|
curl -LsSf https://astral.sh/ruff/0.7.4/install.sh | sh
|
||||||
powershell -c "irm https://astral.sh/ruff/0.7.1/install.ps1 | iex"
|
powershell -c "irm https://astral.sh/ruff/0.7.4/install.ps1 | iex"
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
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
|
```yaml
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.7.1
|
rev: v0.7.4
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
@@ -417,6 +417,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
|||||||
- [Babel](https://github.com/python-babel/babel)
|
- [Babel](https://github.com/python-babel/babel)
|
||||||
- Benchling ([Refac](https://github.com/benchling/refac))
|
- Benchling ([Refac](https://github.com/benchling/refac))
|
||||||
- [Bokeh](https://github.com/bokeh/bokeh)
|
- [Bokeh](https://github.com/bokeh/bokeh)
|
||||||
|
- CrowdCent ([NumerBlox](https://github.com/crowdcent/numerblox)) <!-- typos: ignore -->
|
||||||
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
|
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
|
||||||
- CERN ([Indico](https://getindico.io/))
|
- CERN ([Indico](https://getindico.io/))
|
||||||
- [DVC](https://github.com/iterative/dvc)
|
- [DVC](https://github.com/iterative/dvc)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
[files]
|
[files]
|
||||||
# https://github.com/crate-ci/typos/issues/868
|
# 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]
|
[default.extend-words]
|
||||||
"arange" = "arange" # e.g. `numpy.arange`
|
"arange" = "arange" # e.g. `numpy.arange`
|
||||||
@@ -12,6 +17,7 @@ pn = "pn" # `import panel as pn` is a thing
|
|||||||
poit = "poit"
|
poit = "poit"
|
||||||
BA = "BA" # acronym for "Bad Allowed", used in testing.
|
BA = "BA" # acronym for "Bad Allowed", used in testing.
|
||||||
jod = "jod" # e.g., `jod-thread`
|
jod = "jod" # e.g., `jod-thread`
|
||||||
|
Numer = "Numer" # Library name 'NumerBlox' in "Who's Using Ruff?"
|
||||||
|
|
||||||
[default]
|
[default]
|
||||||
extend-ignore-re = [
|
extend-ignore-re = [
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ tracing-tree = { workspace = true }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
filetime = { workspace = true }
|
filetime = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
ruff_db = { workspace = true, features = ["testing"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ use anyhow::{anyhow, Context};
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use crossbeam::channel as crossbeam_channel;
|
use crossbeam::channel as crossbeam_channel;
|
||||||
use salsa::plumbing::ZalsaDatabase;
|
|
||||||
|
|
||||||
use red_knot_python_semantic::SitePackages;
|
use red_knot_python_semantic::SitePackages;
|
||||||
use red_knot_server::run_server;
|
use red_knot_server::run_server;
|
||||||
use red_knot_workspace::db::RootDatabase;
|
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::watch::WorkspaceWatcher;
|
||||||
use red_knot_workspace::workspace::settings::Configuration;
|
use red_knot_workspace::workspace::settings::Configuration;
|
||||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||||
|
use ruff_db::diagnostic::Diagnostic;
|
||||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||||
|
use salsa::plumbing::ZalsaDatabase;
|
||||||
use target_version::TargetVersion;
|
use target_version::TargetVersion;
|
||||||
|
|
||||||
use crate::logging::{setup_tracing, Verbosity};
|
use crate::logging::{setup_tracing, Verbosity};
|
||||||
@@ -183,10 +183,10 @@ fn run() -> anyhow::Result<ExitStatus> {
|
|||||||
|
|
||||||
let system = OsSystem::new(cwd.clone());
|
let system = OsSystem::new(cwd.clone());
|
||||||
let cli_configuration = args.to_configuration(&cwd);
|
let cli_configuration = args.to_configuration(&cwd);
|
||||||
let workspace_metadata = WorkspaceMetadata::from_path(
|
let workspace_metadata = WorkspaceMetadata::discover(
|
||||||
system.current_directory(),
|
system.current_directory(),
|
||||||
&system,
|
&system,
|
||||||
Some(cli_configuration.clone()),
|
Some(&cli_configuration),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// TODO: Use the `program_settings` to compute the key for the database's persistent
|
// 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();
|
let has_diagnostics = !result.is_empty();
|
||||||
if check_revision == revision {
|
if check_revision == revision {
|
||||||
|
#[allow(clippy::print_stdout)]
|
||||||
for diagnostic in result {
|
for diagnostic in result {
|
||||||
tracing::error!("{}", diagnostic);
|
println!("{}", diagnostic.display(db));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
@@ -378,7 +379,10 @@ impl MainLoopCancellationToken {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum MainLoopMessage {
|
enum MainLoopMessage {
|
||||||
CheckWorkspace,
|
CheckWorkspace,
|
||||||
CheckCompleted { result: Vec<String>, revision: u64 },
|
CheckCompleted {
|
||||||
|
result: Vec<Box<dyn Diagnostic>>,
|
||||||
|
revision: u64,
|
||||||
|
},
|
||||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||||
pub enum TargetVersion {
|
pub enum TargetVersion {
|
||||||
Py37,
|
Py37,
|
||||||
#[default]
|
|
||||||
Py38,
|
Py38,
|
||||||
|
#[default]
|
||||||
Py39,
|
Py39,
|
||||||
Py310,
|
Py310,
|
||||||
Py311,
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::time::Duration;
|
|||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
|
|
||||||
use red_knot_python_semantic::{resolve_module, ModuleName, Program, PythonVersion, SitePackages};
|
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;
|
||||||
use red_knot_workspace::watch::{directory_watcher, WorkspaceWatcher};
|
use red_knot_workspace::watch::{directory_watcher, WorkspaceWatcher};
|
||||||
use red_knot_workspace::workspace::settings::{Configuration, SearchPathConfiguration};
|
use red_knot_workspace::workspace::settings::{Configuration, SearchPathConfiguration};
|
||||||
@@ -14,6 +14,7 @@ use red_knot_workspace::workspace::WorkspaceMetadata;
|
|||||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||||
use ruff_db::source::source_text;
|
use ruff_db::source::source_text;
|
||||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||||
|
use ruff_db::testing::setup_logging;
|
||||||
use ruff_db::Upcast;
|
use ruff_db::Upcast;
|
||||||
|
|
||||||
struct TestCase {
|
struct TestCase {
|
||||||
@@ -69,7 +70,6 @@ impl TestCase {
|
|||||||
Some(all_events)
|
Some(all_events)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn take_watch_changes(&self) -> Vec<watch::ChangeEvent> {
|
fn take_watch_changes(&self) -> Vec<watch::ChangeEvent> {
|
||||||
self.try_take_watch_changes(Duration::from_secs(10))
|
self.try_take_watch_changes(Duration::from_secs(10))
|
||||||
.expect("Expected watch changes but observed none")
|
.expect("Expected watch changes but observed none")
|
||||||
@@ -110,8 +110,8 @@ impl TestCase {
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let program = Program::get(self.db());
|
let program = Program::get(self.db());
|
||||||
|
|
||||||
self.configuration.search_paths = configuration.clone();
|
let new_settings = configuration.to_settings(self.db.workspace().root(&self.db));
|
||||||
let new_settings = configuration.into_settings(self.db.workspace().root(&self.db));
|
self.configuration.search_paths = configuration;
|
||||||
|
|
||||||
program.update_search_paths(&mut self.db, &new_settings)?;
|
program.update_search_paths(&mut self.db, &new_settings)?;
|
||||||
|
|
||||||
@@ -204,7 +204,9 @@ where
|
|||||||
.as_utf8_path()
|
.as_utf8_path()
|
||||||
.canonicalize_utf8()
|
.canonicalize_utf8()
|
||||||
.with_context(|| "Failed to canonicalize root path.")?,
|
.with_context(|| "Failed to canonicalize root path.")?,
|
||||||
);
|
)
|
||||||
|
.simplified()
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
let workspace_path = root_path.join("workspace");
|
let workspace_path = root_path.join("workspace");
|
||||||
|
|
||||||
@@ -241,8 +243,7 @@ where
|
|||||||
search_paths,
|
search_paths,
|
||||||
};
|
};
|
||||||
|
|
||||||
let workspace =
|
let workspace = WorkspaceMetadata::discover(&workspace_path, &system, Some(&configuration))?;
|
||||||
WorkspaceMetadata::from_path(&workspace_path, &system, Some(configuration.clone()))?;
|
|
||||||
|
|
||||||
let db = RootDatabase::new(workspace, system)?;
|
let db = RootDatabase::new(workspace, system)?;
|
||||||
|
|
||||||
@@ -1311,3 +1312,138 @@ mod unix {
|
|||||||
Ok(())
|
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_db = { workspace = true }
|
||||||
ruff_index = { workspace = true }
|
ruff_index = { workspace = true }
|
||||||
ruff_python_ast = { workspace = true }
|
ruff_python_ast = { workspace = true }
|
||||||
|
ruff_python_parser = { workspace = true }
|
||||||
ruff_python_stdlib = { workspace = true }
|
ruff_python_stdlib = { workspace = true }
|
||||||
ruff_source_file = { workspace = true }
|
ruff_source_file = { workspace = true }
|
||||||
ruff_text_size = { workspace = true }
|
ruff_text_size = { workspace = true }
|
||||||
@@ -24,13 +25,15 @@ bitflags = { workspace = true }
|
|||||||
camino = { workspace = true }
|
camino = { workspace = true }
|
||||||
compact_str = { workspace = true }
|
compact_str = { workspace = true }
|
||||||
countme = { workspace = true }
|
countme = { workspace = true }
|
||||||
itertools = { workspace = true}
|
indexmap = { workspace = true }
|
||||||
|
itertools = { workspace = true }
|
||||||
ordermap = { workspace = true }
|
ordermap = { workspace = true }
|
||||||
salsa = { workspace = true }
|
salsa = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
hashbrown = { workspace = true }
|
hashbrown = { workspace = true }
|
||||||
|
serde = { workspace = true, optional = true }
|
||||||
smallvec = { workspace = true }
|
smallvec = { workspace = true }
|
||||||
static_assertions = { workspace = true }
|
static_assertions = { workspace = true }
|
||||||
test-case = { workspace = true }
|
test-case = { workspace = true }
|
||||||
@@ -43,10 +46,9 @@ red_knot_test = { workspace = true }
|
|||||||
red_knot_vendored = { workspace = true }
|
red_knot_vendored = { workspace = true }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
dir-test = {workspace = true}
|
dir-test = { workspace = true }
|
||||||
insta = { workspace = true }
|
insta = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
wrap = 100
|
||||||
@@ -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
|
||||||
|
|
||||||
|
return (*args, 1)
|
||||||
|
|
||||||
|
# TODO should be tuple[Literal[True], Literal["a"], int]
|
||||||
|
reveal_type(append_int(True, "a")) # revealed: @Todo
|
||||||
|
```
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
|
||||||
|
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
|
||||||
@@ -23,12 +23,116 @@ x: int
|
|||||||
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||||
```
|
```
|
||||||
|
|
||||||
## PEP-604 annotations not yet supported
|
## 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
|
||||||
|
reveal_type(f) # revealed: @Todo
|
||||||
|
reveal_type(g) # revealed: @Todo
|
||||||
|
|
||||||
|
# TODO: support more kinds of type expressions in annotations
|
||||||
|
reveal_type(h) # revealed: @Todo
|
||||||
|
|
||||||
|
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||||
|
reveal_type(j) # revealed: tuple[str | int]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Incorrect tuple assignments are complained about
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def f() -> str | None:
|
# 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
|
||||||
|
def foo() -> str | int | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# TODO: should be `str | None` (but Todo is better than `Unknown`)
|
reveal_type(foo()) # revealed: str | int | None
|
||||||
reveal_type(f()) # revealed: @Todo
|
|
||||||
|
def bar() -> str | str | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
reveal_type(bar()) # revealed: str | None
|
||||||
|
|
||||||
|
def baz() -> str | str:
|
||||||
|
return "Hello, world!"
|
||||||
|
|
||||||
|
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"
|
x = foo # error: [unresolved-reference] "Name `foo` used when not defined"
|
||||||
foo = 1
|
foo = 1
|
||||||
|
|
||||||
# error: [unresolved-reference]
|
# No error `unresolved-reference` diagnostic is reported for `x`. This is
|
||||||
# revealed: Unbound
|
# 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)
|
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
|
## Unbound class variable
|
||||||
|
|
||||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
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:
|
if flag:
|
||||||
x = 2
|
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.x) # revealed: Literal[2]
|
||||||
reveal_type(C.y) # revealed: Literal[1]
|
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()
|
flag = bool_instance()
|
||||||
|
|
||||||
if flag:
|
if flag:
|
||||||
class C:
|
class C1:
|
||||||
x = 1
|
x = 1
|
||||||
|
|
||||||
else:
|
else:
|
||||||
class C:
|
class C1:
|
||||||
x = 2
|
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
|
## Operations
|
||||||
|
|
||||||
We support inference for all Python's binary operators:
|
We support inference for all Python's binary operators: `+`, `-`, `*`, `@`, `/`, `//`, `%`, `**`,
|
||||||
`+`, `-`, `*`, `@`, `/`, `//`, `%`, `**`, `<<`, `>>`, `&`, `^`, and `|`.
|
`<<`, `>>`, `&`, `^`, and `|`.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class A:
|
class A:
|
||||||
@@ -152,9 +152,8 @@ reveal_type(B() - A()) # revealed: int
|
|||||||
|
|
||||||
## Non-reflected precedence in general
|
## Non-reflected precedence in general
|
||||||
|
|
||||||
In general, if the left-hand side defines `__add__` and the right-hand side
|
In general, if the left-hand side defines `__add__` and the right-hand side defines `__radd__` and
|
||||||
defines `__radd__` and the right-hand side is not a subtype of the left-hand
|
the right-hand side is not a subtype of the left-hand side, `lhs.__add__` will take precedence:
|
||||||
side, `lhs.__add__` will take precedence:
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class A:
|
class A:
|
||||||
@@ -181,9 +180,8 @@ reveal_type(C() + C()) # revealed: int
|
|||||||
|
|
||||||
## Reflected precedence for subtypes (in some cases)
|
## Reflected precedence for subtypes (in some cases)
|
||||||
|
|
||||||
If the right-hand operand is a subtype of the left-hand operand and has a
|
If the right-hand operand is a subtype of the left-hand operand and has a different implementation
|
||||||
different implementation of the reflected method, the reflected method on the
|
of the reflected method, the reflected method on the right-hand operand takes precedence.
|
||||||
right-hand operand takes precedence.
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class A:
|
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__`
|
# N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__`
|
||||||
class C(B): ...
|
class C(B): ...
|
||||||
|
|
||||||
# TODO: we currently only understand direct subclasses as subtypes of the superclass.
|
reveal_type(A() + C()) # revealed: MyString
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reflected precedence 2
|
## Reflected precedence 2
|
||||||
|
|
||||||
If the right-hand operand is a subtype of the left-hand operand, but does not
|
If the right-hand operand is a subtype of the left-hand operand, but does not override the reflected
|
||||||
override the reflected method, the left-hand operand's non-reflected method
|
method, the left-hand operand's non-reflected method still takes precedence:
|
||||||
still takes precedence:
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class A:
|
class A:
|
||||||
@@ -232,17 +225,15 @@ reveal_type(A() + B()) # revealed: str
|
|||||||
|
|
||||||
## Only reflected supported
|
## Only reflected supported
|
||||||
|
|
||||||
For example, at runtime, `(1).__add__(1.2)` is `NotImplemented`, but
|
For example, at runtime, `(1).__add__(1.2)` is `NotImplemented`, but `(1.2).__radd__(1) == 2.2`,
|
||||||
`(1.2).__radd__(1) == 2.2`, meaning that `1 + 1.2` succeeds at runtime
|
meaning that `1 + 1.2` succeeds at runtime (producing `2.2`). The runtime tries the second one only
|
||||||
(producing `2.2`). The runtime tries the second one only if the first one
|
if the first one returns `NotImplemented` to signal failure.
|
||||||
returns `NotImplemented` to signal failure.
|
|
||||||
|
|
||||||
Typeshed and other stubs annotate dunder-method calls that would return
|
Typeshed and other stubs annotate dunder-method calls that would return `NotImplemented` as being
|
||||||
`NotImplemented` as being "illegal" calls. `int.__add__` is annotated as only
|
"illegal" calls. `int.__add__` is annotated as only "accepting" `int`s, even though it
|
||||||
"accepting" `int`s, even though it strictly-speaking "accepts" any other object
|
strictly-speaking "accepts" any other object without raising an exception -- it will simply return
|
||||||
without raising an exception -- it will simply return `NotImplemented`,
|
`NotImplemented`, allowing the runtime to try the `__radd__` method of the right-hand operand as
|
||||||
allowing the runtime to try the `__radd__` method of the right-hand operand
|
well.
|
||||||
as well.
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class A:
|
class A:
|
||||||
@@ -308,8 +299,8 @@ reveal_type(y + 4.12) # revealed: int
|
|||||||
|
|
||||||
## With literal types
|
## With literal types
|
||||||
|
|
||||||
When we have a literal type for one operand, we're able to fall back to the
|
When we have a literal type for one operand, we're able to fall back to the instance handling for
|
||||||
instance handling for its instance super-type.
|
its instance super-type.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class A:
|
class A:
|
||||||
@@ -348,15 +339,13 @@ reveal_type(literal_string_instance + A()) # revealed: @Todo
|
|||||||
|
|
||||||
## Operations involving instances of classes inheriting from `Any`
|
## Operations involving instances of classes inheriting from `Any`
|
||||||
|
|
||||||
`Any` and `Unknown` represent a set of possible runtime objects, wherein the
|
`Any` and `Unknown` represent a set of possible runtime objects, wherein the bounds of the set are
|
||||||
bounds of the set are unknown. Whether the left-hand operand's dunder or the
|
unknown. Whether the left-hand operand's dunder or the right-hand operand's reflected dunder depends
|
||||||
right-hand operand's reflected dunder depends on whether the right-hand operand
|
on whether the right-hand operand is an instance of a class that is a subclass of the left-hand
|
||||||
is an instance of a class that is a subclass of the left-hand operand's class
|
operand's class and overrides the reflected dunder. In the following example, because of the
|
||||||
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
|
||||||
unknowable nature of `Any`/`Unknown`, we must consider both possibilities:
|
resolve to an unknown third class that inherits from `X` and overrides `__radd__`; but it also might
|
||||||
`Any`/`Unknown` might resolve to an unknown third class that inherits from `X`
|
not. Thus, the correct answer here for the `reveal_type` is `int | Unknown`.
|
||||||
and overrides `__radd__`; but it also might not. Thus, the correct answer here
|
|
||||||
for the `reveal_type` is `int | Unknown`.
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from does_not_exist import Foo # error: [unresolved-import]
|
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
|
### 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
|
For the specific case where the left-hand operand is the exact same type as the right-hand operand,
|
||||||
right-hand operand, the reflected dunder of the right-hand operand is not
|
the reflected dunder of the right-hand operand is not tried; the runtime short-circuits after trying
|
||||||
tried; the runtime short-circuits after trying the unreflected dunder of the
|
the unreflected dunder of the left-hand operand. For context, see
|
||||||
left-hand operand. For context, see
|
|
||||||
[this mailing list discussion](https://mail.python.org/archives/list/python-dev@python.org/thread/7NZUCODEAPQFMRFXYRMGJXDSIS3WJYIV/).
|
[this mailing list discussion](https://mail.python.org/archives/list/python-dev@python.org/thread/7NZUCODEAPQFMRFXYRMGJXDSIS3WJYIV/).
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Short-Circuit Evaluation
|
||||||
|
|
||||||
|
## Not all boolean expressions must be evaluated
|
||||||
|
|
||||||
|
In `or` expressions, if the left-hand side is truthy, the right-hand side is not evaluated.
|
||||||
|
Similarly, in `and` expressions, if the left-hand side is falsy, the right-hand side is not
|
||||||
|
evaluated.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if bool_instance() or (x := 1):
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
|
||||||
|
if bool_instance() and (x := 1):
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## First expression is always evaluated
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if (x := 1) or bool_instance():
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
|
||||||
|
if (x := 1) and bool_instance():
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Statically known truthiness
|
||||||
|
|
||||||
|
```py
|
||||||
|
if True or (x := 1):
|
||||||
|
# TODO: infer that the second arm is never executed, and raise `unresolved-reference`.
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
|
||||||
|
if True and (x := 1):
|
||||||
|
# TODO: infer that the second arm is always executed, do not raise a diagnostic
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Later expressions can always use variables from earlier expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
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: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nested expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if bool_instance() or ((x := 1) and bool_instance()):
|
||||||
|
# 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: Literal[1]
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
reveal_type(z) # revealed: Literal[1]
|
||||||
|
```
|
||||||
@@ -18,3 +18,58 @@ class Unit: ...
|
|||||||
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
|
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
|
||||||
reveal_type(b) # revealed: Unknown
|
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
|
||||||
|
```
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ async def get_int_async() -> int:
|
|||||||
reveal_type(get_int_async()) # revealed: @Todo
|
reveal_type(get_int_async()) # revealed: @Todo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Generic
|
||||||
|
|
||||||
|
```py
|
||||||
|
def get_int[T]() -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
reveal_type(get_int()) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
## Decorated
|
## Decorated
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -44,3 +53,16 @@ reveal_type(bar()) # revealed: @Todo
|
|||||||
nonsense = 123
|
nonsense = 123
|
||||||
x = nonsense() # error: "Object of type `Literal[123]` is not callable"
|
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
|
# Comparison: Byte literals
|
||||||
|
|
||||||
These tests assert that we infer precise `Literal` types for comparisons between objects
|
These tests assert that we infer precise `Literal` types for comparisons between objects inferred as
|
||||||
inferred as having `Literal` bytes types:
|
having `Literal` bytes types:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
reveal_type(b"abc" == b"abc") # revealed: Literal[True]
|
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
|
||||||
|
```
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# 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__`.
|
||||||
|
|
||||||
|
For references, see:
|
||||||
|
|
||||||
|
- <https://docs.python.org/3/reference/expressions.html#membership-test-details>
|
||||||
|
- <https://docs.python.org/3/reference/datamodel.html#object.__contains__>
|
||||||
|
- <https://snarky.ca/unravelling-membership-testing/>
|
||||||
|
|
||||||
|
## Implements `__contains__`
|
||||||
|
|
||||||
|
Classes can support membership tests by implementing the `__contains__` method:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def __contains__(self, item: str) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
reveal_type("hello" in A()) # revealed: bool
|
||||||
|
reveal_type("hello" not in A()) # revealed: bool
|
||||||
|
# TODO: should emit diagnostic, need to check arg type, will fail
|
||||||
|
reveal_type(42 in A()) # revealed: bool
|
||||||
|
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:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class StringIterator:
|
||||||
|
def __next__(self) -> str:
|
||||||
|
return "foo"
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __iter__(self) -> StringIterator:
|
||||||
|
return StringIterator()
|
||||||
|
|
||||||
|
reveal_type("hello" in A()) # revealed: bool
|
||||||
|
reveal_type("hello" not in A()) # revealed: bool
|
||||||
|
reveal_type(42 in A()) # revealed: bool
|
||||||
|
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`).
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def __getitem__(self, key: int) -> str:
|
||||||
|
return "foo"
|
||||||
|
|
||||||
|
reveal_type("hello" in A()) # revealed: bool
|
||||||
|
reveal_type("hello" not in A()) # revealed: bool
|
||||||
|
reveal_type(42 in A()) # revealed: bool
|
||||||
|
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:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def __contains__(self, item: str) -> str:
|
||||||
|
return "foo"
|
||||||
|
|
||||||
|
reveal_type("hello" in A()) # revealed: bool
|
||||||
|
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.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
class AlwaysTrue:
|
||||||
|
def __contains__(self, item: int) -> Literal[1]:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
class AlwaysFalse:
|
||||||
|
def __contains__(self, item: int) -> Literal[""]:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
reveal_type(42 in AlwaysTrue()) # revealed: Literal[True]
|
||||||
|
reveal_type(42 not in AlwaysTrue()) # revealed: Literal[False]
|
||||||
|
|
||||||
|
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__`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class CheckContains: ...
|
||||||
|
class CheckIter: ...
|
||||||
|
class CheckGetItem: ...
|
||||||
|
|
||||||
|
class CheckIterIterator:
|
||||||
|
def __next__(self) -> CheckIter:
|
||||||
|
return CheckIter()
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __contains__(self, item: CheckContains) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __iter__(self) -> CheckIterIterator:
|
||||||
|
return CheckIterIterator()
|
||||||
|
|
||||||
|
def __getitem__(self, key: int) -> CheckGetItem:
|
||||||
|
return CheckGetItem()
|
||||||
|
|
||||||
|
reveal_type(CheckContains() in A()) # revealed: bool
|
||||||
|
|
||||||
|
# TODO: should emit diagnostic, need to check arg type,
|
||||||
|
# should not fall back to __iter__ or __getitem__
|
||||||
|
reveal_type(CheckIter() in A()) # revealed: bool
|
||||||
|
reveal_type(CheckGetItem() in A()) # revealed: bool
|
||||||
|
|
||||||
|
class B:
|
||||||
|
def __iter__(self) -> CheckIterIterator:
|
||||||
|
return CheckIterIterator()
|
||||||
|
|
||||||
|
def __getitem__(self, key: int) -> CheckGetItem:
|
||||||
|
return CheckGetItem()
|
||||||
|
|
||||||
|
reveal_type(CheckIter() in B()) # revealed: bool
|
||||||
|
# Always use `__iter__`, regardless of iterated type; there's no NotImplemented
|
||||||
|
# in this case, so there's no fallback to `__getitem__`
|
||||||
|
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.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def __getitem__(self, key: str) -> str:
|
||||||
|
return "foo"
|
||||||
|
|
||||||
|
# TODO should emit a diagnostic
|
||||||
|
reveal_type(42 in A()) # revealed: bool
|
||||||
|
reveal_type("hello" in A()) # revealed: bool
|
||||||
|
```
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
# Comparison: Rich Comparison
|
||||||
|
|
||||||
|
Rich comparison operations (`==`, `!=`, `<`, `<=`, `>`, `>=`) in Python are implemented through
|
||||||
|
double-underscore methods that allow customization of comparison behavior.
|
||||||
|
|
||||||
|
For references, see:
|
||||||
|
|
||||||
|
- <https://docs.python.org/3/reference/datamodel.html#object.__lt__>
|
||||||
|
- <https://snarky.ca/unravelling-rich-comparison-operators/>
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __eq__(self, other: A) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def __ne__(self, other: A) -> float:
|
||||||
|
return 42.0
|
||||||
|
|
||||||
|
def __lt__(self, other: A) -> str:
|
||||||
|
return "42"
|
||||||
|
|
||||||
|
def __le__(self, other: A) -> bytes:
|
||||||
|
return b"42"
|
||||||
|
|
||||||
|
def __gt__(self, other: A) -> list:
|
||||||
|
return [42]
|
||||||
|
|
||||||
|
def __ge__(self, other: A) -> set:
|
||||||
|
return {42}
|
||||||
|
|
||||||
|
reveal_type(A() == A()) # revealed: int
|
||||||
|
reveal_type(A() != A()) # revealed: float
|
||||||
|
reveal_type(A() < A()) # revealed: str
|
||||||
|
reveal_type(A() <= A()) # revealed: bytes
|
||||||
|
reveal_type(A() > A()) # revealed: list
|
||||||
|
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:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __eq__(self, other: B) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def __ne__(self, other: B) -> float:
|
||||||
|
return 42.0
|
||||||
|
|
||||||
|
def __lt__(self, other: B) -> str:
|
||||||
|
return "42"
|
||||||
|
|
||||||
|
def __le__(self, other: B) -> bytes:
|
||||||
|
return b"42"
|
||||||
|
|
||||||
|
def __gt__(self, other: B) -> list:
|
||||||
|
return [42]
|
||||||
|
|
||||||
|
def __ge__(self, other: B) -> set:
|
||||||
|
return {42}
|
||||||
|
|
||||||
|
class B: ...
|
||||||
|
|
||||||
|
reveal_type(A() == B()) # revealed: int
|
||||||
|
reveal_type(A() != B()) # revealed: float
|
||||||
|
reveal_type(A() < B()) # revealed: str
|
||||||
|
reveal_type(A() <= B()) # revealed: bytes
|
||||||
|
reveal_type(A() > B()) # revealed: list
|
||||||
|
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.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __eq__(self, other: B) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def __ne__(self, other: B) -> float:
|
||||||
|
return 42.0
|
||||||
|
|
||||||
|
def __lt__(self, other: B) -> str:
|
||||||
|
return "42"
|
||||||
|
|
||||||
|
def __le__(self, other: B) -> bytes:
|
||||||
|
return b"42"
|
||||||
|
|
||||||
|
def __gt__(self, other: B) -> list:
|
||||||
|
return [42]
|
||||||
|
|
||||||
|
def __ge__(self, other: B) -> set:
|
||||||
|
return {42}
|
||||||
|
|
||||||
|
class B:
|
||||||
|
# To override builtins.object.__eq__ and builtins.object.__ne__
|
||||||
|
# TODO these should emit an invalid override diagnostic
|
||||||
|
def __eq__(self, other: str) -> B:
|
||||||
|
return B()
|
||||||
|
|
||||||
|
def __ne__(self, other: str) -> B:
|
||||||
|
return B()
|
||||||
|
|
||||||
|
# TODO: should be `int` and `float`.
|
||||||
|
# Need to check arg type and fall back to `rhs.__eq__` and `rhs.__ne__`.
|
||||||
|
#
|
||||||
|
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
|
||||||
|
# this can only happen with an invalid override of these methods,
|
||||||
|
# but we still support it.
|
||||||
|
reveal_type(B() == A()) # revealed: B
|
||||||
|
reveal_type(B() != A()) # revealed: B
|
||||||
|
|
||||||
|
reveal_type(B() < A()) # revealed: list
|
||||||
|
reveal_type(B() <= A()) # revealed: set
|
||||||
|
|
||||||
|
reveal_type(B() > A()) # revealed: str
|
||||||
|
reveal_type(B() >= A()) # revealed: bytes
|
||||||
|
|
||||||
|
class C:
|
||||||
|
def __gt__(self, other: C) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def __ge__(self, other: C) -> float:
|
||||||
|
return 42.0
|
||||||
|
|
||||||
|
reveal_type(C() < C()) # revealed: int
|
||||||
|
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`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __eq__(self, other: A) -> A:
|
||||||
|
return A()
|
||||||
|
|
||||||
|
def __ne__(self, other: A) -> A:
|
||||||
|
return A()
|
||||||
|
|
||||||
|
def __lt__(self, other: A) -> A:
|
||||||
|
return A()
|
||||||
|
|
||||||
|
def __le__(self, other: A) -> A:
|
||||||
|
return A()
|
||||||
|
|
||||||
|
def __gt__(self, other: A) -> A:
|
||||||
|
return A()
|
||||||
|
|
||||||
|
def __ge__(self, other: A) -> A:
|
||||||
|
return A()
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
def __eq__(self, other: A) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def __ne__(self, other: A) -> float:
|
||||||
|
return 42.0
|
||||||
|
|
||||||
|
def __lt__(self, other: A) -> str:
|
||||||
|
return "42"
|
||||||
|
|
||||||
|
def __le__(self, other: A) -> bytes:
|
||||||
|
return b"42"
|
||||||
|
|
||||||
|
def __gt__(self, other: A) -> list:
|
||||||
|
return [42]
|
||||||
|
|
||||||
|
def __ge__(self, other: A) -> set:
|
||||||
|
return {42}
|
||||||
|
|
||||||
|
reveal_type(A() == B()) # revealed: int
|
||||||
|
reveal_type(A() != B()) # revealed: float
|
||||||
|
|
||||||
|
reveal_type(A() < B()) # revealed: list
|
||||||
|
reveal_type(A() <= B()) # revealed: set
|
||||||
|
|
||||||
|
reveal_type(A() > B()) # revealed: str
|
||||||
|
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.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __lt__(self, other: A) -> A:
|
||||||
|
return A()
|
||||||
|
|
||||||
|
def __gt__(self, other: A) -> A:
|
||||||
|
return A()
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
def __lt__(self, other: int) -> B:
|
||||||
|
return B()
|
||||||
|
|
||||||
|
def __gt__(self, other: int) -> B:
|
||||||
|
return B()
|
||||||
|
|
||||||
|
# TODO: should be `A`, need to check argument type and fall back to LHS method
|
||||||
|
reveal_type(A() < B()) # revealed: B
|
||||||
|
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`.
|
||||||
|
|
||||||
|
(This test is referenced from `mdtest/binary/instances.md`)
|
||||||
|
|
||||||
|
```py
|
||||||
|
from does_not_exist import Foo # error: [unresolved-import]
|
||||||
|
|
||||||
|
reveal_type(Foo) # revealed: Unknown
|
||||||
|
|
||||||
|
class X:
|
||||||
|
def __lt__(self, other: object) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
class Y(Foo): ...
|
||||||
|
|
||||||
|
# TODO: Should be `int | Unknown`; see above discussion.
|
||||||
|
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.
|
||||||
|
|
||||||
|
Please refer to the [docs](https://docs.python.org/3/reference/datamodel.html#object.__eq__)
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
# TODO both these overrides should emit invalid-override diagnostic
|
||||||
|
def __eq__(self, other: int) -> A:
|
||||||
|
return A()
|
||||||
|
|
||||||
|
def __ne__(self, other: int) -> A:
|
||||||
|
return A()
|
||||||
|
|
||||||
|
# TODO: it should be `bool`, need to check arg type and fall back to `is` and `is not`
|
||||||
|
reveal_type(A() == A()) # revealed: A
|
||||||
|
reveal_type(A() != A()) # revealed: A
|
||||||
|
```
|
||||||
|
|
||||||
|
## Object Comparisons with Typeshed
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
reveal_type(A() == object()) # revealed: bool
|
||||||
|
reveal_type(A() != object()) # revealed: bool
|
||||||
|
reveal_type(object() == A()) # revealed: bool
|
||||||
|
reveal_type(object() != A()) # revealed: bool
|
||||||
|
|
||||||
|
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `object`"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(A() < object())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Numbers Comparison with typeshed
|
||||||
|
|
||||||
|
```py
|
||||||
|
reveal_type(1 == 1.0) # revealed: bool
|
||||||
|
reveal_type(1 != 1.0) # revealed: bool
|
||||||
|
reveal_type(1 < 1.0) # revealed: bool
|
||||||
|
reveal_type(1 <= 1.0) # revealed: bool
|
||||||
|
reveal_type(1 > 1.0) # revealed: bool
|
||||||
|
reveal_type(1 >= 1.0) # revealed: bool
|
||||||
|
|
||||||
|
reveal_type(1 == 2j) # revealed: bool
|
||||||
|
reveal_type(1 != 2j) # revealed: bool
|
||||||
|
|
||||||
|
# TODO: should be Unknown and emit diagnostic,
|
||||||
|
# need to check arg type and should be failed
|
||||||
|
reveal_type(1 < 2j) # revealed: bool
|
||||||
|
reveal_type(1 <= 2j) # revealed: bool
|
||||||
|
reveal_type(1 > 2j) # revealed: bool
|
||||||
|
reveal_type(1 >= 2j) # revealed: bool
|
||||||
|
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def int_instance() -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
x = bool_instance()
|
||||||
|
y = int_instance()
|
||||||
|
|
||||||
|
reveal_type(x < y) # revealed: bool
|
||||||
|
reveal_type(y < x) # revealed: bool
|
||||||
|
reveal_type(4.2 < x) # revealed: bool
|
||||||
|
reveal_type(x < 4.2) # revealed: bool
|
||||||
|
```
|
||||||
@@ -12,16 +12,18 @@ reveal_type(1 is 1) # revealed: bool
|
|||||||
reveal_type(1 is not 1) # revealed: bool
|
reveal_type(1 is not 1) # revealed: bool
|
||||||
reveal_type(1 is 2) # revealed: Literal[False]
|
reveal_type(1 is 2) # revealed: Literal[False]
|
||||||
reveal_type(1 is not 7) # revealed: Literal[True]
|
reveal_type(1 is not 7) # revealed: Literal[True]
|
||||||
reveal_type(1 <= "" and 0 < 1) # revealed: @Todo | Literal[True]
|
# TODO: should be Unknown, and emit diagnostic, once we check call argument types
|
||||||
|
reveal_type(1 <= "" and 0 < 1) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
## Integer instance
|
## Integer instance
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: implement lookup of `__eq__` on typeshed `int` stub.
|
# TODO: implement lookup of `__eq__` on typeshed `int` stub.
|
||||||
def int_instance() -> int: ...
|
def int_instance() -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
reveal_type(1 == int_instance()) # revealed: @Todo
|
reveal_type(1 == int_instance()) # revealed: bool
|
||||||
reveal_type(9 < int_instance()) # revealed: bool
|
reveal_type(9 < int_instance()) # revealed: bool
|
||||||
reveal_type(int_instance() < int_instance()) # revealed: bool
|
reveal_type(int_instance() < int_instance()) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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()`
|
- `a = A() < B() < C()`
|
||||||
|
|
||||||
1. `A() < B() and B() < C()` - split in N comparison
|
1. `A() < B() and B() < C()` - split in N comparison
|
||||||
1. `A()` and `B()` - evaluate outcome types
|
1. `A()` and `B()` - evaluate outcome types
|
||||||
1. `bool` and `bool` - evaluate truthiness
|
1. `bool` and `bool` - evaluate truthiness
|
||||||
1. `A | B` - union of "first true" types
|
1. `A | B` - union of "first true" types
|
||||||
|
|
||||||
- `b = 0 < 1 < A() < 3`
|
- `b = 0 < 1 < A() < 3`
|
||||||
|
|
||||||
@@ -59,51 +59,51 @@ reveal_type(c >= d) # revealed: Literal[True]
|
|||||||
|
|
||||||
```py
|
```py
|
||||||
def bool_instance() -> bool: ...
|
def bool_instance() -> bool: ...
|
||||||
def int_instance() -> int: ...
|
def int_instance() -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
a = (bool_instance(),)
|
a = (bool_instance(),)
|
||||||
b = (int_instance(),)
|
b = (int_instance(),)
|
||||||
|
|
||||||
# TODO: All @Todo should be `bool`
|
reveal_type(a == a) # revealed: bool
|
||||||
reveal_type(a == a) # revealed: @Todo
|
reveal_type(a != a) # revealed: bool
|
||||||
reveal_type(a != a) # revealed: @Todo
|
reveal_type(a < a) # revealed: bool
|
||||||
reveal_type(a < a) # revealed: @Todo
|
reveal_type(a <= a) # revealed: bool
|
||||||
reveal_type(a <= a) # revealed: @Todo
|
reveal_type(a > a) # revealed: bool
|
||||||
reveal_type(a > a) # revealed: @Todo
|
reveal_type(a >= a) # revealed: bool
|
||||||
reveal_type(a >= a) # revealed: @Todo
|
|
||||||
|
|
||||||
reveal_type(a == b) # revealed: @Todo
|
reveal_type(a == b) # revealed: bool
|
||||||
reveal_type(a != b) # revealed: @Todo
|
reveal_type(a != b) # revealed: bool
|
||||||
reveal_type(a < b) # revealed: @Todo
|
reveal_type(a < b) # revealed: bool
|
||||||
reveal_type(a <= b) # revealed: @Todo
|
reveal_type(a <= b) # revealed: bool
|
||||||
reveal_type(a > b) # revealed: @Todo
|
reveal_type(a > b) # revealed: bool
|
||||||
reveal_type(a >= b) # revealed: @Todo
|
reveal_type(a >= b) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Comparison Unsupported
|
#### Comparison Unsupported
|
||||||
|
|
||||||
If two tuples contain types that do not support comparison, the result may be `Unknown`.
|
If two tuples contain types that do not support comparison, the result may be `Unknown`. However,
|
||||||
However, `==` and `!=` are exceptions and can still provide definite results.
|
`==` and `!=` are exceptions and can still provide definite results.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
a = (1, 2)
|
a = (1, 2)
|
||||||
b = (1, "hello")
|
b = (1, "hello")
|
||||||
|
|
||||||
# TODO: should be Literal[False]
|
# TODO: should be Literal[False], once we implement (in)equality for mismatched literals
|
||||||
reveal_type(a == b) # revealed: @Todo
|
reveal_type(a == b) # revealed: bool
|
||||||
|
|
||||||
# TODO: should be Literal[True]
|
# TODO: should be Literal[True], once we implement (in)equality for mismatched literals
|
||||||
reveal_type(a != b) # revealed: @Todo
|
reveal_type(a != b) # revealed: bool
|
||||||
|
|
||||||
# TODO: should be Unknown and add more informative diagnostics
|
# TODO: should be Unknown and add more informative diagnostics
|
||||||
reveal_type(a < b) # revealed: @Todo
|
reveal_type(a < b) # revealed: bool
|
||||||
reveal_type(a <= b) # revealed: @Todo
|
reveal_type(a <= b) # revealed: bool
|
||||||
reveal_type(a > b) # revealed: @Todo
|
reveal_type(a > b) # revealed: bool
|
||||||
reveal_type(a >= b) # revealed: @Todo
|
reveal_type(a >= b) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
However, if the lexicographic comparison completes without reaching a point where str and int are compared,
|
However, if the lexicographic comparison completes without reaching a point where str and int are
|
||||||
Python will still produce a result based on the prior elements.
|
compared, Python will still produce a result based on the prior elements.
|
||||||
|
|
||||||
```py path=short_circuit.py
|
```py path=short_circuit.py
|
||||||
a = (1, 2)
|
a = (1, 2)
|
||||||
@@ -145,13 +145,12 @@ class A:
|
|||||||
|
|
||||||
a = (A(), A())
|
a = (A(), A())
|
||||||
|
|
||||||
# TODO: All @Todo should be bool
|
reveal_type(a == a) # revealed: bool
|
||||||
reveal_type(a == a) # revealed: @Todo
|
reveal_type(a != a) # revealed: bool
|
||||||
reveal_type(a != a) # revealed: @Todo
|
reveal_type(a < a) # revealed: bool
|
||||||
reveal_type(a < a) # revealed: @Todo
|
reveal_type(a <= a) # revealed: bool
|
||||||
reveal_type(a <= a) # revealed: @Todo
|
reveal_type(a > a) # revealed: bool
|
||||||
reveal_type(a > a) # revealed: @Todo
|
reveal_type(a >= a) # revealed: bool
|
||||||
reveal_type(a >= a) # revealed: @Todo
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Membership Test Comparisons
|
### Membership Test Comparisons
|
||||||
@@ -159,7 +158,8 @@ reveal_type(a >= a) # revealed: @Todo
|
|||||||
"Membership Test Comparisons" refers to the operators `in` and `not in`.
|
"Membership Test Comparisons" refers to the operators `in` and `not in`.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def int_instance() -> int: ...
|
def int_instance() -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
a = (1, 2)
|
a = (1, 2)
|
||||||
b = ((3, 4), (1, 2))
|
b = ((3, 4), (1, 2))
|
||||||
@@ -172,9 +172,8 @@ reveal_type(a not in b) # revealed: Literal[False]
|
|||||||
reveal_type(a in c) # revealed: Literal[False]
|
reveal_type(a in c) # revealed: Literal[False]
|
||||||
reveal_type(a not in c) # revealed: Literal[True]
|
reveal_type(a not in c) # revealed: Literal[True]
|
||||||
|
|
||||||
# TODO: All @Todo should be bool
|
reveal_type(a in d) # revealed: bool
|
||||||
reveal_type(a in d) # revealed: @Todo
|
reveal_type(a not in d) # revealed: bool
|
||||||
reveal_type(a not in d) # revealed: @Todo
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Identity Comparisons
|
### Identity Comparisons
|
||||||
@@ -189,10 +188,10 @@ c = (1, 2, 3)
|
|||||||
reveal_type(a is (1, 2)) # revealed: bool
|
reveal_type(a is (1, 2)) # revealed: bool
|
||||||
reveal_type(a is not (1, 2)) # revealed: bool
|
reveal_type(a is not (1, 2)) # revealed: bool
|
||||||
|
|
||||||
# TODO: Update to Literal[False] once str == int comparison is implemented
|
# TODO should be Literal[False] once we implement comparison of mismatched literal types
|
||||||
reveal_type(a is b) # revealed: @Todo
|
reveal_type(a is b) # revealed: bool
|
||||||
# TODO: Update to Literal[True] once str == int comparison is implemented
|
# TODO should be Literal[True] once we implement comparison of mismatched literal types
|
||||||
reveal_type(a is not b) # revealed: @Todo
|
reveal_type(a is not b) # revealed: bool
|
||||||
|
|
||||||
reveal_type(a is c) # revealed: Literal[False]
|
reveal_type(a is c) # revealed: Literal[False]
|
||||||
reveal_type(a is not c) # revealed: Literal[True]
|
reveal_type(a is not c) # revealed: Literal[True]
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ reveal_type(one_or_none is not None) # revealed: bool
|
|||||||
|
|
||||||
## Union on both sides of the comparison
|
## Union on both sides of the comparison
|
||||||
|
|
||||||
With unions on both sides, we need to consider the full cross product of
|
With unions on both sides, we need to consider the full cross product of options when building the
|
||||||
options when building the resulting (union) type:
|
resulting (union) type:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def bool_instance() -> bool:
|
def bool_instance() -> bool:
|
||||||
@@ -72,9 +72,9 @@ reveal_type(small > large) # revealed: Literal[False]
|
|||||||
|
|
||||||
## Unsupported operations
|
## Unsupported operations
|
||||||
|
|
||||||
Make sure we emit a diagnostic if *any* of the possible comparisons is
|
Make sure we emit a diagnostic if *any* of the possible comparisons is unsupported. For now, we fall
|
||||||
unsupported. For now, we fall back to `bool` for the result type instead of
|
back to `bool` for the result type instead of trying to infer something more precise from the other
|
||||||
trying to infer something more precise from the other (supported) variants:
|
(supported) variants:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def bool_instance() -> bool:
|
def bool_instance() -> bool:
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ reveal_type(a) # revealed: bool
|
|||||||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||||
reveal_type(b) # revealed: bool
|
reveal_type(b) # revealed: bool
|
||||||
|
|
||||||
c = object() < 5 # error: "Operator `<` is not supported for types `object` and `int`"
|
# TODO: should error, once operand type check is implemented
|
||||||
reveal_type(c) # revealed: Unknown
|
# ("Operator `<` is not supported for types `object` and `int`")
|
||||||
|
c = object() < 5
|
||||||
|
# TODO: should be Unknown, once operand type check is implemented
|
||||||
|
reveal_type(c) # revealed: bool
|
||||||
|
|
||||||
# TODO should error, need to check if __lt__ signature is valid for right operand
|
# TODO: should error, once operand type check is implemented
|
||||||
|
# ("Operator `<` is not supported for types `int` and `object`")
|
||||||
d = 5 < object()
|
d = 5 < object()
|
||||||
# TODO: should be `Unknown`
|
# TODO: should be Unknown, once operand type check is implemented
|
||||||
reveal_type(d) # revealed: bool
|
reveal_type(d) # revealed: bool
|
||||||
|
|
||||||
flag = bool_instance()
|
flag = bool_instance()
|
||||||
@@ -27,5 +31,6 @@ reveal_type(e) # revealed: bool
|
|||||||
# TODO: should error, need to check if __lt__ signature is valid for right operand
|
# TODO: should error, need to check if __lt__ signature is valid for right operand
|
||||||
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
|
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
|
||||||
f = (1, 2) < (1, "hello")
|
f = (1, 2) < (1, "hello")
|
||||||
reveal_type(f) # revealed: @Todo
|
# TODO: should be Unknown, once operand type check is implemented
|
||||||
|
reveal_type(f) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ x = y
|
|||||||
|
|
||||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||||
|
|
||||||
# revealed: Unbound | Literal[2]
|
# revealed: Literal[2]
|
||||||
# error: [possibly-unresolved-reference]
|
# error: [possibly-unresolved-reference]
|
||||||
reveal_type(r)
|
reveal_type(r)
|
||||||
|
|
||||||
# revealed: Unbound | Literal[5]
|
# revealed: Literal[5]
|
||||||
# error: [possibly-unresolved-reference]
|
# error: [possibly-unresolved-reference]
|
||||||
reveal_type(s)
|
reveal_type(s)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ match 0:
|
|||||||
case 2:
|
case 2:
|
||||||
y = 3
|
y = 3
|
||||||
|
|
||||||
# revealed: Unbound | Literal[2, 3]
|
# revealed: Literal[2, 3]
|
||||||
# error: [possibly-unresolved-reference]
|
# error: [possibly-unresolved-reference]
|
||||||
reveal_type(y)
|
reveal_type(y)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ except EXCEPTIONS as f:
|
|||||||
## Dynamic exception types
|
## Dynamic exception types
|
||||||
|
|
||||||
```py
|
```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:
|
try:
|
||||||
help()
|
help()
|
||||||
except x as e:
|
except x as e:
|
||||||
|
|||||||
@@ -1,40 +1,33 @@
|
|||||||
# Control flow for exception handlers
|
# Control flow for exception handlers
|
||||||
|
|
||||||
These tests assert that we understand the possible "definition states" (which
|
These tests assert that we understand the possible "definition states" (which symbols might or might
|
||||||
symbols might or might not be defined) in the various branches of a
|
not be defined) in the various branches of a `try`/`except`/`else`/`finally` block.
|
||||||
`try`/`except`/`else`/`finally` block.
|
|
||||||
|
|
||||||
For a full writeup on the semantics of exception handlers,
|
For a full writeup on the semantics of exception handlers, see [this document][1].
|
||||||
see [this document][1].
|
|
||||||
|
|
||||||
The tests throughout this Markdown document use functions with names starting
|
The tests throughout this Markdown document use functions with names starting with `could_raise_*`
|
||||||
with `could_raise_*` to mark definitions that might or might not succeed
|
to mark definitions that might or might not succeed (as the function could raise an exception). A
|
||||||
(as the function could raise an exception). A type checker must assume that any
|
type checker must assume that any arbitrary function call could raise an exception in Python; this
|
||||||
arbitrary function call could raise an exception in Python; this is just a
|
is just a naming convention used in these tests for clarity, and to future-proof the tests against
|
||||||
naming convention used in these tests for clarity, and to future-proof the
|
possible future improvements whereby certain statements or expressions could potentially be inferred
|
||||||
tests against possible future improvements whereby certain statements or
|
as being incapable of causing an exception to be raised.
|
||||||
expressions could potentially be inferred as being incapable of causing an
|
|
||||||
exception to be raised.
|
|
||||||
|
|
||||||
## A single bare `except`
|
## A single bare `except`
|
||||||
|
|
||||||
Consider the following `try`/`except` block, with a single bare `except:`.
|
Consider the following `try`/`except` block, with a single bare `except:`. There are different types
|
||||||
There are different types for the variable `x` in the two branches of this
|
for the variable `x` in the two branches of this block, and we can't determine which branch might
|
||||||
block, and we can't determine which branch might have been taken from the
|
have been taken from the perspective of code following this block. The inferred type after the
|
||||||
perspective of code following this block. The inferred type after the block's
|
block's conclusion is therefore the union of the type at the end of the `try` suite (`str`) and the
|
||||||
conclusion is therefore the union of the type at the end of the `try` suite
|
type at the end of the `except` suite (`Literal[2]`).
|
||||||
(`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
|
*Within* the `except` suite, we must infer a union of all possible "definition states" we could have
|
||||||
states" we could have been in at any point during the `try` suite. This is
|
been in at any point during the `try` suite. This is because control flow could have jumped to the
|
||||||
because control flow could have jumped to the `except` suite without any of the
|
`except` suite without any of the `try`-suite definitions successfully completing, with only *some*
|
||||||
`try`-suite definitions successfully completing, with only *some* of the
|
of the `try`-suite definitions successfully completing, or indeed with *all* of them successfully
|
||||||
`try`-suite definitions successfully completing, or indeed with *all* of them
|
completing. The type of `x` at the beginning of the `except` suite in this example is therefore
|
||||||
successfully completing. The type of `x` at the beginning of the `except` suite
|
`Literal[1] | str`, taking into account that we might have jumped to the `except` suite before the
|
||||||
in this example is therefore `Literal[1] | str`, taking into account that we
|
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
|
||||||
might have jumped to the `except` suite before the
|
*after* that redefinition.
|
||||||
`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
|
```py path=union_type_inferred.py
|
||||||
def could_raise_returns_str() -> str:
|
def could_raise_returns_str() -> str:
|
||||||
@@ -54,9 +47,8 @@ except:
|
|||||||
reveal_type(x) # revealed: str | Literal[2]
|
reveal_type(x) # revealed: str | Literal[2]
|
||||||
```
|
```
|
||||||
|
|
||||||
If `x` has the same type at the end of both branches, however, the branches
|
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
|
||||||
unify and `x` is not inferred as having a union type following the
|
inferred as having a union type following the `try`/`except` block:
|
||||||
`try`/`except` block:
|
|
||||||
|
|
||||||
```py path=branches_unify_to_non_union_type.py
|
```py path=branches_unify_to_non_union_type.py
|
||||||
def could_raise_returns_str() -> str:
|
def could_raise_returns_str() -> str:
|
||||||
@@ -74,13 +66,12 @@ reveal_type(x) # revealed: str
|
|||||||
|
|
||||||
## A non-bare `except`
|
## A non-bare `except`
|
||||||
|
|
||||||
For simple `try`/`except` blocks, an `except TypeError:` handler has the same
|
For simple `try`/`except` blocks, an `except TypeError:` handler has the same control flow semantics
|
||||||
control flow semantics as an `except:` handler. An `except TypeError:` handler
|
as an `except:` handler. An `except TypeError:` handler will not catch *all* exceptions: if this is
|
||||||
will not catch *all* exceptions: if this is the only handler, it opens up the
|
the only handler, it opens up the possibility that an exception might occur that would not be
|
||||||
possibility that an exception might occur that would not be handled. However,
|
handled. However, as described in [the document on exception-handling semantics][1], that would lead
|
||||||
as described in [the document on exception-handling semantics][1], that would
|
to termination of the scope. It's therefore irrelevant to consider this possibility when it comes to
|
||||||
lead to termination of the scope. It's therefore irrelevant to consider this
|
control-flow analysis.
|
||||||
possibility when it comes to control-flow analysis.
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def could_raise_returns_str() -> str:
|
def could_raise_returns_str() -> str:
|
||||||
@@ -102,11 +93,9 @@ reveal_type(x) # revealed: str | Literal[2]
|
|||||||
|
|
||||||
## Multiple `except` branches
|
## Multiple `except` branches
|
||||||
|
|
||||||
If the scope reaches the final `reveal_type` call in this example,
|
If the scope reaches the final `reveal_type` call in this example, either the `try`-block suite of
|
||||||
either the `try`-block suite of statements was executed in its entirety,
|
statements was executed in its entirety, or exactly one `except` suite 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:
|
||||||
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 `try`, `type(x) == str`
|
||||||
- At the end of `except TypeError`, `x == 2`
|
- 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`)
|
## Exception handlers with `else` branches (but no `finally`)
|
||||||
|
|
||||||
If we reach the `reveal_type` call at the end of this scope,
|
If we reach the `reveal_type` call at the end of this scope, either the `try` and `else` suites were
|
||||||
either the `try` and `else` suites were both executed in their entireties,
|
both executed in their entireties, or the `except` suite was executed in its entirety. The type of
|
||||||
or the `except` suite was executed in its entirety. The type of `x` at this
|
`x` at this point is the union of the type at the end of the `else` suite and the type at the end of
|
||||||
point is the union of the type at the end of the `else` suite and the type at
|
the `except` suite:
|
||||||
the end of the `except` suite:
|
|
||||||
|
|
||||||
- At the end of `else`, `x == 3`
|
- At the end of `else`, `x == 3`
|
||||||
- At the end of `except`, `x == 2`
|
- At the end of `except`, `x == 2`
|
||||||
@@ -167,10 +155,9 @@ else:
|
|||||||
reveal_type(x) # revealed: Literal[2, 3]
|
reveal_type(x) # revealed: Literal[2, 3]
|
||||||
```
|
```
|
||||||
|
|
||||||
For a block that has multiple `except` branches and an `else` branch, the same
|
For a block that has multiple `except` branches and an `else` branch, the same principle applies. In
|
||||||
principle applies. In order to reach the final `reveal_type` call,
|
order to reach the final `reveal_type` call, either exactly one of the `except` suites must have
|
||||||
either exactly one of the `except` suites must have been executed in its
|
been executed in its entirety, or the `try` suite and the `else` suite must both have been executed
|
||||||
entirety, or the `try` suite and the `else` suite must both have been executed
|
|
||||||
in their entireties:
|
in their entireties:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -201,10 +188,9 @@ reveal_type(x) # revealed: Literal[2, 3, 4]
|
|||||||
|
|
||||||
## Exception handlers with `finally` branches (but no `except` branches)
|
## Exception handlers with `finally` branches (but no `except` branches)
|
||||||
|
|
||||||
A `finally` suite is *always* executed. As such, if we reach the `reveal_type`
|
A `finally` suite is *always* executed. As such, if we reach the `reveal_type` call at the end of
|
||||||
call at the end of this example, we know that `x` *must* have been reassigned
|
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
|
||||||
to `2` during the `finally` suite. The type of `x` at the end of the example is
|
type of `x` at the end of the example is therefore `Literal[2]`:
|
||||||
therefore `Literal[2]`:
|
|
||||||
|
|
||||||
```py path=redef_in_finally.py
|
```py path=redef_in_finally.py
|
||||||
def could_raise_returns_str() -> str:
|
def could_raise_returns_str() -> str:
|
||||||
@@ -223,15 +209,13 @@ finally:
|
|||||||
reveal_type(x) # revealed: Literal[2]
|
reveal_type(x) # revealed: Literal[2]
|
||||||
```
|
```
|
||||||
|
|
||||||
If `x` was *not* redefined in the `finally` suite, however, things are somewhat
|
If `x` was *not* redefined in the `finally` suite, however, things are somewhat more complicated. If
|
||||||
more complicated. If we reach the final `reveal_type` call,
|
we reach the final `reveal_type` call, unlike the state when we're visiting the `finally` suite, we
|
||||||
unlike the state when we're visiting the `finally` suite,
|
know that the `try`-block suite ran to completion. This means that there are fewer possible states
|
||||||
we know that the `try`-block suite ran to completion.
|
at this point than there were when we were inside the `finally` block.
|
||||||
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`
|
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
|
||||||
suites, however; this is still a TODO item for us.)
|
still a TODO item for us.)
|
||||||
|
|
||||||
```py path=no_redef_in_finally.py
|
```py path=no_redef_in_finally.py
|
||||||
def could_raise_returns_str() -> str:
|
def could_raise_returns_str() -> str:
|
||||||
@@ -252,18 +236,18 @@ reveal_type(x) # revealed: str
|
|||||||
|
|
||||||
## Combining an `except` branch with a `finally` branch
|
## Combining an `except` branch with a `finally` branch
|
||||||
|
|
||||||
As previously stated, we do not yet have accurate inference for types *inside*
|
As previously stated, we do not yet have accurate inference for types *inside* `finally` suites.
|
||||||
`finally` suites. When we do, however, we will have to take account of the
|
When we do, however, we will have to take account of the following possibilities inside `finally`
|
||||||
following possibilities inside `finally` suites:
|
suites:
|
||||||
|
|
||||||
- The `try` suite could have run to completion
|
- The `try` suite could have run to completion
|
||||||
- Or we could have jumped from halfway through the `try` suite to an `except`
|
- Or we could have jumped from halfway through the `try` suite to an `except` suite, and the
|
||||||
suite, and the `except` suite ran to completion
|
`except` suite ran to completion
|
||||||
- Or we could have jumped from halfway through the `try` suite straight to the
|
- Or we could have jumped from halfway through the `try` suite straight to the `finally` suite due
|
||||||
`finally` suite due to an unhandled exception
|
to an unhandled exception
|
||||||
- Or we could have jumped from halfway through the `try` suite to an
|
- Or we could have jumped from halfway through the `try` suite to an `except` suite, only for an
|
||||||
`except` suite, only for an exception raised in the `except` suite to cause
|
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
|
||||||
us to jump to the `finally` suite before the `except` suite ran to completion
|
`except` suite ran to completion
|
||||||
|
|
||||||
```py path=redef_in_finally.py
|
```py path=redef_in_finally.py
|
||||||
def could_raise_returns_str() -> str:
|
def could_raise_returns_str() -> str:
|
||||||
@@ -296,12 +280,11 @@ finally:
|
|||||||
reveal_type(x) # revealed: Literal[2]
|
reveal_type(x) # revealed: Literal[2]
|
||||||
```
|
```
|
||||||
|
|
||||||
Now for an example without a redefinition in the `finally` suite.
|
Now for an example without a redefinition in the `finally` suite. As before, there *should* be fewer
|
||||||
As before, there *should* be fewer possibilities after completion of the
|
possibilities after completion of the `finally` suite than there were during the `finally` suite
|
||||||
`finally` suite than there were during the `finally` suite itself.
|
itself. (In some control-flow possibilities, some exceptions were merely *suspended* during the
|
||||||
(In some control-flow possibilities, some exceptions were merely *suspended*
|
`finally` suite; these lead to the scope's termination following the conclusion of the `finally`
|
||||||
during the `finally` suite; these lead to the scope's termination following the
|
suite.)
|
||||||
conclusion of the `finally` suite.)
|
|
||||||
|
|
||||||
```py path=no_redef_in_finally.py
|
```py path=no_redef_in_finally.py
|
||||||
def could_raise_returns_str() -> str:
|
def could_raise_returns_str() -> str:
|
||||||
@@ -377,9 +360,9 @@ reveal_type(x) # revealed: str | bool | float
|
|||||||
|
|
||||||
## Combining `except`, `else` and `finally` branches
|
## Combining `except`, `else` and `finally` branches
|
||||||
|
|
||||||
If the exception handler has an `else` branch, we must also take into account
|
If the exception handler has an `else` branch, we must also take into account the possibility that
|
||||||
the possibility that control flow could have jumped to the `finally` suite from
|
control flow could have jumped to the `finally` suite from partway through the `else` suite due to
|
||||||
partway through the `else` suite due to an exception raised *there*.
|
an exception raised *there*.
|
||||||
|
|
||||||
```py path=single_except_branch.py
|
```py path=single_except_branch.py
|
||||||
def could_raise_returns_str() -> str:
|
def could_raise_returns_str() -> str:
|
||||||
@@ -479,15 +462,13 @@ reveal_type(x) # revealed: bool | float | slice
|
|||||||
|
|
||||||
## Nested `try`/`except` blocks
|
## Nested `try`/`except` blocks
|
||||||
|
|
||||||
It would take advanced analysis, which we are not yet capable of, to be able
|
It would take advanced analysis, which we are not yet capable of, to be able to determine that an
|
||||||
to determine that an exception handler always suppresses all exceptions. This
|
exception handler always suppresses all exceptions. This is partly because it is possible for
|
||||||
is partly because it is possible for statements in `except`, `else` and
|
statements in `except`, `else` and `finally` suites to raise exceptions as well as statements in
|
||||||
`finally` suites to raise exceptions as well as statements in `try` suites.
|
`try` suites. This means that if an exception handler is nested inside the `try` statement of an
|
||||||
This means that if an exception handler is nested inside the `try` statement of
|
enclosing exception handler, it should (at least for now) be treated the same as any other node: as
|
||||||
an enclosing exception handler, it should (at least for now) be treated the
|
a suite containing statements that could possibly raise exceptions, which would lead to control flow
|
||||||
same as any other node: as a suite containing statements that could possibly
|
jumping out of that suite prior to the suite running to completion.
|
||||||
raise exceptions, which would lead to control flow jumping out of that suite
|
|
||||||
prior to the suite running to completion.
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def could_raise_returns_str() -> str:
|
def could_raise_returns_str() -> str:
|
||||||
@@ -580,8 +561,8 @@ reveal_type(x) # revealed: bytearray | Bar
|
|||||||
|
|
||||||
## Nested scopes inside `try` blocks
|
## Nested scopes inside `try` blocks
|
||||||
|
|
||||||
Shadowing a variable in an inner scope has no effect on type inference of the
|
Shadowing a variable in an inner scope has no effect on type inference of the variable by that name
|
||||||
variable by that name in the outer scope:
|
in the outer scope:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def could_raise_returns_str() -> str:
|
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,24 @@
|
|||||||
|
# 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]
|
||||||
|
```
|
||||||
@@ -6,20 +6,17 @@ Basic PEP 695 generics
|
|||||||
|
|
||||||
```py
|
```py
|
||||||
class MyBox[T]:
|
class MyBox[T]:
|
||||||
# TODO: `T` is defined here
|
|
||||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
|
||||||
data: T
|
data: T
|
||||||
box_model_number = 695
|
box_model_number = 695
|
||||||
|
|
||||||
# TODO: `T` is defined here
|
|
||||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
|
||||||
def __init__(self, data: T):
|
def __init__(self, data: T):
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
# TODO not error (should be subscriptable)
|
box: MyBox[int] = MyBox(5)
|
||||||
box: MyBox[int] = MyBox(5) # error: [non-subscriptable]
|
|
||||||
# TODO error differently (str and int don't unify)
|
# TODO should emit a diagnostic here (str is not assignable to int)
|
||||||
wrong_innards: MyBox[int] = MyBox("five") # error: [non-subscriptable]
|
wrong_innards: MyBox[int] = MyBox("five")
|
||||||
|
|
||||||
# TODO reveal int
|
# TODO reveal int
|
||||||
reveal_type(box.data) # revealed: @Todo
|
reveal_type(box.data) # revealed: @Todo
|
||||||
|
|
||||||
@@ -30,17 +27,12 @@ reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
|||||||
|
|
||||||
```py
|
```py
|
||||||
class MyBox[T]:
|
class MyBox[T]:
|
||||||
# TODO: `T` is defined here
|
|
||||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
|
||||||
data: T
|
data: T
|
||||||
|
|
||||||
# TODO: `T` is defined here
|
|
||||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
|
||||||
def __init__(self, data: T):
|
def __init__(self, data: T):
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
# TODO not error on the subscripting or the use of type param
|
# TODO not error on the subscripting
|
||||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
|
||||||
# error: [non-subscriptable]
|
# error: [non-subscriptable]
|
||||||
class MySecureBox[T](MyBox[T]): ...
|
class MySecureBox[T](MyBox[T]): ...
|
||||||
|
|
||||||
@@ -52,7 +44,8 @@ reveal_type(secure_box.data) # revealed: @Todo
|
|||||||
|
|
||||||
## Cyclical class definition
|
## 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.
|
This should hold true even with generics at play.
|
||||||
|
|
||||||
@@ -64,3 +57,55 @@ class S[T](Seq[S]): ... # error: [non-subscriptable]
|
|||||||
|
|
||||||
reveal_type(S) # revealed: Literal[S]
|
reveal_type(S) # revealed: Literal[S]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Type params
|
||||||
|
|
||||||
|
A PEP695 type variable defines a value of type `typing.TypeVar` with attributes `__name__`,
|
||||||
|
`__bounds__`, `__constraints__`, and `__default__` (the latter three all lazily evaluated):
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f[T, U: A, V: (A, B), W = A, X: A = A1]():
|
||||||
|
reveal_type(T) # revealed: T
|
||||||
|
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||||
|
reveal_type(T.__bound__) # revealed: None
|
||||||
|
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||||
|
reveal_type(T.__default__) # revealed: NoDefault
|
||||||
|
|
||||||
|
reveal_type(U) # revealed: U
|
||||||
|
reveal_type(U.__name__) # revealed: Literal["U"]
|
||||||
|
reveal_type(U.__bound__) # revealed: type[A]
|
||||||
|
reveal_type(U.__constraints__) # revealed: tuple[()]
|
||||||
|
reveal_type(U.__default__) # revealed: NoDefault
|
||||||
|
|
||||||
|
reveal_type(V) # revealed: V
|
||||||
|
reveal_type(V.__name__) # revealed: Literal["V"]
|
||||||
|
reveal_type(V.__bound__) # revealed: None
|
||||||
|
reveal_type(V.__constraints__) # revealed: tuple[type[A], type[B]]
|
||||||
|
reveal_type(V.__default__) # revealed: NoDefault
|
||||||
|
|
||||||
|
reveal_type(W) # revealed: W
|
||||||
|
reveal_type(W.__name__) # revealed: Literal["W"]
|
||||||
|
reveal_type(W.__bound__) # revealed: None
|
||||||
|
reveal_type(W.__constraints__) # revealed: tuple[()]
|
||||||
|
reveal_type(W.__default__) # revealed: type[A]
|
||||||
|
|
||||||
|
reveal_type(X) # revealed: X
|
||||||
|
reveal_type(X.__name__) # revealed: Literal["X"]
|
||||||
|
reveal_type(X.__bound__) # revealed: type[A]
|
||||||
|
reveal_type(X.__constraints__) # revealed: tuple[()]
|
||||||
|
reveal_type(X.__default__) # revealed: type[A1]
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
class B: ...
|
||||||
|
class A1(A): ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Minimum two constraints
|
||||||
|
|
||||||
|
A typevar with less than two constraints emits a diagnostic and is treated as unconstrained:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types"
|
||||||
|
def f[T: (int,)]():
|
||||||
|
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||||
|
```
|
||||||
|
|||||||
@@ -12,16 +12,16 @@ if flag:
|
|||||||
|
|
||||||
x = y # error: [possibly-unresolved-reference]
|
x = y # error: [possibly-unresolved-reference]
|
||||||
|
|
||||||
# revealed: Unbound | Literal[3]
|
# revealed: Literal[3]
|
||||||
# error: [possibly-unresolved-reference]
|
|
||||||
reveal_type(x)
|
reveal_type(x)
|
||||||
|
|
||||||
# revealed: Unbound | Literal[3]
|
# revealed: Literal[3]
|
||||||
# error: [possibly-unresolved-reference]
|
# error: [possibly-unresolved-reference]
|
||||||
reveal_type(y)
|
reveal_type(y)
|
||||||
```
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound` is possibly unbound"
|
||||||
from maybe_unbound import x, y
|
from maybe_unbound import x, y
|
||||||
|
|
||||||
reveal_type(x) # revealed: Literal[3]
|
reveal_type(x) # revealed: Literal[3]
|
||||||
@@ -40,11 +40,10 @@ if flag:
|
|||||||
y: int = 3
|
y: int = 3
|
||||||
x = y # error: [possibly-unresolved-reference]
|
x = y # error: [possibly-unresolved-reference]
|
||||||
|
|
||||||
# revealed: Unbound | Literal[3]
|
# revealed: Literal[3]
|
||||||
# error: [possibly-unresolved-reference]
|
|
||||||
reveal_type(x)
|
reveal_type(x)
|
||||||
|
|
||||||
# revealed: Unbound | Literal[3]
|
# revealed: Literal[3]
|
||||||
# error: [possibly-unresolved-reference]
|
# error: [possibly-unresolved-reference]
|
||||||
reveal_type(y)
|
reveal_type(y)
|
||||||
```
|
```
|
||||||
@@ -52,12 +51,31 @@ reveal_type(y)
|
|||||||
Importing an annotated name prefers the declared type over the inferred type:
|
Importing an annotated name prefers the declared type over the inferred type:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound_annotated` is possibly unbound"
|
||||||
from maybe_unbound_annotated import x, y
|
from maybe_unbound_annotated import x, y
|
||||||
|
|
||||||
reveal_type(x) # revealed: Literal[3]
|
reveal_type(x) # revealed: Literal[3]
|
||||||
reveal_type(y) # revealed: int
|
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
|
## Reimport
|
||||||
|
|
||||||
```py path=c.py
|
```py path=c.py
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
a1: Literal[26]
|
||||||
|
|
||||||
|
def f():
|
||||||
|
reveal_type(a1) # revealed: @Todo
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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():
|
async for x in Iterator():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# TODO: should reveal `Unbound | Unknown` because `__aiter__` is not defined
|
# TODO: should reveal `Unknown` because `__aiter__` is not defined
|
||||||
# revealed: Unbound | @Todo
|
# revealed: @Todo
|
||||||
# error: [possibly-unresolved-reference]
|
# error: [possibly-unresolved-reference]
|
||||||
reveal_type(x)
|
reveal_type(x)
|
||||||
```
|
```
|
||||||
@@ -40,6 +40,6 @@ async def foo():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# error: [possibly-unresolved-reference]
|
# error: [possibly-unresolved-reference]
|
||||||
# revealed: Unbound | @Todo
|
# revealed: @Todo
|
||||||
reveal_type(x)
|
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
|
|
||||||
```
|
|
||||||
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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]]
|
||||||
|
```
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
x: A | None = A() if bool_instance() else None
|
||||||
|
|
||||||
|
isinstance(x, A) or reveal_type(x) # revealed: None
|
||||||
|
x is None or reveal_type(x) # revealed: A
|
||||||
|
reveal_type(x) # revealed: A | None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Narrowing in `and`
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
x: A | None = A() if bool_instance() else None
|
||||||
|
|
||||||
|
isinstance(x, A) and reveal_type(x) # revealed: A
|
||||||
|
x is None and reveal_type(x) # revealed: None
|
||||||
|
reveal_type(x) # revealed: A | None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple `and` arms
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
x: A | None = A() if bool_instance() else None
|
||||||
|
|
||||||
|
bool_instance() and isinstance(x, A) and reveal_type(x) # revealed: A
|
||||||
|
isinstance(x, A) and bool_instance() and reveal_type(x) # revealed: A
|
||||||
|
reveal_type(x) and isinstance(x, A) and bool_instance() # revealed: A | None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple `or` arms
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
x: A | None = A() if bool_instance() else None
|
||||||
|
|
||||||
|
bool_instance() or isinstance(x, A) or reveal_type(x) # revealed: None
|
||||||
|
isinstance(x, A) or bool_instance() or reveal_type(x) # revealed: None
|
||||||
|
reveal_type(x) or isinstance(x, A) or bool_instance() # revealed: A | None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple predicates
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1
|
||||||
|
|
||||||
|
x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mix of `and` and `or`
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1
|
||||||
|
|
||||||
|
isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Narrowing for conditionals with elif and else
|
||||||
|
|
||||||
|
## Positive contributions become negative in elif-else blocks
|
||||||
|
|
||||||
|
```py
|
||||||
|
def int_instance() -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
x = int_instance()
|
||||||
|
|
||||||
|
if x == 1:
|
||||||
|
# cannot narrow; could be a subclass of `int`
|
||||||
|
reveal_type(x) # revealed: int
|
||||||
|
elif x == 2:
|
||||||
|
reveal_type(x) # revealed: int & ~Literal[1]
|
||||||
|
elif x != 3:
|
||||||
|
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Positive contributions become negative in elif-else blocks, with simplification
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
x = 1 if bool_instance() else 2 if bool_instance() else 3
|
||||||
|
|
||||||
|
if x == 1:
|
||||||
|
# TODO should be Literal[1]
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||||
|
elif x == 2:
|
||||||
|
# TODO should be Literal[2]
|
||||||
|
reveal_type(x) # revealed: Literal[2, 3]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal[3]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple negative contributions using elif, with simplification
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
x = 1 if bool_instance() else 2 if bool_instance() else 3
|
||||||
|
|
||||||
|
if x != 1:
|
||||||
|
reveal_type(x) # revealed: Literal[2, 3]
|
||||||
|
elif x != 2:
|
||||||
|
# TODO should be `Literal[1]`
|
||||||
|
reveal_type(x) # revealed: Literal[1, 3]
|
||||||
|
elif x == 3:
|
||||||
|
# TODO should be Never
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||||
|
else:
|
||||||
|
# TODO should be Never
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2]
|
||||||
|
```
|
||||||
@@ -11,6 +11,8 @@ x = None if flag else 1
|
|||||||
|
|
||||||
if x is None:
|
if x is None:
|
||||||
reveal_type(x) # revealed: None
|
reveal_type(x) # revealed: None
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
|
||||||
reveal_type(x) # revealed: None | Literal[1]
|
reveal_type(x) # revealed: None | Literal[1]
|
||||||
```
|
```
|
||||||
@@ -30,6 +32,8 @@ y = x if flag else None
|
|||||||
|
|
||||||
if y is x:
|
if y is x:
|
||||||
reveal_type(y) # revealed: A
|
reveal_type(y) # revealed: A
|
||||||
|
else:
|
||||||
|
reveal_type(y) # revealed: A | None
|
||||||
|
|
||||||
reveal_type(y) # revealed: A | None
|
reveal_type(y) # revealed: A | None
|
||||||
```
|
```
|
||||||
@@ -50,4 +54,26 @@ reveal_type(y) # revealed: bool
|
|||||||
if y is x is False: # Interpreted as `(y is x) and (x is False)`
|
if y is x is False: # Interpreted as `(y is x) and (x is False)`
|
||||||
reveal_type(x) # revealed: Literal[False]
|
reveal_type(x) # revealed: Literal[False]
|
||||||
reveal_type(y) # revealed: bool
|
reveal_type(y) # revealed: bool
|
||||||
|
else:
|
||||||
|
# The negation of the clause above is (y is not x) or (x is not False)
|
||||||
|
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
|
||||||
|
reveal_type(x) # revealed: bool
|
||||||
|
reveal_type(y) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
## `is` in elif clause
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
x = None if bool_instance() else (1 if bool_instance() else True)
|
||||||
|
|
||||||
|
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
|
||||||
|
if x is None:
|
||||||
|
reveal_type(x) # revealed: None
|
||||||
|
elif x is True:
|
||||||
|
reveal_type(x) # revealed: Literal[True]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
```
|
```
|
||||||
@@ -13,6 +13,8 @@ x = None if flag else 1
|
|||||||
|
|
||||||
if x is not None:
|
if x is not None:
|
||||||
reveal_type(x) # revealed: Literal[1]
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: None
|
||||||
|
|
||||||
reveal_type(x) # revealed: None | Literal[1]
|
reveal_type(x) # revealed: None | Literal[1]
|
||||||
```
|
```
|
||||||
@@ -29,13 +31,14 @@ reveal_type(x) # revealed: bool
|
|||||||
|
|
||||||
if x is not False:
|
if x is not False:
|
||||||
reveal_type(x) # revealed: Literal[True]
|
reveal_type(x) # revealed: Literal[True]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal[False]
|
||||||
```
|
```
|
||||||
|
|
||||||
## `is not` for non-singleton types
|
## `is not` for non-singleton types
|
||||||
|
|
||||||
Non-singleton types should *not* narrow the type: two instances of a
|
Non-singleton types should *not* narrow the type: two instances of a non-singleton class may occupy
|
||||||
non-singleton class may occupy different addresses in memory even if
|
different addresses in memory even if they compare equal.
|
||||||
they compare equal.
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
x = 345
|
x = 345
|
||||||
@@ -43,6 +46,27 @@ y = 345
|
|||||||
|
|
||||||
if x is not y:
|
if x is not y:
|
||||||
reveal_type(x) # revealed: Literal[345]
|
reveal_type(x) # revealed: Literal[345]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal[345]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `is not` for other types
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
x = A()
|
||||||
|
y = x if bool_instance() else None
|
||||||
|
|
||||||
|
if y is not x:
|
||||||
|
reveal_type(y) # revealed: A | None
|
||||||
|
else:
|
||||||
|
reveal_type(y) # revealed: A
|
||||||
|
|
||||||
|
reveal_type(y) # revealed: A | None
|
||||||
```
|
```
|
||||||
|
|
||||||
## `is not` in chained comparisons
|
## `is not` in chained comparisons
|
||||||
@@ -63,4 +87,10 @@ reveal_type(y) # revealed: bool
|
|||||||
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
|
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
|
||||||
reveal_type(x) # revealed: Literal[True]
|
reveal_type(x) # revealed: Literal[True]
|
||||||
reveal_type(y) # revealed: bool
|
reveal_type(y) # revealed: bool
|
||||||
|
else:
|
||||||
|
# The negation of the clause above is (y is x) or (x is False)
|
||||||
|
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
|
||||||
|
|
||||||
|
reveal_type(x) # revealed: bool
|
||||||
|
reveal_type(y) # revealed: bool
|
||||||
```
|
```
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Narrowing for nested conditionals
|
||||||
|
|
||||||
|
## Multiple negative contributions
|
||||||
|
|
||||||
|
```py
|
||||||
|
def int_instance() -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
x = int_instance()
|
||||||
|
|
||||||
|
if x != 1:
|
||||||
|
if x != 2:
|
||||||
|
if x != 3:
|
||||||
|
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple negative contributions with simplification
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
flag1, flag2 = bool_instance(), bool_instance()
|
||||||
|
x = 1 if flag1 else 2 if flag2 else 3
|
||||||
|
|
||||||
|
if x != 1:
|
||||||
|
reveal_type(x) # revealed: Literal[2, 3]
|
||||||
|
if x != 2:
|
||||||
|
reveal_type(x) # revealed: Literal[3]
|
||||||
|
```
|
||||||
|
|
||||||
|
## elif-else blocks
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
x = 1 if bool_instance() else 2 if bool_instance() else 3
|
||||||
|
|
||||||
|
if x != 1:
|
||||||
|
reveal_type(x) # revealed: Literal[2, 3]
|
||||||
|
if x == 2:
|
||||||
|
# TODO should be `Literal[2]`
|
||||||
|
reveal_type(x) # revealed: Literal[2, 3]
|
||||||
|
elif x == 3:
|
||||||
|
reveal_type(x) # revealed: Literal[3]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Never
|
||||||
|
|
||||||
|
elif x != 2:
|
||||||
|
# TODO should be Literal[1]
|
||||||
|
reveal_type(x) # revealed: Literal[1, 3]
|
||||||
|
else:
|
||||||
|
# TODO should be Never
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||||
|
```
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Narrowing for `not` conditionals
|
||||||
|
|
||||||
|
The `not` operator negates a constraint.
|
||||||
|
|
||||||
|
## `not is None`
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
x = None if bool_instance() else 1
|
||||||
|
|
||||||
|
if not x is None:
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: None
|
||||||
|
|
||||||
|
reveal_type(x) # revealed: None | Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `not isinstance`
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
x = 1 if bool_instance() else "a"
|
||||||
|
|
||||||
|
if not isinstance(x, (int)):
|
||||||
|
reveal_type(x) # revealed: Literal["a"]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
@@ -11,6 +11,9 @@ x = None if flag else 1
|
|||||||
|
|
||||||
if x != None:
|
if x != None:
|
||||||
reveal_type(x) # revealed: Literal[1]
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
else:
|
||||||
|
# TODO should be None
|
||||||
|
reveal_type(x) # revealed: None | Literal[1]
|
||||||
```
|
```
|
||||||
|
|
||||||
## `!=` for other singleton types
|
## `!=` for other singleton types
|
||||||
@@ -24,6 +27,9 @@ x = True if flag else False
|
|||||||
|
|
||||||
if x != False:
|
if x != False:
|
||||||
reveal_type(x) # revealed: Literal[True]
|
reveal_type(x) # revealed: Literal[True]
|
||||||
|
else:
|
||||||
|
# TODO should be Literal[False]
|
||||||
|
reveal_type(x) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
## `x != y` where `y` is of literal type
|
## `x != y` where `y` is of literal type
|
||||||
@@ -54,6 +60,25 @@ C = A if flag else B
|
|||||||
|
|
||||||
if C != A:
|
if C != A:
|
||||||
reveal_type(C) # revealed: Literal[B]
|
reveal_type(C) # revealed: Literal[B]
|
||||||
|
else:
|
||||||
|
# TODO should be Literal[A]
|
||||||
|
reveal_type(C) # revealed: Literal[A, B]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `x != y` where `y` has multiple single-valued options
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
x = 1 if bool_instance() else 2
|
||||||
|
y = 2 if bool_instance() else 3
|
||||||
|
|
||||||
|
if x != y:
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2]
|
||||||
|
else:
|
||||||
|
# TODO should be Literal[2]
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2]
|
||||||
```
|
```
|
||||||
|
|
||||||
## `!=` for non-single-valued types
|
## `!=` for non-single-valued types
|
||||||
@@ -74,3 +99,21 @@ y = int_instance()
|
|||||||
if x != y:
|
if x != y:
|
||||||
reveal_type(x) # revealed: int | None
|
reveal_type(x) # revealed: int | None
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Mix of single-valued and non-single-valued types
|
||||||
|
|
||||||
|
```py
|
||||||
|
def int_instance() -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
x = 1 if bool_instance() else 2
|
||||||
|
y = 2 if bool_instance() else int_instance()
|
||||||
|
|
||||||
|
if x != y:
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2]
|
||||||
|
```
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Narrowing for nested conditionals
|
|
||||||
|
|
||||||
## Multiple negative contributions
|
|
||||||
|
|
||||||
```py
|
|
||||||
def int_instance() -> int: ...
|
|
||||||
|
|
||||||
x = int_instance()
|
|
||||||
|
|
||||||
if x != 1:
|
|
||||||
if x != 2:
|
|
||||||
if x != 3:
|
|
||||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Multiple negative contributions with simplification
|
|
||||||
|
|
||||||
```py
|
|
||||||
def bool_instance() -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
flag1, flag2 = bool_instance(), bool_instance()
|
|
||||||
x = 1 if flag1 else 2 if flag2 else 3
|
|
||||||
|
|
||||||
if x != 1:
|
|
||||||
reveal_type(x) # revealed: Literal[2, 3]
|
|
||||||
if x != 2:
|
|
||||||
reveal_type(x) # revealed: Literal[3]
|
|
||||||
```
|
|
||||||
@@ -26,9 +26,8 @@ if isinstance(x, (int, object)):
|
|||||||
|
|
||||||
## `classinfo` is a tuple of types
|
## `classinfo` is a tuple of types
|
||||||
|
|
||||||
Note: `isinstance(x, (int, str))` should not be confused with
|
Note: `isinstance(x, (int, str))` should not be confused with `isinstance(x, tuple[(int, str)])`.
|
||||||
`isinstance(x, tuple[(int, str)])`. The former is equivalent to
|
The former is equivalent to `isinstance(x, int | str)`:
|
||||||
`isinstance(x, int | str)`:
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def bool_instance() -> bool:
|
def bool_instance() -> bool:
|
||||||
@@ -40,6 +39,8 @@ x = 1 if flag else "a"
|
|||||||
|
|
||||||
if isinstance(x, (int, str)):
|
if isinstance(x, (int, str)):
|
||||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Never
|
||||||
|
|
||||||
if isinstance(x, (int, bytes)):
|
if isinstance(x, (int, bytes)):
|
||||||
reveal_type(x) # revealed: Literal[1]
|
reveal_type(x) # revealed: Literal[1]
|
||||||
@@ -51,6 +52,8 @@ if isinstance(x, (bytes, str)):
|
|||||||
# one of the possibilities:
|
# one of the possibilities:
|
||||||
if isinstance(x, (int, object)):
|
if isinstance(x, (int, object)):
|
||||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Never
|
||||||
|
|
||||||
y = 1 if flag1 else "a" if flag2 else b"b"
|
y = 1 if flag1 else "a" if flag2 else b"b"
|
||||||
if isinstance(y, (int, str)):
|
if isinstance(y, (int, str)):
|
||||||
@@ -75,6 +78,8 @@ x = 1 if flag else "a"
|
|||||||
|
|
||||||
if isinstance(x, (bool, (bytes, int))):
|
if isinstance(x, (bool, (bytes, int))):
|
||||||
reveal_type(x) # revealed: Literal[1]
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal["a"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Class types
|
## Class types
|
||||||
@@ -82,6 +87,7 @@ if isinstance(x, (bool, (bytes, int))):
|
|||||||
```py
|
```py
|
||||||
class A: ...
|
class A: ...
|
||||||
class B: ...
|
class B: ...
|
||||||
|
class C: ...
|
||||||
|
|
||||||
def get_object() -> object: ...
|
def get_object() -> object: ...
|
||||||
|
|
||||||
@@ -91,6 +97,16 @@ if isinstance(x, A):
|
|||||||
reveal_type(x) # revealed: A
|
reveal_type(x) # revealed: A
|
||||||
if isinstance(x, B):
|
if isinstance(x, B):
|
||||||
reveal_type(x) # revealed: A & B
|
reveal_type(x) # revealed: A & B
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: A & ~B
|
||||||
|
|
||||||
|
if isinstance(x, (A, B)):
|
||||||
|
reveal_type(x) # revealed: A | B
|
||||||
|
elif isinstance(x, (A, C)):
|
||||||
|
reveal_type(x) # revealed: C & ~A & ~B
|
||||||
|
else:
|
||||||
|
# TODO: Should be simplified to ~A & ~B & ~C
|
||||||
|
reveal_type(x) # revealed: object & ~A & ~B & ~C
|
||||||
```
|
```
|
||||||
|
|
||||||
## No narrowing for instances of `builtins.type`
|
## No narrowing for instances of `builtins.type`
|
||||||
|
|||||||
@@ -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,137 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# TODO: generics
|
||||||
|
reveal_type(__path__) # revealed: @Todo
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
reveal_type(foo_dict) # revealed: @Todo
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
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
|
```py path=a.py
|
||||||
def f(x: str):
|
def f(x: str):
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
## Cyclical class definition
|
## 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
|
```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
|
```py
|
||||||
b = b"\x00abc\xff"
|
b = b"\x00abc\xff"
|
||||||
@@ -21,14 +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"
|
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
|
reveal_type(y) # revealed: Unknown
|
||||||
```
|
|
||||||
|
|
||||||
## Function return
|
def int_instance() -> int:
|
||||||
|
return 42
|
||||||
```py
|
|
||||||
def int_instance() -> int: ...
|
|
||||||
|
|
||||||
a = b"abcde"[int_instance()]
|
a = b"abcde"[int_instance()]
|
||||||
# TODO: Support overloads... Should be `bytes`
|
# TODO: Support overloads... Should be `bytes`
|
||||||
reveal_type(a) # revealed: @Todo
|
reveal_type(a) # revealed: @Todo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
def bytes_instance() -> bytes: ...
|
||||||
|
|
||||||
|
byte_slice2 = bytes_instance()[0:5]
|
||||||
|
# TODO: Support overloads... Should be `bytes`
|
||||||
|
reveal_type(byte_slice2) # revealed: @Todo
|
||||||
|
```
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ reveal_type(UnionClassGetItem[0]) # revealed: str | int
|
|||||||
## Class getitem with class union
|
## Class getitem with class union
|
||||||
|
|
||||||
```py
|
```py
|
||||||
flag = True
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
class A:
|
class A:
|
||||||
def __class_getitem__(cls, item: int) -> str:
|
def __class_getitem__(cls, item: int) -> str:
|
||||||
@@ -49,7 +50,7 @@ class B:
|
|||||||
def __class_getitem__(cls, item: int) -> int:
|
def __class_getitem__(cls, item: int) -> int:
|
||||||
return item
|
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) # revealed: Literal[A, B]
|
||||||
reveal_type(x[0]) # revealed: str | int
|
reveal_type(x[0]) # revealed: str | int
|
||||||
@@ -68,8 +69,8 @@ if flag:
|
|||||||
else:
|
else:
|
||||||
class Spam: ...
|
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]`"
|
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound"
|
||||||
# revealed: str | Unknown
|
# revealed: str
|
||||||
reveal_type(Spam[42])
|
reveal_type(Spam[42])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ reveal_type(x["a"]) # revealed: @Todo
|
|||||||
|
|
||||||
## Assignments within list assignment
|
## Assignments within list assignment
|
||||||
|
|
||||||
In assignment, we might also have a named assignment.
|
In assignment, we might also have a named assignment. This should also get type checked.
|
||||||
This should also get type checked.
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
x = [1, 2, 3]
|
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
|
```py
|
||||||
s = "abcde"
|
s = "abcde"
|
||||||
@@ -18,14 +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"
|
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
|
reveal_type(b) # revealed: Unknown
|
||||||
```
|
|
||||||
|
|
||||||
## Function return
|
|
||||||
|
|
||||||
```py
|
|
||||||
def int_instance() -> int: ...
|
def int_instance() -> int: ...
|
||||||
|
|
||||||
a = "abcde"[int_instance()]
|
a = "abcde"[int_instance()]
|
||||||
# TODO: Support overloads... Should be `str`
|
# TODO: Support overloads... Should be `str`
|
||||||
reveal_type(a) # revealed: @Todo
|
reveal_type(a) # revealed: @Todo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
def str_instance() -> str: ...
|
||||||
|
|
||||||
|
substring2 = str_instance()[0:5]
|
||||||
|
# TODO: Support overloads... Should be `str`
|
||||||
|
reveal_type(substring2) # revealed: @Todo
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
# Tuple subscripts
|
||||||
|
|
||||||
## Basic
|
## Indexing
|
||||||
|
|
||||||
```py
|
```py
|
||||||
t = (1, "a", "b")
|
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[-1]) # revealed: Literal["b"]
|
||||||
reveal_type(t[-2]) # revealed: Literal["a"]
|
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]
|
a = t[4] # error: [index-out-of-bounds]
|
||||||
reveal_type(a) # revealed: Unknown
|
reveal_type(a) # revealed: Unknown
|
||||||
|
|
||||||
b = t[-4] # error: [index-out-of-bounds]
|
b = t[-4] # error: [index-out-of-bounds]
|
||||||
reveal_type(b) # revealed: Unknown
|
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
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# `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
|
||||||
|
|
||||||
|
# TODO: this should be `Literal[False]`; see #14279
|
||||||
|
reveal_type(sys.version_info == (3, 9, 1, "finallllll", 0)) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
reveal_type(sys.version_info.releaselevel) # revealed: @Todo
|
||||||
|
reveal_type(sys.version_info.serial) # revealed: @Todo
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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]
|
||||||
|
```
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
# Unary Operations
|
# Invert, UAdd, USub
|
||||||
|
|
||||||
|
## Instance
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
class Number:
|
class Number:
|
||||||
def __init__(self, value: int):
|
def __init__(self, value: int):
|
||||||
self.value = 1
|
self.value = 1
|
||||||
@@ -18,7 +22,7 @@ a = Number()
|
|||||||
|
|
||||||
reveal_type(+a) # revealed: int
|
reveal_type(+a) # revealed: int
|
||||||
reveal_type(-a) # revealed: int
|
reveal_type(-a) # revealed: int
|
||||||
reveal_type(~a) # revealed: @Todo
|
reveal_type(~a) # revealed: Literal[True]
|
||||||
|
|
||||||
class NoDunder: ...
|
class NoDunder: ...
|
||||||
|
|
||||||
@@ -10,8 +10,6 @@ reveal_type(not not None) # revealed: Literal[False]
|
|||||||
## Function
|
## Function
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import reveal_type
|
|
||||||
|
|
||||||
def f():
|
def f():
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@@ -115,3 +113,101 @@ reveal_type(not ()) # revealed: Literal[True]
|
|||||||
reveal_type(not ("hello",)) # revealed: Literal[False]
|
reveal_type(not ("hello",)) # revealed: Literal[False]
|
||||||
reveal_type(not (1, "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,8 +81,7 @@ reveal_type(b) # revealed: Literal[2]
|
|||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: Add diagnostic (need more values to unpack)
|
# TODO: Add diagnostic (need more values to unpack)
|
||||||
# TODO: Remove 'not-iterable' diagnostic
|
[a, *b, c, d] = (1, 2)
|
||||||
[a, *b, c, d] = (1, 2) # error: "Object of type `None` is not iterable"
|
|
||||||
reveal_type(a) # revealed: Literal[1]
|
reveal_type(a) # revealed: Literal[1]
|
||||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||||
reveal_type(b) # revealed: @Todo
|
reveal_type(b) # revealed: @Todo
|
||||||
@@ -93,7 +92,7 @@ reveal_type(d) # revealed: Unknown
|
|||||||
### Starred expression (2)
|
### Starred expression (2)
|
||||||
|
|
||||||
```py
|
```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]
|
reveal_type(a) # revealed: Literal[1]
|
||||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||||
reveal_type(b) # revealed: @Todo
|
reveal_type(b) # revealed: @Todo
|
||||||
@@ -103,8 +102,7 @@ reveal_type(c) # revealed: Literal[2]
|
|||||||
### Starred expression (3)
|
### Starred expression (3)
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: Remove 'not-iterable' diagnostic
|
[a, *b, c] = (1, 2, 3)
|
||||||
[a, *b, c] = (1, 2, 3) # error: "Object of type `None` is not iterable"
|
|
||||||
reveal_type(a) # revealed: Literal[1]
|
reveal_type(a) # revealed: Literal[1]
|
||||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||||
reveal_type(b) # revealed: @Todo
|
reveal_type(b) # revealed: @Todo
|
||||||
@@ -114,8 +112,7 @@ reveal_type(c) # revealed: Literal[3]
|
|||||||
### Starred expression (4)
|
### Starred expression (4)
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: Remove 'not-iterable' diagnostic
|
[a, *b, c, d] = (1, 2, 3, 4, 5, 6)
|
||||||
[a, *b, c, d] = (1, 2, 3, 4, 5, 6) # error: "Object of type `None` is not iterable"
|
|
||||||
reveal_type(a) # revealed: Literal[1]
|
reveal_type(a) # revealed: Literal[1]
|
||||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||||
reveal_type(b) # revealed: @Todo
|
reveal_type(b) # revealed: @Todo
|
||||||
@@ -126,22 +123,29 @@ reveal_type(d) # revealed: Literal[6]
|
|||||||
### Starred expression (5)
|
### Starred expression (5)
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: Remove 'not-iterable' diagnostic
|
[a, b, *c] = (1, 2, 3, 4)
|
||||||
[a, b, *c] = (1, 2, 3, 4) # error: "Object of type `None` is not iterable"
|
|
||||||
reveal_type(a) # revealed: Literal[1]
|
reveal_type(a) # revealed: Literal[1]
|
||||||
reveal_type(b) # revealed: Literal[2]
|
reveal_type(b) # revealed: Literal[2]
|
||||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||||
reveal_type(c) # revealed: @Todo
|
reveal_type(c) # revealed: @Todo
|
||||||
```
|
```
|
||||||
|
|
||||||
### Non-iterable unpacking
|
### Starred expression (6)
|
||||||
|
|
||||||
TODO: Remove duplicate diagnostics. This is happening because for a sequence-like
|
```py
|
||||||
assignment target, multiple definitions are created and the inference engine runs
|
# TODO: Add diagnostic (need more values to unpack)
|
||||||
on each of them which results in duplicate diagnostics.
|
(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
|
||||||
|
reveal_type(e) # revealed: Unknown
|
||||||
|
reveal_type(f) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
### Non-iterable unpacking
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# error: "Object of type `Literal[1]` is not iterable"
|
|
||||||
# error: "Object of type `Literal[1]` is not iterable"
|
# error: "Object of type `Literal[1]` is not iterable"
|
||||||
a, b = 1
|
a, b = 1
|
||||||
reveal_type(a) # revealed: Unknown
|
reveal_type(a) # revealed: Unknown
|
||||||
@@ -215,8 +219,7 @@ reveal_type(b) # revealed: LiteralString
|
|||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: Add diagnostic (need more values to unpack)
|
# TODO: Add diagnostic (need more values to unpack)
|
||||||
# TODO: Remove 'not-iterable' diagnostic
|
(a, *b, c, d) = "ab"
|
||||||
(a, *b, c, d) = "ab" # error: "Object of type `None` is not iterable"
|
|
||||||
reveal_type(a) # revealed: LiteralString
|
reveal_type(a) # revealed: LiteralString
|
||||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||||
reveal_type(b) # revealed: @Todo
|
reveal_type(b) # revealed: @Todo
|
||||||
@@ -227,7 +230,7 @@ reveal_type(d) # revealed: Unknown
|
|||||||
### Starred expression (2)
|
### Starred expression (2)
|
||||||
|
|
||||||
```py
|
```py
|
||||||
(a, *b, c) = "ab" # error: "Object of type `None` is not iterable"
|
(a, *b, c) = "ab"
|
||||||
reveal_type(a) # revealed: LiteralString
|
reveal_type(a) # revealed: LiteralString
|
||||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||||
reveal_type(b) # revealed: @Todo
|
reveal_type(b) # revealed: @Todo
|
||||||
@@ -237,8 +240,7 @@ reveal_type(c) # revealed: LiteralString
|
|||||||
### Starred expression (3)
|
### Starred expression (3)
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: Remove 'not-iterable' diagnostic
|
(a, *b, c) = "abc"
|
||||||
(a, *b, c) = "abc" # error: "Object of type `None` is not iterable"
|
|
||||||
reveal_type(a) # revealed: LiteralString
|
reveal_type(a) # revealed: LiteralString
|
||||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||||
reveal_type(b) # revealed: @Todo
|
reveal_type(b) # revealed: @Todo
|
||||||
@@ -248,8 +250,7 @@ reveal_type(c) # revealed: LiteralString
|
|||||||
### Starred expression (4)
|
### Starred expression (4)
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: Remove 'not-iterable' diagnostic
|
(a, *b, c, d) = "abcdef"
|
||||||
(a, *b, c, d) = "abcdef" # error: "Object of type `None` is not iterable"
|
|
||||||
reveal_type(a) # revealed: LiteralString
|
reveal_type(a) # revealed: LiteralString
|
||||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||||
reveal_type(b) # revealed: @Todo
|
reveal_type(b) # revealed: @Todo
|
||||||
@@ -260,8 +261,7 @@ reveal_type(d) # revealed: LiteralString
|
|||||||
### Starred expression (5)
|
### Starred expression (5)
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: Remove 'not-iterable' diagnostic
|
(a, b, *c) = "abcd"
|
||||||
(a, b, *c) = "abcd" # error: "Object of type `None` is not iterable"
|
|
||||||
reveal_type(a) # revealed: LiteralString
|
reveal_type(a) # revealed: LiteralString
|
||||||
reveal_type(b) # revealed: LiteralString
|
reveal_type(b) # revealed: LiteralString
|
||||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
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;
|
mod semantic_model;
|
||||||
pub(crate) mod site_packages;
|
pub(crate) mod site_packages;
|
||||||
mod stdlib;
|
mod stdlib;
|
||||||
|
pub(crate) mod symbol;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
mod unpack;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
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();
|
let parsed_versions = TypeshedVersions::from_str(VERSIONS).unwrap();
|
||||||
assert_eq!(parsed_versions.len(), 3);
|
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: 2.7-3.10
|
||||||
bar.baz: 3.1-3.9
|
bar.baz: 3.1-3.9
|
||||||
foo: 3.8-
|
foo: 3.8-
|
||||||
"###
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use ruff_python_ast::{AnyNodeRef, NodeKind};
|
use ruff_python_ast::AnyNodeRef;
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
|
||||||
|
|
||||||
/// Compact key for a node for use in a hash map.
|
/// 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)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
pub(super) struct NodeKey {
|
pub(super) struct NodeKey(usize);
|
||||||
kind: NodeKind,
|
|
||||||
range: TextRange,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NodeKey {
|
impl NodeKey {
|
||||||
pub(super) fn from_node<'a, N>(node: N) -> Self
|
pub(super) fn from_node<'a, N>(node: N) -> Self
|
||||||
@@ -16,9 +16,6 @@ impl NodeKey {
|
|||||||
N: Into<AnyNodeRef<'a>>,
|
N: Into<AnyNodeRef<'a>>,
|
||||||
{
|
{
|
||||||
let node = node.into();
|
let node = node.into();
|
||||||
NodeKey {
|
NodeKey(node.as_ptr().as_ptr() as usize)
|
||||||
kind: node.kind(),
|
|
||||||
range: node.range(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ impl Program {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
pub struct ProgramSettings {
|
pub struct ProgramSettings {
|
||||||
pub target_version: PythonVersion,
|
pub target_version: PythonVersion,
|
||||||
pub search_paths: SearchPathSettings,
|
pub search_paths: SearchPathSettings,
|
||||||
@@ -61,6 +62,7 @@ pub struct ProgramSettings {
|
|||||||
|
|
||||||
/// Configures the search paths for module resolution.
|
/// Configures the search paths for module resolution.
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Eq, PartialEq, Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
pub struct SearchPathSettings {
|
pub struct SearchPathSettings {
|
||||||
/// List of user-provided paths that should take first priority in the module resolution.
|
/// 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,
|
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
|
||||||
@@ -91,6 +93,7 @@ impl SearchPathSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
pub enum SitePackages {
|
pub enum SitePackages {
|
||||||
Derived {
|
Derived {
|
||||||
venv_path: SystemPathBuf,
|
venv_path: SystemPathBuf,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::fmt;
|
|||||||
/// Unlike the `TargetVersion` enums in the CLI crates,
|
/// Unlike the `TargetVersion` enums in the CLI crates,
|
||||||
/// this does not necessarily represent a Python version that we actually support.
|
/// this does not necessarily represent a Python version that we actually support.
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
pub struct PythonVersion {
|
pub struct PythonVersion {
|
||||||
pub major: u8,
|
pub major: u8,
|
||||||
pub minor: u8,
|
pub minor: u8,
|
||||||
@@ -38,7 +39,7 @@ impl PythonVersion {
|
|||||||
|
|
||||||
impl Default for PythonVersion {
|
impl Default for PythonVersion {
|
||||||
fn default() -> Self {
|
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
|
/// Use the Salsa cached [`symbol_table()`] query if you only need the
|
||||||
/// symbol table for a single scope.
|
/// symbol table for a single scope.
|
||||||
|
#[track_caller]
|
||||||
pub(super) fn symbol_table(&self, scope_id: FileScopeId) -> Arc<SymbolTable> {
|
pub(super) fn symbol_table(&self, scope_id: FileScopeId) -> Arc<SymbolTable> {
|
||||||
self.symbol_tables[scope_id].clone()
|
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 the Salsa cached [`use_def_map()`] query if you only need the
|
||||||
/// use-def map for a single scope.
|
/// use-def map for a single scope.
|
||||||
|
#[track_caller]
|
||||||
pub(super) fn use_def_map(&self, scope_id: FileScopeId) -> Arc<UseDefMap> {
|
pub(super) fn use_def_map(&self, scope_id: FileScopeId) -> Arc<UseDefMap> {
|
||||||
self.use_def_maps[scope_id].clone()
|
self.use_def_maps[scope_id].clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub(crate) fn ast_ids(&self, scope_id: FileScopeId) -> &AstIds {
|
pub(crate) fn ast_ids(&self, scope_id: FileScopeId) -> &AstIds {
|
||||||
&self.ast_ids[scope_id]
|
&self.ast_ids[scope_id]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the ID of the `expression`'s enclosing scope.
|
/// Returns the ID of the `expression`'s enclosing scope.
|
||||||
|
#[track_caller]
|
||||||
pub(crate) fn expression_scope_id(
|
pub(crate) fn expression_scope_id(
|
||||||
&self,
|
&self,
|
||||||
expression: impl Into<ExpressionNodeKey>,
|
expression: impl Into<ExpressionNodeKey>,
|
||||||
@@ -151,11 +155,13 @@ impl<'db> SemanticIndex<'db> {
|
|||||||
|
|
||||||
/// Returns the [`Scope`] of the `expression`'s enclosing scope.
|
/// Returns the [`Scope`] of the `expression`'s enclosing scope.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
#[track_caller]
|
||||||
pub(crate) fn expression_scope(&self, expression: impl Into<ExpressionNodeKey>) -> &Scope {
|
pub(crate) fn expression_scope(&self, expression: impl Into<ExpressionNodeKey>) -> &Scope {
|
||||||
&self.scopes[self.expression_scope_id(expression)]
|
&self.scopes[self.expression_scope_id(expression)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the [`Scope`] with the given id.
|
/// Returns the [`Scope`] with the given id.
|
||||||
|
#[track_caller]
|
||||||
pub(crate) fn scope(&self, id: FileScopeId) -> &Scope {
|
pub(crate) fn scope(&self, id: FileScopeId) -> &Scope {
|
||||||
&self.scopes[id]
|
&self.scopes[id]
|
||||||
}
|
}
|
||||||
@@ -172,6 +178,7 @@ impl<'db> SemanticIndex<'db> {
|
|||||||
|
|
||||||
/// Returns the parent scope of `scope_id`.
|
/// Returns the parent scope of `scope_id`.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
#[track_caller]
|
||||||
pub(crate) fn parent_scope(&self, scope_id: FileScopeId) -> Option<&Scope> {
|
pub(crate) fn parent_scope(&self, scope_id: FileScopeId) -> Option<&Scope> {
|
||||||
Some(&self.scopes[self.parent_scope_id(scope_id)?])
|
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`.
|
/// Returns the [`Definition`] salsa ingredient for `definition_key`.
|
||||||
|
#[track_caller]
|
||||||
pub(crate) fn definition(
|
pub(crate) fn definition(
|
||||||
&self,
|
&self,
|
||||||
definition_key: impl Into<DefinitionNodeKey>,
|
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
|
/// 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
|
/// standalone-inferable expressions, which we call `add_standalone_expression` for in
|
||||||
/// [`SemanticIndexBuilder`].
|
/// [`SemanticIndexBuilder`].
|
||||||
|
#[track_caller]
|
||||||
pub(crate) fn expression(
|
pub(crate) fn expression(
|
||||||
&self,
|
&self,
|
||||||
expression_key: impl Into<ExpressionNodeKey>,
|
expression_key: impl Into<ExpressionNodeKey>,
|
||||||
@@ -213,8 +222,18 @@ impl<'db> SemanticIndex<'db> {
|
|||||||
self.expressions_by_node[&expression_key.into()]
|
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 id of the scope that `node` creates. This is different from [`Definition::scope`] which
|
||||||
/// returns the scope in which that definition is defined in.
|
/// returns the scope in which that definition is defined in.
|
||||||
|
#[track_caller]
|
||||||
pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId {
|
pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId {
|
||||||
self.scopes_by_node[&node.node_key()]
|
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))
|
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`].
|
/// Uniquely identifies a use of a name in a [`crate::semantic_index::symbol::FileScopeId`].
|
||||||
#[newtype_index]
|
#[newtype_index]
|
||||||
pub struct ScopedUseId;
|
pub struct ScopedUseId;
|
||||||
|
|
||||||
impl HasScopedUseId for ast::ExprName {
|
pub trait HasScopedUseId {
|
||||||
type Id = ScopedUseId;
|
/// 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);
|
let expression_ref = ExpressionRef::from(self);
|
||||||
expression_ref.scoped_use_id(db, scope)
|
expression_ref.scoped_use_id(db, scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasScopedUseId for ast::ExpressionRef<'_> {
|
impl HasScopedUseId for ast::ExpressionRef<'_> {
|
||||||
type Id = ScopedUseId;
|
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
|
||||||
|
|
||||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
|
|
||||||
let ast_ids = ast_ids(db, scope);
|
let ast_ids = ast_ids(db, scope);
|
||||||
ast_ids.use_id(*self)
|
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`].
|
/// Uniquely identifies an [`ast::Expr`] in a [`crate::semantic_index::symbol::FileScopeId`].
|
||||||
#[newtype_index]
|
#[newtype_index]
|
||||||
pub struct ScopedExpressionId;
|
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 {
|
macro_rules! impl_has_scoped_expression_id {
|
||||||
($ty: ty) => {
|
($ty: ty) => {
|
||||||
impl HasScopedAstId for $ty {
|
impl HasScopedExpressionId for $ty {
|
||||||
type Id = ScopedExpressionId;
|
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
|
||||||
|
|
||||||
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
|
|
||||||
let expression_ref = ExpressionRef::from(self);
|
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::ExprIpyEscapeCommand);
|
||||||
impl_has_scoped_expression_id!(ast::Expr);
|
impl_has_scoped_expression_id!(ast::Expr);
|
||||||
|
|
||||||
impl HasScopedAstId for ast::ExpressionRef<'_> {
|
impl HasScopedExpressionId for ast::ExpressionRef<'_> {
|
||||||
type Id = ScopedExpressionId;
|
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
|
||||||
|
|
||||||
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
|
|
||||||
let ast_ids = ast_ids(db, scope);
|
let ast_ids = ast_ids(db, scope);
|
||||||
ast_ids.expression_id(*self)
|
ast_ids.expression_id(*self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub(super) struct AstIdsBuilder {
|
pub(super) struct AstIdsBuilder {
|
||||||
expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>,
|
expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>,
|
||||||
uses_map: FxHashMap<ExpressionNodeKey, ScopedUseId>,
|
uses_map: FxHashMap<ExpressionNodeKey, ScopedUseId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AstIdsBuilder {
|
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.
|
/// Adds `expr` to the expression ids map and returns its id.
|
||||||
pub(super) fn record_expression(&mut self, expr: &ast::Expr) -> ScopedExpressionId {
|
pub(super) fn record_expression(&mut self, expr: &ast::Expr) -> ScopedExpressionId {
|
||||||
let expression_id = self.expressions_map.len().into();
|
let expression_id = self.expressions_map.len().into();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use ruff_index::IndexVec;
|
|||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
use ruff_python_ast::name::Name;
|
use ruff_python_ast::name::Name;
|
||||||
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
|
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
|
||||||
use ruff_python_ast::AnyParameterRef;
|
use ruff_python_ast::{AnyParameterRef, BoolOp, Expr};
|
||||||
|
|
||||||
use crate::ast_node_ref::AstNodeRef;
|
use crate::ast_node_ref::AstNodeRef;
|
||||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||||
@@ -25,12 +25,13 @@ use crate::semantic_index::symbol::{
|
|||||||
};
|
};
|
||||||
use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
|
use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
|
||||||
use crate::semantic_index::SemanticIndex;
|
use crate::semantic_index::SemanticIndex;
|
||||||
|
use crate::unpack::Unpack;
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
|
|
||||||
use super::constraint::{Constraint, PatternConstraint};
|
use super::constraint::{Constraint, ConstraintNode, PatternConstraint};
|
||||||
use super::definition::{
|
use super::definition::{
|
||||||
AssignmentKind, DefinitionCategory, ExceptHandlerDefinitionNodeRef,
|
DefinitionCategory, ExceptHandlerDefinitionNodeRef, MatchPatternDefinitionNodeRef,
|
||||||
MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
|
WithItemDefinitionNodeRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod except_handlers;
|
mod except_handlers;
|
||||||
@@ -46,6 +47,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
|
|||||||
current_assignments: Vec<CurrentAssignment<'db>>,
|
current_assignments: Vec<CurrentAssignment<'db>>,
|
||||||
/// The match case we're currently visiting.
|
/// The match case we're currently visiting.
|
||||||
current_match_case: Option<CurrentMatchCase<'db>>,
|
current_match_case: Option<CurrentMatchCase<'db>>,
|
||||||
|
|
||||||
/// Flow states at each `break` in the current loop.
|
/// Flow states at each `break` in the current loop.
|
||||||
loop_break_states: Vec<FlowSnapshot>,
|
loop_break_states: Vec<FlowSnapshot>,
|
||||||
/// Per-scope contexts regarding nested `try`/`except` statements
|
/// Per-scope contexts regarding nested `try`/`except` statements
|
||||||
@@ -112,27 +114,21 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
fn push_scope_with_parent(&mut self, node: NodeWithScopeRef, parent: Option<FileScopeId>) {
|
fn push_scope_with_parent(&mut self, node: NodeWithScopeRef, parent: Option<FileScopeId>) {
|
||||||
let children_start = self.scopes.next_index() + 1;
|
let children_start = self.scopes.next_index() + 1;
|
||||||
|
|
||||||
|
#[allow(unsafe_code)]
|
||||||
let scope = Scope {
|
let scope = Scope {
|
||||||
parent,
|
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,
|
descendents: children_start..children_start,
|
||||||
};
|
};
|
||||||
self.try_node_context_stack_manager.enter_nested_scope();
|
self.try_node_context_stack_manager.enter_nested_scope();
|
||||||
|
|
||||||
let file_scope_id = self.scopes.push(scope);
|
let file_scope_id = self.scopes.push(scope);
|
||||||
self.symbol_tables.push(SymbolTableBuilder::new());
|
self.symbol_tables.push(SymbolTableBuilder::default());
|
||||||
self.use_def_maps.push(UseDefMapBuilder::new());
|
self.use_def_maps.push(UseDefMapBuilder::default());
|
||||||
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::new());
|
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::default());
|
||||||
|
|
||||||
#[allow(unsafe_code)]
|
let scope_id = ScopeId::new(self.db, self.file, file_scope_id, countme::Count::default());
|
||||||
// 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(),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.scope_ids_by_scope.push(scope_id);
|
self.scope_ids_by_scope.push(scope_id);
|
||||||
self.scopes_by_node.insert(node.node_key(), file_scope_id);
|
self.scopes_by_node.insert(node.node_key(), file_scope_id);
|
||||||
@@ -195,14 +191,18 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
self.current_symbol_table().mark_symbol_bound(id);
|
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) {
|
fn mark_symbol_used(&mut self, id: ScopedSymbolId) {
|
||||||
self.current_symbol_table().mark_symbol_used(id);
|
self.current_symbol_table().mark_symbol_used(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_definition<'a>(
|
fn add_definition(
|
||||||
&mut self,
|
&mut self,
|
||||||
symbol: ScopedSymbolId,
|
symbol: ScopedSymbolId,
|
||||||
definition_node: impl Into<DefinitionNodeRef<'a>>,
|
definition_node: impl Into<DefinitionNodeRef<'db>>,
|
||||||
) -> Definition<'db> {
|
) -> Definition<'db> {
|
||||||
let definition_node: DefinitionNodeRef<'_> = definition_node.into();
|
let definition_node: DefinitionNodeRef<'_> = definition_node.into();
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
@@ -226,6 +226,9 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
if category.is_binding() {
|
if category.is_binding() {
|
||||||
self.mark_symbol_bound(symbol);
|
self.mark_symbol_bound(symbol);
|
||||||
}
|
}
|
||||||
|
if category.is_declaration() {
|
||||||
|
self.mark_symbol_declared(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
let use_def = self.current_use_def_map_mut();
|
let use_def = self.current_use_def_map_mut();
|
||||||
match category {
|
match category {
|
||||||
@@ -243,12 +246,30 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
definition
|
definition
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_expression_constraint(&mut self, constraint_node: &ast::Expr) -> Expression<'db> {
|
fn record_expression_constraint(&mut self, constraint_node: &ast::Expr) -> Constraint<'db> {
|
||||||
let expression = self.add_standalone_expression(constraint_node);
|
let constraint = self.build_constraint(constraint_node);
|
||||||
self.current_use_def_map_mut()
|
self.record_constraint(constraint);
|
||||||
.record_constraint(Constraint::Expression(expression));
|
constraint
|
||||||
|
}
|
||||||
|
|
||||||
expression
|
fn record_constraint(&mut self, constraint: Constraint<'db>) {
|
||||||
|
self.current_use_def_map_mut().record_constraint(constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_constraint(&mut self, constraint_node: &Expr) -> Constraint<'db> {
|
||||||
|
let expression = self.add_standalone_expression(constraint_node);
|
||||||
|
Constraint {
|
||||||
|
node: ConstraintNode::Expression(expression),
|
||||||
|
is_positive: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_negated_constraint(&mut self, constraint: Constraint<'db>) {
|
||||||
|
self.current_use_def_map_mut()
|
||||||
|
.record_constraint(Constraint {
|
||||||
|
node: constraint.node,
|
||||||
|
is_positive: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_assignment(&mut self, assignment: CurrentAssignment<'db>) {
|
fn push_assignment(&mut self, assignment: CurrentAssignment<'db>) {
|
||||||
@@ -260,8 +281,12 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
debug_assert!(popped_assignment.is_some());
|
debug_assert!(popped_assignment.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_assignment(&self) -> Option<&CurrentAssignment<'db>> {
|
fn current_assignment(&self) -> Option<CurrentAssignment<'db>> {
|
||||||
self.current_assignments.last()
|
self.current_assignments.last().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_assignment_mut(&mut self) -> Option<&mut CurrentAssignment<'db>> {
|
||||||
|
self.current_assignments.last_mut()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_pattern_constraint(
|
fn add_pattern_constraint(
|
||||||
@@ -285,7 +310,10 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
countme::Count::default(),
|
countme::Count::default(),
|
||||||
);
|
);
|
||||||
self.current_use_def_map_mut()
|
self.current_use_def_map_mut()
|
||||||
.record_constraint(Constraint::Pattern(pattern_constraint));
|
.record_constraint(Constraint {
|
||||||
|
node: ConstraintNode::Pattern(pattern_constraint),
|
||||||
|
is_positive: true,
|
||||||
|
});
|
||||||
pattern_constraint
|
pattern_constraint
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,12 +366,18 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
// note that the "bound" on the typevar is a totally different thing than whether
|
// 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.
|
// or not a name is "bound" by a typevar declaration; the latter is always true.
|
||||||
self.mark_symbol_bound(symbol);
|
self.mark_symbol_bound(symbol);
|
||||||
|
self.mark_symbol_declared(symbol);
|
||||||
if let Some(bounds) = bound {
|
if let Some(bounds) = bound {
|
||||||
self.visit_expr(bounds);
|
self.visit_expr(bounds);
|
||||||
}
|
}
|
||||||
if let Some(default) = default {
|
if let Some(default) = default {
|
||||||
self.visit_expr(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),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +450,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
self.pop_scope();
|
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 symbol = self.add_symbol(parameter.name().id().clone());
|
||||||
|
|
||||||
let definition = self.add_definition(symbol, parameter);
|
let definition = self.add_definition(symbol, parameter);
|
||||||
@@ -590,24 +624,48 @@ where
|
|||||||
}
|
}
|
||||||
ast::Stmt::Assign(node) => {
|
ast::Stmt::Assign(node) => {
|
||||||
debug_assert_eq!(&self.current_assignments, &[]);
|
debug_assert_eq!(&self.current_assignments, &[]);
|
||||||
|
|
||||||
self.visit_expr(&node.value);
|
self.visit_expr(&node.value);
|
||||||
self.add_standalone_expression(&node.value);
|
let value = self.add_standalone_expression(&node.value);
|
||||||
for (target_index, target) in node.targets.iter().enumerate() {
|
|
||||||
let kind = match target {
|
for target in &node.targets {
|
||||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => Some(AssignmentKind::Sequence),
|
// We only handle assignments to names and unpackings here, other targets like
|
||||||
ast::Expr::Name(_) => Some(AssignmentKind::Name),
|
// 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,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(kind) = kind {
|
|
||||||
self.push_assignment(CurrentAssignment::Assign {
|
if let Some(current_assignment) = current_assignment {
|
||||||
assignment: node,
|
self.push_assignment(current_assignment);
|
||||||
target_index,
|
|
||||||
kind,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.visit_expr(target);
|
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();
|
self.pop_assignment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -618,9 +676,18 @@ where
|
|||||||
if let Some(value) = &node.value {
|
if let Some(value) = &node.value {
|
||||||
self.visit_expr(value);
|
self.visit_expr(value);
|
||||||
}
|
}
|
||||||
self.push_assignment(node.into());
|
|
||||||
self.visit_expr(&node.target);
|
// See https://docs.python.org/3/library/ast.html#ast.AnnAssign
|
||||||
self.pop_assignment();
|
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(
|
ast::Stmt::AugAssign(
|
||||||
aug_assign @ ast::StmtAugAssign {
|
aug_assign @ ast::StmtAugAssign {
|
||||||
@@ -632,14 +699,24 @@ where
|
|||||||
) => {
|
) => {
|
||||||
debug_assert_eq!(&self.current_assignments, &[]);
|
debug_assert_eq!(&self.current_assignments, &[]);
|
||||||
self.visit_expr(value);
|
self.visit_expr(value);
|
||||||
self.push_assignment(aug_assign.into());
|
|
||||||
self.visit_expr(target);
|
// See https://docs.python.org/3/library/ast.html#ast.AugAssign
|
||||||
self.pop_assignment();
|
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) => {
|
ast::Stmt::If(node) => {
|
||||||
self.visit_expr(&node.test);
|
self.visit_expr(&node.test);
|
||||||
let pre_if = self.flow_snapshot();
|
let pre_if = self.flow_snapshot();
|
||||||
self.add_expression_constraint(&node.test);
|
let constraint = self.record_expression_constraint(&node.test);
|
||||||
|
let mut constraints = vec![constraint];
|
||||||
self.visit_body(&node.body);
|
self.visit_body(&node.body);
|
||||||
let mut post_clauses: Vec<FlowSnapshot> = vec![];
|
let mut post_clauses: Vec<FlowSnapshot> = vec![];
|
||||||
for clause in &node.elif_else_clauses {
|
for clause in &node.elif_else_clauses {
|
||||||
@@ -649,7 +726,14 @@ where
|
|||||||
// we can only take an elif/else branch if none of the previous ones were
|
// we can only take an elif/else branch if none of the previous ones were
|
||||||
// taken, so the block entry state is always `pre_if`
|
// taken, so the block entry state is always `pre_if`
|
||||||
self.flow_restore(pre_if.clone());
|
self.flow_restore(pre_if.clone());
|
||||||
self.visit_elif_else_clause(clause);
|
for constraint in &constraints {
|
||||||
|
self.record_negated_constraint(*constraint);
|
||||||
|
}
|
||||||
|
if let Some(elif_test) = &clause.test {
|
||||||
|
self.visit_expr(elif_test);
|
||||||
|
constraints.push(self.record_expression_constraint(elif_test));
|
||||||
|
}
|
||||||
|
self.visit_body(&clause.body);
|
||||||
}
|
}
|
||||||
for post_clause_state in post_clauses {
|
for post_clause_state in post_clauses {
|
||||||
self.flow_merge(post_clause_state);
|
self.flow_merge(post_clause_state);
|
||||||
@@ -697,12 +781,20 @@ where
|
|||||||
self.flow_merge(break_state);
|
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 {
|
for item in items {
|
||||||
self.visit_expr(&item.context_expr);
|
self.visit_expr(&item.context_expr);
|
||||||
if let Some(optional_vars) = item.optional_vars.as_deref() {
|
if let Some(optional_vars) = item.optional_vars.as_deref() {
|
||||||
self.add_standalone_expression(&item.context_expr);
|
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.visit_expr(optional_vars);
|
||||||
self.pop_assignment();
|
self.pop_assignment();
|
||||||
}
|
}
|
||||||
@@ -918,20 +1010,26 @@ where
|
|||||||
};
|
};
|
||||||
let symbol = self.add_symbol(id.clone());
|
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 {
|
if is_definition {
|
||||||
match self.current_assignment().copied() {
|
match self.current_assignment() {
|
||||||
Some(CurrentAssignment::Assign {
|
Some(CurrentAssignment::Assign {
|
||||||
assignment,
|
node,
|
||||||
target_index,
|
first,
|
||||||
kind,
|
unpack,
|
||||||
}) => {
|
}) => {
|
||||||
self.add_definition(
|
self.add_definition(
|
||||||
symbol,
|
symbol,
|
||||||
AssignmentDefinitionNodeRef {
|
AssignmentDefinitionNodeRef {
|
||||||
assignment,
|
unpack,
|
||||||
target_index,
|
value: &node.value,
|
||||||
name: name_node,
|
name: name_node,
|
||||||
kind,
|
first,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -968,12 +1066,13 @@ where
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(CurrentAssignment::WithItem(with_item)) => {
|
Some(CurrentAssignment::WithItem { item, is_async }) => {
|
||||||
self.add_definition(
|
self.add_definition(
|
||||||
symbol,
|
symbol,
|
||||||
WithItemDefinitionNodeRef {
|
WithItemDefinitionNodeRef {
|
||||||
node: with_item,
|
node: item,
|
||||||
target: name_node,
|
target: name_node,
|
||||||
|
is_async,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -981,10 +1080,9 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_use {
|
if let Some(CurrentAssignment::Assign { first, .. }) = self.current_assignment_mut()
|
||||||
self.mark_symbol_used(symbol);
|
{
|
||||||
let use_id = self.current_ast_ids().record_use(expr);
|
*first = false;
|
||||||
self.current_use_def_map_mut().record_use(symbol, use_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
walk_expr(self, expr);
|
walk_expr(self, expr);
|
||||||
@@ -992,9 +1090,15 @@ where
|
|||||||
ast::Expr::Named(node) => {
|
ast::Expr::Named(node) => {
|
||||||
// TODO walrus in comprehensions is implicitly nonlocal
|
// TODO walrus in comprehensions is implicitly nonlocal
|
||||||
self.visit_expr(&node.value);
|
self.visit_expr(&node.value);
|
||||||
self.push_assignment(node.into());
|
|
||||||
self.visit_expr(&node.target);
|
// See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements
|
||||||
self.pop_assignment();
|
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) => {
|
ast::Expr::Lambda(lambda) => {
|
||||||
if let Some(parameters) = &lambda.parameters {
|
if let Some(parameters) = &lambda.parameters {
|
||||||
@@ -1027,10 +1131,13 @@ where
|
|||||||
// AST inspection, so we can't simplify here, need to record test expression for
|
// AST inspection, so we can't simplify here, need to record test expression for
|
||||||
// later checking)
|
// later checking)
|
||||||
self.visit_expr(test);
|
self.visit_expr(test);
|
||||||
|
let constraint = self.record_expression_constraint(test);
|
||||||
let pre_if = self.flow_snapshot();
|
let pre_if = self.flow_snapshot();
|
||||||
self.visit_expr(body);
|
self.visit_expr(body);
|
||||||
let post_body = self.flow_snapshot();
|
let post_body = self.flow_snapshot();
|
||||||
self.flow_restore(pre_if);
|
self.flow_restore(pre_if);
|
||||||
|
|
||||||
|
self.record_negated_constraint(constraint);
|
||||||
self.visit_expr(orelse);
|
self.visit_expr(orelse);
|
||||||
self.flow_merge(post_body);
|
self.flow_merge(post_body);
|
||||||
}
|
}
|
||||||
@@ -1084,6 +1191,33 @@ where
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
ast::Expr::BoolOp(ast::ExprBoolOp {
|
||||||
|
values,
|
||||||
|
range: _,
|
||||||
|
op,
|
||||||
|
}) => {
|
||||||
|
// TODO detect statically known truthy or falsy values (via type inference, not naive
|
||||||
|
// AST inspection, so we can't simplify here, need to record test expression for
|
||||||
|
// later checking)
|
||||||
|
let mut snapshots = vec![];
|
||||||
|
|
||||||
|
for (index, value) in values.iter().enumerate() {
|
||||||
|
self.visit_expr(value);
|
||||||
|
// In the last value we don't need to take a snapshot nor add a constraint
|
||||||
|
if index < values.len() - 1 {
|
||||||
|
// Snapshot is taken after visiting the expression but before adding the constraint.
|
||||||
|
snapshots.push(self.flow_snapshot());
|
||||||
|
let constraint = self.build_constraint(value);
|
||||||
|
match op {
|
||||||
|
BoolOp::And => self.record_constraint(constraint),
|
||||||
|
BoolOp::Or => self.record_negated_constraint(constraint),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for snapshot in snapshots {
|
||||||
|
self.flow_merge(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
walk_expr(self, expr);
|
walk_expr(self, expr);
|
||||||
}
|
}
|
||||||
@@ -1156,9 +1290,9 @@ where
|
|||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
enum CurrentAssignment<'a> {
|
enum CurrentAssignment<'a> {
|
||||||
Assign {
|
Assign {
|
||||||
assignment: &'a ast::StmtAssign,
|
node: &'a ast::StmtAssign,
|
||||||
target_index: usize,
|
first: bool,
|
||||||
kind: AssignmentKind,
|
unpack: Option<Unpack<'a>>,
|
||||||
},
|
},
|
||||||
AnnAssign(&'a ast::StmtAnnAssign),
|
AnnAssign(&'a ast::StmtAnnAssign),
|
||||||
AugAssign(&'a ast::StmtAugAssign),
|
AugAssign(&'a ast::StmtAugAssign),
|
||||||
@@ -1168,7 +1302,10 @@ enum CurrentAssignment<'a> {
|
|||||||
node: &'a ast::Comprehension,
|
node: &'a ast::Comprehension,
|
||||||
first: bool,
|
first: bool,
|
||||||
},
|
},
|
||||||
WithItem(&'a ast::WithItem),
|
WithItem {
|
||||||
|
item: &'a ast::WithItem,
|
||||||
|
is_async: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a ast::StmtAnnAssign> for CurrentAssignment<'a> {
|
impl<'a> From<&'a ast::StmtAnnAssign> for CurrentAssignment<'a> {
|
||||||
@@ -1195,12 +1332,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> {
|
struct CurrentMatchCase<'a> {
|
||||||
/// The pattern that's part of the current match case.
|
/// The pattern that's part of the current match case.
|
||||||
pattern: &'a ast::Pattern,
|
pattern: &'a ast::Pattern,
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ use crate::semantic_index::expression::Expression;
|
|||||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
|
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub(crate) enum Constraint<'db> {
|
pub(crate) struct Constraint<'db> {
|
||||||
|
pub(crate) node: ConstraintNode<'db>,
|
||||||
|
pub(crate) is_positive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum ConstraintNode<'db> {
|
||||||
Expression(Expression<'db>),
|
Expression(Expression<'db>),
|
||||||
Pattern(PatternConstraint<'db>),
|
Pattern(PatternConstraint<'db>),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,22 @@ use crate::ast_node_ref::AstNodeRef;
|
|||||||
use crate::module_resolver::file_to_module;
|
use crate::module_resolver::file_to_module;
|
||||||
use crate::node_key::NodeKey;
|
use crate::node_key::NodeKey;
|
||||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId};
|
use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId};
|
||||||
|
use crate::unpack::Unpack;
|
||||||
use crate::Db;
|
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]
|
#[salsa::tracked]
|
||||||
pub struct Definition<'db> {
|
pub struct Definition<'db> {
|
||||||
/// The file in which the definition occurs.
|
/// The file in which the definition occurs.
|
||||||
@@ -24,7 +38,7 @@ pub struct Definition<'db> {
|
|||||||
|
|
||||||
#[no_eq]
|
#[no_eq]
|
||||||
#[return_ref]
|
#[return_ref]
|
||||||
pub(crate) kind: DefinitionKind,
|
pub(crate) kind: DefinitionKind<'db>,
|
||||||
|
|
||||||
#[no_eq]
|
#[no_eq]
|
||||||
count: countme::Count<Definition<'static>>,
|
count: countme::Count<Definition<'static>>,
|
||||||
@@ -78,6 +92,9 @@ pub(crate) enum DefinitionNodeRef<'a> {
|
|||||||
WithItem(WithItemDefinitionNodeRef<'a>),
|
WithItem(WithItemDefinitionNodeRef<'a>),
|
||||||
MatchPattern(MatchPatternDefinitionNodeRef<'a>),
|
MatchPattern(MatchPatternDefinitionNodeRef<'a>),
|
||||||
ExceptHandler(ExceptHandlerDefinitionNodeRef<'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> {
|
impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> {
|
||||||
@@ -116,6 +133,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> {
|
impl<'a> From<ImportFromDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||||
fn from(node_ref: ImportFromDefinitionNodeRef<'a>) -> Self {
|
fn from(node_ref: ImportFromDefinitionNodeRef<'a>) -> Self {
|
||||||
Self::ImportFrom(node_ref)
|
Self::ImportFrom(node_ref)
|
||||||
@@ -166,16 +201,17 @@ pub(crate) struct ImportFromDefinitionNodeRef<'a> {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub(crate) struct AssignmentDefinitionNodeRef<'a> {
|
pub(crate) struct AssignmentDefinitionNodeRef<'a> {
|
||||||
pub(crate) assignment: &'a ast::StmtAssign,
|
pub(crate) unpack: Option<Unpack<'a>>,
|
||||||
pub(crate) target_index: usize,
|
pub(crate) value: &'a ast::Expr,
|
||||||
pub(crate) name: &'a ast::ExprName,
|
pub(crate) name: &'a ast::ExprName,
|
||||||
pub(crate) kind: AssignmentKind,
|
pub(crate) first: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub(crate) struct WithItemDefinitionNodeRef<'a> {
|
pub(crate) struct WithItemDefinitionNodeRef<'a> {
|
||||||
pub(crate) node: &'a ast::WithItem,
|
pub(crate) node: &'a ast::WithItem,
|
||||||
pub(crate) target: &'a ast::ExprName,
|
pub(crate) target: &'a ast::ExprName,
|
||||||
|
pub(crate) is_async: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
@@ -210,9 +246,9 @@ pub(crate) struct MatchPatternDefinitionNodeRef<'a> {
|
|||||||
pub(crate) index: u32,
|
pub(crate) index: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DefinitionNodeRef<'_> {
|
impl<'db> DefinitionNodeRef<'db> {
|
||||||
#[allow(unsafe_code)]
|
#[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 {
|
match self {
|
||||||
DefinitionNodeRef::Import(alias) => {
|
DefinitionNodeRef::Import(alias) => {
|
||||||
DefinitionKind::Import(AstNodeRef::new(parsed, alias))
|
DefinitionKind::Import(AstNodeRef::new(parsed, alias))
|
||||||
@@ -233,15 +269,15 @@ impl DefinitionNodeRef<'_> {
|
|||||||
DefinitionKind::NamedExpression(AstNodeRef::new(parsed, named))
|
DefinitionKind::NamedExpression(AstNodeRef::new(parsed, named))
|
||||||
}
|
}
|
||||||
DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef {
|
DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef {
|
||||||
assignment,
|
unpack,
|
||||||
target_index,
|
value,
|
||||||
name,
|
name,
|
||||||
kind,
|
first,
|
||||||
}) => DefinitionKind::Assignment(AssignmentDefinitionKind {
|
}) => DefinitionKind::Assignment(AssignmentDefinitionKind {
|
||||||
assignment: AstNodeRef::new(parsed.clone(), assignment),
|
target: TargetKind::from(unpack),
|
||||||
target_index,
|
value: AstNodeRef::new(parsed.clone(), value),
|
||||||
name: AstNodeRef::new(parsed, name),
|
name: AstNodeRef::new(parsed, name),
|
||||||
kind,
|
first,
|
||||||
}),
|
}),
|
||||||
DefinitionNodeRef::AnnotatedAssignment(assign) => {
|
DefinitionNodeRef::AnnotatedAssignment(assign) => {
|
||||||
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
|
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
|
||||||
@@ -277,12 +313,15 @@ impl DefinitionNodeRef<'_> {
|
|||||||
DefinitionKind::ParameterWithDefault(AstNodeRef::new(parsed, parameter))
|
DefinitionKind::ParameterWithDefault(AstNodeRef::new(parsed, parameter))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef { node, target }) => {
|
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef {
|
||||||
DefinitionKind::WithItem(WithItemDefinitionKind {
|
node,
|
||||||
node: AstNodeRef::new(parsed.clone(), node),
|
target,
|
||||||
target: AstNodeRef::new(parsed, target),
|
is_async,
|
||||||
})
|
}) => DefinitionKind::WithItem(WithItemDefinitionKind {
|
||||||
}
|
node: AstNodeRef::new(parsed.clone(), node),
|
||||||
|
target: AstNodeRef::new(parsed, target),
|
||||||
|
is_async,
|
||||||
|
}),
|
||||||
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
|
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
|
||||||
pattern,
|
pattern,
|
||||||
identifier,
|
identifier,
|
||||||
@@ -299,6 +338,15 @@ impl DefinitionNodeRef<'_> {
|
|||||||
handler: AstNodeRef::new(parsed, handler),
|
handler: AstNodeRef::new(parsed, handler),
|
||||||
is_star,
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,10 +360,10 @@ impl DefinitionNodeRef<'_> {
|
|||||||
Self::Class(node) => node.into(),
|
Self::Class(node) => node.into(),
|
||||||
Self::NamedExpression(node) => node.into(),
|
Self::NamedExpression(node) => node.into(),
|
||||||
Self::Assignment(AssignmentDefinitionNodeRef {
|
Self::Assignment(AssignmentDefinitionNodeRef {
|
||||||
assignment: _,
|
value: _,
|
||||||
target_index: _,
|
unpack: _,
|
||||||
name,
|
name,
|
||||||
kind: _,
|
first: _,
|
||||||
}) => name.into(),
|
}) => name.into(),
|
||||||
Self::AnnotatedAssignment(node) => node.into(),
|
Self::AnnotatedAssignment(node) => node.into(),
|
||||||
Self::AugmentedAssignment(node) => node.into(),
|
Self::AugmentedAssignment(node) => node.into(),
|
||||||
@@ -329,11 +377,18 @@ impl DefinitionNodeRef<'_> {
|
|||||||
ast::AnyParameterRef::Variadic(parameter) => parameter.into(),
|
ast::AnyParameterRef::Variadic(parameter) => parameter.into(),
|
||||||
ast::AnyParameterRef::NonVariadic(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, .. }) => {
|
Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => {
|
||||||
identifier.into()
|
identifier.into()
|
||||||
}
|
}
|
||||||
Self::ExceptHandler(ExceptHandlerDefinitionNodeRef { handler, .. }) => handler.into(),
|
Self::ExceptHandler(ExceptHandlerDefinitionNodeRef { handler, .. }) => handler.into(),
|
||||||
|
Self::TypeVar(node) => node.into(),
|
||||||
|
Self::ParamSpec(node) => node.into(),
|
||||||
|
Self::TypeVarTuple(node) => node.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,13 +429,13 @@ impl DefinitionCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum DefinitionKind {
|
pub enum DefinitionKind<'db> {
|
||||||
Import(AstNodeRef<ast::Alias>),
|
Import(AstNodeRef<ast::Alias>),
|
||||||
ImportFrom(ImportFromDefinitionKind),
|
ImportFrom(ImportFromDefinitionKind),
|
||||||
Function(AstNodeRef<ast::StmtFunctionDef>),
|
Function(AstNodeRef<ast::StmtFunctionDef>),
|
||||||
Class(AstNodeRef<ast::StmtClassDef>),
|
Class(AstNodeRef<ast::StmtClassDef>),
|
||||||
NamedExpression(AstNodeRef<ast::ExprNamed>),
|
NamedExpression(AstNodeRef<ast::ExprNamed>),
|
||||||
Assignment(AssignmentDefinitionKind),
|
Assignment(AssignmentDefinitionKind<'db>),
|
||||||
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
|
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
|
||||||
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
|
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
|
||||||
For(ForStmtDefinitionKind),
|
For(ForStmtDefinitionKind),
|
||||||
@@ -390,16 +445,22 @@ pub enum DefinitionKind {
|
|||||||
WithItem(WithItemDefinitionKind),
|
WithItem(WithItemDefinitionKind),
|
||||||
MatchPattern(MatchPatternDefinitionKind),
|
MatchPattern(MatchPatternDefinitionKind),
|
||||||
ExceptHandler(ExceptHandlerDefinitionKind),
|
ExceptHandler(ExceptHandlerDefinitionKind),
|
||||||
|
TypeVar(AstNodeRef<ast::TypeParamTypeVar>),
|
||||||
|
ParamSpec(AstNodeRef<ast::TypeParamParamSpec>),
|
||||||
|
TypeVarTuple(AstNodeRef<ast::TypeParamTypeVarTuple>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DefinitionKind {
|
impl DefinitionKind<'_> {
|
||||||
pub(crate) fn category(&self) -> DefinitionCategory {
|
pub(crate) fn category(&self) -> DefinitionCategory {
|
||||||
match self {
|
match self {
|
||||||
// functions, classes, and imports always bind, and we consider them declarations
|
// functions, classes, and imports always bind, and we consider them declarations
|
||||||
DefinitionKind::Function(_)
|
DefinitionKind::Function(_)
|
||||||
| DefinitionKind::Class(_)
|
| DefinitionKind::Class(_)
|
||||||
| DefinitionKind::Import(_)
|
| 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
|
// a parameter always binds a value, but is only a declaration if annotated
|
||||||
DefinitionKind::Parameter(parameter) => {
|
DefinitionKind::Parameter(parameter) => {
|
||||||
if parameter.annotation.is_some() {
|
if parameter.annotation.is_some() {
|
||||||
@@ -437,6 +498,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)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct MatchPatternDefinitionKind {
|
pub struct MatchPatternDefinitionKind {
|
||||||
@@ -498,42 +574,36 @@ impl ImportFromDefinitionKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AssignmentDefinitionKind {
|
pub struct AssignmentDefinitionKind<'db> {
|
||||||
assignment: AstNodeRef<ast::StmtAssign>,
|
target: TargetKind<'db>,
|
||||||
target_index: usize,
|
value: AstNodeRef<ast::Expr>,
|
||||||
name: AstNodeRef<ast::ExprName>,
|
name: AstNodeRef<ast::ExprName>,
|
||||||
kind: AssignmentKind,
|
first: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssignmentDefinitionKind {
|
impl<'db> AssignmentDefinitionKind<'db> {
|
||||||
pub(crate) fn value(&self) -> &ast::Expr {
|
pub(crate) fn target(&self) -> TargetKind<'db> {
|
||||||
&self.assignment.node().value
|
self.target
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn target(&self) -> &ast::Expr {
|
pub(crate) fn value(&self) -> &ast::Expr {
|
||||||
&self.assignment.node().targets[self.target_index]
|
self.value.node()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn name(&self) -> &ast::ExprName {
|
pub(crate) fn name(&self) -> &ast::ExprName {
|
||||||
self.name.node()
|
self.name.node()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn kind(&self) -> AssignmentKind {
|
pub(crate) fn is_first(&self) -> bool {
|
||||||
self.kind
|
self.first
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The kind of assignment target expression.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum AssignmentKind {
|
|
||||||
Sequence,
|
|
||||||
Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct WithItemDefinitionKind {
|
pub struct WithItemDefinitionKind {
|
||||||
node: AstNodeRef<ast::WithItem>,
|
node: AstNodeRef<ast::WithItem>,
|
||||||
target: AstNodeRef<ast::ExprName>,
|
target: AstNodeRef<ast::ExprName>,
|
||||||
|
is_async: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WithItemDefinitionKind {
|
impl WithItemDefinitionKind {
|
||||||
@@ -544,6 +614,10 @@ impl WithItemDefinitionKind {
|
|||||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||||
self.target.node()
|
self.target.node()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn is_async(&self) -> bool {
|
||||||
|
self.is_async
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -661,3 +735,21 @@ impl From<&ast::ExceptHandlerExceptHandler> for DefinitionNodeKey {
|
|||||||
Self(NodeKey::from_node(handler))
|
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.
|
/// An independently type-inferable expression.
|
||||||
///
|
///
|
||||||
/// Includes constraint expressions (e.g. if tests) and the RHS of an unpacking assignment.
|
/// 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]
|
#[salsa::tracked]
|
||||||
pub(crate) struct Expression<'db> {
|
pub(crate) struct Expression<'db> {
|
||||||
/// The file in which the expression occurs.
|
/// The file in which the expression occurs.
|
||||||
|
|||||||
@@ -47,17 +47,27 @@ impl Symbol {
|
|||||||
pub fn is_bound(&self) -> bool {
|
pub fn is_bound(&self) -> bool {
|
||||||
self.flags.contains(SymbolFlags::IS_BOUND)
|
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! {
|
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)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
struct SymbolFlags: u8 {
|
struct SymbolFlags: u8 {
|
||||||
const IS_USED = 1 << 0;
|
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
|
/// 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
|
/// 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> {
|
pub struct ScopeId<'db> {
|
||||||
#[id]
|
#[id]
|
||||||
pub file: File,
|
pub file: File,
|
||||||
|
|
||||||
#[id]
|
#[id]
|
||||||
pub file_scope_id: FileScopeId,
|
pub file_scope_id: FileScopeId,
|
||||||
|
|
||||||
/// The node that introduces this scope.
|
|
||||||
#[no_eq]
|
|
||||||
#[return_ref]
|
|
||||||
pub node: NodeWithScopeKind,
|
|
||||||
|
|
||||||
#[no_eq]
|
#[no_eq]
|
||||||
count: countme::Count<ScopeId<'static>>,
|
count: countme::Count<ScopeId<'static>>,
|
||||||
}
|
}
|
||||||
@@ -121,6 +127,14 @@ impl<'db> ScopeId<'db> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
pub(crate) fn name(self, db: &'db dyn Db) -> &'db str {
|
pub(crate) fn name(self, db: &'db dyn Db) -> &'db str {
|
||||||
match self.node(db) {
|
match self.node(db) {
|
||||||
@@ -159,10 +173,10 @@ impl FileScopeId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug)]
|
||||||
pub struct Scope {
|
pub struct Scope {
|
||||||
pub(super) parent: Option<FileScopeId>,
|
pub(super) parent: Option<FileScopeId>,
|
||||||
pub(super) kind: ScopeKind,
|
pub(super) node: NodeWithScopeKind,
|
||||||
pub(super) descendents: Range<FileScopeId>,
|
pub(super) descendents: Range<FileScopeId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +185,12 @@ impl Scope {
|
|||||||
self.parent
|
self.parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn node(&self) -> &NodeWithScopeKind {
|
||||||
|
&self.node
|
||||||
|
}
|
||||||
|
|
||||||
pub fn kind(&self) -> ScopeKind {
|
pub fn kind(&self) -> ScopeKind {
|
||||||
self.kind
|
self.node().scope_kind()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +210,7 @@ impl ScopeKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Symbol table for a specific [`Scope`].
|
/// Symbol table for a specific [`Scope`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct SymbolTable {
|
pub struct SymbolTable {
|
||||||
/// The symbols in this scope.
|
/// The symbols in this scope.
|
||||||
symbols: IndexVec<ScopedSymbolId, Symbol>,
|
symbols: IndexVec<ScopedSymbolId, Symbol>,
|
||||||
@@ -202,13 +220,6 @@ pub struct SymbolTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SymbolTable {
|
impl SymbolTable {
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
symbols: IndexVec::new(),
|
|
||||||
symbols_by_name: SymbolMap::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shrink_to_fit(&mut self) {
|
fn shrink_to_fit(&mut self) {
|
||||||
self.symbols.shrink_to_fit();
|
self.symbols.shrink_to_fit();
|
||||||
}
|
}
|
||||||
@@ -260,18 +271,12 @@ impl PartialEq for SymbolTable {
|
|||||||
|
|
||||||
impl Eq for SymbolTable {}
|
impl Eq for SymbolTable {}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub(super) struct SymbolTableBuilder {
|
pub(super) struct SymbolTableBuilder {
|
||||||
table: SymbolTable,
|
table: SymbolTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SymbolTableBuilder {
|
impl SymbolTableBuilder {
|
||||||
pub(super) fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
table: SymbolTable::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) {
|
pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) {
|
||||||
let hash = SymbolTable::hash_name(&name);
|
let hash = SymbolTable::hash_name(&name);
|
||||||
let entry = self
|
let entry = self
|
||||||
@@ -298,6 +303,10 @@ impl SymbolTableBuilder {
|
|||||||
self.table.symbols[id].insert_flags(SymbolFlags::IS_BOUND);
|
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) {
|
pub(super) fn mark_symbol_used(&mut self, id: ScopedSymbolId) {
|
||||||
self.table.symbols[id].insert_flags(SymbolFlags::IS_USED);
|
self.table.symbols[id].insert_flags(SymbolFlags::IS_USED);
|
||||||
}
|
}
|
||||||
@@ -362,21 +371,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 {
|
pub(crate) fn node_key(self) -> NodeWithScopeKey {
|
||||||
match self {
|
match self {
|
||||||
NodeWithScopeRef::Module => NodeWithScopeKey::Module,
|
NodeWithScopeRef::Module => NodeWithScopeKey::Module,
|
||||||
@@ -424,6 +418,36 @@ pub enum NodeWithScopeKind {
|
|||||||
GeneratorExpression(AstNodeRef<ast::ExprGenerator>),
|
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(_) => ScopeKind::Function,
|
||||||
|
Self::Lambda(_) => ScopeKind::Function,
|
||||||
|
Self::FunctionTypeParameters(_) | Self::ClassTypeParameters(_) => ScopeKind::Annotation,
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
pub(crate) enum NodeWithScopeKey {
|
pub(crate) enum NodeWithScopeKey {
|
||||||
Module,
|
Module,
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ use self::symbol_state::{
|
|||||||
use crate::semantic_index::ast_ids::ScopedUseId;
|
use crate::semantic_index::ast_ids::ScopedUseId;
|
||||||
use crate::semantic_index::definition::Definition;
|
use crate::semantic_index::definition::Definition;
|
||||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||||
|
use crate::symbol::Boundness;
|
||||||
use ruff_index::IndexVec;
|
use ruff_index::IndexVec;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
@@ -274,8 +275,12 @@ impl<'db> UseDefMap<'db> {
|
|||||||
self.bindings_iterator(&self.bindings_by_use[use_id])
|
self.bindings_iterator(&self.bindings_by_use[use_id])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn use_may_be_unbound(&self, use_id: ScopedUseId) -> bool {
|
pub(crate) fn use_boundness(&self, use_id: ScopedUseId) -> Boundness {
|
||||||
self.bindings_by_use[use_id].may_be_unbound()
|
if self.bindings_by_use[use_id].may_be_unbound() {
|
||||||
|
Boundness::PossiblyUnbound
|
||||||
|
} else {
|
||||||
|
Boundness::Bound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn public_bindings(
|
pub(crate) fn public_bindings(
|
||||||
@@ -285,8 +290,12 @@ impl<'db> UseDefMap<'db> {
|
|||||||
self.bindings_iterator(self.public_symbols[symbol].bindings())
|
self.bindings_iterator(self.public_symbols[symbol].bindings())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn public_may_be_unbound(&self, symbol: ScopedSymbolId) -> bool {
|
pub(crate) fn public_boundness(&self, symbol: ScopedSymbolId) -> Boundness {
|
||||||
self.public_symbols[symbol].may_be_unbound()
|
if self.public_symbols[symbol].may_be_unbound() {
|
||||||
|
Boundness::PossiblyUnbound
|
||||||
|
} else {
|
||||||
|
Boundness::Bound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn bindings_at_declaration(
|
pub(crate) fn bindings_at_declaration(
|
||||||
@@ -450,10 +459,6 @@ pub(super) struct UseDefMapBuilder<'db> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> UseDefMapBuilder<'db> {
|
impl<'db> UseDefMapBuilder<'db> {
|
||||||
pub(super) fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
||||||
let new_symbol = self.symbol_states.push(SymbolState::undefined());
|
let new_symbol = self.symbol_states.push(SymbolState::undefined());
|
||||||
debug_assert_eq!(symbol, new_symbol);
|
debug_assert_eq!(symbol, new_symbol);
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ use ruff_source_file::LineIndex;
|
|||||||
|
|
||||||
use crate::module_name::ModuleName;
|
use crate::module_name::ModuleName;
|
||||||
use crate::module_resolver::{resolve_module, Module};
|
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::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;
|
use crate::Db;
|
||||||
|
|
||||||
pub struct SemanticModel<'db> {
|
pub struct SemanticModel<'db> {
|
||||||
@@ -38,10 +38,6 @@ impl<'db> SemanticModel<'db> {
|
|||||||
pub fn resolve_module(&self, module_name: &ModuleName) -> Option<Module> {
|
pub fn resolve_module(&self, module_name: &ModuleName) -> Option<Module> {
|
||||||
resolve_module(self.db, module_name)
|
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 {
|
pub trait HasTy {
|
||||||
@@ -58,7 +54,7 @@ impl HasTy for ast::ExpressionRef<'_> {
|
|||||||
let file_scope = index.expression_scope_id(*self);
|
let file_scope = index.expression_scope_id(*self);
|
||||||
let scope = file_scope.to_scope_id(model.db, model.file);
|
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)
|
infer_scope_types(model.db, scope).expression_ty(expression_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -732,7 +732,20 @@ mod tests {
|
|||||||
let system = TestSystem::default();
|
let system = TestSystem::default();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
VirtualEnvironment::new("/.venv", &system),
|
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(..))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,81 +2,76 @@ use crate::module_name::ModuleName;
|
|||||||
use crate::module_resolver::resolve_module;
|
use crate::module_resolver::resolve_module;
|
||||||
use crate::semantic_index::global_scope;
|
use crate::semantic_index::global_scope;
|
||||||
use crate::semantic_index::symbol::ScopeId;
|
use crate::semantic_index::symbol::ScopeId;
|
||||||
use crate::types::{global_symbol_ty, Type};
|
use crate::symbol::Symbol;
|
||||||
|
use crate::types::global_symbol;
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
|
|
||||||
/// Enumeration of various core stdlib modules, for which we have dedicated Salsa queries.
|
/// Enumeration of various core stdlib modules, for which we have dedicated Salsa queries.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum CoreStdlibModule {
|
pub(crate) enum CoreStdlibModule {
|
||||||
Builtins,
|
Builtins,
|
||||||
Types,
|
Types,
|
||||||
Typeshed,
|
Typeshed,
|
||||||
TypingExtensions,
|
TypingExtensions,
|
||||||
|
Typing,
|
||||||
|
Sys,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoreStdlibModule {
|
impl CoreStdlibModule {
|
||||||
fn name(self) -> ModuleName {
|
pub(crate) const fn as_str(self) -> &'static str {
|
||||||
let module_name = match self {
|
match self {
|
||||||
Self::Builtins => "builtins",
|
Self::Builtins => "builtins",
|
||||||
Self::Types => "types",
|
Self::Types => "types",
|
||||||
|
Self::Typing => "typing",
|
||||||
Self::Typeshed => "_typeshed",
|
Self::Typeshed => "_typeshed",
|
||||||
Self::TypingExtensions => "typing_extensions",
|
Self::TypingExtensions => "typing_extensions",
|
||||||
};
|
Self::Sys => "sys",
|
||||||
ModuleName::new_static(module_name)
|
}
|
||||||
.unwrap_or_else(|| panic!("{module_name} should be a valid module name!"))
|
}
|
||||||
|
|
||||||
|
pub(crate) fn name(self) -> ModuleName {
|
||||||
|
let self_as_str = self.as_str();
|
||||||
|
ModuleName::new_static(self_as_str)
|
||||||
|
.unwrap_or_else(|| panic!("{self_as_str} should be a valid module name!"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lookup the type of `symbol` in a given core module
|
/// Lookup the type of `symbol` in a given core module
|
||||||
///
|
///
|
||||||
/// Returns `Unbound` if the given core module cannot be resolved for some reason
|
/// Returns `Symbol::Unbound` if the given core module cannot be resolved for some reason
|
||||||
fn core_module_symbol_ty<'db>(
|
pub(crate) fn core_module_symbol<'db>(
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
core_module: CoreStdlibModule,
|
core_module: CoreStdlibModule,
|
||||||
symbol: &str,
|
symbol: &str,
|
||||||
) -> Type<'db> {
|
) -> Symbol<'db> {
|
||||||
resolve_module(db, &core_module.name())
|
resolve_module(db, &core_module.name())
|
||||||
.map(|module| global_symbol_ty(db, module.file(), symbol))
|
.map(|module| global_symbol(db, module.file(), symbol))
|
||||||
.map(|ty| {
|
.unwrap_or(Symbol::Unbound)
|
||||||
if ty.is_unbound() {
|
|
||||||
ty
|
|
||||||
} else {
|
|
||||||
ty.replace_unbound_with(db, Type::Never)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(Type::Unbound)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lookup the type of `symbol` in the builtins namespace.
|
/// Lookup the type of `symbol` in the builtins namespace.
|
||||||
///
|
///
|
||||||
/// Returns `Unbound` if the `builtins` module isn't available for some reason.
|
/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn builtins_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||||
core_module_symbol_ty(db, CoreStdlibModule::Builtins, symbol)
|
core_module_symbol(db, CoreStdlibModule::Builtins, symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lookup the type of `symbol` in the `types` module namespace.
|
/// Lookup the type of `symbol` in the `typing` module namespace.
|
||||||
///
|
///
|
||||||
/// Returns `Unbound` if the `types` module isn't available for some reason.
|
/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn types_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
#[cfg(test)]
|
||||||
core_module_symbol_ty(db, CoreStdlibModule::Types, symbol)
|
pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||||
}
|
core_module_symbol(db, CoreStdlibModule::Typing, symbol)
|
||||||
|
|
||||||
/// Lookup the type of `symbol` in the `_typeshed` module namespace.
|
|
||||||
///
|
|
||||||
/// Returns `Unbound` if the `_typeshed` module isn't available for some reason.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn typeshed_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
|
||||||
core_module_symbol_ty(db, CoreStdlibModule::Typeshed, symbol)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lookup the type of `symbol` in the `typing_extensions` module namespace.
|
/// Lookup the type of `symbol` in the `typing_extensions` module namespace.
|
||||||
///
|
///
|
||||||
/// Returns `Unbound` if the `typing_extensions` module isn't available for some reason.
|
/// Returns `Symbol::Unbound` if the `typing_extensions` module isn't available for some reason.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn typing_extensions_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
pub(crate) fn typing_extensions_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||||
core_module_symbol_ty(db, CoreStdlibModule::TypingExtensions, symbol)
|
core_module_symbol(db, CoreStdlibModule::TypingExtensions, symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the scope of a core stdlib module.
|
/// Get the scope of a core stdlib module.
|
||||||
|
|||||||
146
crates/red_knot_python_semantic/src/symbol.rs
Normal file
146
crates/red_knot_python_semantic/src/symbol.rs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
use crate::{
|
||||||
|
types::{Type, UnionType},
|
||||||
|
Db,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum Boundness {
|
||||||
|
Bound,
|
||||||
|
PossiblyUnbound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Boundness {
|
||||||
|
pub(crate) fn or(self, other: Boundness) -> Boundness {
|
||||||
|
match (self, other) {
|
||||||
|
(Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound,
|
||||||
|
(Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of a symbol lookup, which can either be a (possibly unbound) type
|
||||||
|
/// or a completely unbound symbol.
|
||||||
|
///
|
||||||
|
/// Consider this example:
|
||||||
|
/// ```py
|
||||||
|
/// bound = 1
|
||||||
|
///
|
||||||
|
/// if flag:
|
||||||
|
/// possibly_unbound = 2
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// If we look up symbols in this scope, we would get the following results:
|
||||||
|
/// ```rs
|
||||||
|
/// bound: Symbol::Type(Type::IntLiteral(1), Boundness::Bound),
|
||||||
|
/// possibly_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound),
|
||||||
|
/// non_existent: Symbol::Unbound,
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub(crate) enum Symbol<'db> {
|
||||||
|
Type(Type<'db>, Boundness),
|
||||||
|
Unbound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'db> Symbol<'db> {
|
||||||
|
pub(crate) fn is_unbound(&self) -> bool {
|
||||||
|
matches!(self, Symbol::Unbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn possibly_unbound(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Symbol::Type(_, Boundness::PossiblyUnbound) | Symbol::Unbound => true,
|
||||||
|
Symbol::Type(_, Boundness::Bound) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the type of the symbol, ignoring possible unboundness.
|
||||||
|
///
|
||||||
|
/// If the symbol is *definitely* unbound, this function will return `None`. Otherwise,
|
||||||
|
/// if there is at least one control-flow path where the symbol is bound, return the type.
|
||||||
|
pub(crate) fn ignore_possibly_unbound(&self) -> Option<Type<'db>> {
|
||||||
|
match self {
|
||||||
|
Symbol::Type(ty, _) => Some(*ty),
|
||||||
|
Symbol::Unbound => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[track_caller]
|
||||||
|
pub(crate) fn expect_type(self) -> Type<'db> {
|
||||||
|
self.ignore_possibly_unbound()
|
||||||
|
.expect("Expected a (possibly unbound) type, not an unbound symbol")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn or_fall_back_to(self, db: &'db dyn Db, fallback: &Symbol<'db>) -> Symbol<'db> {
|
||||||
|
match fallback {
|
||||||
|
Symbol::Type(fallback_ty, fallback_boundness) => match self {
|
||||||
|
Symbol::Type(_, Boundness::Bound) => self,
|
||||||
|
Symbol::Type(ty, boundness @ Boundness::PossiblyUnbound) => Symbol::Type(
|
||||||
|
UnionType::from_elements(db, [*fallback_ty, ty]),
|
||||||
|
fallback_boundness.or(boundness),
|
||||||
|
),
|
||||||
|
Symbol::Unbound => fallback.clone(),
|
||||||
|
},
|
||||||
|
Symbol::Unbound => self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::types::tests::setup_db;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symbol_or_fall_back_to() {
|
||||||
|
use Boundness::{Bound, PossiblyUnbound};
|
||||||
|
|
||||||
|
let db = setup_db();
|
||||||
|
let ty1 = Type::IntLiteral(1);
|
||||||
|
let ty2 = Type::IntLiteral(2);
|
||||||
|
|
||||||
|
// Start from an unbound symbol
|
||||||
|
assert_eq!(
|
||||||
|
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Unbound),
|
||||||
|
Symbol::Unbound
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, PossiblyUnbound)),
|
||||||
|
Symbol::Type(ty1, PossiblyUnbound)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, Bound)),
|
||||||
|
Symbol::Type(ty1, Bound)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start from a possibly unbound symbol
|
||||||
|
assert_eq!(
|
||||||
|
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Unbound),
|
||||||
|
Symbol::Type(ty1, PossiblyUnbound)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Symbol::Type(ty1, PossiblyUnbound)
|
||||||
|
.or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
|
||||||
|
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), PossiblyUnbound)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
|
||||||
|
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), Bound)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start from a definitely bound symbol
|
||||||
|
assert_eq!(
|
||||||
|
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Unbound),
|
||||||
|
Symbol::Type(ty1, Bound)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
|
||||||
|
Symbol::Type(ty1, Bound)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
|
||||||
|
Symbol::Type(ty1, Bound)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -25,12 +25,11 @@
|
|||||||
//! * No type in an intersection can be a supertype of any other type in the intersection (just
|
//! * No type in an intersection can be a supertype of any other type in the intersection (just
|
||||||
//! eliminate the supertype from the intersection).
|
//! eliminate the supertype from the intersection).
|
||||||
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
|
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
|
||||||
use crate::types::{IntersectionType, Type, UnionType};
|
|
||||||
|
use crate::types::{InstanceType, IntersectionType, KnownClass, Type, UnionType};
|
||||||
use crate::{Db, FxOrderSet};
|
use crate::{Db, FxOrderSet};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use super::KnownClass;
|
|
||||||
|
|
||||||
pub(crate) struct UnionBuilder<'db> {
|
pub(crate) struct UnionBuilder<'db> {
|
||||||
elements: Vec<Type<'db>>,
|
elements: Vec<Type<'db>>,
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
@@ -80,7 +79,6 @@ impl<'db> UnionBuilder<'db> {
|
|||||||
to_remove.push(index);
|
to_remove.push(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match to_remove[..] {
|
match to_remove[..] {
|
||||||
[] => self.elements.push(to_add),
|
[] => self.elements.push(to_add),
|
||||||
[index] => self.elements[index] = to_add,
|
[index] => self.elements[index] = to_add,
|
||||||
@@ -103,7 +101,6 @@ impl<'db> UnionBuilder<'db> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +128,7 @@ impl<'db> IntersectionBuilder<'db> {
|
|||||||
pub(crate) fn new(db: &'db dyn Db) -> Self {
|
pub(crate) fn new(db: &'db dyn Db) -> Self {
|
||||||
Self {
|
Self {
|
||||||
db,
|
db,
|
||||||
intersections: vec![InnerIntersectionBuilder::new()],
|
intersections: vec![InnerIntersectionBuilder::default()],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,14 +170,37 @@ impl<'db> IntersectionBuilder<'db> {
|
|||||||
pub(crate) fn add_negative(mut self, ty: Type<'db>) -> Self {
|
pub(crate) fn add_negative(mut self, ty: Type<'db>) -> Self {
|
||||||
// See comments above in `add_positive`; this is just the negated version.
|
// See comments above in `add_positive`; this is just the negated version.
|
||||||
if let Type::Union(union) = ty {
|
if let Type::Union(union) = ty {
|
||||||
union
|
for elem in union.elements(self.db) {
|
||||||
.elements(self.db)
|
self = self.add_negative(*elem);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
} else if let Type::Intersection(intersection) = ty {
|
||||||
|
// (A | B) & ~(C & ~D)
|
||||||
|
// -> (A | B) & (~C | D)
|
||||||
|
// -> ((A | B) & ~C) | ((A | B) & D)
|
||||||
|
// i.e. if we have an intersection of positive constraints C
|
||||||
|
// and negative constraints D, then our new intersection
|
||||||
|
// is (existing & ~C) | (existing & D)
|
||||||
|
|
||||||
|
let positive_side = intersection
|
||||||
|
.positive(self.db)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|elem| self.clone().add_negative(*elem))
|
// we negate all the positive constraints while distributing
|
||||||
.fold(IntersectionBuilder::empty(self.db), |mut builder, sub| {
|
.map(|elem| self.clone().add_negative(*elem));
|
||||||
|
|
||||||
|
let negative_side = intersection
|
||||||
|
.negative(self.db)
|
||||||
|
.iter()
|
||||||
|
// all negative constraints end up becoming positive constraints
|
||||||
|
.map(|elem| self.clone().add_positive(*elem));
|
||||||
|
|
||||||
|
positive_side.chain(negative_side).fold(
|
||||||
|
IntersectionBuilder::empty(self.db),
|
||||||
|
|mut builder, sub| {
|
||||||
builder.intersections.extend(sub.intersections);
|
builder.intersections.extend(sub.intersections);
|
||||||
builder
|
builder
|
||||||
})
|
},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
for inner in &mut self.intersections {
|
for inner in &mut self.intersections {
|
||||||
inner.add_negative(self.db, ty);
|
inner.add_negative(self.db, ty);
|
||||||
@@ -211,10 +231,6 @@ struct InnerIntersectionBuilder<'db> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> InnerIntersectionBuilder<'db> {
|
impl<'db> InnerIntersectionBuilder<'db> {
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a positive type to this intersection.
|
/// Adds a positive type to this intersection.
|
||||||
fn add_positive(&mut self, db: &'db dyn Db, new_positive: Type<'db>) {
|
fn add_positive(&mut self, db: &'db dyn Db, new_positive: Type<'db>) {
|
||||||
if let Type::Intersection(other) = new_positive {
|
if let Type::Intersection(other) = new_positive {
|
||||||
@@ -226,14 +242,14 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ~Literal[True] & bool = Literal[False]
|
// ~Literal[True] & bool = Literal[False]
|
||||||
if let Type::Instance(class_type) = new_positive {
|
if let Type::Instance(InstanceType { class }) = new_positive {
|
||||||
if class_type.is_known(db, KnownClass::Bool) {
|
if class.is_known(db, KnownClass::Bool) {
|
||||||
if let Some(&Type::BooleanLiteral(value)) = self
|
if let Some(&Type::BooleanLiteral(value)) = self
|
||||||
.negative
|
.negative
|
||||||
.iter()
|
.iter()
|
||||||
.find(|element| element.is_boolean_literal())
|
.find(|element| element.is_boolean_literal())
|
||||||
{
|
{
|
||||||
*self = Self::new();
|
*self = Self::default();
|
||||||
self.positive.insert(Type::BooleanLiteral(!value));
|
self.positive.insert(Type::BooleanLiteral(!value));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -252,7 +268,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||||||
}
|
}
|
||||||
// A & B = Never if A and B are disjoint
|
// A & B = Never if A and B are disjoint
|
||||||
if new_positive.is_disjoint_from(db, *existing_positive) {
|
if new_positive.is_disjoint_from(db, *existing_positive) {
|
||||||
*self = Self::new();
|
*self = Self::default();
|
||||||
self.positive.insert(Type::Never);
|
self.positive.insert(Type::Never);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -265,7 +281,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||||||
for (index, existing_negative) in self.negative.iter().enumerate() {
|
for (index, existing_negative) in self.negative.iter().enumerate() {
|
||||||
// S & ~T = Never if S <: T
|
// S & ~T = Never if S <: T
|
||||||
if new_positive.is_subtype_of(db, *existing_negative) {
|
if new_positive.is_subtype_of(db, *existing_negative) {
|
||||||
*self = Self::new();
|
*self = Self::default();
|
||||||
self.positive.insert(Type::Never);
|
self.positive.insert(Type::Never);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -293,12 +309,11 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||||||
self.add_positive(db, *neg);
|
self.add_positive(db, *neg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type::Unbound => {}
|
|
||||||
ty @ (Type::Any | Type::Unknown | Type::Todo) => {
|
ty @ (Type::Any | Type::Unknown | Type::Todo) => {
|
||||||
// Adding any of these types to the negative side of an intersection
|
// Adding any of these types to the negative side of an intersection
|
||||||
// is equivalent to adding it to the positive side. We do this to
|
// is equivalent to adding it to the positive side. We do this to
|
||||||
// simplify the representation.
|
// simplify the representation.
|
||||||
self.positive.insert(ty);
|
self.add_positive(db, ty);
|
||||||
}
|
}
|
||||||
// ~Literal[True] & bool = Literal[False]
|
// ~Literal[True] & bool = Literal[False]
|
||||||
Type::BooleanLiteral(bool)
|
Type::BooleanLiteral(bool)
|
||||||
@@ -307,7 +322,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|pos| *pos == KnownClass::Bool.to_instance(db)) =>
|
.any(|pos| *pos == KnownClass::Bool.to_instance(db)) =>
|
||||||
{
|
{
|
||||||
*self = Self::new();
|
*self = Self::default();
|
||||||
self.positive.insert(Type::BooleanLiteral(!bool));
|
self.positive.insert(Type::BooleanLiteral(!bool));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -329,7 +344,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||||||
for existing_positive in &self.positive {
|
for existing_positive in &self.positive {
|
||||||
// S & ~T = Never if S <: T
|
// S & ~T = Never if S <: T
|
||||||
if existing_positive.is_subtype_of(db, new_negative) {
|
if existing_positive.is_subtype_of(db, new_negative) {
|
||||||
*self = Self::new();
|
*self = Self::default();
|
||||||
self.positive.insert(Type::Never);
|
self.positive.insert(Type::Never);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -344,15 +359,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn simplify_unbound(&mut self) {
|
|
||||||
if self.positive.contains(&Type::Unbound) {
|
|
||||||
self.positive.retain(Type::is_unbound);
|
|
||||||
self.negative.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
|
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
|
||||||
self.simplify_unbound();
|
|
||||||
match (self.positive.len(), self.negative.len()) {
|
match (self.positive.len(), self.negative.len()) {
|
||||||
(0, 0) => KnownClass::Object.to_instance(db),
|
(0, 0) => KnownClass::Object.to_instance(db),
|
||||||
(1, 0) => self.positive[0],
|
(1, 0) => self.positive[0],
|
||||||
@@ -371,8 +378,10 @@ mod tests {
|
|||||||
use crate::db::tests::TestDb;
|
use crate::db::tests::TestDb;
|
||||||
use crate::program::{Program, SearchPathSettings};
|
use crate::program::{Program, SearchPathSettings};
|
||||||
use crate::python_version::PythonVersion;
|
use crate::python_version::PythonVersion;
|
||||||
use crate::types::{KnownClass, StringLiteralType, UnionBuilder};
|
use crate::stdlib::typing_symbol;
|
||||||
|
use crate::types::{global_symbol, KnownClass, UnionBuilder};
|
||||||
use crate::ProgramSettings;
|
use crate::ProgramSettings;
|
||||||
|
use ruff_db::files::system_path_to_file;
|
||||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||||
use test_case::test_case;
|
use test_case::test_case;
|
||||||
|
|
||||||
@@ -561,18 +570,38 @@ mod tests {
|
|||||||
let ta = Type::Any;
|
let ta = Type::Any;
|
||||||
let t1 = Type::IntLiteral(1);
|
let t1 = Type::IntLiteral(1);
|
||||||
let t2 = KnownClass::Int.to_instance(&db);
|
let t2 = KnownClass::Int.to_instance(&db);
|
||||||
|
// i0 = Any & ~Literal[1]
|
||||||
let i0 = IntersectionBuilder::new(&db)
|
let i0 = IntersectionBuilder::new(&db)
|
||||||
.add_positive(ta)
|
.add_positive(ta)
|
||||||
.add_negative(t1)
|
.add_negative(t1)
|
||||||
.build();
|
.build();
|
||||||
let intersection = IntersectionBuilder::new(&db)
|
// ta_not_i0 = int & ~(Any & ~Literal[1])
|
||||||
|
// -> int & (~Any | Literal[1])
|
||||||
|
// (~Any is equivalent to Any)
|
||||||
|
// -> (int & Any) | (int & Literal[1])
|
||||||
|
// -> (int & Any) | Literal[1]
|
||||||
|
let ta_not_i0 = IntersectionBuilder::new(&db)
|
||||||
.add_positive(t2)
|
.add_positive(t2)
|
||||||
.add_negative(i0)
|
.add_negative(i0)
|
||||||
.build()
|
.build();
|
||||||
.expect_intersection();
|
|
||||||
|
|
||||||
assert_eq!(intersection.pos_vec(&db), &[ta, t1]);
|
assert_eq!(ta_not_i0.display(&db).to_string(), "int & Any | Literal[1]");
|
||||||
assert_eq!(intersection.neg_vec(&db), &[]);
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_intersection_simplify_negative_any() {
|
||||||
|
let db = setup_db();
|
||||||
|
|
||||||
|
let ty = IntersectionBuilder::new(&db)
|
||||||
|
.add_negative(Type::Any)
|
||||||
|
.build();
|
||||||
|
assert_eq!(ty, Type::Any);
|
||||||
|
|
||||||
|
let ty = IntersectionBuilder::new(&db)
|
||||||
|
.add_positive(Type::Never)
|
||||||
|
.add_negative(Type::Any)
|
||||||
|
.build();
|
||||||
|
assert_eq!(ty, Type::Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -595,12 +624,69 @@ mod tests {
|
|||||||
assert_eq!(i1.pos_vec(&db), &[ta, t1]);
|
assert_eq!(i1.pos_vec(&db), &[ta, t1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intersection_negation_distributes_over_union() {
|
||||||
|
let db = setup_db();
|
||||||
|
let st = typing_symbol(&db, "Sized").expect_type().to_instance(&db);
|
||||||
|
let ht = typing_symbol(&db, "Hashable")
|
||||||
|
.expect_type()
|
||||||
|
.to_instance(&db);
|
||||||
|
// sh_t: Sized & Hashable
|
||||||
|
let sh_t = IntersectionBuilder::new(&db)
|
||||||
|
.add_positive(st)
|
||||||
|
.add_positive(ht)
|
||||||
|
.build()
|
||||||
|
.expect_intersection();
|
||||||
|
assert_eq!(sh_t.pos_vec(&db), &[st, ht]);
|
||||||
|
assert_eq!(sh_t.neg_vec(&db), &[]);
|
||||||
|
|
||||||
|
// ~sh_t => ~Sized | ~Hashable
|
||||||
|
let not_s_h_t = IntersectionBuilder::new(&db)
|
||||||
|
.add_negative(Type::Intersection(sh_t))
|
||||||
|
.build()
|
||||||
|
.expect_union();
|
||||||
|
|
||||||
|
// should have as elements: (~Sized),(~Hashable)
|
||||||
|
let not_st = st.negate(&db);
|
||||||
|
let not_ht = ht.negate(&db);
|
||||||
|
assert_eq!(not_s_h_t.elements(&db), &[not_st, not_ht]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_intersection_negation_distributes_over_union() {
|
||||||
|
let db = setup_db();
|
||||||
|
let it = KnownClass::Int.to_instance(&db);
|
||||||
|
let st = typing_symbol(&db, "Sized").expect_type().to_instance(&db);
|
||||||
|
let ht = typing_symbol(&db, "Hashable")
|
||||||
|
.expect_type()
|
||||||
|
.to_instance(&db);
|
||||||
|
// s_not_h_t: Sized & ~Hashable
|
||||||
|
let s_not_h_t = IntersectionBuilder::new(&db)
|
||||||
|
.add_positive(st)
|
||||||
|
.add_negative(ht)
|
||||||
|
.build()
|
||||||
|
.expect_intersection();
|
||||||
|
assert_eq!(s_not_h_t.pos_vec(&db), &[st]);
|
||||||
|
assert_eq!(s_not_h_t.neg_vec(&db), &[ht]);
|
||||||
|
|
||||||
|
// let's build int & ~(Sized & ~Hashable)
|
||||||
|
let tt = IntersectionBuilder::new(&db)
|
||||||
|
.add_positive(it)
|
||||||
|
.add_negative(Type::Intersection(s_not_h_t))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// int & ~(Sized & ~Hashable)
|
||||||
|
// -> int & (~Sized | Hashable)
|
||||||
|
// -> (int & ~Sized) | (int & Hashable)
|
||||||
|
assert_eq!(tt.display(&db).to_string(), "int & ~Sized | int & Hashable");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_intersection_self_negation() {
|
fn build_intersection_self_negation() {
|
||||||
let db = setup_db();
|
let db = setup_db();
|
||||||
let ty = IntersectionBuilder::new(&db)
|
let ty = IntersectionBuilder::new(&db)
|
||||||
.add_positive(Type::None)
|
.add_positive(Type::none(&db))
|
||||||
.add_negative(Type::None)
|
.add_negative(Type::none(&db))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assert_eq!(ty, Type::Never);
|
assert_eq!(ty, Type::Never);
|
||||||
@@ -610,63 +696,62 @@ mod tests {
|
|||||||
fn build_intersection_simplify_negative_never() {
|
fn build_intersection_simplify_negative_never() {
|
||||||
let db = setup_db();
|
let db = setup_db();
|
||||||
let ty = IntersectionBuilder::new(&db)
|
let ty = IntersectionBuilder::new(&db)
|
||||||
.add_positive(Type::None)
|
.add_positive(Type::none(&db))
|
||||||
.add_negative(Type::Never)
|
.add_negative(Type::Never)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assert_eq!(ty, Type::None);
|
assert_eq!(ty, Type::none(&db));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_intersection_simplify_positive_never() {
|
fn build_intersection_simplify_positive_never() {
|
||||||
let db = setup_db();
|
let db = setup_db();
|
||||||
let ty = IntersectionBuilder::new(&db)
|
let ty = IntersectionBuilder::new(&db)
|
||||||
.add_positive(Type::None)
|
.add_positive(Type::none(&db))
|
||||||
.add_positive(Type::Never)
|
.add_positive(Type::Never)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assert_eq!(ty, Type::Never);
|
assert_eq!(ty, Type::Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_intersection_simplify_positive_unbound() {
|
|
||||||
let db = setup_db();
|
|
||||||
let ty = IntersectionBuilder::new(&db)
|
|
||||||
.add_positive(Type::Unbound)
|
|
||||||
.add_positive(Type::IntLiteral(1))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assert_eq!(ty, Type::Unbound);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_intersection_simplify_negative_unbound() {
|
|
||||||
let db = setup_db();
|
|
||||||
let ty = IntersectionBuilder::new(&db)
|
|
||||||
.add_negative(Type::Unbound)
|
|
||||||
.add_positive(Type::IntLiteral(1))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assert_eq!(ty, Type::IntLiteral(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_intersection_simplify_negative_none() {
|
fn build_intersection_simplify_negative_none() {
|
||||||
let db = setup_db();
|
let db = setup_db();
|
||||||
|
|
||||||
let ty = IntersectionBuilder::new(&db)
|
let ty = IntersectionBuilder::new(&db)
|
||||||
.add_negative(Type::None)
|
.add_negative(Type::none(&db))
|
||||||
.add_positive(Type::IntLiteral(1))
|
.add_positive(Type::IntLiteral(1))
|
||||||
.build();
|
.build();
|
||||||
assert_eq!(ty, Type::IntLiteral(1));
|
assert_eq!(ty, Type::IntLiteral(1));
|
||||||
|
|
||||||
let ty = IntersectionBuilder::new(&db)
|
let ty = IntersectionBuilder::new(&db)
|
||||||
.add_positive(Type::IntLiteral(1))
|
.add_positive(Type::IntLiteral(1))
|
||||||
.add_negative(Type::None)
|
.add_negative(Type::none(&db))
|
||||||
.build();
|
.build();
|
||||||
assert_eq!(ty, Type::IntLiteral(1));
|
assert_eq!(ty, Type::IntLiteral(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_negative_union_de_morgan() {
|
||||||
|
let db = setup_db();
|
||||||
|
|
||||||
|
let union = UnionBuilder::new(&db)
|
||||||
|
.add(Type::IntLiteral(1))
|
||||||
|
.add(Type::IntLiteral(2))
|
||||||
|
.build();
|
||||||
|
assert_eq!(union.display(&db).to_string(), "Literal[1, 2]");
|
||||||
|
|
||||||
|
let ty = IntersectionBuilder::new(&db).add_negative(union).build();
|
||||||
|
|
||||||
|
let expected = IntersectionBuilder::new(&db)
|
||||||
|
.add_negative(Type::IntLiteral(1))
|
||||||
|
.add_negative(Type::IntLiteral(2))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_eq!(ty.display(&db).to_string(), "~Literal[1] & ~Literal[2]");
|
||||||
|
assert_eq!(ty, expected);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_intersection_simplify_positive_type_and_positive_subtype() {
|
fn build_intersection_simplify_positive_type_and_positive_subtype() {
|
||||||
let db = setup_db();
|
let db = setup_db();
|
||||||
@@ -686,7 +771,7 @@ mod tests {
|
|||||||
.build();
|
.build();
|
||||||
assert_eq!(ty, s);
|
assert_eq!(ty, s);
|
||||||
|
|
||||||
let literal = Type::StringLiteral(StringLiteralType::new(&db, "a"));
|
let literal = Type::string_literal(&db, "a");
|
||||||
let expected = IntersectionBuilder::new(&db)
|
let expected = IntersectionBuilder::new(&db)
|
||||||
.add_positive(s)
|
.add_positive(s)
|
||||||
.add_negative(literal)
|
.add_negative(literal)
|
||||||
@@ -789,7 +874,7 @@ mod tests {
|
|||||||
|
|
||||||
let ty = IntersectionBuilder::new(&db)
|
let ty = IntersectionBuilder::new(&db)
|
||||||
.add_positive(s)
|
.add_positive(s)
|
||||||
.add_negative(Type::StringLiteral(StringLiteralType::new(&db, "a")))
|
.add_negative(Type::string_literal(&db, "a"))
|
||||||
.add_negative(t)
|
.add_negative(t)
|
||||||
.build();
|
.build();
|
||||||
assert_eq!(ty, Type::Never);
|
assert_eq!(ty, Type::Never);
|
||||||
@@ -800,7 +885,7 @@ mod tests {
|
|||||||
let db = setup_db();
|
let db = setup_db();
|
||||||
|
|
||||||
let t1 = Type::IntLiteral(1);
|
let t1 = Type::IntLiteral(1);
|
||||||
let t2 = Type::None;
|
let t2 = Type::none(&db);
|
||||||
|
|
||||||
let ty = IntersectionBuilder::new(&db)
|
let ty = IntersectionBuilder::new(&db)
|
||||||
.add_positive(t1)
|
.add_positive(t1)
|
||||||
@@ -823,7 +908,7 @@ mod tests {
|
|||||||
let db = setup_db();
|
let db = setup_db();
|
||||||
|
|
||||||
let t_p = KnownClass::Int.to_instance(&db);
|
let t_p = KnownClass::Int.to_instance(&db);
|
||||||
let t_n = Type::StringLiteral(StringLiteralType::new(&db, "t_n"));
|
let t_n = Type::string_literal(&db, "t_n");
|
||||||
|
|
||||||
let ty = IntersectionBuilder::new(&db)
|
let ty = IntersectionBuilder::new(&db)
|
||||||
.add_positive(t_p)
|
.add_positive(t_p)
|
||||||
@@ -918,4 +1003,66 @@ mod tests {
|
|||||||
.build();
|
.build();
|
||||||
assert_eq!(result, ty);
|
assert_eq!(result, ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_intersection_of_two_unions_simplify() {
|
||||||
|
let mut db = setup_db();
|
||||||
|
db.write_dedented(
|
||||||
|
"/src/module.py",
|
||||||
|
"
|
||||||
|
class A: ...
|
||||||
|
class B: ...
|
||||||
|
a = A()
|
||||||
|
b = B()
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
|
||||||
|
|
||||||
|
let a = global_symbol(&db, file, "a").expect_type();
|
||||||
|
let b = global_symbol(&db, file, "b").expect_type();
|
||||||
|
let union = UnionBuilder::new(&db).add(a).add(b).build();
|
||||||
|
assert_eq!(union.display(&db).to_string(), "A | B");
|
||||||
|
let reversed_union = UnionBuilder::new(&db).add(b).add(a).build();
|
||||||
|
assert_eq!(reversed_union.display(&db).to_string(), "B | A");
|
||||||
|
let intersection = IntersectionBuilder::new(&db)
|
||||||
|
.add_positive(union)
|
||||||
|
.add_positive(reversed_union)
|
||||||
|
.build();
|
||||||
|
assert_eq!(intersection.display(&db).to_string(), "B | A");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_union_of_two_intersections_simplify() {
|
||||||
|
let mut db = setup_db();
|
||||||
|
db.write_dedented(
|
||||||
|
"/src/module.py",
|
||||||
|
"
|
||||||
|
class A: ...
|
||||||
|
class B: ...
|
||||||
|
a = A()
|
||||||
|
b = B()
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
|
||||||
|
|
||||||
|
let a = global_symbol(&db, file, "a").expect_type();
|
||||||
|
let b = global_symbol(&db, file, "b").expect_type();
|
||||||
|
let intersection = IntersectionBuilder::new(&db)
|
||||||
|
.add_positive(a)
|
||||||
|
.add_positive(b)
|
||||||
|
.build();
|
||||||
|
let reversed_intersection = IntersectionBuilder::new(&db)
|
||||||
|
.add_positive(b)
|
||||||
|
.add_positive(a)
|
||||||
|
.build();
|
||||||
|
let union = UnionBuilder::new(&db)
|
||||||
|
.add(intersection)
|
||||||
|
.add(reversed_intersection)
|
||||||
|
.build();
|
||||||
|
assert_eq!(union.display(&db).to_string(), "A & B");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user