Compare commits
76 Commits
micha/shri
...
dhruv/serv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
752ad4d579 | ||
|
|
64effa4aea | ||
|
|
224a36f5f3 | ||
|
|
5347abc766 | ||
|
|
5fab97f1ef | ||
|
|
3aa7ba31b1 | ||
|
|
4dae09ecff | ||
|
|
b9b094869a | ||
|
|
b3c5932fda | ||
|
|
fe3ae587ea | ||
|
|
c2b9fa84f7 | ||
|
|
793264db13 | ||
|
|
4d63c16c19 | ||
|
|
d2e034adcd | ||
|
|
f62e5406f2 | ||
|
|
1be4394155 | ||
|
|
470f852f04 | ||
|
|
b385c7d22a | ||
|
|
529950fba1 | ||
|
|
205222ca6b | ||
|
|
54fccb3ee2 | ||
|
|
8198668fc3 | ||
|
|
fc6b03c8da | ||
|
|
fb09d63e55 | ||
|
|
16d0625dfb | ||
|
|
25920fe489 | ||
|
|
97d0659ce3 | ||
|
|
cfc6941d5c | ||
|
|
f50849aeef | ||
|
|
55ea09401a | ||
|
|
3032867603 | ||
|
|
3ea32e2cdd | ||
|
|
87668e24b1 | ||
|
|
18a9eddf60 | ||
|
|
222660170c | ||
|
|
e84985e9b3 | ||
|
|
01c3e6b94f | ||
|
|
e92d43dfcd | ||
|
|
877c1066d3 | ||
|
|
00b022d472 | ||
|
|
a23e489c79 | ||
|
|
1907e60fab | ||
|
|
a9efdea113 | ||
|
|
0868e73d2c | ||
|
|
711af0d929 | ||
|
|
d8e3fcca97 | ||
|
|
66a0467305 | ||
|
|
4ed5db0d42 | ||
|
|
5cd0de3e4c | ||
|
|
ed9c18d9b4 | ||
|
|
bb2a712f6a | ||
|
|
2d8ccfe6f2 | ||
|
|
31180a84e4 | ||
|
|
82eae511ca | ||
|
|
b5cd4f2f70 | ||
|
|
9f111eaebf | ||
|
|
9304fdf4ec | ||
|
|
0babbca43f | ||
|
|
b6b1947010 | ||
|
|
21999b3be7 | ||
|
|
1ecc6a0d19 | ||
|
|
79f43c9cab | ||
|
|
4ea397adb0 | ||
|
|
033f16233d | ||
|
|
b10be97eae | ||
|
|
f3743e30d0 | ||
|
|
4d083e579d | ||
|
|
7899e8756e | ||
|
|
f3d1bf845e | ||
|
|
4941975e74 | ||
|
|
d4b4f65e20 | ||
|
|
96dd1b1587 | ||
|
|
f29c7b03ec | ||
|
|
3a0d45c85b | ||
|
|
1f17916224 | ||
|
|
61fef0a64a |
4
.github/workflows/publish-playground.yml
vendored
4
.github/workflows/publish-playground.yml
vendored
@@ -35,6 +35,8 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: playground/package-lock.json
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
- uses: jetli/wasm-bindgen-action@v0.2.0
|
||||
- name: "Run wasm-pack"
|
||||
run: wasm-pack build --target web --out-dir ../../playground/src/pkg crates/ruff_wasm
|
||||
@@ -49,7 +51,7 @@ jobs:
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.13.1
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
2
.github/workflows/publish-wasm.yml
vendored
2
.github/workflows/publish-wasm.yml
vendored
@@ -35,6 +35,8 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
- uses: jetli/wasm-bindgen-action@v0.2.0
|
||||
- name: "Run wasm-pack build"
|
||||
run: wasm-pack build --target ${{ matrix.target }} crates/ruff_wasm
|
||||
|
||||
@@ -60,7 +60,7 @@ repos:
|
||||
- black==25.1.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.29.5
|
||||
rev: v1.29.7
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -74,7 +74,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.5
|
||||
rev: v0.9.6
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -84,7 +84,7 @@ repos:
|
||||
|
||||
# Prettier
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.4.2
|
||||
rev: v3.5.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
types: [yaml]
|
||||
|
||||
@@ -209,8 +209,8 @@ This change only affects those using Ruff under its default rule set. Users that
|
||||
|
||||
### Remove support for emoji identifiers ([#7212](https://github.com/astral-sh/ruff/pull/7212))
|
||||
|
||||
Previously, Ruff supported the non-standard compliant emoji identifiers e.g. `📦 = 1`.
|
||||
We decided to remove this non-standard language extension, and Ruff now reports syntax errors for emoji identifiers in your code, the same as CPython.
|
||||
Previously, Ruff supported non-standards-compliant emoji identifiers such as `📦 = 1`.
|
||||
We decided to remove this non-standard language extension. Ruff now reports syntax errors for invalid emoji identifiers in your code, the same as CPython.
|
||||
|
||||
### Improved GitLab fingerprints ([#7203](https://github.com/astral-sh/ruff/pull/7203))
|
||||
|
||||
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -1,5 +1,55 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.7
|
||||
|
||||
### Preview features
|
||||
|
||||
- Consider `__new__` methods as special function type for enforcing class method or static method rules ([#13305](https://github.com/astral-sh/ruff/pull/13305))
|
||||
- \[`airflow`\] Improve the internal logic to differentiate deprecated symbols (`AIR303`) ([#16013](https://github.com/astral-sh/ruff/pull/16013))
|
||||
- \[`refurb`\] Manual timezone monkeypatching (`FURB162`) ([#16113](https://github.com/astral-sh/ruff/pull/16113))
|
||||
- \[`ruff`\] Implicit class variable in dataclass (`RUF045`) ([#14349](https://github.com/astral-sh/ruff/pull/14349))
|
||||
- \[`ruff`\] Skip singleton starred expressions for `incorrectly-parenthesized-tuple-in-subscript` (`RUF031`) ([#16083](https://github.com/astral-sh/ruff/pull/16083))
|
||||
- \[`refurb`\] Check for subclasses includes subscript expressions (`FURB189`) ([#16155](https://github.com/astral-sh/ruff/pull/16155))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-comprehensions`\]: Handle trailing comma in `C403` fix ([#16110](https://github.com/astral-sh/ruff/pull/16110))
|
||||
- \[`flake8-debugger`\] Also flag `sys.breakpointhook` and `sys.__breakpointhook__` (`T100`) ([#16191](https://github.com/astral-sh/ruff/pull/16191))
|
||||
- \[`pydocstyle`\] Handle arguments with the same names as sections (`D417`) ([#16011](https://github.com/astral-sh/ruff/pull/16011))
|
||||
- \[`pylint`\] Correct ordering of arguments in fix for `if-stmt-min-max` (`PLR1730`) ([#16080](https://github.com/astral-sh/ruff/pull/16080))
|
||||
- \[`pylint`\] Do not offer fix for raw strings (`PLE251`) ([#16132](https://github.com/astral-sh/ruff/pull/16132))
|
||||
- \[`pyupgrade`\] Do not upgrade functional `TypedDicts` with private field names to the class-based syntax (`UP013`) ([#16219](https://github.com/astral-sh/ruff/pull/16219))
|
||||
- \[`pyupgrade`\] Handle micro version numbers correctly (`UP036`) ([#16091](https://github.com/astral-sh/ruff/pull/16091))
|
||||
- \[`pyupgrade`\] Unwrap unary expressions correctly (`UP018`) ([#15919](https://github.com/astral-sh/ruff/pull/15919))
|
||||
- \[`ruff`\] Skip `RUF001` diagnostics when visiting string type definitions ([#16122](https://github.com/astral-sh/ruff/pull/16122))
|
||||
- \[`flake8-pyi`\] Avoid flagging `custom-typevar-for-self` on metaclass methods (`PYI019`) ([#16141](https://github.com/astral-sh/ruff/pull/16141))
|
||||
- \[`pycodestyle`\] Exempt `site.addsitedir(...)` calls (`E402`) ([#16251](https://github.com/astral-sh/ruff/pull/16251))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Fix unstable formatting of trailing end-of-line comments of parenthesized attribute values ([#16187](https://github.com/astral-sh/ruff/pull/16187))
|
||||
|
||||
### Server
|
||||
|
||||
- Fix handling of requests received after shutdown message ([#16262](https://github.com/astral-sh/ruff/pull/16262))
|
||||
- Ignore `source.organizeImports.ruff` and `source.fixAll.ruff` code actions for a notebook cell ([#16154](https://github.com/astral-sh/ruff/pull/16154))
|
||||
- Include document specific debug info for `ruff.printDebugInformation` ([#16215](https://github.com/astral-sh/ruff/pull/16215))
|
||||
- Update server to return the debug info as string with `ruff.printDebugInformation` ([#16214](https://github.com/astral-sh/ruff/pull/16214))
|
||||
|
||||
### CLI
|
||||
|
||||
- Warn on invalid `noqa` even when there are no diagnostics ([#16178](https://github.com/astral-sh/ruff/pull/16178))
|
||||
- Better error messages while loading configuration `extend`s ([#15658](https://github.com/astral-sh/ruff/pull/15658))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`refurb`\] Correctly handle lengths of literal strings in `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#16237](https://github.com/astral-sh/ruff/pull/16237))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add FAQ entry for `source.*` code actions in Notebook ([#16212](https://github.com/astral-sh/ruff/pull/16212))
|
||||
- Add `SECURITY.md` ([#16224](https://github.com/astral-sh/ruff/pull/16224))
|
||||
|
||||
## 0.9.6
|
||||
|
||||
### Preview features
|
||||
|
||||
@@ -526,7 +526,7 @@ cargo benchmark
|
||||
#### Benchmark-driven Development
|
||||
|
||||
Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use
|
||||
`--save-baseline=<name>` to store an initial baseline benchmark (e.g. on `main`) and then use
|
||||
`--save-baseline=<name>` to store an initial baseline benchmark (e.g., on `main`) and then use
|
||||
`--benchmark=<name>` to compare against that benchmark. Criterion will print a message telling you
|
||||
if the benchmark improved/regressed compared to that baseline.
|
||||
|
||||
@@ -678,9 +678,9 @@ utils with it:
|
||||
23 Newline 24
|
||||
```
|
||||
|
||||
- `cargo dev print-cst <file>`: Print the CST of a python file using
|
||||
- `cargo dev print-cst <file>`: Print the CST of a Python file using
|
||||
[LibCST](https://github.com/Instagram/LibCST), which is used in addition to the RustPython parser
|
||||
in Ruff. E.g. for `if True: pass # comment` everything including the whitespace is represented:
|
||||
in Ruff. For example, for `if True: pass # comment`, everything, including the whitespace, is represented:
|
||||
|
||||
```text
|
||||
Module {
|
||||
|
||||
193
Cargo.lock
generated
193
Cargo.lock
generated
@@ -8,18 +8,6 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -140,12 +128,6 @@ version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
|
||||
[[package]]
|
||||
name = "append-only-vec"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7992085ec035cfe96992dd31bfd495a2ebd31969bb95f624471cb6c0b349e571"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.7.1"
|
||||
@@ -227,9 +209,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "boxcar"
|
||||
version = "0.2.8"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2721c3c5a6f0e7f7e607125d963fedeb765f545f67adc9d71ed934693881eb42"
|
||||
checksum = "225450ee9328e1e828319b48a89726cffc1b0ad26fd9211ad435de9fa376acae"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
@@ -360,9 +345,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.28"
|
||||
version = "4.5.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
|
||||
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -370,9 +355,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.27"
|
||||
version = "4.5.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
|
||||
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -444,20 +429,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codspeed"
|
||||
version = "2.7.2"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "450a0e9df9df1c154156f4344f99d8f6f6e69d0fc4de96ef6e2e68b2ec3bce97"
|
||||
checksum = "25d2f5a6570db487f5258e0bded6352fa2034c2aeb46bb5cc3ff060a0fcfba2f"
|
||||
dependencies = [
|
||||
"colored 2.2.0",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codspeed-criterion-compat"
|
||||
version = "2.7.2"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eb1a6cb9c20e177fde58cdef97c1c7c9264eb1424fe45c4fccedc2fb078a569"
|
||||
checksum = "f53a55558dedec742b14aae3c5fec389361b8b5ca28c1aadf09dd91faf710074"
|
||||
dependencies = [
|
||||
"codspeed",
|
||||
"colored 2.2.0",
|
||||
@@ -477,7 +464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -486,7 +473,7 @@ version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1011,6 +998,19 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -1100,10 +1100,6 @@ name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
@@ -1111,17 +1107,18 @@ version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1177,7 +1174,7 @@ dependencies = [
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"windows-core 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1681,6 +1678,19 @@ version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lsp-server"
|
||||
version = "0.7.8"
|
||||
@@ -2648,7 +2658,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.6"
|
||||
version = "0.9.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2882,7 +2892,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.6"
|
||||
version = "0.9.7"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3201,7 +3211,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.9.6"
|
||||
version = "0.9.7"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3235,6 +3245,7 @@ dependencies = [
|
||||
"glob",
|
||||
"globset",
|
||||
"ignore",
|
||||
"indexmap",
|
||||
"is-macro",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
@@ -3312,14 +3323,14 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
"crossbeam",
|
||||
"crossbeam-queue",
|
||||
"dashmap 6.1.0",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.15.2",
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"parking_lot",
|
||||
@@ -3334,12 +3345,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=c8826fa4d1d9e3cba4c6e578763878b71fa9a10d#c8826fa4d1d9e3cba4c6e578763878b71fa9a10d"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3381,6 +3392,12 @@ dependencies = [
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -3548,9 +3565,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||
|
||||
[[package]]
|
||||
name = "snapbox"
|
||||
@@ -3610,18 +3627,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.0"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce1475c515a4f03a8a7129bb5228b81a781a86cb0b3fbbc19e1c556d491a401f"
|
||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.0"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9688894b43459159c82bfa5a5fa0435c19cbe3c9b427fa1dd7b1ce0c279b18a7"
|
||||
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3665,9 +3682,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.16.0"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
|
||||
checksum = "a40f762a77d2afa88c2d919489e390a12bdd261ed568e60cfa7e48d4e20f0d33"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
@@ -4439,7 +4456,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4448,6 +4465,16 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core 0.58.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@@ -4457,6 +4484,60 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
||||
@@ -123,7 +123,7 @@ rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "c8826fa4d1d9e3cba4c6e578763878b71fa9a10d" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
@@ -149,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.9.6/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.6/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.9.7/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.7/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.9.6
|
||||
rev: v0.9.7
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
15
SECURITY.md
Normal file
15
SECURITY.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Security policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
If you have found a possible vulnerability, please email `security at astral dot sh`.
|
||||
|
||||
## Bug bounties
|
||||
|
||||
While we sincerely appreciate and encourage reports of suspected security problems, please note that
|
||||
Astral does not currently run any bug bounty programs.
|
||||
|
||||
## Vulnerability disclosures
|
||||
|
||||
Critical vulnerabilities will be disclosed via GitHub's
|
||||
[security advisory](https://github.com/astral-sh/ruff/security) system.
|
||||
25
crates/red_knot/README.md
Normal file
25
crates/red_knot/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Red Knot
|
||||
|
||||
Red Knot is an extremely fast type checker.
|
||||
Currently, it is a work-in-progress and not ready for user testing.
|
||||
|
||||
Red Knot is designed to prioritize good type inference, even in unannotated code,
|
||||
and aims to avoid false positives.
|
||||
|
||||
While Red Knot will produce similar results to mypy and pyright on many codebases,
|
||||
100% compatibility with these tools is a non-goal.
|
||||
On some codebases, Red Knot's design decisions lead to different outcomes
|
||||
than you would get from running one of these more established tools.
|
||||
|
||||
## Contributing
|
||||
|
||||
Core type checking tests are written as Markdown code blocks.
|
||||
They can be found in [`red_knot_python_semantic/resources/mdtest`][resources-mdtest].
|
||||
See [`red_knot_test/README.md`][mdtest-readme] for more information
|
||||
on the test framework itself.
|
||||
|
||||
The list of open issues can be found [here][open-issues].
|
||||
|
||||
[mdtest-readme]: ../red_knot_test/README.md
|
||||
[open-issues]: https://github.com/astral-sh/ruff/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3Ared-knot
|
||||
[resources-mdtest]: ../red_knot_python_semantic/resources/mdtest
|
||||
@@ -40,7 +40,7 @@ impl std::fmt::Display for PythonVersion {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PythonVersion> for ruff_python_ast::python_version::PythonVersion {
|
||||
impl From<PythonVersion> for ruff_python_ast::PythonVersion {
|
||||
fn from(value: PythonVersion) -> Self {
|
||||
match value {
|
||||
PythonVersion::Py37 => Self::PY37,
|
||||
@@ -61,8 +61,8 @@ mod tests {
|
||||
#[test]
|
||||
fn same_default_as_python_version() {
|
||||
assert_eq!(
|
||||
ruff_python_ast::python_version::PythonVersion::from(PythonVersion::default()),
|
||||
ruff_python_ast::python_version::PythonVersion::default()
|
||||
ruff_python_ast::PythonVersion::from(PythonVersion::default()),
|
||||
ruff_python_ast::PythonVersion::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use ruff_db::system::{
|
||||
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
|
||||
};
|
||||
use ruff_db::Upcast;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
struct TestCase {
|
||||
db: ProjectDatabase,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{collections::HashMap, hash::BuildHasher};
|
||||
|
||||
use red_knot_python_semantic::{PythonPlatform, SitePackages};
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
/// Combine two values, preferring the values in `self`.
|
||||
///
|
||||
|
||||
@@ -310,7 +310,7 @@ mod tests {
|
||||
use anyhow::{anyhow, Context};
|
||||
use insta::assert_ron_snapshot;
|
||||
use ruff_db::system::{SystemPathBuf, TestSystem};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::{ProjectDiscoveryError, ProjectMetadata};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::Combine;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::metadata::options::Options;
|
||||
use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard};
|
||||
use pep440_rs::{release_specifiers_to_ranges, Version, VersionSpecifiers};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::Bound;
|
||||
use std::ops::Deref;
|
||||
|
||||
@@ -73,12 +73,12 @@ qux = (foo, bar)
|
||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||
|
||||
# TODO: Infer "LiteralString"
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(Attribute access on `StringLiteral` types)
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(decorated method)
|
||||
|
||||
template: LiteralString = "{}, {}"
|
||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||
# TODO: Infer `LiteralString`
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(Attribute access on `StringLiteral` types)
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(decorated method)
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
@@ -40,7 +40,7 @@ class C:
|
||||
return 42
|
||||
|
||||
x = C()
|
||||
# error: [invalid-argument-type]
|
||||
# error: [unsupported-operator] "Operator `-=` is unsupported between objects of type `C` and `Literal[1]`"
|
||||
x -= 1
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
@@ -30,7 +30,11 @@ reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a
|
||||
# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||
|
||||
# TODO: should be `int | None`
|
||||
# There is no special handling of attributes that are (directly) assigned to a declared parameter,
|
||||
# which means we union with `Unknown` here, since the attribute itself is not declared. This is
|
||||
# something that we might want to change in the future.
|
||||
#
|
||||
# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion.
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
||||
|
||||
reveal_type(c_instance.declared_only) # revealed: bytes
|
||||
@@ -45,10 +49,10 @@ reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str
|
||||
c_instance.inferred_from_value = "value set on instance"
|
||||
|
||||
# This assignment is also fine:
|
||||
c_instance.inferred_from_param = None
|
||||
c_instance.declared_and_bound = False
|
||||
|
||||
# TODO: this should be an error (incompatible types in assignment)
|
||||
c_instance.inferred_from_param = "incompatible"
|
||||
# error: [invalid-assignment] "Object of type `Literal["incompatible"]` is not assignable to attribute `declared_and_bound` of type `bool`"
|
||||
c_instance.declared_and_bound = "incompatible"
|
||||
|
||||
# TODO: we already show an error here but the message might be improved?
|
||||
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
||||
@@ -181,7 +185,6 @@ reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a
|
||||
# TODO: Should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||
|
||||
# TODO: Should be `int | None`
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
||||
|
||||
reveal_type(c_instance.declared_only) # revealed: bytes
|
||||
@@ -881,13 +884,18 @@ def _(flag: bool):
|
||||
|
||||
## Objects of all types have a `__class__` method
|
||||
|
||||
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
|
||||
`type(x)`.
|
||||
|
||||
```py
|
||||
import typing_extensions
|
||||
|
||||
reveal_type(typing_extensions.__class__) # revealed: Literal[ModuleType]
|
||||
reveal_type(type(typing_extensions)) # revealed: Literal[ModuleType]
|
||||
|
||||
a = 42
|
||||
reveal_type(a.__class__) # revealed: Literal[int]
|
||||
reveal_type(type(a)) # revealed: Literal[int]
|
||||
|
||||
b = "42"
|
||||
reveal_type(b.__class__) # revealed: Literal[str]
|
||||
@@ -903,8 +911,13 @@ reveal_type(e.__class__) # revealed: Literal[tuple]
|
||||
|
||||
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
|
||||
reveal_type(a.__class__) # revealed: type[int]
|
||||
reveal_type(type(a)) # revealed: type[int]
|
||||
|
||||
reveal_type(b.__class__) # revealed: Literal[str]
|
||||
reveal_type(type(b)) # revealed: Literal[str]
|
||||
|
||||
reveal_type(c.__class__) # revealed: type[int] | type[str]
|
||||
reveal_type(type(c)) # revealed: type[int] | type[str]
|
||||
|
||||
# `type[type]`, a.k.a., either the class `type` or some subclass of `type`.
|
||||
# It would be incorrect to infer `Literal[type]` here,
|
||||
@@ -1002,8 +1015,8 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py
|
||||
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
|
||||
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
|
||||
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
reveal_type(f.__call__) # revealed: <bound method `__call__` of `Literal[f]`>
|
||||
```
|
||||
|
||||
### Int-literal attributes
|
||||
@@ -1012,7 +1025,7 @@ Most attribute accesses on int-literal types are delegated to `builtins.int`, si
|
||||
integers are instances of that class:
|
||||
|
||||
```py
|
||||
reveal_type((2).bit_length) # revealed: @Todo(bound method)
|
||||
reveal_type((2).bit_length) # revealed: <bound method `bit_length` of `Literal[2]`>
|
||||
reveal_type((2).denominator) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
@@ -1026,11 +1039,11 @@ reveal_type((2).real) # revealed: Literal[2]
|
||||
### Bool-literal attributes
|
||||
|
||||
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
|
||||
bols are instances of that class:
|
||||
bools are instances of that class:
|
||||
|
||||
```py
|
||||
reveal_type(True.__and__) # revealed: @Todo(bound method)
|
||||
reveal_type(False.__or__) # revealed: @Todo(bound method)
|
||||
reveal_type(True.__and__) # revealed: @Todo(decorated method)
|
||||
reveal_type(False.__or__) # revealed: @Todo(decorated method)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
@@ -1042,11 +1055,11 @@ reveal_type(False.real) # revealed: Literal[0]
|
||||
|
||||
### Bytes-literal attributes
|
||||
|
||||
All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`:
|
||||
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
|
||||
|
||||
```py
|
||||
reveal_type(b"foo".join) # revealed: @Todo(bound method)
|
||||
reveal_type(b"foo".endswith) # revealed: @Todo(bound method)
|
||||
reveal_type(b"foo".join) # revealed: <bound method `join` of `Literal[b"foo"]`>
|
||||
reveal_type(b"foo".endswith) # revealed: <bound method `endswith` of `Literal[b"foo"]`>
|
||||
```
|
||||
|
||||
## Instance attribute edge cases
|
||||
@@ -1133,6 +1146,40 @@ class C:
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Builtin types attributes
|
||||
|
||||
This test can probably be removed eventually, but we currently include it because we do not yet
|
||||
understand generic bases and protocols, and we want to make sure that we can still use builtin types
|
||||
in our tests in the meantime. See the corresponding TODO in `Type::static_member` for more
|
||||
information.
|
||||
|
||||
```py
|
||||
class C:
|
||||
a_int: int = 1
|
||||
a_str: str = "a"
|
||||
a_bytes: bytes = b"a"
|
||||
a_bool: bool = True
|
||||
a_float: float = 1.0
|
||||
a_complex: complex = 1 + 1j
|
||||
a_tuple: tuple[int] = (1,)
|
||||
a_range: range = range(1)
|
||||
a_slice: slice = slice(1)
|
||||
a_type: type = int
|
||||
a_none: None = None
|
||||
|
||||
reveal_type(C.a_int) # revealed: int
|
||||
reveal_type(C.a_str) # revealed: str
|
||||
reveal_type(C.a_bytes) # revealed: bytes
|
||||
reveal_type(C.a_bool) # revealed: bool
|
||||
reveal_type(C.a_float) # revealed: int | float
|
||||
reveal_type(C.a_complex) # revealed: int | float | complex
|
||||
reveal_type(C.a_tuple) # revealed: tuple[int]
|
||||
reveal_type(C.a_range) # revealed: range
|
||||
reveal_type(C.a_slice) # revealed: slice
|
||||
reveal_type(C.a_type) # revealed: type
|
||||
reveal_type(C.a_none) # revealed: None
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||
|
||||
@@ -244,10 +244,7 @@ class B:
|
||||
def __rsub__(self, other: A) -> B:
|
||||
return B()
|
||||
|
||||
# TODO: this should be `B` (the return annotation of `B.__rsub__`),
|
||||
# because `A.__sub__` is annotated as only accepting `A`,
|
||||
# but `B.__rsub__` will accept `A`.
|
||||
reveal_type(A() - B()) # revealed: A
|
||||
reveal_type(A() - B()) # revealed: B
|
||||
```
|
||||
|
||||
## Callable instances as dunders
|
||||
@@ -263,7 +260,10 @@ class B:
|
||||
__add__ = A()
|
||||
|
||||
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type
|
||||
reveal_type(B() + B()) # revealed: Unknown | int
|
||||
# TODO: Should not be an error: `A` instance is not a method descriptor, don't prepend `self` arg.
|
||||
# Revealed type should be `Unknown | int`.
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `B` and `B`"
|
||||
reveal_type(B() + B()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Integration test: numbers from typeshed
|
||||
@@ -277,22 +277,14 @@ return annotations from the widening, and preserve a bit more precision here?
|
||||
reveal_type(3j + 3.14) # revealed: int | float | complex
|
||||
reveal_type(4.2 + 42) # revealed: int | float
|
||||
reveal_type(3j + 3) # revealed: int | float | complex
|
||||
|
||||
# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3.14 + 3j) # revealed: int | float
|
||||
|
||||
# TODO should be int | float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(42 + 4.2) # revealed: int
|
||||
|
||||
# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3 + 3j) # revealed: int
|
||||
reveal_type(3.14 + 3j) # revealed: int | float | complex
|
||||
reveal_type(42 + 4.2) # revealed: int | float
|
||||
reveal_type(3 + 3j) # revealed: int | float | complex
|
||||
|
||||
def _(x: bool, y: int):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(4.2 + x) # revealed: int | float
|
||||
|
||||
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(y + 4.12) # revealed: int
|
||||
reveal_type(y + 4.12) # revealed: int | float
|
||||
```
|
||||
|
||||
## With literal types
|
||||
@@ -309,8 +301,7 @@ class A:
|
||||
return self
|
||||
|
||||
reveal_type(A() + 1) # revealed: A
|
||||
# TODO should be `A` since `int.__add__` doesn't support `A` instances
|
||||
reveal_type(1 + A()) # revealed: int
|
||||
reveal_type(1 + A()) # revealed: A
|
||||
|
||||
reveal_type(A() + "foo") # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
@@ -360,6 +351,20 @@ class Y(Foo): ...
|
||||
reveal_type(X() + Y()) # revealed: int
|
||||
```
|
||||
|
||||
## Operations involving types with invalid `__bool__` methods
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
a = NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 and a and True
|
||||
```
|
||||
|
||||
## Unsupported
|
||||
|
||||
### Dunder as instance attribute
|
||||
|
||||
@@ -10,9 +10,9 @@ reveal_type(-3 // 3) # revealed: Literal[-1]
|
||||
reveal_type(-3 / 3) # revealed: float
|
||||
reveal_type(5 % 3) # revealed: Literal[2]
|
||||
|
||||
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
|
||||
# formal parameter type.
|
||||
reveal_type(2 + "f") # revealed: int
|
||||
# TODO: Should emit `unsupported-operator` but we don't understand the bases of `str`, so we think
|
||||
# it inherits `Unknown`, so we think `str.__radd__` is `Unknown` instead of nonexistent.
|
||||
reveal_type(2 + "f") # revealed: Unknown
|
||||
|
||||
def lhs(x: int):
|
||||
reveal_type(x + 1) # revealed: int
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Calling builtins
|
||||
|
||||
## `bool` with incorrect arguments
|
||||
|
||||
```py
|
||||
class NotBool:
|
||||
__bool__ = None
|
||||
|
||||
# TODO: We should emit an `invalid-argument` error here for `2` because `bool` only takes one argument.
|
||||
bool(1, 2)
|
||||
|
||||
# TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly.
|
||||
bool(NotBool())
|
||||
```
|
||||
|
||||
## Calls to `type()`
|
||||
|
||||
A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
|
||||
tested more extensively in `crates/red_knot_python_semantic/resources/mdtest/attributes.md`,
|
||||
alongside the tests for the `__class__` attribute.)
|
||||
|
||||
```py
|
||||
reveal_type(type(1)) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
But a three-argument call to type creates a dynamic instance of the `type` class:
|
||||
|
||||
```py
|
||||
reveal_type(type("Foo", (), {})) # revealed: type
|
||||
```
|
||||
|
||||
Other numbers of arguments are invalid (TODO -- these should emit a diagnostic)
|
||||
|
||||
```py
|
||||
type("Foo", ())
|
||||
type("Foo", (), {}, weird_other_arg=42)
|
||||
```
|
||||
@@ -52,7 +52,7 @@ class NonCallable:
|
||||
__call__ = 1
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Unknown | Literal[1]` is not callable (due to union element `Literal[1]`)"
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(a()) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -67,8 +67,8 @@ def _(flag: bool):
|
||||
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
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(a()) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Call binding errors
|
||||
@@ -99,3 +99,26 @@ c = C()
|
||||
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
|
||||
reveal_type(c()) # revealed: int
|
||||
```
|
||||
|
||||
## Union over callables
|
||||
|
||||
### Possibly unbound `__call__`
|
||||
|
||||
```py
|
||||
def outer(cond1: bool):
|
||||
class Test:
|
||||
if cond1:
|
||||
def __call__(self): ...
|
||||
|
||||
class Other:
|
||||
def __call__(self): ...
|
||||
|
||||
def inner(cond2: bool):
|
||||
if cond2:
|
||||
a = Test()
|
||||
else:
|
||||
a = Other()
|
||||
|
||||
# error: [call-non-callable] "Object of type `Test` is not callable (possibly unbound `__call__` method)"
|
||||
a()
|
||||
```
|
||||
|
||||
@@ -278,10 +278,10 @@ proper diagnostics in case of missing or superfluous arguments.
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
|
||||
reveal_type() # revealed: Unknown
|
||||
reveal_type()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
|
||||
reveal_type(1, 2) # revealed: Literal[1]
|
||||
reveal_type(1, 2)
|
||||
```
|
||||
|
||||
### `static_assert`
|
||||
@@ -290,7 +290,6 @@ reveal_type(1, 2) # revealed: Literal[1]
|
||||
from knot_extensions import static_assert
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
|
||||
# error: [static-assert-error]
|
||||
static_assert()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# `inspect.getattr_static`
|
||||
|
||||
## Basic usage
|
||||
|
||||
`inspect.getattr_static` is a function that returns attributes of an object without invoking the
|
||||
descriptor protocol (for caveats, see the [official documentation]).
|
||||
|
||||
Consider the following example:
|
||||
|
||||
```py
|
||||
import inspect
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance, owner) -> str:
|
||||
return 1
|
||||
|
||||
class C:
|
||||
normal: int = 1
|
||||
descriptor: Descriptor = Descriptor()
|
||||
```
|
||||
|
||||
If we access attributes on an instance of `C` as usual, the descriptor protocol is invoked, and we
|
||||
get a type of `str` for the `descriptor` attribute:
|
||||
|
||||
```py
|
||||
c = C()
|
||||
|
||||
reveal_type(c.normal) # revealed: int
|
||||
reveal_type(c.descriptor) # revealed: str
|
||||
```
|
||||
|
||||
However, if we use `inspect.getattr_static`, we can see the underlying `Descriptor` type:
|
||||
|
||||
```py
|
||||
reveal_type(inspect.getattr_static(c, "normal")) # revealed: int
|
||||
reveal_type(inspect.getattr_static(c, "descriptor")) # revealed: Descriptor
|
||||
```
|
||||
|
||||
For non-existent attributes, a default value can be provided:
|
||||
|
||||
```py
|
||||
reveal_type(inspect.getattr_static(C, "normal", "default-arg")) # revealed: int
|
||||
reveal_type(inspect.getattr_static(C, "non_existent", "default-arg")) # revealed: Literal["default-arg"]
|
||||
```
|
||||
|
||||
When a non-existent attribute is accessed without a default value, the runtime raises an
|
||||
`AttributeError`. We could emit a diagnostic for this case, but that is currently not supported:
|
||||
|
||||
```py
|
||||
# TODO: we could emit a diagnostic here
|
||||
reveal_type(inspect.getattr_static(C, "non_existent")) # revealed: Never
|
||||
```
|
||||
|
||||
We can access attributes on objects of all kinds:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(inspect.getattr_static(sys, "platform")) # revealed: LiteralString
|
||||
reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static]
|
||||
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
(Implicit) instance attributes can also be accessed through `inspect.getattr_static`:
|
||||
|
||||
```py
|
||||
class D:
|
||||
def __init__(self) -> None:
|
||||
self.instance_attr: int = 1
|
||||
|
||||
reveal_type(inspect.getattr_static(D(), "instance_attr")) # revealed: int
|
||||
```
|
||||
|
||||
## Error cases
|
||||
|
||||
We can only infer precise types if the attribute is a literal string. In all other cases, we fall
|
||||
back to `Any`:
|
||||
|
||||
```py
|
||||
import inspect
|
||||
|
||||
class C:
|
||||
x: int = 1
|
||||
|
||||
def _(attr_name: str):
|
||||
reveal_type(inspect.getattr_static(C(), attr_name)) # revealed: Any
|
||||
reveal_type(inspect.getattr_static(C(), attr_name, 1)) # revealed: Any
|
||||
```
|
||||
|
||||
But we still detect errors in the number or type of arguments:
|
||||
|
||||
```py
|
||||
# error: [missing-argument] "No arguments provided for required parameters `obj`, `attr` of function `getattr_static`"
|
||||
inspect.getattr_static()
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `attr`"
|
||||
inspect.getattr_static(C())
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`attr`) of function `getattr_static`; expected type `str`"
|
||||
inspect.getattr_static(C(), 1)
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `getattr_static`: expected 3, got 4"
|
||||
inspect.getattr_static(C(), "x", "default-arg", "one too many")
|
||||
```
|
||||
|
||||
## Possibly unbound attributes
|
||||
|
||||
```py
|
||||
import inspect
|
||||
|
||||
def _(flag: bool):
|
||||
class C:
|
||||
if flag:
|
||||
x: int = 1
|
||||
|
||||
reveal_type(inspect.getattr_static(C, "x", "default")) # revealed: int | Literal["default"]
|
||||
```
|
||||
|
||||
## Gradual types
|
||||
|
||||
```py
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
def _(a: Any, tuple_of_any: tuple[Any]):
|
||||
reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"]
|
||||
|
||||
# TODO: Ideally, this would just be `Literal[index]`
|
||||
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default")) # revealed: Literal[index] | Literal["default"]
|
||||
```
|
||||
|
||||
[official documentation]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static
|
||||
258
crates/red_knot_python_semantic/resources/mdtest/call/methods.md
Normal file
258
crates/red_knot_python_semantic/resources/mdtest/call/methods.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Methods
|
||||
|
||||
## Background: Functions as descriptors
|
||||
|
||||
> Note: See also this related section in the descriptor guide: [Functions and methods].
|
||||
|
||||
Say we have a simple class `C` with a function definition `f` inside its body:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def f(self, x: int) -> str:
|
||||
return "a"
|
||||
```
|
||||
|
||||
Whenever we access the `f` attribute through the class object itself (`C.f`) or through an instance
|
||||
(`C().f`), this access happens via the descriptor protocol. Functions are (non-data) descriptors
|
||||
because they implement a `__get__` method. This is crucial in making sure that method calls work as
|
||||
expected. In general, the signature of the `__get__` method in the descriptor protocol is
|
||||
`__get__(self, instance, owner)`. The `self` argument is the descriptor object itself (`f`). The
|
||||
passed value for the `instance` argument depends on whether the attribute is accessed from the class
|
||||
object (in which case it is `None`), or from an instance (in which case it is the instance of type
|
||||
`C`). The `owner` argument is the class itself (`C` of type `Literal[C]`). To summarize:
|
||||
|
||||
- `C.f` is equivalent to `getattr_static(C, "f").__get__(None, C)`
|
||||
- `C().f` is equivalent to `getattr_static(C, "f").__get__(C(), C)`
|
||||
|
||||
Here, `inspect.getattr_static` is used to bypass the descriptor protocol and directly access the
|
||||
function attribute. The way the special `__get__` method *on functions* works is as follows. In the
|
||||
former case, if the `instance` argument is `None`, `__get__` simply returns the function itself. In
|
||||
the latter case, it returns a *bound method* object:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
|
||||
|
||||
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
|
||||
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: Literal[f]
|
||||
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `C`>
|
||||
```
|
||||
|
||||
In conclusion, this is why we see the following two types when accessing the `f` attribute on the
|
||||
class object `C` and on an instance `C()`:
|
||||
|
||||
```py
|
||||
reveal_type(C.f) # revealed: Literal[f]
|
||||
reveal_type(C().f) # revealed: <bound method `f` of `C`>
|
||||
```
|
||||
|
||||
A bound method is a callable object that contains a reference to the `instance` that it was called
|
||||
on (can be inspected via `__self__`), and the function object that it refers to (can be inspected
|
||||
via `__func__`):
|
||||
|
||||
```py
|
||||
bound_method = C().f
|
||||
|
||||
reveal_type(bound_method.__self__) # revealed: C
|
||||
reveal_type(bound_method.__func__) # revealed: Literal[f]
|
||||
```
|
||||
|
||||
When we call the bound method, the `instance` is implicitly passed as the first argument (`self`):
|
||||
|
||||
```py
|
||||
reveal_type(C().f(1)) # revealed: str
|
||||
reveal_type(bound_method(1)) # revealed: str
|
||||
```
|
||||
|
||||
When we call the function object itself, we need to pass the `instance` explicitly:
|
||||
|
||||
```py
|
||||
C.f(1) # error: [missing-argument]
|
||||
|
||||
reveal_type(C.f(C(), 1)) # revealed: str
|
||||
```
|
||||
|
||||
When we access methods from derived classes, they will be bound to instances of the derived class:
|
||||
|
||||
```py
|
||||
class D(C):
|
||||
pass
|
||||
|
||||
reveal_type(D().f) # revealed: <bound method `f` of `D`>
|
||||
```
|
||||
|
||||
If we access an attribute on a bound method object itself, it will defer to `types.MethodType`:
|
||||
|
||||
```py
|
||||
reveal_type(bound_method.__hash__) # revealed: <bound method `__hash__` of `MethodType`>
|
||||
```
|
||||
|
||||
If an attribute is not available on the bound method object, it will be looked up on the underlying
|
||||
function object. We model this explicitly, which means that we can access `__kwdefaults__` on bound
|
||||
methods, even though it is not available on `types.MethodType`:
|
||||
|
||||
```py
|
||||
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None
|
||||
```
|
||||
|
||||
## Basic method calls on class objects and instances
|
||||
|
||||
```py
|
||||
class Base:
|
||||
def method_on_base(self, x: int | None) -> str:
|
||||
return "a"
|
||||
|
||||
class Derived(Base):
|
||||
def method_on_derived(self, x: bytes) -> tuple[int, str]:
|
||||
return (1, "a")
|
||||
|
||||
reveal_type(Base().method_on_base(1)) # revealed: str
|
||||
reveal_type(Base.method_on_base(Base(), 1)) # revealed: str
|
||||
|
||||
Base().method_on_base("incorrect") # error: [invalid-argument-type]
|
||||
Base().method_on_base() # error: [missing-argument]
|
||||
Base().method_on_base(1, 2) # error: [too-many-positional-arguments]
|
||||
|
||||
reveal_type(Derived().method_on_base(1)) # revealed: str
|
||||
reveal_type(Derived().method_on_derived(b"abc")) # revealed: tuple[int, str]
|
||||
reveal_type(Derived.method_on_base(Derived(), 1)) # revealed: str
|
||||
reveal_type(Derived.method_on_derived(Derived(), b"abc")) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Method calls on literals
|
||||
|
||||
### Boolean literals
|
||||
|
||||
```py
|
||||
reveal_type(True.bit_length()) # revealed: int
|
||||
reveal_type(True.as_integer_ratio()) # revealed: tuple[int, Literal[1]]
|
||||
```
|
||||
|
||||
### Integer literals
|
||||
|
||||
```py
|
||||
reveal_type((42).bit_length()) # revealed: int
|
||||
```
|
||||
|
||||
### String literals
|
||||
|
||||
```py
|
||||
reveal_type("abcde".find("abc")) # revealed: int
|
||||
reveal_type("foo".encode(encoding="utf-8")) # revealed: bytes
|
||||
|
||||
"abcde".find(123) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Bytes literals
|
||||
|
||||
```py
|
||||
reveal_type(b"abcde".startswith(b"abc")) # revealed: bool
|
||||
```
|
||||
|
||||
## Method calls on `LiteralString`
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
def f(s: LiteralString) -> None:
|
||||
reveal_type(s.find("a")) # revealed: int
|
||||
```
|
||||
|
||||
## Method calls on `tuple`
|
||||
|
||||
```py
|
||||
def f(t: tuple[int, str]) -> None:
|
||||
reveal_type(t.index("a")) # revealed: int
|
||||
```
|
||||
|
||||
## Method calls on unions
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class A:
|
||||
def f(self) -> int:
|
||||
return 1
|
||||
|
||||
class B:
|
||||
def f(self) -> str:
|
||||
return "a"
|
||||
|
||||
def f(a_or_b: A | B, any_or_a: Any | A):
|
||||
reveal_type(a_or_b.f) # revealed: <bound method `f` of `A`> | <bound method `f` of `B`>
|
||||
reveal_type(a_or_b.f()) # revealed: int | str
|
||||
|
||||
reveal_type(any_or_a.f) # revealed: Any | <bound method `f` of `A`>
|
||||
reveal_type(any_or_a.f()) # revealed: Any | int
|
||||
```
|
||||
|
||||
## Method calls on `KnownInstance` types
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr.__or__) # revealed: <bound method `__or__` of `typing.TypeAliasType`>
|
||||
```
|
||||
|
||||
## Error cases: Calling `__get__` for methods
|
||||
|
||||
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
|
||||
|
||||
```py
|
||||
from types import FunctionType, MethodType
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: type, /) -> FunctionType: ...
|
||||
@overload
|
||||
def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
|
||||
```
|
||||
|
||||
Here, we test that this signature is enforced correctly:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
class C:
|
||||
def f(self, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
method_wrapper = getattr_static(C, "f").__get__
|
||||
|
||||
reveal_type(method_wrapper) # revealed: <method-wrapper `__get__` of `f`>
|
||||
|
||||
# All of these are fine:
|
||||
method_wrapper(C(), C)
|
||||
method_wrapper(C())
|
||||
method_wrapper(C(), None)
|
||||
method_wrapper(None, C)
|
||||
|
||||
# Passing `None` without an `owner` argument is an
|
||||
# error: [missing-argument] "No argument provided for required parameter `owner`"
|
||||
method_wrapper(None)
|
||||
|
||||
# Passing something that is not assignable to `type` as the `owner` argument is an
|
||||
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`); expected type `type`"
|
||||
method_wrapper(None, 1)
|
||||
|
||||
# Passing `None` as the `owner` argument when `instance` is `None` is an
|
||||
# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`); expected type `type`"
|
||||
method_wrapper(None, None)
|
||||
|
||||
# Calling `__get__` without any arguments is an
|
||||
# error: [missing-argument] "No argument provided for required parameter `instance`"
|
||||
method_wrapper()
|
||||
|
||||
# Calling `__get__` with too many positional arguments is an
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments: expected 2, got 3"
|
||||
method_wrapper(C(), C, "one too many")
|
||||
```
|
||||
|
||||
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
||||
@@ -39,8 +39,8 @@ def _(flag: bool):
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(x) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Multiple non-callable elements in a union
|
||||
@@ -56,8 +56,8 @@ def _(flag: bool, flag2: bool):
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
# revealed: Unknown | int
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
# revealed: int | Unknown
|
||||
reveal_type(f())
|
||||
```
|
||||
|
||||
@@ -72,6 +72,39 @@ def _(flag: bool):
|
||||
else:
|
||||
f = "foo"
|
||||
|
||||
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
|
||||
x = f() # error: [call-non-callable] "Object of type `Literal[1, "foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Mismatching signatures
|
||||
|
||||
Calling a union where the arguments don't match the signature of all variants.
|
||||
|
||||
```py
|
||||
def f1(a: int) -> int: ...
|
||||
def f2(a: str) -> str: ...
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = f1
|
||||
else:
|
||||
f = f2
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal[3]` cannot be assigned to parameter 1 (`a`) of function `f2`; expected type `str`"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
## Any non-callable variant
|
||||
|
||||
```py
|
||||
def f1(a: int): ...
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = f1
|
||||
else:
|
||||
f = "This is a string literal"
|
||||
|
||||
# error: [call-non-callable] "Object of type `Literal["This is a string literal"]` is not callable"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -21,8 +21,9 @@ class A:
|
||||
|
||||
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
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
|
||||
reveal_type(42 in A()) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `not in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
|
||||
reveal_type(42 not in A()) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -126,9 +127,9 @@ class A:
|
||||
|
||||
reveal_type(CheckContains() in A()) # revealed: bool
|
||||
|
||||
# TODO: should emit diagnostic, need to check arg type,
|
||||
# should not fall back to __iter__ or __getitem__
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckIter` and `A`"
|
||||
reveal_type(CheckIter() in A()) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckGetItem` and `A`"
|
||||
reveal_type(CheckGetItem() in A()) # revealed: bool
|
||||
|
||||
class B:
|
||||
@@ -154,7 +155,50 @@ class A:
|
||||
def __getitem__(self, key: str) -> str:
|
||||
return "foo"
|
||||
|
||||
# TODO should emit a diagnostic
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
|
||||
reveal_type(42 in A()) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `str` and `A`, in comparing `Literal["hello"]` with `A`"
|
||||
reveal_type("hello" in A()) # revealed: bool
|
||||
```
|
||||
|
||||
## Return type that doesn't implement `__bool__` correctly
|
||||
|
||||
`in` and `not in` operations will fail at runtime if the object on the right-hand side of the
|
||||
operation has a `__contains__` method that returns a type which is not convertible to `bool`. This
|
||||
is because of the way these operations are handled by the Python interpreter at runtime. If we
|
||||
assume that `y` is an object that has a `__contains__` method, the Python expression `x in y`
|
||||
desugars to a `contains(y, x)` call, where `contains` looks something like this:
|
||||
|
||||
```ignore
|
||||
def contains(y, x):
|
||||
return bool(type(y).__contains__(y, x))
|
||||
```
|
||||
|
||||
where the `bool()` conversion itself implicitly calls `__bool__` under the hood.
|
||||
|
||||
TODO: Ideally the message would explain to the user what's wrong. E.g,
|
||||
|
||||
```ignore
|
||||
error: [operator] cannot use `in` operator on object of type `WithContains`
|
||||
note: This is because the `in` operator implicitly calls `WithContains.__contains__`, but `WithContains.__contains__` is invalidly defined
|
||||
note: `WithContains.__contains__` is invalidly defined because it returns an instance of `NotBoolable`, which cannot be evaluated in a boolean context
|
||||
note: `NotBoolable` cannot be evaluated in a boolean context because its `__bool__` attribute is not callable
|
||||
```
|
||||
|
||||
It may also be more appropriate to use `unsupported-operator` as the error code.
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
class WithContains:
|
||||
def __contains__(self, item) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 in WithContains()
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 not in WithContains()
|
||||
```
|
||||
|
||||
@@ -16,31 +16,38 @@ most common case involves implementing these methods for the same type:
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class EqReturnType: ...
|
||||
class NeReturnType: ...
|
||||
class LtReturnType: ...
|
||||
class LeReturnType: ...
|
||||
class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: A) -> int:
|
||||
return 42
|
||||
def __eq__(self, other: A) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: A) -> bytearray:
|
||||
return bytearray()
|
||||
def __ne__(self, other: A) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: A) -> str:
|
||||
return "42"
|
||||
def __lt__(self, other: A) -> LtReturnType:
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, other: A) -> bytes:
|
||||
return b"42"
|
||||
def __le__(self, other: A) -> LeReturnType:
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, other: A) -> list:
|
||||
return [42]
|
||||
def __gt__(self, other: A) -> GtReturnType:
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, other: A) -> set:
|
||||
return {42}
|
||||
def __ge__(self, other: A) -> GeReturnType:
|
||||
return GeReturnType()
|
||||
|
||||
reveal_type(A() == A()) # revealed: int
|
||||
reveal_type(A() != A()) # revealed: bytearray
|
||||
reveal_type(A() < A()) # revealed: str
|
||||
reveal_type(A() <= A()) # revealed: bytes
|
||||
reveal_type(A() > A()) # revealed: list
|
||||
reveal_type(A() >= A()) # revealed: set
|
||||
reveal_type(A() == A()) # revealed: EqReturnType
|
||||
reveal_type(A() != A()) # revealed: NeReturnType
|
||||
reveal_type(A() < A()) # revealed: LtReturnType
|
||||
reveal_type(A() <= A()) # revealed: LeReturnType
|
||||
reveal_type(A() > A()) # revealed: GtReturnType
|
||||
reveal_type(A() >= A()) # revealed: GeReturnType
|
||||
```
|
||||
|
||||
## Rich Comparison Dunder Implementations for Other Class
|
||||
@@ -51,33 +58,40 @@ type:
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class EqReturnType: ...
|
||||
class NeReturnType: ...
|
||||
class LtReturnType: ...
|
||||
class LeReturnType: ...
|
||||
class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: B) -> int:
|
||||
return 42
|
||||
def __eq__(self, other: B) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: B) -> bytearray:
|
||||
return bytearray()
|
||||
def __ne__(self, other: B) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: B) -> str:
|
||||
return "42"
|
||||
def __lt__(self, other: B) -> LtReturnType:
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, other: B) -> bytes:
|
||||
return b"42"
|
||||
def __le__(self, other: B) -> LeReturnType:
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, other: B) -> list:
|
||||
return [42]
|
||||
def __gt__(self, other: B) -> GtReturnType:
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, other: B) -> set:
|
||||
return {42}
|
||||
def __ge__(self, other: B) -> GeReturnType:
|
||||
return GeReturnType()
|
||||
|
||||
class B: ...
|
||||
|
||||
reveal_type(A() == B()) # revealed: int
|
||||
reveal_type(A() != B()) # revealed: bytearray
|
||||
reveal_type(A() < B()) # revealed: str
|
||||
reveal_type(A() <= B()) # revealed: bytes
|
||||
reveal_type(A() > B()) # revealed: list
|
||||
reveal_type(A() >= B()) # revealed: set
|
||||
reveal_type(A() == B()) # revealed: EqReturnType
|
||||
reveal_type(A() != B()) # revealed: NeReturnType
|
||||
reveal_type(A() < B()) # revealed: LtReturnType
|
||||
reveal_type(A() <= B()) # revealed: LeReturnType
|
||||
reveal_type(A() > B()) # revealed: GtReturnType
|
||||
reveal_type(A() >= B()) # revealed: GeReturnType
|
||||
```
|
||||
|
||||
## Reflected Comparisons
|
||||
@@ -89,58 +103,64 @@ these methods will be ignored here because they require a mismatched operand typ
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class EqReturnType: ...
|
||||
class NeReturnType: ...
|
||||
class LtReturnType: ...
|
||||
class LeReturnType: ...
|
||||
class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: B) -> int:
|
||||
return 42
|
||||
def __eq__(self, other: B) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: B) -> bytearray:
|
||||
return bytearray()
|
||||
def __ne__(self, other: B) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: B) -> str:
|
||||
return "42"
|
||||
def __lt__(self, other: B) -> LtReturnType:
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, other: B) -> bytes:
|
||||
return b"42"
|
||||
def __le__(self, other: B) -> LeReturnType:
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, other: B) -> list:
|
||||
return [42]
|
||||
def __gt__(self, other: B) -> GtReturnType:
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, other: B) -> set:
|
||||
return {42}
|
||||
def __ge__(self, other: B) -> GeReturnType:
|
||||
return GeReturnType()
|
||||
|
||||
class Unrelated: ...
|
||||
|
||||
class B:
|
||||
# To override builtins.object.__eq__ and builtins.object.__ne__
|
||||
# TODO these should emit an invalid override diagnostic
|
||||
def __eq__(self, other: str) -> B:
|
||||
def __eq__(self, other: Unrelated) -> B:
|
||||
return B()
|
||||
|
||||
def __ne__(self, other: str) -> B:
|
||||
def __ne__(self, other: Unrelated) -> B:
|
||||
return B()
|
||||
|
||||
# TODO: should be `int` and `bytearray`.
|
||||
# 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: EqReturnType
|
||||
reveal_type(B() != A()) # revealed: NeReturnType
|
||||
|
||||
reveal_type(B() < A()) # revealed: list
|
||||
reveal_type(B() <= A()) # revealed: set
|
||||
reveal_type(B() < A()) # revealed: GtReturnType
|
||||
reveal_type(B() <= A()) # revealed: GeReturnType
|
||||
|
||||
reveal_type(B() > A()) # revealed: str
|
||||
reveal_type(B() >= A()) # revealed: bytes
|
||||
reveal_type(B() > A()) # revealed: LtReturnType
|
||||
reveal_type(B() >= A()) # revealed: LeReturnType
|
||||
|
||||
class C:
|
||||
def __gt__(self, other: C) -> int:
|
||||
def __gt__(self, other: C) -> EqReturnType:
|
||||
return 42
|
||||
|
||||
def __ge__(self, other: C) -> bytearray:
|
||||
return bytearray()
|
||||
def __ge__(self, other: C) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
reveal_type(C() < C()) # revealed: int
|
||||
reveal_type(C() <= C()) # revealed: bytearray
|
||||
reveal_type(C() < C()) # revealed: EqReturnType
|
||||
reveal_type(C() <= C()) # revealed: NeReturnType
|
||||
```
|
||||
|
||||
## Reflected Comparisons with Subclasses
|
||||
@@ -152,6 +172,13 @@ than `A`.
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class EqReturnType: ...
|
||||
class NeReturnType: ...
|
||||
class LtReturnType: ...
|
||||
class LeReturnType: ...
|
||||
class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: A) -> A:
|
||||
return A()
|
||||
@@ -172,32 +199,32 @@ class A:
|
||||
return A()
|
||||
|
||||
class B(A):
|
||||
def __eq__(self, other: A) -> int:
|
||||
return 42
|
||||
def __eq__(self, other: A) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: A) -> bytearray:
|
||||
return bytearray()
|
||||
def __ne__(self, other: A) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: A) -> str:
|
||||
return "42"
|
||||
def __lt__(self, other: A) -> LtReturnType:
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, other: A) -> bytes:
|
||||
return b"42"
|
||||
def __le__(self, other: A) -> LeReturnType:
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, other: A) -> list:
|
||||
return [42]
|
||||
def __gt__(self, other: A) -> GtReturnType:
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, other: A) -> set:
|
||||
return {42}
|
||||
def __ge__(self, other: A) -> GeReturnType:
|
||||
return GeReturnType()
|
||||
|
||||
reveal_type(A() == B()) # revealed: int
|
||||
reveal_type(A() != B()) # revealed: bytearray
|
||||
reveal_type(A() == B()) # revealed: EqReturnType
|
||||
reveal_type(A() != B()) # revealed: NeReturnType
|
||||
|
||||
reveal_type(A() < B()) # revealed: list
|
||||
reveal_type(A() <= B()) # revealed: set
|
||||
reveal_type(A() < B()) # revealed: GtReturnType
|
||||
reveal_type(A() <= B()) # revealed: GeReturnType
|
||||
|
||||
reveal_type(A() > B()) # revealed: str
|
||||
reveal_type(A() >= B()) # revealed: bytes
|
||||
reveal_type(A() > B()) # revealed: LtReturnType
|
||||
reveal_type(A() >= B()) # revealed: LeReturnType
|
||||
```
|
||||
|
||||
## Reflected Comparisons with Subclass But Falls Back to LHS
|
||||
@@ -222,9 +249,8 @@ class B(A):
|
||||
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
|
||||
reveal_type(A() < B()) # revealed: A
|
||||
reveal_type(A() > B()) # revealed: A
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
@@ -272,9 +298,8 @@ class 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
|
||||
reveal_type(A() == A()) # revealed: bool
|
||||
reveal_type(A() != A()) # revealed: bool
|
||||
```
|
||||
|
||||
## Object Comparisons with Typeshed
|
||||
@@ -305,12 +330,14 @@ 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
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 < 2j) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 <= 2j) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 > 2j) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 >= 2j) # revealed: Unknown
|
||||
|
||||
def f(x: bool, y: int):
|
||||
reveal_type(x < y) # revealed: bool
|
||||
@@ -318,3 +345,29 @@ def f(x: bool, y: int):
|
||||
reveal_type(4.2 < x) # revealed: bool
|
||||
reveal_type(x < 4.2) # revealed: bool
|
||||
```
|
||||
|
||||
## Chained comparisons with objects that don't implement `__bool__` correctly
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Python implicitly calls `bool` on the comparison result of preceding elements (but not for the last
|
||||
element) of a chained comparison.
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
class Comparable:
|
||||
def __lt__(self, item) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
def __gt__(self, item) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 < Comparable() < 20
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 < Comparable() < Comparable()
|
||||
|
||||
Comparable() < Comparable() # fine
|
||||
```
|
||||
|
||||
@@ -12,8 +12,8 @@ reveal_type(1 is 1) # revealed: bool
|
||||
reveal_type(1 is not 1) # revealed: bool
|
||||
reveal_type(1 is 2) # revealed: Literal[False]
|
||||
reveal_type(1 is not 7) # revealed: Literal[True]
|
||||
# TODO: should be Unknown, and emit diagnostic, once we check call argument types
|
||||
reveal_type(1 <= "" and 0 < 1) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
|
||||
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True]
|
||||
```
|
||||
|
||||
## Integer instance
|
||||
|
||||
@@ -8,7 +8,9 @@ types, we can infer that the result for the intersection type is also true/false
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Base: ...
|
||||
class Base:
|
||||
def __gt__(self, other) -> bool:
|
||||
return False
|
||||
|
||||
class Child1(Base):
|
||||
def __eq__(self, other) -> Literal[True]:
|
||||
|
||||
@@ -23,6 +23,7 @@ from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __lt__(self, other) -> A: ...
|
||||
def __gt__(self, other) -> bool: ...
|
||||
|
||||
class B:
|
||||
def __lt__(self, other) -> B: ...
|
||||
|
||||
@@ -92,11 +92,14 @@ reveal_type(a == b) # revealed: bool
|
||||
# TODO: should be Literal[True], once we implement (in)equality for mismatched literals
|
||||
reveal_type(a != b) # revealed: bool
|
||||
|
||||
# TODO: should be Unknown and add more informative diagnostics
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a < b) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a <= b) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a > b) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a >= b) # revealed: Unknown
|
||||
```
|
||||
|
||||
However, if the lexicographic comparison completes without reaching a point where str and int are
|
||||
@@ -144,33 +147,40 @@ of the dunder methods.)
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class EqReturnType: ...
|
||||
class NeReturnType: ...
|
||||
class LtReturnType: ...
|
||||
class LeReturnType: ...
|
||||
class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, o: object) -> str:
|
||||
return "hello"
|
||||
def __eq__(self, o: object) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, o: object) -> bytes:
|
||||
return b"world"
|
||||
def __ne__(self, o: object) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, o: A) -> bytearray:
|
||||
return bytearray()
|
||||
def __lt__(self, o: A) -> LtReturnType:
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, o: A) -> memoryview:
|
||||
return memoryview(b"")
|
||||
def __le__(self, o: A) -> LeReturnType:
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, o: A) -> tuple:
|
||||
return (1, 2, 3)
|
||||
def __gt__(self, o: A) -> GtReturnType:
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, o: A) -> list:
|
||||
return [1, 2, 3]
|
||||
def __ge__(self, o: A) -> GeReturnType:
|
||||
return GeReturnType()
|
||||
|
||||
a = (A(), A())
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bytearray | Literal[False]
|
||||
reveal_type(a <= a) # revealed: memoryview | Literal[True]
|
||||
reveal_type(a > a) # revealed: tuple | Literal[False]
|
||||
reveal_type(a >= a) # revealed: list | Literal[True]
|
||||
reveal_type(a < a) # revealed: LtReturnType | Literal[False]
|
||||
reveal_type(a <= a) # revealed: LeReturnType | Literal[True]
|
||||
reveal_type(a > a) # revealed: GtReturnType | Literal[False]
|
||||
reveal_type(a >= a) # revealed: GeReturnType | Literal[True]
|
||||
|
||||
# If lexicographic comparison is finished before comparing A()
|
||||
b = ("1_foo", A())
|
||||
@@ -183,11 +193,13 @@ reveal_type(b <= c) # revealed: Literal[True]
|
||||
reveal_type(b > c) # revealed: Literal[False]
|
||||
reveal_type(b >= c) # revealed: Literal[False]
|
||||
|
||||
class LtReturnTypeOnB: ...
|
||||
|
||||
class B:
|
||||
def __lt__(self, o: B) -> set:
|
||||
def __lt__(self, o: B) -> LtReturnTypeOnB:
|
||||
return set()
|
||||
|
||||
reveal_type((A(), B()) < (A(), B())) # revealed: bytearray | set | Literal[False]
|
||||
reveal_type((A(), B()) < (A(), B())) # revealed: LtReturnType | LtReturnTypeOnB | Literal[False]
|
||||
```
|
||||
|
||||
#### Special Handling of Eq and NotEq in Lexicographic Comparisons
|
||||
@@ -322,3 +334,61 @@ reveal_type(a is not c) # revealed: Literal[True]
|
||||
For tuples like `tuple[int, ...]`, `tuple[Any, ...]`
|
||||
|
||||
// TODO
|
||||
|
||||
## Chained comparisons with elements that incorrectly implement `__bool__`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
For an operation `A() < A()` to succeed at runtime, the `A.__lt__` method does not necessarily need
|
||||
to return an object that is convertible to a `bool`. However, the return type _does_ need to be
|
||||
convertible to a `bool` for the operation `A() < A() < A()` (a _chained_ comparison) to succeed.
|
||||
This is because `A() < A() < A()` desugars to something like this, which involves several implicit
|
||||
conversions to `bool`:
|
||||
|
||||
```ignore
|
||||
def compute_chained_comparison():
|
||||
a1 = A()
|
||||
a2 = A()
|
||||
first_comparison = a1 < a2
|
||||
return first_comparison and (a2 < A())
|
||||
```
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 5
|
||||
|
||||
class Comparable:
|
||||
def __lt__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
def __gt__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
a = (1, Comparable())
|
||||
b = (1, Comparable())
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
a < b < b
|
||||
|
||||
a < b # fine
|
||||
```
|
||||
|
||||
## Equality with elements that incorrectly implement `__bool__`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Python does not generally attempt to coerce the result of `==` and `!=` operations between two
|
||||
arbitrary objects to a `bool`, but a comparison of tuples will fail if the result of comparing any
|
||||
pair of elements at equivalent positions cannot be converted to a `bool`:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __eq__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
class NotBoolable:
|
||||
__bool__ = None
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
(A(),) == (A(),)
|
||||
```
|
||||
|
||||
@@ -9,28 +9,22 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||
reveal_type(b) # revealed: bool
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `object` and `int`")
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `object` and `int`, in comparing `object` with `Literal[5]`"
|
||||
c = object() < 5
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(c) # revealed: bool
|
||||
reveal_type(c) # revealed: Unknown
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `int` and `object`")
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `object`, in comparing `Literal[5]` with `object`"
|
||||
d = 5 < object()
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(d) # revealed: bool
|
||||
reveal_type(d) # revealed: Unknown
|
||||
|
||||
int_literal_or_str_literal = 1 if flag else "foo"
|
||||
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
reveal_type(e) # revealed: bool
|
||||
|
||||
# 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: [unsupported-operator] "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")
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(f) # revealed: bool
|
||||
reveal_type(f) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
|
||||
g = (flag1, A()) < (flag2, A())
|
||||
|
||||
@@ -43,8 +43,7 @@ class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# TODO: This could be a `tuple[int, int]` if we model that `y` can not be modified in the outer comprehension scope
|
||||
# revealed: tuple[int, Unknown | int]
|
||||
# revealed: tuple[int, int]
|
||||
[[reveal_type((x, y)) for x in IntIterable()] for y in IntIterable()]
|
||||
```
|
||||
|
||||
@@ -67,8 +66,7 @@ class IterableOfIterables:
|
||||
def __iter__(self) -> IteratorOfIterables:
|
||||
return IteratorOfIterables()
|
||||
|
||||
# TODO: This could be a `tuple[int, int]` (see above)
|
||||
# revealed: tuple[int, Unknown | IntIterable]
|
||||
# revealed: tuple[int, IntIterable]
|
||||
[[reveal_type((x, y)) for x in y] for y in IterableOfIterables()]
|
||||
```
|
||||
|
||||
|
||||
@@ -35,3 +35,13 @@ def _(flag: bool):
|
||||
x = 1 if flag else None
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
|
||||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
3 if NotBoolable() else 4
|
||||
```
|
||||
|
||||
@@ -147,3 +147,17 @@ def _(flag: bool):
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
if NotBoolable():
|
||||
...
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
elif NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
@@ -43,3 +43,21 @@ def _(target: int):
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## Guard with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
def _(target: int, flag: NotBoolable):
|
||||
y = 1
|
||||
match target:
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
case 1 if flag:
|
||||
y = 2
|
||||
case 2:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
@@ -22,22 +22,26 @@ class Ten:
|
||||
pass
|
||||
|
||||
class C:
|
||||
ten = Ten()
|
||||
ten: Ten = Ten()
|
||||
|
||||
c = C()
|
||||
|
||||
# TODO: this should be `Literal[10]`
|
||||
reveal_type(c.ten) # revealed: Unknown | Ten
|
||||
reveal_type(c.ten) # revealed: Literal[10]
|
||||
|
||||
# TODO: This should `Literal[10]`
|
||||
reveal_type(C.ten) # revealed: Unknown | Ten
|
||||
reveal_type(C.ten) # revealed: Literal[10]
|
||||
|
||||
# These are fine:
|
||||
c.ten = 10
|
||||
# TODO: This should not be an error
|
||||
c.ten = 10 # error: [invalid-assignment]
|
||||
C.ten = 10
|
||||
|
||||
# TODO: Both of these should be errors
|
||||
# TODO: This should be an error (as the wrong type is being implicitly passed to `Ten.__set__`),
|
||||
# but the error message is misleading.
|
||||
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Ten`"
|
||||
c.ten = 11
|
||||
|
||||
# TODO: same as above
|
||||
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`"
|
||||
C.ten = 11
|
||||
```
|
||||
|
||||
@@ -57,24 +61,86 @@ class FlexibleInt:
|
||||
self._value = int(value)
|
||||
|
||||
class C:
|
||||
flexible_int = FlexibleInt()
|
||||
flexible_int: FlexibleInt = FlexibleInt()
|
||||
|
||||
c = C()
|
||||
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
|
||||
reveal_type(c.flexible_int) # revealed: int | None
|
||||
|
||||
# TODO: These should not be errors
|
||||
# error: [invalid-assignment]
|
||||
c.flexible_int = 42 # okay
|
||||
# error: [invalid-assignment]
|
||||
c.flexible_int = "42" # also okay!
|
||||
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
|
||||
reveal_type(c.flexible_int) # revealed: int | None
|
||||
|
||||
# TODO: should be an error
|
||||
# TODO: This should be an error, but the message needs to be improved.
|
||||
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `flexible_int` of type `FlexibleInt`"
|
||||
c.flexible_int = None # not okay
|
||||
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
|
||||
reveal_type(c.flexible_int) # revealed: int | None
|
||||
```
|
||||
|
||||
## Data and non-data descriptors
|
||||
|
||||
Descriptors that define `__set__` or `__delete__` are called *data descriptors*. An example\
|
||||
of a data descriptor is a `property` with a setter and/or a deleter.\
|
||||
Descriptors that only define `__get__`, meanwhile, are called *non-data descriptors*. Examples
|
||||
include\
|
||||
functions, `classmethod` or `staticmethod`).
|
||||
|
||||
The precedence chain for attribute access is (1) data descriptors, (2) instance attributes, and (3)
|
||||
non-data descriptors.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class DataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
|
||||
return "data"
|
||||
|
||||
def __set__(self, instance: int, value) -> None:
|
||||
pass
|
||||
|
||||
class NonDataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
|
||||
return "non-data"
|
||||
|
||||
class C:
|
||||
data_descriptor = DataDescriptor()
|
||||
non_data_descriptor = NonDataDescriptor()
|
||||
|
||||
def f(self):
|
||||
# This explains why data descriptors come first in the precedence chain. If
|
||||
# instance attributes would take priority, we would override the descriptor
|
||||
# here. Instead, this calls `DataDescriptor.__set__`, i.e. it does not affect
|
||||
# the type of the `data_descriptor` attribute.
|
||||
self.data_descriptor = 1
|
||||
|
||||
# However, for non-data descriptors, instance attributes do take precedence.
|
||||
# So it is possible to override them.
|
||||
self.non_data_descriptor = 1
|
||||
|
||||
c = C()
|
||||
|
||||
# TODO: This should ideally be `Unknown | Literal["data"]`.
|
||||
#
|
||||
# - Pyright also wrongly shows `int | Literal['data']` here
|
||||
# - Mypy shows Literal["data"] here, but also shows Literal["non-data"] below.
|
||||
#
|
||||
reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data", 1]
|
||||
|
||||
reveal_type(c.non_data_descriptor) # revealed: Unknown | Literal["non-data", 1]
|
||||
|
||||
reveal_type(C.data_descriptor) # revealed: Unknown | Literal["data"]
|
||||
|
||||
reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
|
||||
|
||||
# It is possible to override data descriptors via class objects. The following
|
||||
# assignment does not call `DataDescriptor.__set__`. For this reason, we infer
|
||||
# `Unknown | …` for all (descriptor) attributes.
|
||||
C.data_descriptor = "something else" # This is okay
|
||||
```
|
||||
|
||||
## Built-in `property` descriptor
|
||||
@@ -101,7 +167,7 @@ c = C()
|
||||
reveal_type(c._name) # revealed: str | None
|
||||
|
||||
# Should be `str`
|
||||
reveal_type(c.name) # revealed: @Todo(bound method)
|
||||
reveal_type(c.name) # revealed: @Todo(decorated method)
|
||||
|
||||
# Should be `builtins.property`
|
||||
reveal_type(C.name) # revealed: Literal[name]
|
||||
@@ -142,7 +208,7 @@ reveal_type(c1) # revealed: @Todo(return type)
|
||||
reveal_type(C.get_name()) # revealed: @Todo(return type)
|
||||
|
||||
# TODO: should be `str`
|
||||
reveal_type(C("42").get_name()) # revealed: @Todo(bound method)
|
||||
reveal_type(C("42").get_name()) # revealed: @Todo(decorated method)
|
||||
```
|
||||
|
||||
## Descriptors only work when used as class variables
|
||||
@@ -160,9 +226,10 @@ class Ten:
|
||||
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.ten = Ten()
|
||||
self.ten: Ten = Ten()
|
||||
|
||||
reveal_type(C().ten) # revealed: Unknown | Ten
|
||||
# TODO: Should be Ten
|
||||
reveal_type(C().ten) # revealed: Literal[10]
|
||||
```
|
||||
|
||||
## Descriptors distinguishing between class and instance access
|
||||
@@ -186,13 +253,191 @@ class Descriptor:
|
||||
return "called on class object"
|
||||
|
||||
class C:
|
||||
d = Descriptor()
|
||||
d: Descriptor = Descriptor()
|
||||
|
||||
# TODO: should be `Literal["called on class object"]
|
||||
reveal_type(C.d) # revealed: Unknown | Descriptor
|
||||
reveal_type(C.d) # revealed: LiteralString
|
||||
|
||||
# TODO: should be `Literal["called on instance"]
|
||||
reveal_type(C().d) # revealed: Unknown | Descriptor
|
||||
reveal_type(C().d) # revealed: LiteralString
|
||||
```
|
||||
|
||||
## Undeclared descriptor arguments
|
||||
|
||||
If a descriptor attribute is not declared, we union with `Unknown`, just like for regular
|
||||
attributes, since that attribute could be overwritten externally. Even a data descriptor with a
|
||||
`__set__` method can be overwritten when accessed through a class object.
|
||||
|
||||
```py
|
||||
class Descriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int:
|
||||
return 1
|
||||
|
||||
def __set__(self, instance: object, value: int) -> None:
|
||||
pass
|
||||
|
||||
class C:
|
||||
descriptor = Descriptor()
|
||||
|
||||
C.descriptor = "something else"
|
||||
|
||||
# This could also be `Literal["something else"]` if we support narrowing of attribute types based on assignments
|
||||
reveal_type(C.descriptor) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## `__get__` is called with correct arguments
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class TailoredForClassObjectAccess:
|
||||
def __get__(self, instance: None, owner: type[C]) -> int:
|
||||
return 1
|
||||
|
||||
class TailoredForInstanceAccess:
|
||||
def __get__(self, instance: C, owner: type[C] | None = None) -> str:
|
||||
return "a"
|
||||
|
||||
class C:
|
||||
class_object_access: TailoredForClassObjectAccess = TailoredForClassObjectAccess()
|
||||
instance_access: TailoredForInstanceAccess = TailoredForInstanceAccess()
|
||||
|
||||
reveal_type(C.class_object_access) # revealed: int
|
||||
reveal_type(C().instance_access) # revealed: str
|
||||
|
||||
# TODO: These should emit a diagnostic
|
||||
reveal_type(C().class_object_access) # revealed: TailoredForClassObjectAccess
|
||||
reveal_type(C.instance_access) # revealed: TailoredForInstanceAccess
|
||||
```
|
||||
|
||||
## Descriptors with incorrect `__get__` signature
|
||||
|
||||
```py
|
||||
class Descriptor:
|
||||
# `__get__` method with missing parameters:
|
||||
def __get__(self) -> int:
|
||||
return 1
|
||||
|
||||
class C:
|
||||
descriptor: Descriptor = Descriptor()
|
||||
|
||||
# TODO: This should be an error
|
||||
reveal_type(C.descriptor) # revealed: Descriptor
|
||||
```
|
||||
|
||||
## Possibly-unbound `__get__` method
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class MaybeDescriptor:
|
||||
if flag:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int:
|
||||
return 1
|
||||
|
||||
class C:
|
||||
descriptor: MaybeDescriptor = MaybeDescriptor()
|
||||
|
||||
# TODO: This should be `MaybeDescriptor | int`
|
||||
reveal_type(C.descriptor) # revealed: int
|
||||
```
|
||||
|
||||
## Dunder methods
|
||||
|
||||
Dunder methods are looked up on the meta type, but we still need to invoke the descriptor protocol:
|
||||
|
||||
```py
|
||||
class SomeCallable:
|
||||
def __call__(self, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> SomeCallable:
|
||||
return SomeCallable()
|
||||
|
||||
class B:
|
||||
__call__: Descriptor = Descriptor()
|
||||
|
||||
b_instance = B()
|
||||
reveal_type(b_instance(1)) # revealed: str
|
||||
|
||||
b_instance("bla") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Functions as descriptors
|
||||
|
||||
Functions are descriptors because they implement a `__get__` method. This is crucial in making sure
|
||||
that method calls work as expected. See [this test suite](./call/methods.md) for more information.
|
||||
Here, we only demonstrate how `__get__` works on functions:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
def f(x: object) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(f) # revealed: Literal[f]
|
||||
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
reveal_type(f.__get__(None, type(f))) # revealed: Literal[f]
|
||||
reveal_type(f.__get__(None, type(f))(1)) # revealed: str
|
||||
|
||||
wrapper_descriptor = getattr_static(f, "__get__")
|
||||
|
||||
reveal_type(wrapper_descriptor) # revealed: <wrapper-descriptor `__get__` of `function` objects>
|
||||
reveal_type(wrapper_descriptor(f, None, type(f))) # revealed: Literal[f]
|
||||
|
||||
# Attribute access on the method-wrapper `f.__get__` falls back to `MethodWrapperType`:
|
||||
reveal_type(f.__get__.__hash__) # revealed: <bound method `__hash__` of `MethodWrapperType`>
|
||||
|
||||
# Attribute access on the wrapper-descriptor falls back to `WrapperDescriptorType`:
|
||||
reveal_type(wrapper_descriptor.__qualname__) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
We can also bind the free function `f` to an instance of a class `C`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
bound_method = wrapper_descriptor(f, C(), C)
|
||||
|
||||
reveal_type(bound_method) # revealed: <bound method `f` of `C`>
|
||||
```
|
||||
|
||||
We can then call it, and the instance of `C` is implicitly passed to the first parameter of `f`
|
||||
(`x`):
|
||||
|
||||
```py
|
||||
reveal_type(bound_method()) # revealed: str
|
||||
```
|
||||
|
||||
Finally, we test some error cases for the call to the wrapper descriptor:
|
||||
|
||||
```py
|
||||
# Calling the wrapper descriptor without any arguments is an
|
||||
# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`"
|
||||
wrapper_descriptor()
|
||||
|
||||
# Calling it without the `instance` argument is an also an
|
||||
# error: [missing-argument] "No argument provided for required parameter `instance`"
|
||||
wrapper_descriptor(f)
|
||||
|
||||
# Calling it without the `owner` argument if `instance` is not `None` is an
|
||||
# error: [missing-argument] "No argument provided for required parameter `owner`"
|
||||
wrapper_descriptor(f, None)
|
||||
|
||||
# But calling it with an instance is fine (in this case, the `owner` argument is optional):
|
||||
wrapper_descriptor(f, C())
|
||||
|
||||
# Calling it with something that is not a `FunctionType` as the first argument is an
|
||||
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`); expected type `FunctionType`"
|
||||
wrapper_descriptor(1, None, type(f))
|
||||
|
||||
# Calling it with something that is not a `type` as the `owner` argument is an
|
||||
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`); expected type `type`"
|
||||
wrapper_descriptor(f, None, f)
|
||||
|
||||
# Calling it with too many positional arguments is an
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments: expected 3, got 4"
|
||||
wrapper_descriptor(f, None, type(f), "one too many")
|
||||
```
|
||||
|
||||
[descriptors]: https://docs.python.org/3/howto/descriptor.html
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# Invalid argument type diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Basic
|
||||
|
||||
This is a basic test demonstrating that a diagnostic points to the function definition corresponding
|
||||
to the invalid argument.
|
||||
|
||||
```py
|
||||
def foo(x: int) -> int:
|
||||
return x * x
|
||||
|
||||
foo("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Different source order
|
||||
|
||||
This is like the basic test, except we put the call site above the function definition.
|
||||
|
||||
```py
|
||||
def bar():
|
||||
foo("hello") # error: [invalid-argument-type]
|
||||
|
||||
def foo(x: int) -> int:
|
||||
return x * x
|
||||
```
|
||||
|
||||
## Different files
|
||||
|
||||
This tests that a diagnostic can point to a function definition in a different file in which an
|
||||
invalid call site was found.
|
||||
|
||||
`package.py`:
|
||||
|
||||
```py
|
||||
def foo(x: int) -> int:
|
||||
return x * x
|
||||
```
|
||||
|
||||
```py
|
||||
import package
|
||||
|
||||
package.foo("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Many parameters
|
||||
|
||||
This checks that a diagnostic renders reasonably when there are multiple parameters.
|
||||
|
||||
```py
|
||||
def foo(x: int, y: int, z: int) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Many parameters across multiple lines
|
||||
|
||||
This checks that a diagnostic renders reasonably when there are multiple parameters spread out
|
||||
across multiple lines.
|
||||
|
||||
```py
|
||||
def foo(
|
||||
x: int,
|
||||
y: int,
|
||||
z: int,
|
||||
) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Many parameters with multiple invalid arguments
|
||||
|
||||
This checks that a diagnostic renders reasonably when there are multiple parameters and multiple
|
||||
invalid argument types.
|
||||
|
||||
```py
|
||||
def foo(x: int, y: int, z: int) -> int:
|
||||
return x * y * z
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
foo("a", "b", "c")
|
||||
```
|
||||
|
||||
At present (2025-02-18), this renders three different diagnostic messages. But arguably, these could
|
||||
all be folded into one diagnostic. Fixing this requires at least better support for multi-spans in
|
||||
the diagnostic model and possibly also how diagnostics are emitted by the type checker itself.
|
||||
|
||||
## Test calling a function whose type is vendored from `typeshed`
|
||||
|
||||
This tests that diagnostic rendering is reasonable when the function being called is from the
|
||||
standard library.
|
||||
|
||||
```py
|
||||
import json
|
||||
|
||||
json.loads(5) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Tests for a variety of argument types
|
||||
|
||||
These tests check that diagnostic output is reasonable regardless of the kinds of arguments used in
|
||||
a function definition.
|
||||
|
||||
### Only positional
|
||||
|
||||
Tests a function definition with only positional parameters.
|
||||
|
||||
```py
|
||||
def foo(x: int, y: int, z: int, /) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Variadic arguments
|
||||
|
||||
Tests a function definition with variadic arguments.
|
||||
|
||||
```py
|
||||
def foo(*numbers: int) -> int:
|
||||
return len(numbers)
|
||||
|
||||
foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Keyword only arguments
|
||||
|
||||
Tests a function definition with keyword-only arguments.
|
||||
|
||||
```py
|
||||
def foo(x: int, y: int, *, z: int = 0) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, 2, z="hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### One keyword argument
|
||||
|
||||
Tests a function definition with keyword-only arguments.
|
||||
|
||||
```py
|
||||
def foo(x: int, y: int, z: int = 0) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, 2, "hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Variadic keyword arguments
|
||||
|
||||
```py
|
||||
def foo(**numbers: int) -> int:
|
||||
return len(numbers)
|
||||
|
||||
foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Mix of arguments
|
||||
|
||||
Tests a function definition with multiple different kinds of arguments.
|
||||
|
||||
```py
|
||||
def foo(x: int, /, y: int, *, z: int = 0) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, 2, z="hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Synthetic arguments
|
||||
|
||||
Tests a function call with synthetic arguments.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __call__(self, x: int) -> int:
|
||||
return 1
|
||||
|
||||
c = C()
|
||||
c("wrong") # error: [invalid-argument-type]
|
||||
```
|
||||
@@ -241,30 +241,34 @@ suites:
|
||||
`except` suite ran to completion
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
def could_raise_returns_A() -> A:
|
||||
return A()
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
def could_raise_returns_B() -> B:
|
||||
return B()
|
||||
|
||||
def could_raise_returns_C() -> C:
|
||||
return C()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_bytes()
|
||||
reveal_type(x) # revealed: bytes
|
||||
x = could_raise_returns_bool()
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | str | bytes | bool`
|
||||
reveal_type(x) # revealed: str | bool
|
||||
# TODO: should be `Literal[1] | A | B | C`
|
||||
reveal_type(x) # revealed: A | C
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
|
||||
@@ -282,53 +286,56 @@ x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_bytes()
|
||||
reveal_type(x) # revealed: bytes
|
||||
x = could_raise_returns_bool()
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | str | bytes | bool`
|
||||
reveal_type(x) # revealed: str | bool
|
||||
# TODO: should be `Literal[1] | A | B | C`
|
||||
reveal_type(x) # revealed: A | C
|
||||
|
||||
reveal_type(x) # revealed: str | bool
|
||||
reveal_type(x) # revealed: A | C
|
||||
```
|
||||
|
||||
An example with multiple `except` branches and a `finally` branch:
|
||||
|
||||
```py
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
class D: ...
|
||||
class E: ...
|
||||
|
||||
def could_raise_returns_bytearray() -> bytearray:
|
||||
return bytearray()
|
||||
def could_raise_returns_D() -> D:
|
||||
return D()
|
||||
|
||||
def could_raise_returns_E() -> E:
|
||||
return E()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_bytes()
|
||||
reveal_type(x) # revealed: bytes
|
||||
x = could_raise_returns_bool()
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
except ValueError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_memoryview()
|
||||
reveal_type(x) # revealed: memoryview
|
||||
x = could_raise_returns_bytearray()
|
||||
reveal_type(x) # revealed: bytearray
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_D()
|
||||
reveal_type(x) # revealed: D
|
||||
x = could_raise_returns_E()
|
||||
reveal_type(x) # revealed: E
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray`
|
||||
reveal_type(x) # revealed: str | bool | bytearray
|
||||
# TODO: should be `Literal[1] | A | B | C | D | E`
|
||||
reveal_type(x) # revealed: A | C | E
|
||||
|
||||
reveal_type(x) # revealed: str | bool | bytearray
|
||||
reveal_type(x) # revealed: A | C | E
|
||||
```
|
||||
|
||||
## Combining `except`, `else` and `finally` branches
|
||||
@@ -338,84 +345,93 @@ control flow could have jumped to the `finally` suite from partway through the `
|
||||
an exception raised *there*.
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
class E: ...
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
def could_raise_returns_A() -> A:
|
||||
return A()
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
def could_raise_returns_B() -> B:
|
||||
return B()
|
||||
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
def could_raise_returns_C() -> C:
|
||||
return C()
|
||||
|
||||
def could_raise_returns_bytearray() -> bytearray:
|
||||
return bytearray()
|
||||
def could_raise_returns_D() -> D:
|
||||
return D()
|
||||
|
||||
def could_raise_returns_E() -> E:
|
||||
return E()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_bytes()
|
||||
reveal_type(x) # revealed: bytes
|
||||
x = could_raise_returns_bool()
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_memoryview()
|
||||
reveal_type(x) # revealed: memoryview
|
||||
x = could_raise_returns_bytearray()
|
||||
reveal_type(x) # revealed: bytearray
|
||||
reveal_type(x) # revealed: A
|
||||
x = could_raise_returns_D()
|
||||
reveal_type(x) # revealed: D
|
||||
x = could_raise_returns_E()
|
||||
reveal_type(x) # revealed: E
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray`
|
||||
reveal_type(x) # revealed: bool | bytearray
|
||||
# TODO: should be `Literal[1] | A | B | C | D | E`
|
||||
reveal_type(x) # revealed: C | E
|
||||
|
||||
reveal_type(x) # revealed: bool | bytearray
|
||||
reveal_type(x) # revealed: C | E
|
||||
```
|
||||
|
||||
The same again, this time with multiple `except` branches:
|
||||
|
||||
```py
|
||||
def could_raise_returns_range() -> range:
|
||||
return range(42)
|
||||
class F: ...
|
||||
class G: ...
|
||||
|
||||
def could_raise_returns_slice() -> slice:
|
||||
return slice(None)
|
||||
def could_raise_returns_F() -> F:
|
||||
return F()
|
||||
|
||||
def could_raise_returns_G() -> G:
|
||||
return G()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_bytes()
|
||||
reveal_type(x) # revealed: bytes
|
||||
x = could_raise_returns_bool()
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
except ValueError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_memoryview()
|
||||
reveal_type(x) # revealed: memoryview
|
||||
x = could_raise_returns_bytearray()
|
||||
reveal_type(x) # revealed: bytearray
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_D()
|
||||
reveal_type(x) # revealed: D
|
||||
x = could_raise_returns_E()
|
||||
reveal_type(x) # revealed: E
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_range()
|
||||
reveal_type(x) # revealed: range
|
||||
x = could_raise_returns_slice()
|
||||
reveal_type(x) # revealed: slice
|
||||
reveal_type(x) # revealed: A
|
||||
x = could_raise_returns_F()
|
||||
reveal_type(x) # revealed: F
|
||||
x = could_raise_returns_G()
|
||||
reveal_type(x) # revealed: G
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray | range | slice`
|
||||
reveal_type(x) # revealed: bool | bytearray | slice
|
||||
# TODO: should be `Literal[1] | A | B | C | D | E | F | G`
|
||||
reveal_type(x) # revealed: C | E | G
|
||||
|
||||
reveal_type(x) # revealed: bool | bytearray | slice
|
||||
reveal_type(x) # revealed: C | E | G
|
||||
```
|
||||
|
||||
## Nested `try`/`except` blocks
|
||||
@@ -429,92 +445,101 @@ a suite containing statements that could possibly raise exceptions, which would
|
||||
jumping out of that suite prior to the suite running to completion.
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
class E: ...
|
||||
class F: ...
|
||||
class G: ...
|
||||
class H: ...
|
||||
class I: ...
|
||||
class J: ...
|
||||
class K: ...
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
def could_raise_returns_A() -> A:
|
||||
return A()
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
def could_raise_returns_B() -> B:
|
||||
return B()
|
||||
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
def could_raise_returns_C() -> C:
|
||||
return C()
|
||||
|
||||
def could_raise_returns_property() -> property:
|
||||
return property()
|
||||
def could_raise_returns_D() -> D:
|
||||
return D()
|
||||
|
||||
def could_raise_returns_range() -> range:
|
||||
return range(42)
|
||||
def could_raise_returns_E() -> E:
|
||||
return E()
|
||||
|
||||
def could_raise_returns_slice() -> slice:
|
||||
return slice(None)
|
||||
def could_raise_returns_F() -> F:
|
||||
return F()
|
||||
|
||||
def could_raise_returns_super() -> super:
|
||||
return super()
|
||||
def could_raise_returns_G() -> G:
|
||||
return G()
|
||||
|
||||
def could_raise_returns_bytearray() -> bytearray:
|
||||
return bytearray()
|
||||
def could_raise_returns_H() -> H:
|
||||
return H()
|
||||
|
||||
class Foo: ...
|
||||
class Bar: ...
|
||||
def could_raise_returns_I() -> I:
|
||||
return I()
|
||||
|
||||
def could_raise_returns_Foo() -> Foo:
|
||||
return Foo()
|
||||
def could_raise_returns_J() -> J:
|
||||
return J()
|
||||
|
||||
def could_raise_returns_Bar() -> Bar:
|
||||
return Bar()
|
||||
def could_raise_returns_K() -> K:
|
||||
return K()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_bytes()
|
||||
reveal_type(x) # revealed: bytes
|
||||
x = could_raise_returns_bool()
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
except ValueError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = could_raise_returns_memoryview()
|
||||
reveal_type(x) # revealed: memoryview
|
||||
x = could_raise_returns_property()
|
||||
reveal_type(x) # revealed: property
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_D()
|
||||
reveal_type(x) # revealed: D
|
||||
x = could_raise_returns_E()
|
||||
reveal_type(x) # revealed: E
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_range()
|
||||
reveal_type(x) # revealed: range
|
||||
x = could_raise_returns_slice()
|
||||
reveal_type(x) # revealed: slice
|
||||
reveal_type(x) # revealed: A
|
||||
x = could_raise_returns_F()
|
||||
reveal_type(x) # revealed: F
|
||||
x = could_raise_returns_G()
|
||||
reveal_type(x) # revealed: G
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | property | range | slice`
|
||||
reveal_type(x) # revealed: bool | property | slice
|
||||
# TODO: should be `Literal[1] | A | B | C | D | E | F | G`
|
||||
reveal_type(x) # revealed: C | E | G
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
except:
|
||||
reveal_type(x) # revealed: Literal[1, 2] | str | bytes | bool | memoryview | property | range | slice
|
||||
x = could_raise_returns_super()
|
||||
reveal_type(x) # revealed: super
|
||||
x = could_raise_returns_bytearray()
|
||||
reveal_type(x) # revealed: bytearray
|
||||
reveal_type(x) # revealed: Literal[1, 2] | A | B | C | D | E | F | G
|
||||
x = could_raise_returns_H()
|
||||
reveal_type(x) # revealed: H
|
||||
x = could_raise_returns_I()
|
||||
reveal_type(x) # revealed: I
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
x = could_raise_returns_Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
x = could_raise_returns_Bar()
|
||||
reveal_type(x) # revealed: Bar
|
||||
x = could_raise_returns_J()
|
||||
reveal_type(x) # revealed: J
|
||||
x = could_raise_returns_K()
|
||||
reveal_type(x) # revealed: K
|
||||
finally:
|
||||
# TODO: should be `Literal[1, 2] | str | bytes | bool | memoryview | property | range | slice | super | bytearray | Foo | Bar`
|
||||
reveal_type(x) # revealed: bytearray | Bar
|
||||
# TODO: should be `Literal[1, 2] | A | B | C | D | E | F | G | H | I | J | K`
|
||||
reveal_type(x) # revealed: I | K
|
||||
|
||||
# Either one `except` branch or the `else`
|
||||
# must have been taken and completed to get here:
|
||||
reveal_type(x) # revealed: bytearray | Bar
|
||||
reveal_type(x) # revealed: I | K
|
||||
```
|
||||
|
||||
## Nested scopes inside `try` blocks
|
||||
@@ -523,50 +548,56 @@ Shadowing a variable in an inner scope has no effect on type inference of the va
|
||||
in the outer scope:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
class E: ...
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
def could_raise_returns_A() -> A:
|
||||
return A()
|
||||
|
||||
def could_raise_returns_range() -> range:
|
||||
return range(42)
|
||||
def could_raise_returns_B() -> B:
|
||||
return B()
|
||||
|
||||
def could_raise_returns_bytearray() -> bytearray:
|
||||
return bytearray()
|
||||
def could_raise_returns_C() -> C:
|
||||
return C()
|
||||
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
def could_raise_returns_D() -> D:
|
||||
return D()
|
||||
|
||||
def could_raise_returns_E() -> E:
|
||||
return E()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
|
||||
def foo(param=could_raise_returns_str()):
|
||||
x = could_raise_returns_str()
|
||||
def foo(param=could_raise_returns_A()):
|
||||
x = could_raise_returns_A()
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: str
|
||||
x = could_raise_returns_bytes()
|
||||
reveal_type(x) # revealed: bytes
|
||||
reveal_type(x) # revealed: A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
except:
|
||||
reveal_type(x) # revealed: str | bytes
|
||||
x = could_raise_returns_bytearray()
|
||||
reveal_type(x) # revealed: bytearray
|
||||
x = could_raise_returns_memoryview()
|
||||
reveal_type(x) # revealed: memoryview
|
||||
reveal_type(x) # revealed: A | B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
x = could_raise_returns_D()
|
||||
reveal_type(x) # revealed: D
|
||||
finally:
|
||||
# TODO: should be `str | bytes | bytearray | memoryview`
|
||||
reveal_type(x) # revealed: bytes | memoryview
|
||||
reveal_type(x) # revealed: bytes | memoryview
|
||||
# TODO: should be `A | B | C | D`
|
||||
reveal_type(x) # revealed: B | D
|
||||
reveal_type(x) # revealed: B | D
|
||||
x = foo
|
||||
reveal_type(x) # revealed: Literal[foo]
|
||||
except:
|
||||
reveal_type(x) # revealed: Literal[1] | Literal[foo]
|
||||
|
||||
class Bar:
|
||||
x = could_raise_returns_range()
|
||||
reveal_type(x) # revealed: range
|
||||
x = could_raise_returns_E()
|
||||
reveal_type(x) # revealed: E
|
||||
|
||||
x = Bar
|
||||
reveal_type(x) # revealed: Literal[Bar]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
assert NotBoolable()
|
||||
```
|
||||
@@ -101,3 +101,55 @@ reveal_type(bool([])) # revealed: bool
|
||||
reveal_type(bool({})) # revealed: bool
|
||||
reveal_type(bool(set())) # revealed: bool
|
||||
```
|
||||
|
||||
## `__bool__` returning `NoReturn`
|
||||
|
||||
```py
|
||||
from typing import NoReturn
|
||||
|
||||
class NotBoolable:
|
||||
def __bool__(self) -> NoReturn:
|
||||
raise NotImplementedError("This object can't be converted to a boolean")
|
||||
|
||||
# TODO: This should emit an error that `NotBoolable` can't be converted to a bool but it currently doesn't
|
||||
# because `Never` is assignable to `bool`. This probably requires dead code analysis to fix.
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
## Not callable `__bool__`
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = None
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
## Not-boolable union
|
||||
|
||||
```py
|
||||
def test(cond: bool):
|
||||
class NotBoolable:
|
||||
__bool__ = None if cond else 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; it incorrectly implements `__bool__`"
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
## Union with some variants implementing `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
def test(cond: bool):
|
||||
class NotBoolable:
|
||||
__bool__: int
|
||||
|
||||
a = 10 if cond else NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`; its `__bool__` method isn't callable"
|
||||
if a:
|
||||
...
|
||||
```
|
||||
|
||||
@@ -183,25 +183,32 @@ for x in Test():
|
||||
## Union type as iterable and union type as iterator
|
||||
|
||||
```py
|
||||
class TestIter:
|
||||
def __next__(self) -> int | Exception:
|
||||
return 42
|
||||
class Result1A: ...
|
||||
class Result1B: ...
|
||||
class Result2A: ...
|
||||
class Result2B: ...
|
||||
class Result3: ...
|
||||
class Result4: ...
|
||||
|
||||
class TestIter1:
|
||||
def __next__(self) -> Result1A | Result1B:
|
||||
return Result1B()
|
||||
|
||||
class TestIter2:
|
||||
def __next__(self) -> str | tuple[int, int]:
|
||||
return "42"
|
||||
def __next__(self) -> Result2A | Result2B:
|
||||
return Result2B()
|
||||
|
||||
class TestIter3:
|
||||
def __next__(self) -> bytes:
|
||||
return b"42"
|
||||
def __next__(self) -> Result3:
|
||||
return Result3()
|
||||
|
||||
class TestIter4:
|
||||
def __next__(self) -> memoryview:
|
||||
return memoryview(b"42")
|
||||
def __next__(self) -> Result4:
|
||||
return Result4()
|
||||
|
||||
class Test:
|
||||
def __iter__(self) -> TestIter | TestIter2:
|
||||
return TestIter()
|
||||
def __iter__(self) -> TestIter1 | TestIter2:
|
||||
return TestIter1()
|
||||
|
||||
class Test2:
|
||||
def __iter__(self) -> TestIter3 | TestIter4:
|
||||
@@ -209,7 +216,7 @@ class Test2:
|
||||
|
||||
def _(flag: bool):
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
|
||||
reveal_type(x) # revealed: Result1A | Result1B | Result2A | Result2B | Result3 | Result4
|
||||
```
|
||||
|
||||
## Union type as iterable where one union element has no `__iter__` method
|
||||
@@ -245,9 +252,10 @@ class Test2:
|
||||
return 42
|
||||
|
||||
def _(flag: bool):
|
||||
# TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
|
||||
# error: "Object of type `Test | Test2` is not iterable"
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterator where one union element has no `__next__` method
|
||||
@@ -263,5 +271,5 @@ class Test:
|
||||
|
||||
# error: [not-iterable] "Object of type `Test` is not iterable"
|
||||
for x in Test():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
@@ -116,3 +116,14 @@ def _(flag: bool, flag2: bool):
|
||||
# error: [possibly-unresolved-reference]
|
||||
y
|
||||
```
|
||||
|
||||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
while NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
@@ -64,3 +64,39 @@ def _(flag1: bool, flag2: bool):
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## `is` for `EllipsisType` (Python 3.10+)
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from types import EllipsisType
|
||||
|
||||
def _(x: int | EllipsisType):
|
||||
if x is ...:
|
||||
reveal_type(x) # revealed: EllipsisType
|
||||
else:
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## `is` for `EllipsisType` (Python 3.9 and below)
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = ... if flag else 42
|
||||
|
||||
reveal_type(x) # revealed: ellipsis | Literal[42]
|
||||
|
||||
if x is ...:
|
||||
reveal_type(x) # revealed: ellipsis
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
@@ -97,12 +97,7 @@ else:
|
||||
## No narrowing for instances of `builtins.type`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
t = type("t", (), {})
|
||||
|
||||
# This isn't testing what we want it to test if we infer anything more precise here:
|
||||
reveal_type(t) # revealed: type
|
||||
|
||||
def _(flag: bool, t: type):
|
||||
x = 1 if flag else "foo"
|
||||
|
||||
if isinstance(x, t):
|
||||
|
||||
@@ -112,8 +112,7 @@ def _(flag: bool):
|
||||
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]
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
```
|
||||
|
||||
## `classinfo` contains multiple types
|
||||
|
||||
@@ -266,7 +266,7 @@ def _(
|
||||
if af:
|
||||
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
|
||||
|
||||
# TODO: Emit a diagnostic (`d` is not valid in boolean context)
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`; the return type of its bool method (`MetaAmbiguous`) isn't assignable to `bool"
|
||||
if d:
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Protocols
|
||||
|
||||
We do not support protocols yet, but to avoid false positives, we *partially* support some known
|
||||
protocols.
|
||||
|
||||
## `typing.SupportsIndex`
|
||||
|
||||
```py
|
||||
from typing import SupportsIndex, Literal
|
||||
|
||||
def _(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex):
|
||||
a: SupportsIndex = some_int
|
||||
b: SupportsIndex = some_literal_int
|
||||
c: SupportsIndex = some_indexable
|
||||
```
|
||||
382
crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md
Normal file
382
crates/red_knot_python_semantic/resources/mdtest/scopes/eager.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# Eager scopes
|
||||
|
||||
Some scopes are executed eagerly: references to variables defined in enclosing scopes are resolved
|
||||
_immediately_. This is in constrast to (for instance) function scopes, where those references are
|
||||
resolved when the function is called.
|
||||
|
||||
## Function definitions
|
||||
|
||||
Function definitions are evaluated lazily.
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Unknown | Literal[2]
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
## Class definitions
|
||||
|
||||
Class definitions are evaluated eagerly.
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
class A:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
y = x
|
||||
|
||||
x = 2
|
||||
|
||||
reveal_type(A.y) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## List comprehensions
|
||||
|
||||
List comprehensions are evaluated eagerly.
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
[reveal_type(x) for a in range(1)]
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
## Set comprehensions
|
||||
|
||||
Set comprehensions are evaluated eagerly.
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
{reveal_type(x) for a in range(1)}
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
## Dict comprehensions
|
||||
|
||||
Dict comprehensions are evaluated eagerly.
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
{a: reveal_type(x) for a in range(1)}
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
## Generator expressions
|
||||
|
||||
Generator expressions don't necessarily run eagerly, but in practice usually they do, so assuming
|
||||
they do is the better default.
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
list(reveal_type(x) for a in range(1))
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
But that does lead to incorrect results when the generator expression isn't run immediately:
|
||||
|
||||
```py
|
||||
def evaluated_later():
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
y = (reveal_type(x) for a in range(1))
|
||||
|
||||
x = 2
|
||||
|
||||
# The generator isn't evaluated until here, so at runtime, `x` will evaluate to 2, contradicting
|
||||
# our inferred type.
|
||||
print(next(y))
|
||||
```
|
||||
|
||||
Though note that “the iterable expression in the leftmost `for` clause is immediately evaluated”
|
||||
\[[spec][generators]\]:
|
||||
|
||||
```py
|
||||
def iterable_evaluated_eagerly():
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
y = (a for a in [reveal_type(x)])
|
||||
|
||||
x = 2
|
||||
|
||||
# Even though the generator isn't evaluated until here, the first iterable was evaluated
|
||||
# immediately, so our inferred type is correct.
|
||||
print(next(y))
|
||||
```
|
||||
|
||||
## Top-level eager scopes
|
||||
|
||||
All of the above examples behave identically when the eager scopes are directly nested in the global
|
||||
scope.
|
||||
|
||||
### Class definitions
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
class A:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
y = x
|
||||
|
||||
x = 2
|
||||
|
||||
reveal_type(A.y) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
### List comprehensions
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
[reveal_type(x) for a in range(1)]
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
### Set comprehensions
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
{reveal_type(x) for a in range(1)}
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
### Dict comprehensions
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
{a: reveal_type(x) for a in range(1)}
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
### Generator expressions
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
list(reveal_type(x) for a in range(1))
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
`evaluated_later.py`:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
y = (reveal_type(x) for a in range(1))
|
||||
|
||||
x = 2
|
||||
|
||||
# The generator isn't evaluated until here, so at runtime, `x` will evaluate to 2, contradicting
|
||||
# our inferred type.
|
||||
print(next(y))
|
||||
```
|
||||
|
||||
`iterable_evaluated_eagerly.py`:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
# revealed: Literal[1]
|
||||
y = (a for a in [reveal_type(x)])
|
||||
|
||||
x = 2
|
||||
|
||||
# Even though the generator isn't evaluated until here, the first iterable was evaluated
|
||||
# immediately, so our inferred type is correct.
|
||||
print(next(y))
|
||||
```
|
||||
|
||||
## Lazy scopes are "sticky"
|
||||
|
||||
As we look through each enclosing scope when resolving a reference, lookups become lazy as soon as
|
||||
we encounter any lazy scope, even if there are other eager scopes that enclose it.
|
||||
|
||||
### Eager scope within eager scope
|
||||
|
||||
If we don't encounter a lazy scope, lookup remains eager. The resolved binding is not necessarily in
|
||||
the immediately enclosing scope. Here, the list comprehension and class definition are both eager
|
||||
scopes, and we immediately resolve the use of `x` to (only) the `x = 1` binding.
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
class A:
|
||||
# revealed: Literal[1]
|
||||
[reveal_type(x) for a in range(1)]
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
### Class definition bindings are not visible in nested scopes
|
||||
|
||||
Class definitions are eager scopes, but any bindings in them are explicitly not visible to any
|
||||
nested scopes. (Those nested scopes are typically (lazy) function definitions, but the rule also
|
||||
applies to nested eager scopes like comprehensions and other class definitions.)
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
class A:
|
||||
x = 4
|
||||
|
||||
# revealed: Literal[1]
|
||||
[reveal_type(x) for a in range(1)]
|
||||
|
||||
class B:
|
||||
# revealed: Literal[1]
|
||||
[reveal_type(x) for a in range(1)]
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
### Eager scope within a lazy scope
|
||||
|
||||
The list comprehension is an eager scope, and it is enclosed within a function definition, which is
|
||||
a lazy scope. Because we pass through this lazy scope before encountering any bindings or
|
||||
definitions, the lookup is lazy.
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
# revealed: Unknown | Literal[2]
|
||||
[reveal_type(x) for a in range(1)]
|
||||
x = 2
|
||||
```
|
||||
|
||||
### Lazy scope within an eager scope
|
||||
|
||||
The function definition is a lazy scope, and it is enclosed within a class definition, which is an
|
||||
eager scope. Even though we pass through an eager scope before encountering any bindings or
|
||||
definitions, the lookup remains lazy.
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
class A:
|
||||
def f():
|
||||
# revealed: Unknown | Literal[2]
|
||||
reveal_type(x)
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
### Lazy scope within a lazy scope
|
||||
|
||||
No matter how many lazy scopes we pass through before encountering a binding or definition, the
|
||||
lookup remains lazy.
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
def g():
|
||||
# revealed: Unknown | Literal[2]
|
||||
reveal_type(x)
|
||||
x = 2
|
||||
```
|
||||
|
||||
### Eager scope within a lazy scope within another eager scope
|
||||
|
||||
We have a list comprehension (eager scope), enclosed within a function definition (lazy scope),
|
||||
enclosed within a class definition (eager scope), all of which we must pass through before
|
||||
encountering any binding of `x`. Even though the last scope we pass through is eager, the lookup is
|
||||
lazy, since we encountered a lazy scope on the way.
|
||||
|
||||
```py
|
||||
def _():
|
||||
x = 1
|
||||
|
||||
class A:
|
||||
def f():
|
||||
# revealed: Unknown | Literal[2]
|
||||
[reveal_type(x) for a in range(1)]
|
||||
|
||||
x = 2
|
||||
```
|
||||
|
||||
## Annotations
|
||||
|
||||
Type annotations are sometimes deferred. When they are, the types that are referenced in an
|
||||
annotation are looked up lazily, even if they occur in an eager scope.
|
||||
|
||||
### Eager annotations in a Python file
|
||||
|
||||
```py
|
||||
x = int
|
||||
|
||||
class C:
|
||||
var: x
|
||||
|
||||
reveal_type(C.var) # revealed: int
|
||||
|
||||
x = str
|
||||
```
|
||||
|
||||
### Deferred annotations in a Python file
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
x = int
|
||||
|
||||
class C:
|
||||
var: x
|
||||
|
||||
reveal_type(C.var) # revealed: Unknown | str
|
||||
|
||||
x = str
|
||||
```
|
||||
|
||||
### Deferred annotations in a stub file
|
||||
|
||||
```pyi
|
||||
x = int
|
||||
|
||||
class C:
|
||||
var: x
|
||||
|
||||
reveal_type(C.var) # revealed: Unknown | str
|
||||
|
||||
x = str
|
||||
```
|
||||
|
||||
[generators]: https://docs.python.org/3/reference/expressions.html#generator-expressions
|
||||
@@ -9,7 +9,7 @@ is unbound.
|
||||
```py
|
||||
reveal_type(__name__) # revealed: str
|
||||
reveal_type(__file__) # revealed: str | None
|
||||
reveal_type(__loader__) # revealed: LoaderProtocol | None
|
||||
reveal_type(__loader__) # revealed: @Todo(instance attribute on class with dynamic base) | None
|
||||
reveal_type(__package__) # revealed: str | None
|
||||
reveal_type(__doc__) # revealed: str | None
|
||||
|
||||
@@ -54,10 +54,10 @@ inside the module:
|
||||
import typing
|
||||
|
||||
reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: @Todo(bound method)
|
||||
reveal_type(typing.__init__) # revealed: <bound method `__init__` of `ModuleType`>
|
||||
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
reveal_type(typing.__eq__) # revealed: @Todo(bound method)
|
||||
reveal_type(typing.__eq__) # revealed: <bound method `__eq__` of `ModuleType`>
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
||||
|
||||
@@ -136,3 +136,42 @@ if returns_bool():
|
||||
reveal_type(__file__) # revealed: Literal[42]
|
||||
reveal_type(__name__) # revealed: Literal[1] | str
|
||||
```
|
||||
|
||||
## Implicit global attributes in the current module override implicit globals from builtins
|
||||
|
||||
Here, we take the type of the implicit global symbol `__name__` from the `types.ModuleType` stub
|
||||
(which in this custom typeshed specifies the type as `bytes`). This is because the `main` module has
|
||||
an implicit `__name__` global that shadows the builtin `__name__` symbol.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
class int: ...
|
||||
class bytes: ...
|
||||
|
||||
__name__: int = 42
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/types.pyi`:
|
||||
|
||||
```pyi
|
||||
class ModuleType:
|
||||
__name__: bytes
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
reveal_type(__name__) # revealed: bytes
|
||||
```
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: instances.md - Binary operations on instances - Operations involving types with invalid `__bool__` methods
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
3 |
|
||||
4 | a = NotBoolable()
|
||||
5 |
|
||||
6 | # error: [unsupported-bool-conversion]
|
||||
7 | 10 and a and True
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:7:8
|
||||
|
|
||||
6 | # error: [unsupported-bool-conversion]
|
||||
7 | 10 and a and True
|
||||
| ^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Basic
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def foo(x: int) -> int:
|
||||
2 | return x * x
|
||||
3 |
|
||||
4 | foo("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:4:5
|
||||
|
|
||||
2 | return x * x
|
||||
3 |
|
||||
4 | foo("hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:9
|
||||
|
|
||||
1 | def foo(x: int) -> int:
|
||||
| ------ info: parameter declared in function definition here
|
||||
2 | return x * x
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different files
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## package.py
|
||||
|
||||
```
|
||||
1 | def foo(x: int) -> int:
|
||||
2 | return x * x
|
||||
```
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | import package
|
||||
2 |
|
||||
3 | package.foo("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:3:13
|
||||
|
|
||||
1 | import package
|
||||
2 |
|
||||
3 | package.foo("hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/package.py:1:9
|
||||
|
|
||||
1 | def foo(x: int) -> int:
|
||||
| ------ info: parameter declared in function definition here
|
||||
2 | return x * x
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different source order
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def bar():
|
||||
2 | foo("hello") # error: [invalid-argument-type]
|
||||
3 |
|
||||
4 | def foo(x: int) -> int:
|
||||
5 | return x * x
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:2:9
|
||||
|
|
||||
1 | def bar():
|
||||
2 | foo("hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
||||
3 |
|
||||
4 | def foo(x: int) -> int:
|
||||
|
|
||||
::: /src/mdtest_snippet.py:4:9
|
||||
|
|
||||
2 | foo("hello") # error: [invalid-argument-type]
|
||||
3 |
|
||||
4 | def foo(x: int) -> int:
|
||||
| ------ info: parameter declared in function definition here
|
||||
5 | return x * x
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def foo(x: int, y: int, z: int) -> int:
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:4:8
|
||||
|
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:17
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int) -> int:
|
||||
| ------ info: parameter declared in function definition here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters across multiple lines
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def foo(
|
||||
2 | x: int,
|
||||
3 | y: int,
|
||||
4 | z: int,
|
||||
5 | ) -> int:
|
||||
6 | return x * y * z
|
||||
7 |
|
||||
8 | foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:8:8
|
||||
|
|
||||
6 | return x * y * z
|
||||
7 |
|
||||
8 | foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:3:5
|
||||
|
|
||||
1 | def foo(
|
||||
2 | x: int,
|
||||
3 | y: int,
|
||||
| ------ info: parameter declared in function definition here
|
||||
4 | z: int,
|
||||
5 | ) -> int:
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters with multiple invalid arguments
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def foo(x: int, y: int, z: int) -> int:
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | # error: [invalid-argument-type]
|
||||
5 | # error: [invalid-argument-type]
|
||||
6 | # error: [invalid-argument-type]
|
||||
7 | foo("a", "b", "c")
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:7:5
|
||||
|
|
||||
5 | # error: [invalid-argument-type]
|
||||
6 | # error: [invalid-argument-type]
|
||||
7 | foo("a", "b", "c")
|
||||
| ^^^ Object of type `Literal["a"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:9
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int) -> int:
|
||||
| ------ info: parameter declared in function definition here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:7:10
|
||||
|
|
||||
5 | # error: [invalid-argument-type]
|
||||
6 | # error: [invalid-argument-type]
|
||||
7 | foo("a", "b", "c")
|
||||
| ^^^ Object of type `Literal["b"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:17
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int) -> int:
|
||||
| ------ info: parameter declared in function definition here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:7:15
|
||||
|
|
||||
5 | # error: [invalid-argument-type]
|
||||
6 | # error: [invalid-argument-type]
|
||||
7 | foo("a", "b", "c")
|
||||
| ^^^ Object of type `Literal["c"]` cannot be assigned to parameter 3 (`z`) of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:25
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int) -> int:
|
||||
| ------ info: parameter declared in function definition here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Test calling a function whose type is vendored from `typeshed`
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | import json
|
||||
2 |
|
||||
3 | json.loads(5) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:3:12
|
||||
|
|
||||
1 | import json
|
||||
2 |
|
||||
3 | json.loads(5) # error: [invalid-argument-type]
|
||||
| ^ Object of type `Literal[5]` cannot be assigned to parameter 1 (`s`) of function `loads`; expected type `str | bytes | bytearray`
|
||||
|
|
||||
::: vendored://stdlib/json/__init__.pyi:40:5
|
||||
|
|
||||
38 | ) -> None: ...
|
||||
39 | def loads(
|
||||
40 | s: str | bytes | bytearray,
|
||||
| -------------------------- info: parameter declared in function definition here
|
||||
41 | *,
|
||||
42 | cls: type[JSONDecoder] | None = None,
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Keyword only arguments
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def foo(x: int, y: int, *, z: int = 0) -> int:
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:4:11
|
||||
|
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `z` of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:28
|
||||
|
|
||||
1 | def foo(x: int, y: int, *, z: int = 0) -> int:
|
||||
| ---------- info: parameter declared in function definition here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Mix of arguments
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def foo(x: int, /, y: int, *, z: int = 0) -> int:
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:4:11
|
||||
|
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `z` of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:31
|
||||
|
|
||||
1 | def foo(x: int, /, y: int, *, z: int = 0) -> int:
|
||||
| ---------- info: parameter declared in function definition here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - One keyword argument
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def foo(x: int, y: int, z: int = 0) -> int:
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, 2, "hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:4:11
|
||||
|
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, 2, "hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 3 (`z`) of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:25
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int = 0) -> int:
|
||||
| ---------- info: parameter declared in function definition here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Only positional
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def foo(x: int, y: int, z: int, /) -> int:
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:4:8
|
||||
|
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:17
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int, /) -> int:
|
||||
| ------ info: parameter declared in function definition here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Synthetic arguments
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class C:
|
||||
2 | def __call__(self, x: int) -> int:
|
||||
3 | return 1
|
||||
4 |
|
||||
5 | c = C()
|
||||
6 | c("wrong") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:6:3
|
||||
|
|
||||
5 | c = C()
|
||||
6 | c("wrong") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["wrong"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:2:24
|
||||
|
|
||||
1 | class C:
|
||||
2 | def __call__(self, x: int) -> int:
|
||||
| ------ info: parameter declared in function definition here
|
||||
3 | return 1
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic arguments
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def foo(*numbers: int) -> int:
|
||||
2 | return len(numbers)
|
||||
3 |
|
||||
4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:4:14
|
||||
|
|
||||
2 | return len(numbers)
|
||||
3 |
|
||||
4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `*numbers` of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:9
|
||||
|
|
||||
1 | def foo(*numbers: int) -> int:
|
||||
| ------------- info: parameter declared in function definition here
|
||||
2 | return len(numbers)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic keyword arguments
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | def foo(**numbers: int) -> int:
|
||||
2 | return len(numbers)
|
||||
3 |
|
||||
4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:4:20
|
||||
|
|
||||
2 | return len(numbers)
|
||||
3 |
|
||||
4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
|
||||
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `**numbers` of function `foo`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:1:9
|
||||
|
|
||||
1 | def foo(**numbers: int) -> int:
|
||||
| -------------- info: parameter declared in function definition here
|
||||
2 | return len(numbers)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: membership_test.md - Comparison: Membership Test - Return type that doesn't implement `__bool__` correctly
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
3 |
|
||||
4 | class WithContains:
|
||||
5 | def __contains__(self, item) -> NotBoolable:
|
||||
6 | return NotBoolable()
|
||||
7 |
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | 10 in WithContains()
|
||||
10 | # error: [unsupported-bool-conversion]
|
||||
11 | 10 not in WithContains()
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:9:1
|
||||
|
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | 10 in WithContains()
|
||||
| ^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
10 | # error: [unsupported-bool-conversion]
|
||||
11 | 10 not in WithContains()
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:11:1
|
||||
|
|
||||
9 | 10 in WithContains()
|
||||
10 | # error: [unsupported-bool-conversion]
|
||||
11 | 10 not in WithContains()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: not.md - Unary not - Object that implements `__bool__` incorrectly
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
3 |
|
||||
4 | # error: [unsupported-bool-conversion]
|
||||
5 | not NotBoolable()
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:5:1
|
||||
|
|
||||
4 | # error: [unsupported-bool-conversion]
|
||||
5 | not NotBoolable()
|
||||
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: rich_comparison.md - Comparison: Rich Comparison - Chained comparisons with objects that don't implement `__bool__` correctly
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
3 |
|
||||
4 | class Comparable:
|
||||
5 | def __lt__(self, item) -> NotBoolable:
|
||||
6 | return NotBoolable()
|
||||
7 |
|
||||
8 | def __gt__(self, item) -> NotBoolable:
|
||||
9 | return NotBoolable()
|
||||
10 |
|
||||
11 | # error: [unsupported-bool-conversion]
|
||||
12 | 10 < Comparable() < 20
|
||||
13 | # error: [unsupported-bool-conversion]
|
||||
14 | 10 < Comparable() < Comparable()
|
||||
15 |
|
||||
16 | Comparable() < Comparable() # fine
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:12:1
|
||||
|
|
||||
11 | # error: [unsupported-bool-conversion]
|
||||
12 | 10 < Comparable() < 20
|
||||
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
13 | # error: [unsupported-bool-conversion]
|
||||
14 | 10 < Comparable() < Comparable()
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:14:1
|
||||
|
|
||||
12 | 10 < Comparable() < 20
|
||||
13 | # error: [unsupported-bool-conversion]
|
||||
14 | 10 < Comparable() < Comparable()
|
||||
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
15 |
|
||||
16 | Comparable() < Comparable() # fine
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: tuples.md - Comparison: Tuples - Chained comparisons with elements that incorrectly implement `__bool__`
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 5
|
||||
3 |
|
||||
4 | class Comparable:
|
||||
5 | def __lt__(self, other) -> NotBoolable:
|
||||
6 | return NotBoolable()
|
||||
7 |
|
||||
8 | def __gt__(self, other) -> NotBoolable:
|
||||
9 | return NotBoolable()
|
||||
10 |
|
||||
11 | a = (1, Comparable())
|
||||
12 | b = (1, Comparable())
|
||||
13 |
|
||||
14 | # error: [unsupported-bool-conversion]
|
||||
15 | a < b < b
|
||||
16 |
|
||||
17 | a < b # fine
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:15:1
|
||||
|
|
||||
14 | # error: [unsupported-bool-conversion]
|
||||
15 | a < b < b
|
||||
| ^^^^^ Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable
|
||||
16 |
|
||||
17 | a < b # fine
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: tuples.md - Comparison: Tuples - Equality with elements that incorrectly implement `__bool__`
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class A:
|
||||
2 | def __eq__(self, other) -> NotBoolable:
|
||||
3 | return NotBoolable()
|
||||
4 |
|
||||
5 | class NotBoolable:
|
||||
6 | __bool__ = None
|
||||
7 |
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | (A(),) == (A(),)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:9:1
|
||||
|
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | (A(),) == (A(),)
|
||||
| ^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -66,6 +66,6 @@ It is [recommended](https://docs.python.org/3/library/sys.html#sys.platform) to
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.platform.startswith("freebsd")) # revealed: @Todo(Attribute access on `LiteralString` types)
|
||||
reveal_type(sys.platform.startswith("linux")) # revealed: @Todo(Attribute access on `LiteralString` types)
|
||||
reveal_type(sys.platform.startswith("freebsd")) # revealed: bool
|
||||
reveal_type(sys.platform.startswith("linux")) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -223,7 +223,7 @@ class InvalidBoolDunder:
|
||||
def __bool__(self) -> int:
|
||||
return 1
|
||||
|
||||
# error: "Static assertion error: argument of type `InvalidBoolDunder` has an ambiguous static truthiness"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`; the return type of its bool method (`int`) isn't assignable to `bool"
|
||||
static_assert(InvalidBoolDunder())
|
||||
```
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ in strict mode.
|
||||
```py
|
||||
def f(x: type):
|
||||
reveal_type(x) # revealed: type
|
||||
reveal_type(x.__repr__) # revealed: @Todo(bound method)
|
||||
reveal_type(x.__repr__) # revealed: <bound method `__repr__` of `type`>
|
||||
|
||||
class A: ...
|
||||
|
||||
@@ -50,7 +50,7 @@ x: type = A() # error: [invalid-assignment]
|
||||
```py
|
||||
def f(x: type[object]):
|
||||
reveal_type(x) # revealed: type
|
||||
reveal_type(x.__repr__) # revealed: @Todo(bound method)
|
||||
reveal_type(x.__repr__) # revealed: <bound method `__repr__` of `type`>
|
||||
|
||||
class A: ...
|
||||
|
||||
|
||||
@@ -54,3 +54,41 @@ from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(_NoDefaultType))
|
||||
```
|
||||
|
||||
## `builtins.ellipsis`/`types.EllipsisType`
|
||||
|
||||
### All Python versions
|
||||
|
||||
The type of the builtin symbol `Ellipsis` is the same as the type of an ellipsis literal (`...`).
|
||||
The type is not actually exposed from the standard library on Python \<3.10, but we still recognise
|
||||
the type as a singleton on any Python version.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(Ellipsis.__class__))
|
||||
static_assert(is_singleton((...).__class__))
|
||||
```
|
||||
|
||||
### Python 3.10+
|
||||
|
||||
On Python 3.10+, the standard library exposes the type of `...` as `types.EllipsisType`, and we also
|
||||
recognise this as a singleton type when it is referenced directly:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
import types
|
||||
from knot_extensions import static_assert, is_singleton
|
||||
|
||||
static_assert(is_singleton(types.EllipsisType))
|
||||
```
|
||||
|
||||
@@ -75,3 +75,48 @@ class Boom:
|
||||
|
||||
reveal_type(bool(Boom())) # revealed: bool
|
||||
```
|
||||
|
||||
### Possibly unbound __bool__ method
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
class PossiblyUnboundTrue:
|
||||
if flag():
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
reveal_type(bool(PossiblyUnboundTrue())) # revealed: bool
|
||||
```
|
||||
|
||||
### Special-cased classes
|
||||
|
||||
Some special-cased `@final` classes are known by red-knot to have instances that are either always
|
||||
truthy or always falsy.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
import types
|
||||
import typing
|
||||
import sys
|
||||
from knot_extensions import AlwaysTruthy, static_assert, is_subtype_of
|
||||
from typing_extensions import _NoDefaultType
|
||||
|
||||
static_assert(is_subtype_of(sys.version_info.__class__, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(types.EllipsisType, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(_NoDefaultType, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(slice, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(types.FunctionType, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(types.MethodType, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(typing.TypeVar, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(typing.TypeAliasType, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(types.MethodWrapperType, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(types.WrapperDescriptorType, AlwaysTruthy))
|
||||
```
|
||||
|
||||
@@ -183,12 +183,11 @@ class WithBothLenAndBool2:
|
||||
# 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
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MethodBoolInvalid`; the return type of its bool method (`int`) isn't assignable to `bool"
|
||||
# revealed: bool
|
||||
reveal_type(not MethodBoolInvalid())
|
||||
|
||||
@@ -204,3 +203,15 @@ class PossiblyUnboundBool:
|
||||
# revealed: bool
|
||||
reveal_type(not PossiblyUnboundBool())
|
||||
```
|
||||
|
||||
## Object that implements `__bool__` incorrectly
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
not NotBoolable()
|
||||
```
|
||||
|
||||
@@ -80,7 +80,7 @@ class Manager:
|
||||
|
||||
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 `int` is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
@@ -95,7 +95,7 @@ class Manager:
|
||||
|
||||
__exit__: int = 32
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `int` is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
@@ -134,3 +134,19 @@ def _(flag: bool):
|
||||
with Manager() as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Invalid `__enter__` signature
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
def __enter__() -> str:
|
||||
return "foo"
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
|
||||
context_expr = Manager()
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`"
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
@@ -28,7 +28,7 @@ pub(crate) mod tests {
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -22,7 +22,6 @@ mod python_platform;
|
||||
pub mod semantic_index;
|
||||
mod semantic_model;
|
||||
pub(crate) mod site_packages;
|
||||
mod stdlib;
|
||||
mod suppression;
|
||||
pub(crate) mod symbol;
|
||||
pub mod types;
|
||||
|
||||
@@ -109,6 +109,7 @@ pub enum KnownModule {
|
||||
#[allow(dead_code)]
|
||||
Abc, // currently only used in tests
|
||||
Collections,
|
||||
Inspect,
|
||||
KnotExtensions,
|
||||
}
|
||||
|
||||
@@ -123,6 +124,7 @@ impl KnownModule {
|
||||
Self::Sys => "sys",
|
||||
Self::Abc => "abc",
|
||||
Self::Collections => "collections",
|
||||
Self::Inspect => "inspect",
|
||||
Self::KnotExtensions => "knot_extensions",
|
||||
}
|
||||
}
|
||||
@@ -149,6 +151,7 @@ impl KnownModule {
|
||||
"sys" => Some(Self::Sys),
|
||||
"abc" => Some(Self::Abc),
|
||||
"collections" => Some(Self::Collections),
|
||||
"inspect" => Some(Self::Inspect),
|
||||
"knot_extensions" => Some(Self::KnotExtensions),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -631,7 +631,7 @@ impl PartialEq<SearchPath> for VendoredPathBuf {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::Db;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
|
||||
@@ -6,7 +6,7 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use ruff_db::files::{File, FilePath, FileRootKind};
|
||||
use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::{VendoredFileSystem, VendoredPath};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
@@ -725,7 +725,7 @@ mod tests {
|
||||
assert_const_function_query_was_not_run, assert_function_query_was_not_run,
|
||||
};
|
||||
use ruff_db::Db;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::module_name::ModuleName;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPathBuf;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::num::{NonZeroU16, NonZeroUsize};
|
||||
use std::ops::{RangeFrom, RangeInclusive};
|
||||
use std::str::FromStr;
|
||||
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::db::Db;
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::Db;
|
||||
|
||||
use anyhow::Context;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use salsa::Durability;
|
||||
use salsa::Setter;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{
|
||||
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId, SymbolTable,
|
||||
};
|
||||
use crate::semantic_index::use_def::UseDefMap;
|
||||
use crate::semantic_index::use_def::{EagerBindingsKey, ScopedEagerBindingsId, UseDefMap};
|
||||
use crate::Db;
|
||||
|
||||
pub mod ast_ids;
|
||||
@@ -165,6 +165,9 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
/// Maps from class body scopes to attribute assignments that were found
|
||||
/// in methods of that class.
|
||||
attribute_assignments: FxHashMap<FileScopeId, Arc<AttributeAssignments<'db>>>,
|
||||
|
||||
/// Map of all of the eager bindings that appear in this file.
|
||||
eager_bindings: FxHashMap<EagerBindingsKey, ScopedEagerBindingsId>,
|
||||
}
|
||||
|
||||
impl<'db> SemanticIndex<'db> {
|
||||
@@ -220,7 +223,7 @@ impl<'db> SemanticIndex<'db> {
|
||||
/// Returns the id of the parent scope.
|
||||
pub(crate) fn parent_scope_id(&self, scope_id: FileScopeId) -> Option<FileScopeId> {
|
||||
let scope = self.scope(scope_id);
|
||||
scope.parent
|
||||
scope.parent()
|
||||
}
|
||||
|
||||
/// Returns the parent scope of `scope_id`.
|
||||
@@ -243,7 +246,6 @@ impl<'db> SemanticIndex<'db> {
|
||||
}
|
||||
|
||||
/// Returns an iterator over all ancestors of `scope`, starting with `scope` itself.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn ancestor_scopes(&self, scope: FileScopeId) -> AncestorsIter {
|
||||
AncestorsIter::new(self, scope)
|
||||
}
|
||||
@@ -290,6 +292,23 @@ impl<'db> SemanticIndex<'db> {
|
||||
pub(super) fn has_future_annotations(&self) -> bool {
|
||||
self.has_future_annotations
|
||||
}
|
||||
|
||||
/// Returns an iterator of bindings for a particular nested eager scope reference.
|
||||
pub(crate) fn eager_bindings(
|
||||
&self,
|
||||
enclosing_scope: FileScopeId,
|
||||
symbol: &str,
|
||||
nested_scope: FileScopeId,
|
||||
) -> Option<BindingWithConstraintsIterator<'_, 'db>> {
|
||||
let symbol_id = self.symbol_tables[enclosing_scope].symbol_id_by_name(symbol)?;
|
||||
let key = EagerBindingsKey {
|
||||
enclosing_scope,
|
||||
enclosing_symbol: symbol_id,
|
||||
nested_scope,
|
||||
};
|
||||
let id = self.eager_bindings.get(&key)?;
|
||||
self.use_def_maps[enclosing_scope].eager_bindings(*id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AncestorsIter<'a> {
|
||||
@@ -312,7 +331,7 @@ impl<'a> Iterator for AncestorsIter<'a> {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let current_id = self.next_id?;
|
||||
let current = &self.scopes[current_id];
|
||||
self.next_id = current.parent;
|
||||
self.next_id = current.parent();
|
||||
|
||||
Some((current_id, current))
|
||||
}
|
||||
@@ -328,7 +347,7 @@ pub struct DescendentsIter<'a> {
|
||||
impl<'a> DescendentsIter<'a> {
|
||||
fn new(symbol_table: &'a SemanticIndex, scope_id: FileScopeId) -> Self {
|
||||
let scope = &symbol_table.scopes[scope_id];
|
||||
let scopes = &symbol_table.scopes[scope.descendents.clone()];
|
||||
let scopes = &symbol_table.scopes[scope.descendents()];
|
||||
|
||||
Self {
|
||||
next_id: scope_id + 1,
|
||||
@@ -378,7 +397,7 @@ impl<'a> Iterator for ChildrenIter<'a> {
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.descendents
|
||||
.find(|(_, scope)| scope.parent == Some(self.parent))
|
||||
.find(|(_, scope)| scope.parent() == Some(self.parent))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::module_name::ModuleName;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
|
||||
use crate::semantic_index::constraint::PatternConstraintKind;
|
||||
use crate::semantic_index::constraint::{PatternConstraintKind, ScopedConstraintId};
|
||||
use crate::semantic_index::definition::{
|
||||
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionNodeKey,
|
||||
DefinitionNodeRef, ForStmtDefinitionNodeRef, ImportFromDefinitionNodeRef,
|
||||
@@ -25,7 +25,9 @@ use crate::semantic_index::symbol::{
|
||||
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId,
|
||||
SymbolTableBuilder,
|
||||
};
|
||||
use crate::semantic_index::use_def::{FlowSnapshot, ScopedConstraintId, UseDefMapBuilder};
|
||||
use crate::semantic_index::use_def::{
|
||||
EagerBindingsKey, FlowSnapshot, ScopedEagerBindingsId, UseDefMapBuilder,
|
||||
};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::unpack::{Unpack, UnpackValue};
|
||||
use crate::visibility_constraints::{ScopedVisibilityConstraintId, VisibilityConstraintsBuilder};
|
||||
@@ -91,6 +93,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
|
||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||
imported_modules: FxHashSet<ModuleName>,
|
||||
attribute_assignments: FxHashMap<FileScopeId, AttributeAssignments<'db>>,
|
||||
eager_bindings: FxHashMap<EagerBindingsKey, ScopedEagerBindingsId>,
|
||||
}
|
||||
|
||||
impl<'db> SemanticIndexBuilder<'db> {
|
||||
@@ -122,6 +125,8 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
imported_modules: FxHashSet::default(),
|
||||
|
||||
attribute_assignments: FxHashMap::default(),
|
||||
|
||||
eager_bindings: FxHashMap::default(),
|
||||
};
|
||||
|
||||
builder.push_scope_with_parent(NodeWithScopeRef::Module, None);
|
||||
@@ -134,13 +139,13 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
.scope_stack
|
||||
.last()
|
||||
.map(|ScopeInfo { file_scope_id, .. }| file_scope_id)
|
||||
.expect("Always to have a root scope")
|
||||
.expect("SemanticIndexBuilder should have created a root scope")
|
||||
}
|
||||
|
||||
fn loop_state(&self) -> LoopState {
|
||||
self.scope_stack
|
||||
.last()
|
||||
.expect("Always to have a root scope")
|
||||
.expect("SemanticIndexBuilder should have created a root scope")
|
||||
.loop_state
|
||||
}
|
||||
|
||||
@@ -177,13 +182,11 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
fn push_scope_with_parent(&mut self, node: NodeWithScopeRef, parent: Option<FileScopeId>) {
|
||||
let children_start = self.scopes.next_index() + 1;
|
||||
|
||||
// SAFETY: `node` is guaranteed to be a child of `self.module`
|
||||
#[allow(unsafe_code)]
|
||||
let scope = Scope {
|
||||
parent,
|
||||
// SAFETY: `node` is guaranteed to be a child of `self.module`
|
||||
node: unsafe { node.to_kind(self.module.clone()) },
|
||||
descendents: children_start..children_start,
|
||||
};
|
||||
let node_with_kind = unsafe { node.to_kind(self.module.clone()) };
|
||||
|
||||
let scope = Scope::new(parent, node_with_kind, children_start..children_start);
|
||||
self.try_node_context_stack_manager.enter_nested_scope();
|
||||
|
||||
let file_scope_id = self.scopes.push(scope);
|
||||
@@ -206,13 +209,74 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
}
|
||||
|
||||
fn pop_scope(&mut self) -> FileScopeId {
|
||||
let ScopeInfo { file_scope_id, .. } =
|
||||
self.scope_stack.pop().expect("Root scope to be present");
|
||||
let children_end = self.scopes.next_index();
|
||||
let scope = &mut self.scopes[file_scope_id];
|
||||
scope.descendents = scope.descendents.start..children_end;
|
||||
self.try_node_context_stack_manager.exit_scope();
|
||||
file_scope_id
|
||||
|
||||
let ScopeInfo {
|
||||
file_scope_id: popped_scope_id,
|
||||
..
|
||||
} = self
|
||||
.scope_stack
|
||||
.pop()
|
||||
.expect("Root scope should be present");
|
||||
|
||||
let children_end = self.scopes.next_index();
|
||||
let popped_scope = &mut self.scopes[popped_scope_id];
|
||||
popped_scope.extend_descendents(children_end);
|
||||
|
||||
if !popped_scope.is_eager() {
|
||||
return popped_scope_id;
|
||||
}
|
||||
|
||||
// If the scope that we just popped off is an eager scope, we need to "lock" our view of
|
||||
// which bindings reach each of the uses in the scope. Loop through each enclosing scope,
|
||||
// looking for any that bind each symbol.
|
||||
for enclosing_scope_info in self.scope_stack.iter().rev() {
|
||||
let enclosing_scope_id = enclosing_scope_info.file_scope_id;
|
||||
let enclosing_scope_kind = self.scopes[enclosing_scope_id].kind();
|
||||
let enclosing_symbol_table = &self.symbol_tables[enclosing_scope_id];
|
||||
|
||||
// Names bound in class scopes are never visible to nested scopes, so we never need to
|
||||
// save eager scope bindings in a class scope.
|
||||
if enclosing_scope_kind.is_class() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for nested_symbol in self.symbol_tables[popped_scope_id].symbols() {
|
||||
// Skip this symbol if this enclosing scope doesn't contain any bindings for
|
||||
// it, or if the nested scope _does_.
|
||||
if nested_symbol.is_bound() {
|
||||
continue;
|
||||
}
|
||||
let Some(enclosing_symbol_id) =
|
||||
enclosing_symbol_table.symbol_id_by_name(nested_symbol.name())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let enclosing_symbol = enclosing_symbol_table.symbol(enclosing_symbol_id);
|
||||
if !enclosing_symbol.is_bound() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Snapshot the bindings of this symbol that are visible at this point in this
|
||||
// enclosing scope.
|
||||
let key = EagerBindingsKey {
|
||||
enclosing_scope: enclosing_scope_id,
|
||||
enclosing_symbol: enclosing_symbol_id,
|
||||
nested_scope: popped_scope_id,
|
||||
};
|
||||
let eager_bindings = self.use_def_maps[enclosing_scope_id]
|
||||
.snapshot_eager_bindings(enclosing_symbol_id);
|
||||
self.eager_bindings.insert(key, eager_bindings);
|
||||
}
|
||||
|
||||
// Lazy scopes are "sticky": once we see a lazy scope we stop doing lookups
|
||||
// eagerly, even if we would encounter another eager enclosing scope later on.
|
||||
if !enclosing_scope_kind.is_eager() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
popped_scope_id
|
||||
}
|
||||
|
||||
fn current_symbol_table(&mut self) -> &mut SymbolTableBuilder {
|
||||
@@ -230,7 +294,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
&self.use_def_maps[scope_id]
|
||||
}
|
||||
|
||||
fn current_visibility_constraints_mut(&mut self) -> &mut VisibilityConstraintsBuilder<'db> {
|
||||
fn current_visibility_constraints_mut(&mut self) -> &mut VisibilityConstraintsBuilder {
|
||||
let scope_id = self.current_scope();
|
||||
&mut self.use_def_maps[scope_id].visibility_constraints
|
||||
}
|
||||
@@ -282,12 +346,14 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
// SAFETY: `definition_node` is guaranteed to be a child of `self.module`
|
||||
let kind = unsafe { definition_node.into_owned(self.module.clone()) };
|
||||
let category = kind.category();
|
||||
let is_reexported = kind.is_reexported();
|
||||
let definition = Definition::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
symbol,
|
||||
kind,
|
||||
is_reexported,
|
||||
countme::Count::default(),
|
||||
);
|
||||
|
||||
@@ -340,16 +406,12 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
}
|
||||
|
||||
/// Negates a constraint and adds it to the list of all constraints, does not record it.
|
||||
fn add_negated_constraint(
|
||||
&mut self,
|
||||
constraint: Constraint<'db>,
|
||||
) -> (Constraint<'db>, ScopedConstraintId) {
|
||||
fn add_negated_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
let negated = Constraint {
|
||||
node: constraint.node,
|
||||
is_positive: false,
|
||||
};
|
||||
let id = self.current_use_def_map_mut().add_constraint(negated);
|
||||
(negated, id)
|
||||
self.current_use_def_map_mut().add_constraint(negated)
|
||||
}
|
||||
|
||||
/// Records a previously added constraint by adding it to all live bindings.
|
||||
@@ -365,7 +427,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
|
||||
/// Negates the given constraint and then adds it to all live bindings.
|
||||
fn record_negated_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
let (_, id) = self.add_negated_constraint(constraint);
|
||||
let id = self.add_negated_constraint(constraint);
|
||||
self.record_constraint_id(id);
|
||||
id
|
||||
}
|
||||
@@ -394,9 +456,10 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
&mut self,
|
||||
constraint: Constraint<'db>,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
let constraint_id = self.current_use_def_map_mut().add_constraint(constraint);
|
||||
let id = self
|
||||
.current_visibility_constraints_mut()
|
||||
.add_atom(constraint, 0);
|
||||
.add_atom(constraint_id);
|
||||
self.record_visibility_constraint_id(id);
|
||||
id
|
||||
}
|
||||
@@ -729,6 +792,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
|
||||
self.scope_ids_by_scope.shrink_to_fit();
|
||||
self.scopes_by_node.shrink_to_fit();
|
||||
self.eager_bindings.shrink_to_fit();
|
||||
|
||||
SemanticIndex {
|
||||
symbol_tables,
|
||||
@@ -747,6 +811,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, Arc::new(v)))
|
||||
.collect(),
|
||||
eager_bindings: self.eager_bindings,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1124,12 +1189,14 @@ where
|
||||
// We need multiple copies of the visibility constraint for the while condition,
|
||||
// since we need to model situations where the first evaluation of the condition
|
||||
// returns True, but a later evaluation returns False.
|
||||
let first_constraint_id = self.current_use_def_map_mut().add_constraint(constraint);
|
||||
let later_constraint_id = self.current_use_def_map_mut().add_constraint(constraint);
|
||||
let first_vis_constraint_id = self
|
||||
.current_visibility_constraints_mut()
|
||||
.add_atom(constraint, 0);
|
||||
.add_atom(first_constraint_id);
|
||||
let later_vis_constraint_id = self
|
||||
.current_visibility_constraints_mut()
|
||||
.add_atom(constraint, 1);
|
||||
.add_atom(later_constraint_id);
|
||||
|
||||
// Save aside any break states from an outer loop
|
||||
let saved_break_states = std::mem::take(&mut self.loop_break_states);
|
||||
@@ -1710,13 +1777,13 @@ where
|
||||
// anymore.
|
||||
if index < values.len() - 1 {
|
||||
let constraint = self.build_constraint(value);
|
||||
let (constraint, constraint_id) = match op {
|
||||
ast::BoolOp::And => (constraint, self.add_constraint(constraint)),
|
||||
let constraint_id = match op {
|
||||
ast::BoolOp::And => self.add_constraint(constraint),
|
||||
ast::BoolOp::Or => self.add_negated_constraint(constraint),
|
||||
};
|
||||
let visibility_constraint = self
|
||||
.current_visibility_constraints_mut()
|
||||
.add_atom(constraint, 0);
|
||||
.add_atom(constraint_id);
|
||||
|
||||
let after_expr = self.flow_snapshot();
|
||||
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use ruff_python_ast::Singleton;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
|
||||
|
||||
// A scoped identifier for each `Constraint` in a scope.
|
||||
#[newtype_index]
|
||||
#[derive(Ord, PartialOrd)]
|
||||
pub(crate) struct ScopedConstraintId;
|
||||
|
||||
// A collection of constraints. This is currently stored in `UseDefMap`, which means we maintain a
|
||||
// separate set of constraints for each scope in a file.
|
||||
pub(crate) type Constraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct ConstraintsBuilder<'db> {
|
||||
constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> ConstraintsBuilder<'db> {
|
||||
/// Adds a constraint. Note that we do not deduplicate constraints. If you add a `Constraint`
|
||||
/// more than once, you will get distinct `ScopedConstraintId`s for each one. (This lets you
|
||||
/// model constraint expressions that might evaluate to different values at different points of
|
||||
/// execution.)
|
||||
pub(crate) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
self.constraints.push(constraint)
|
||||
}
|
||||
|
||||
pub(crate) fn build(mut self) -> Constraints<'db> {
|
||||
self.constraints.shrink_to_fit();
|
||||
self.constraints
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) struct Constraint<'db> {
|
||||
pub(crate) node: ConstraintNode<'db>,
|
||||
|
||||
@@ -33,11 +33,16 @@ pub struct Definition<'db> {
|
||||
/// The symbol defined.
|
||||
pub(crate) symbol: ScopedSymbolId,
|
||||
|
||||
/// WARNING: Only access this field when doing type inference for the same
|
||||
/// file as where `Definition` is defined to avoid cross-file query dependencies.
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
#[tracked]
|
||||
pub(crate) kind: DefinitionKind<'db>,
|
||||
|
||||
/// This is a dedicated field to avoid accessing `kind` to compute this value.
|
||||
pub(crate) is_reexported: bool,
|
||||
|
||||
count: countme::Count<Definition<'static>>,
|
||||
}
|
||||
|
||||
@@ -45,22 +50,6 @@ impl<'db> Definition<'db> {
|
||||
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
|
||||
self.file_scope(db).to_scope_id(db, self.file(db))
|
||||
}
|
||||
|
||||
pub(crate) fn category(self, db: &'db dyn Db) -> DefinitionCategory {
|
||||
self.kind(db).category()
|
||||
}
|
||||
|
||||
pub(crate) fn is_declaration(self, db: &'db dyn Db) -> bool {
|
||||
self.kind(db).category().is_declaration()
|
||||
}
|
||||
|
||||
pub(crate) fn is_binding(self, db: &'db dyn Db) -> bool {
|
||||
self.kind(db).category().is_binding()
|
||||
}
|
||||
|
||||
pub(crate) fn is_reexported(self, db: &'db dyn Db) -> bool {
|
||||
self.kind(db).is_reexported()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
||||
@@ -111,16 +111,7 @@ pub struct ScopeId<'db> {
|
||||
|
||||
impl<'db> ScopeId<'db> {
|
||||
pub(crate) fn is_function_like(self, db: &'db dyn Db) -> bool {
|
||||
// Type parameter scopes behave like function scopes in terms of name resolution; CPython
|
||||
// symbol table also uses the term "function-like" for these scopes.
|
||||
matches!(
|
||||
self.node(db).scope_kind(),
|
||||
ScopeKind::Annotation
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::Lambda
|
||||
| ScopeKind::TypeAlias
|
||||
| ScopeKind::Comprehension
|
||||
)
|
||||
self.node(db).scope_kind().is_function_like()
|
||||
}
|
||||
|
||||
pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind {
|
||||
@@ -178,13 +169,25 @@ impl FileScopeId {
|
||||
|
||||
#[derive(Debug, salsa::Update)]
|
||||
pub struct Scope {
|
||||
pub(super) parent: Option<FileScopeId>,
|
||||
pub(super) node: NodeWithScopeKind,
|
||||
pub(super) descendents: Range<FileScopeId>,
|
||||
parent: Option<FileScopeId>,
|
||||
node: NodeWithScopeKind,
|
||||
descendents: Range<FileScopeId>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
pub fn parent(self) -> Option<FileScopeId> {
|
||||
pub(super) fn new(
|
||||
parent: Option<FileScopeId>,
|
||||
node: NodeWithScopeKind,
|
||||
descendents: Range<FileScopeId>,
|
||||
) -> Self {
|
||||
Scope {
|
||||
parent,
|
||||
node,
|
||||
descendents,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Option<FileScopeId> {
|
||||
self.parent
|
||||
}
|
||||
|
||||
@@ -195,6 +198,18 @@ impl Scope {
|
||||
pub fn kind(&self) -> ScopeKind {
|
||||
self.node().scope_kind()
|
||||
}
|
||||
|
||||
pub fn descendents(&self) -> Range<FileScopeId> {
|
||||
self.descendents.clone()
|
||||
}
|
||||
|
||||
pub(super) fn extend_descendents(&mut self, children_end: FileScopeId) {
|
||||
self.descendents = self.descendents.start..children_end;
|
||||
}
|
||||
|
||||
pub(crate) fn is_eager(&self) -> bool {
|
||||
self.kind().is_eager()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -209,8 +224,32 @@ pub enum ScopeKind {
|
||||
}
|
||||
|
||||
impl ScopeKind {
|
||||
pub const fn is_comprehension(self) -> bool {
|
||||
matches!(self, ScopeKind::Comprehension)
|
||||
pub(crate) fn is_eager(self) -> bool {
|
||||
match self {
|
||||
ScopeKind::Class | ScopeKind::Comprehension => true,
|
||||
ScopeKind::Module
|
||||
| ScopeKind::Annotation
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::Lambda
|
||||
| ScopeKind::TypeAlias => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_function_like(self) -> bool {
|
||||
// Type parameter scopes behave like function scopes in terms of name resolution; CPython
|
||||
// symbol table also uses the term "function-like" for these scopes.
|
||||
matches!(
|
||||
self,
|
||||
ScopeKind::Annotation
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::Lambda
|
||||
| ScopeKind::TypeAlias
|
||||
| ScopeKind::Comprehension
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_class(self) -> bool {
|
||||
matches!(self, ScopeKind::Class)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +355,18 @@ impl SymbolTableBuilder {
|
||||
self.table.symbols[id].insert_flags(SymbolFlags::IS_USED);
|
||||
}
|
||||
|
||||
pub(super) fn symbols(&self) -> impl Iterator<Item = &Symbol> {
|
||||
self.table.symbols()
|
||||
}
|
||||
|
||||
pub(super) fn symbol_id_by_name(&self, name: &str) -> Option<ScopedSymbolId> {
|
||||
self.table.symbol_id_by_name(name)
|
||||
}
|
||||
|
||||
pub(super) fn symbol(&self, symbol_id: impl Into<ScopedSymbolId>) -> &Symbol {
|
||||
self.table.symbol(symbol_id)
|
||||
}
|
||||
|
||||
pub(super) fn finish(mut self) -> SymbolTable {
|
||||
self.table.shrink_to_fit();
|
||||
self.table
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
//! don't actually store these "list of visible definitions" as a vector of [`Definition`].
|
||||
//! Instead, [`SymbolBindings`] and [`SymbolDeclarations`] are structs which use bit-sets to track
|
||||
//! definitions (and constraints, in the case of bindings) in terms of [`ScopedDefinitionId`] and
|
||||
//! [`ScopedConstraintId`], which are indices into the `all_definitions` and `all_constraints`
|
||||
//! [`ScopedConstraintId`], which are indices into the `all_definitions` and `constraints`
|
||||
//! indexvecs in the [`UseDefMap`].
|
||||
//!
|
||||
//! There is another special kind of possible "definition" for a symbol: there might be a path from
|
||||
@@ -255,28 +255,27 @@
|
||||
//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in
|
||||
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it
|
||||
//! visits a `StmtIf` node.
|
||||
pub(crate) use self::symbol_state::ScopedConstraintId;
|
||||
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use self::symbol_state::{
|
||||
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
|
||||
ConstraintIndexIterator, LiveBindingsIterator, LiveDeclaration, LiveDeclarationsIterator,
|
||||
ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
|
||||
};
|
||||
use crate::semantic_index::ast_ids::ScopedUseId;
|
||||
use crate::semantic_index::constraint::{
|
||||
Constraint, Constraints, ConstraintsBuilder, ScopedConstraintId,
|
||||
};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
use crate::semantic_index::use_def::symbol_state::DeclarationIdWithConstraint;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId};
|
||||
use crate::visibility_constraints::{
|
||||
ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder,
|
||||
};
|
||||
use ruff_index::IndexVec;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use super::constraint::Constraint;
|
||||
|
||||
mod bitset;
|
||||
mod symbol_state;
|
||||
|
||||
type AllConstraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>;
|
||||
|
||||
/// Applicable definitions and constraints for every use of a name.
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) struct UseDefMap<'db> {
|
||||
@@ -285,19 +284,22 @@ pub(crate) struct UseDefMap<'db> {
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
|
||||
/// Array of [`Constraint`] in this scope.
|
||||
all_constraints: AllConstraints<'db>,
|
||||
constraints: Constraints<'db>,
|
||||
|
||||
/// Array of visibility constraints in this scope.
|
||||
visibility_constraints: VisibilityConstraints<'db>,
|
||||
visibility_constraints: VisibilityConstraints,
|
||||
|
||||
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
|
||||
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
|
||||
|
||||
/// [`SymbolBindings`] or [`SymbolDeclarations`] reaching a given [`Definition`].
|
||||
///
|
||||
/// If the definition is a binding (only) -- `x = 1` for example -- then we need
|
||||
/// [`SymbolDeclarations`] to know whether this binding is permitted by the live declarations.
|
||||
///
|
||||
/// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then
|
||||
/// we don't actually need anything here, all we'll need to validate is that our own RHS is a
|
||||
/// valid assignment to our own annotation.
|
||||
declarations_by_binding: FxHashMap<Definition<'db>, SymbolDeclarations>,
|
||||
|
||||
/// If the definition is a declaration (only) -- `x: int` for example -- then we need
|
||||
/// [`SymbolBindings`] to know whether this declaration is consistent with the previously
|
||||
/// inferred type.
|
||||
@@ -305,10 +307,14 @@ pub(crate) struct UseDefMap<'db> {
|
||||
/// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then
|
||||
/// we don't actually need anything here, all we'll need to validate is that our own RHS is a
|
||||
/// valid assignment to our own annotation.
|
||||
definitions_by_definition: FxHashMap<Definition<'db>, SymbolDefinitions>,
|
||||
bindings_by_declaration: FxHashMap<Definition<'db>, SymbolBindings>,
|
||||
|
||||
/// [`SymbolState`] visible at end of scope for each symbol.
|
||||
public_symbols: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
|
||||
/// Snapshot of bindings in this scope that can be used to resolve a reference in a nested
|
||||
/// eager scope.
|
||||
eager_bindings: EagerBindings,
|
||||
}
|
||||
|
||||
impl<'db> UseDefMap<'db> {
|
||||
@@ -326,29 +332,27 @@ impl<'db> UseDefMap<'db> {
|
||||
self.bindings_iterator(self.public_symbols[symbol].bindings())
|
||||
}
|
||||
|
||||
pub(crate) fn eager_bindings(
|
||||
&self,
|
||||
eager_bindings: ScopedEagerBindingsId,
|
||||
) -> Option<BindingWithConstraintsIterator<'_, 'db>> {
|
||||
self.eager_bindings
|
||||
.get(eager_bindings)
|
||||
.map(|symbol_bindings| self.bindings_iterator(symbol_bindings))
|
||||
}
|
||||
|
||||
pub(crate) fn bindings_at_declaration(
|
||||
&self,
|
||||
declaration: Definition<'db>,
|
||||
) -> BindingWithConstraintsIterator<'_, 'db> {
|
||||
if let SymbolDefinitions::Bindings(bindings) = &self.definitions_by_definition[&declaration]
|
||||
{
|
||||
self.bindings_iterator(bindings)
|
||||
} else {
|
||||
unreachable!("Declaration has non-Bindings in definitions_by_definition");
|
||||
}
|
||||
self.bindings_iterator(&self.bindings_by_declaration[&declaration])
|
||||
}
|
||||
|
||||
pub(crate) fn declarations_at_binding<'map>(
|
||||
&'map self,
|
||||
pub(crate) fn declarations_at_binding(
|
||||
&self,
|
||||
binding: Definition<'db>,
|
||||
) -> DeclarationsIterator<'map, 'db> {
|
||||
if let SymbolDefinitions::Declarations(declarations) =
|
||||
&self.definitions_by_definition[&binding]
|
||||
{
|
||||
self.declarations_iterator(declarations)
|
||||
} else {
|
||||
unreachable!("Binding has non-Declarations in definitions_by_definition");
|
||||
}
|
||||
) -> DeclarationsIterator<'_, 'db> {
|
||||
self.declarations_iterator(&self.declarations_by_binding[&binding])
|
||||
}
|
||||
|
||||
pub(crate) fn public_declarations<'map>(
|
||||
@@ -365,7 +369,7 @@ impl<'db> UseDefMap<'db> {
|
||||
) -> BindingWithConstraintsIterator<'map, 'db> {
|
||||
BindingWithConstraintsIterator {
|
||||
all_definitions: &self.all_definitions,
|
||||
all_constraints: &self.all_constraints,
|
||||
constraints: &self.constraints,
|
||||
visibility_constraints: &self.visibility_constraints,
|
||||
inner: bindings.iter(),
|
||||
}
|
||||
@@ -377,42 +381,60 @@ impl<'db> UseDefMap<'db> {
|
||||
) -> DeclarationsIterator<'map, 'db> {
|
||||
DeclarationsIterator {
|
||||
all_definitions: &self.all_definitions,
|
||||
constraints: &self.constraints,
|
||||
visibility_constraints: &self.visibility_constraints,
|
||||
inner: declarations.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Either live bindings or live declarations for a symbol.
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update)]
|
||||
enum SymbolDefinitions {
|
||||
Bindings(SymbolBindings),
|
||||
Declarations(SymbolDeclarations),
|
||||
/// Uniquely identifies a snapshot of bindings that can be used to resolve a reference in a nested
|
||||
/// eager scope.
|
||||
///
|
||||
/// An eager scope has its entire body executed immediately at the location where it is defined.
|
||||
/// For any free references in the nested scope, we use the bindings that are visible at the point
|
||||
/// where the nested scope is defined, instead of using the public type of the symbol.
|
||||
///
|
||||
/// There is a unique ID for each distinct [`EagerBindingsKey`] in the file.
|
||||
#[newtype_index]
|
||||
pub(crate) struct ScopedEagerBindingsId;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub(crate) struct EagerBindingsKey {
|
||||
/// The enclosing scope containing the bindings
|
||||
pub(crate) enclosing_scope: FileScopeId,
|
||||
/// The referenced symbol (in the enclosing scope)
|
||||
pub(crate) enclosing_symbol: ScopedSymbolId,
|
||||
/// The nested eager scope containing the reference
|
||||
pub(crate) nested_scope: FileScopeId,
|
||||
}
|
||||
|
||||
/// A snapshot of bindings that can be used to resolve a reference in a nested eager scope.
|
||||
type EagerBindings = IndexVec<ScopedEagerBindingsId, SymbolBindings>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
all_constraints: &'map AllConstraints<'db>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
|
||||
inner: BindingIdWithConstraintsIterator<'map>,
|
||||
pub(crate) constraints: &'map Constraints<'db>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints,
|
||||
inner: LiveBindingsIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
|
||||
type Item = BindingWithConstraints<'map, 'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let all_constraints = self.all_constraints;
|
||||
let constraints = self.constraints;
|
||||
|
||||
self.inner
|
||||
.next()
|
||||
.map(|binding_id_with_constraints| BindingWithConstraints {
|
||||
binding: self.all_definitions[binding_id_with_constraints.definition],
|
||||
constraints: ConstraintsIterator {
|
||||
all_constraints,
|
||||
constraint_ids: binding_id_with_constraints.constraint_ids,
|
||||
.map(|live_binding| BindingWithConstraints {
|
||||
binding: self.all_definitions[live_binding.binding],
|
||||
narrowing_constraints: ConstraintsIterator {
|
||||
constraints,
|
||||
constraint_ids: live_binding.narrowing_constraints.iter(),
|
||||
},
|
||||
visibility_constraint: binding_id_with_constraints.visibility_constraint,
|
||||
visibility_constraint: live_binding.visibility_constraint,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -421,13 +443,13 @@ impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
|
||||
|
||||
pub(crate) struct BindingWithConstraints<'map, 'db> {
|
||||
pub(crate) binding: Option<Definition<'db>>,
|
||||
pub(crate) constraints: ConstraintsIterator<'map, 'db>,
|
||||
pub(crate) narrowing_constraints: ConstraintsIterator<'map, 'db>,
|
||||
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
pub(crate) struct ConstraintsIterator<'map, 'db> {
|
||||
all_constraints: &'map AllConstraints<'db>,
|
||||
constraint_ids: ConstraintIdIterator<'map>,
|
||||
constraints: &'map Constraints<'db>,
|
||||
constraint_ids: ConstraintIndexIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'db> Iterator for ConstraintsIterator<'_, 'db> {
|
||||
@@ -436,7 +458,7 @@ impl<'db> Iterator for ConstraintsIterator<'_, 'db> {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.constraint_ids
|
||||
.next()
|
||||
.map(|constraint_id| self.all_constraints[constraint_id])
|
||||
.map(|constraint_id| self.constraints[ScopedConstraintId::from_u32(constraint_id)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,8 +466,9 @@ impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
|
||||
|
||||
pub(crate) struct DeclarationsIterator<'map, 'db> {
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
|
||||
inner: DeclarationIdIterator<'map>,
|
||||
pub(crate) constraints: &'map Constraints<'db>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints,
|
||||
inner: LiveDeclarationsIterator<'map>,
|
||||
}
|
||||
|
||||
pub(crate) struct DeclarationWithConstraint<'db> {
|
||||
@@ -458,13 +481,13 @@ impl<'db> Iterator for DeclarationsIterator<'_, 'db> {
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(
|
||||
|DeclarationIdWithConstraint {
|
||||
definition,
|
||||
|LiveDeclaration {
|
||||
declaration,
|
||||
visibility_constraint,
|
||||
}| {
|
||||
DeclarationWithConstraint {
|
||||
declaration: self.all_definitions[definition],
|
||||
visibility_constraint,
|
||||
declaration: self.all_definitions[*declaration],
|
||||
visibility_constraint: *visibility_constraint,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -485,11 +508,11 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
/// Append-only array of [`Definition`].
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
|
||||
/// Append-only array of [`Constraint`].
|
||||
all_constraints: AllConstraints<'db>,
|
||||
/// Builder of constraints.
|
||||
constraints: ConstraintsBuilder<'db>,
|
||||
|
||||
/// Builder of visibility constraints.
|
||||
pub(super) visibility_constraints: VisibilityConstraintsBuilder<'db>,
|
||||
pub(super) visibility_constraints: VisibilityConstraintsBuilder,
|
||||
|
||||
/// A constraint which describes the visibility of the unbound/undeclared state, i.e.
|
||||
/// whether or not the start of the scope is visible. This is important for cases like
|
||||
@@ -500,23 +523,32 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
/// Live bindings at each so-far-recorded use.
|
||||
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
|
||||
|
||||
/// Live bindings or declarations for each so-far-recorded definition.
|
||||
definitions_by_definition: FxHashMap<Definition<'db>, SymbolDefinitions>,
|
||||
/// Live declarations for each so-far-recorded binding.
|
||||
declarations_by_binding: FxHashMap<Definition<'db>, SymbolDeclarations>,
|
||||
|
||||
/// Live bindings for each so-far-recorded declaration.
|
||||
bindings_by_declaration: FxHashMap<Definition<'db>, SymbolBindings>,
|
||||
|
||||
/// Currently live bindings and declarations for each symbol.
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
|
||||
/// Snapshot of bindings in this scope that can be used to resolve a reference in a nested
|
||||
/// eager scope.
|
||||
eager_bindings: EagerBindings,
|
||||
}
|
||||
|
||||
impl Default for UseDefMapBuilder<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
all_definitions: IndexVec::from_iter([None]),
|
||||
all_constraints: IndexVec::new(),
|
||||
constraints: ConstraintsBuilder::default(),
|
||||
visibility_constraints: VisibilityConstraintsBuilder::default(),
|
||||
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
bindings_by_use: IndexVec::new(),
|
||||
definitions_by_definition: FxHashMap::default(),
|
||||
declarations_by_binding: FxHashMap::default(),
|
||||
bindings_by_declaration: FxHashMap::default(),
|
||||
symbol_states: IndexVec::new(),
|
||||
eager_bindings: EagerBindings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,15 +568,13 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn record_binding(&mut self, symbol: ScopedSymbolId, binding: Definition<'db>) {
|
||||
let def_id = self.all_definitions.push(Some(binding));
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
self.definitions_by_definition.insert(
|
||||
binding,
|
||||
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
|
||||
);
|
||||
self.declarations_by_binding
|
||||
.insert(binding, symbol_state.declarations().clone());
|
||||
symbol_state.record_binding(def_id, self.scope_start_visibility);
|
||||
}
|
||||
|
||||
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
self.all_constraints.push(constraint)
|
||||
self.constraints.add_constraint(constraint)
|
||||
}
|
||||
|
||||
pub(super) fn record_constraint_id(&mut self, constraint: ScopedConstraintId) {
|
||||
@@ -616,10 +646,8 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
) {
|
||||
let def_id = self.all_definitions.push(Some(declaration));
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
self.definitions_by_definition.insert(
|
||||
declaration,
|
||||
SymbolDefinitions::Bindings(symbol_state.bindings().clone()),
|
||||
);
|
||||
self.bindings_by_declaration
|
||||
.insert(declaration, symbol_state.bindings().clone());
|
||||
symbol_state.record_declaration(def_id);
|
||||
}
|
||||
|
||||
@@ -628,7 +656,8 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
symbol: ScopedSymbolId,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
// We don't need to store anything in self.definitions_by_definition.
|
||||
// We don't need to store anything in self.bindings_by_declaration or
|
||||
// self.declarations_by_binding.
|
||||
let def_id = self.all_definitions.push(Some(definition));
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
symbol_state.record_declaration(def_id);
|
||||
@@ -644,6 +673,14 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
debug_assert_eq!(use_id, new_use);
|
||||
}
|
||||
|
||||
pub(super) fn snapshot_eager_bindings(
|
||||
&mut self,
|
||||
enclosing_symbol: ScopedSymbolId,
|
||||
) -> ScopedEagerBindingsId {
|
||||
self.eager_bindings
|
||||
.push(self.symbol_states[enclosing_symbol].bindings().clone())
|
||||
}
|
||||
|
||||
/// Take a snapshot of the current visible-symbols state.
|
||||
pub(super) fn snapshot(&self) -> FlowSnapshot {
|
||||
FlowSnapshot {
|
||||
@@ -717,18 +754,21 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
|
||||
pub(super) fn finish(mut self) -> UseDefMap<'db> {
|
||||
self.all_definitions.shrink_to_fit();
|
||||
self.all_constraints.shrink_to_fit();
|
||||
self.symbol_states.shrink_to_fit();
|
||||
self.bindings_by_use.shrink_to_fit();
|
||||
self.definitions_by_definition.shrink_to_fit();
|
||||
self.declarations_by_binding.shrink_to_fit();
|
||||
self.bindings_by_declaration.shrink_to_fit();
|
||||
self.eager_bindings.shrink_to_fit();
|
||||
|
||||
UseDefMap {
|
||||
all_definitions: self.all_definitions,
|
||||
all_constraints: self.all_constraints,
|
||||
constraints: self.constraints.build(),
|
||||
visibility_constraints: self.visibility_constraints.build(),
|
||||
bindings_by_use: self.bindings_by_use,
|
||||
public_symbols: self.symbol_states,
|
||||
definitions_by_definition: self.definitions_by_definition,
|
||||
declarations_by_binding: self.declarations_by_binding,
|
||||
bindings_by_declaration: self.bindings_by_declaration,
|
||||
eager_bindings: self.eager_bindings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,6 @@ impl<const B: usize> Default for BitSet<B> {
|
||||
}
|
||||
|
||||
impl<const B: usize> BitSet<B> {
|
||||
/// Create and return a new [`BitSet`] with a single `value` inserted.
|
||||
pub(super) fn with(value: u32) -> Self {
|
||||
let mut bitset = Self::default();
|
||||
bitset.insert(value);
|
||||
bitset
|
||||
}
|
||||
|
||||
/// Convert from Inline to Heap, if needed, and resize the Heap vector, if needed.
|
||||
fn resize(&mut self, value: u32) {
|
||||
let num_blocks_needed = (value / 64) + 1;
|
||||
@@ -93,19 +86,6 @@ impl<const B: usize> BitSet<B> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Union in-place with another [`BitSet`].
|
||||
pub(super) fn union(&mut self, other: &BitSet<B>) {
|
||||
let mut max_len = self.blocks().len();
|
||||
let other_len = other.blocks().len();
|
||||
if other_len > max_len {
|
||||
max_len = other_len;
|
||||
self.resize_blocks(max_len);
|
||||
}
|
||||
for (my_block, other_block) in self.blocks_mut().iter_mut().zip(other.blocks()) {
|
||||
*my_block |= other_block;
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an iterator over the values (in ascending order) in this [`BitSet`].
|
||||
pub(super) fn iter(&self) -> BitSetIterator<'_, B> {
|
||||
let blocks = self.blocks();
|
||||
@@ -158,6 +138,15 @@ impl<const B: usize> std::iter::FusedIterator for BitSetIterator<'_, B> {}
|
||||
mod tests {
|
||||
use super::BitSet;
|
||||
|
||||
impl<const B: usize> BitSet<B> {
|
||||
/// Create and return a new [`BitSet`] with a single `value` inserted.
|
||||
pub(super) fn with(value: u32) -> Self {
|
||||
let mut bitset = Self::default();
|
||||
bitset.insert(value);
|
||||
bitset
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_bitset<const B: usize>(bitset: &BitSet<B>, contents: &[u32]) {
|
||||
assert_eq!(bitset.iter().collect::<Vec<_>>(), contents);
|
||||
}
|
||||
@@ -235,59 +224,6 @@ mod tests {
|
||||
assert_bitset(&b1, &[89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union() {
|
||||
let mut b1 = BitSet::<1>::with(2);
|
||||
let b2 = BitSet::<1>::with(4);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[2, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_mixed_1() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(5);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 5, 89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_mixed_2() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(23);
|
||||
b2.insert(89);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 23, 89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_heap() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(90);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 89, 90]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_heap_2() {
|
||||
let mut b1 = BitSet::<1>::with(89);
|
||||
let mut b2 = BitSet::<1>::with(89);
|
||||
b1.insert(91);
|
||||
b2.insert(90);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[89, 90, 91]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_blocks() {
|
||||
let mut b = BitSet::<2>::with(120);
|
||||
|
||||
@@ -46,14 +46,16 @@
|
||||
|
||||
use itertools::{EitherOrBoth, Itertools};
|
||||
use ruff_index::newtype_index;
|
||||
use smallvec::SmallVec;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
use crate::semantic_index::constraint::ScopedConstraintId;
|
||||
use crate::semantic_index::use_def::bitset::{BitSet, BitSetIterator};
|
||||
use crate::semantic_index::use_def::VisibilityConstraintsBuilder;
|
||||
use crate::visibility_constraints::ScopedVisibilityConstraintId;
|
||||
|
||||
/// A newtype-index for a definition in a particular scope.
|
||||
#[newtype_index]
|
||||
#[derive(Ord, PartialOrd)]
|
||||
pub(super) struct ScopedDefinitionId;
|
||||
|
||||
impl ScopedDefinitionId {
|
||||
@@ -65,89 +67,54 @@ impl ScopedDefinitionId {
|
||||
pub(super) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0);
|
||||
}
|
||||
|
||||
/// A newtype-index for a constraint expression in a particular scope.
|
||||
#[newtype_index]
|
||||
pub(crate) struct ScopedConstraintId;
|
||||
|
||||
/// Can reference this * 64 total definitions inline; more will fall back to the heap.
|
||||
const INLINE_BINDING_BLOCKS: usize = 3;
|
||||
|
||||
/// A [`BitSet`] of [`ScopedDefinitionId`], representing live bindings of a symbol in a scope.
|
||||
type Bindings = BitSet<INLINE_BINDING_BLOCKS>;
|
||||
type BindingsIterator<'a> = BitSetIterator<'a, INLINE_BINDING_BLOCKS>;
|
||||
|
||||
/// Can reference this * 64 total declarations inline; more will fall back to the heap.
|
||||
const INLINE_DECLARATION_BLOCKS: usize = 3;
|
||||
|
||||
/// A [`BitSet`] of [`ScopedDefinitionId`], representing live declarations of a symbol in a scope.
|
||||
type Declarations = BitSet<INLINE_DECLARATION_BLOCKS>;
|
||||
type DeclarationsIterator<'a> = BitSetIterator<'a, INLINE_DECLARATION_BLOCKS>;
|
||||
|
||||
/// Can reference this * 64 total constraints inline; more will fall back to the heap.
|
||||
const INLINE_CONSTRAINT_BLOCKS: usize = 2;
|
||||
|
||||
/// Can keep inline this many live bindings per symbol at a given time; more will go to heap.
|
||||
const INLINE_BINDINGS_PER_SYMBOL: usize = 4;
|
||||
/// Can keep inline this many live bindings or declarations per symbol at a given time; more will
|
||||
/// go to heap.
|
||||
const INLINE_DEFINITIONS_PER_SYMBOL: usize = 4;
|
||||
|
||||
/// Which constraints apply to a given binding?
|
||||
type Constraints = BitSet<INLINE_CONSTRAINT_BLOCKS>;
|
||||
|
||||
type InlineConstraintArray = [Constraints; INLINE_BINDINGS_PER_SYMBOL];
|
||||
|
||||
/// One [`BitSet`] of applicable [`ScopedConstraintId`]s per live binding.
|
||||
type ConstraintsPerBinding = SmallVec<InlineConstraintArray>;
|
||||
|
||||
/// Iterate over all constraints for a single binding.
|
||||
type ConstraintsIterator<'a> = std::slice::Iter<'a, Constraints>;
|
||||
|
||||
const INLINE_VISIBILITY_CONSTRAINTS: usize = 4;
|
||||
type InlineVisibilityConstraintsArray =
|
||||
[ScopedVisibilityConstraintId; INLINE_VISIBILITY_CONSTRAINTS];
|
||||
|
||||
/// One [`ScopedVisibilityConstraintId`] per live declaration.
|
||||
type VisibilityConstraintPerDeclaration = SmallVec<InlineVisibilityConstraintsArray>;
|
||||
|
||||
/// One [`ScopedVisibilityConstraintId`] per live binding.
|
||||
type VisibilityConstraintPerBinding = SmallVec<InlineVisibilityConstraintsArray>;
|
||||
|
||||
/// Iterator over the visibility constraints for all live bindings/declarations.
|
||||
type VisibilityConstraintsIterator<'a> = std::slice::Iter<'a, ScopedVisibilityConstraintId>;
|
||||
pub(super) type ConstraintIndexIterator<'a> = BitSetIterator<'a, INLINE_CONSTRAINT_BLOCKS>;
|
||||
|
||||
/// Live declarations for a single symbol at some point in control flow, with their
|
||||
/// corresponding visibility constraints.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) struct SymbolDeclarations {
|
||||
/// [`BitSet`]: which declarations (as [`ScopedDefinitionId`]) can reach the current location?
|
||||
///
|
||||
/// Invariant: Because this is a `BitSet`, it can be viewed as a _sorted_ set of definition
|
||||
/// IDs. The `visibility_constraints` field stores constraints for each definition. Therefore
|
||||
/// those fields must always have the same `len()` as `live_declarations`, and the elements
|
||||
/// must appear in the same order. Effectively, this means that elements must always be added
|
||||
/// in sorted order, or via a binary search that determines the correct place to insert new
|
||||
/// constraints.
|
||||
pub(crate) live_declarations: Declarations,
|
||||
|
||||
/// For each live declaration, which visibility constraint applies to it?
|
||||
pub(crate) visibility_constraints: VisibilityConstraintPerDeclaration,
|
||||
/// A list of live declarations for this symbol, sorted by their `ScopedDefinitionId`
|
||||
live_declarations: SmallVec<[LiveDeclaration; INLINE_DEFINITIONS_PER_SYMBOL]>,
|
||||
}
|
||||
|
||||
/// One of the live declarations for a single symbol at some point in control flow.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct LiveDeclaration {
|
||||
pub(super) declaration: ScopedDefinitionId,
|
||||
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
pub(super) type LiveDeclarationsIterator<'a> = std::slice::Iter<'a, LiveDeclaration>;
|
||||
|
||||
impl SymbolDeclarations {
|
||||
fn undeclared(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
|
||||
let initial_declaration = LiveDeclaration {
|
||||
declaration: ScopedDefinitionId::UNBOUND,
|
||||
visibility_constraint: scope_start_visibility,
|
||||
};
|
||||
Self {
|
||||
live_declarations: Declarations::with(0),
|
||||
visibility_constraints: VisibilityConstraintPerDeclaration::from_iter([
|
||||
scope_start_visibility,
|
||||
]),
|
||||
live_declarations: smallvec![initial_declaration],
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a newly-encountered declaration for this symbol.
|
||||
fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
|
||||
self.live_declarations = Declarations::with(declaration_id.into());
|
||||
|
||||
self.visibility_constraints = VisibilityConstraintPerDeclaration::with_capacity(1);
|
||||
self.visibility_constraints
|
||||
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
fn record_declaration(&mut self, declaration: ScopedDefinitionId) {
|
||||
// The new declaration replaces all previous live declaration in this path.
|
||||
self.live_declarations.clear();
|
||||
self.live_declarations.push(LiveDeclaration {
|
||||
declaration,
|
||||
visibility_constraint: ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
});
|
||||
}
|
||||
|
||||
/// Add given visibility constraint to all live declarations.
|
||||
@@ -156,45 +123,62 @@ impl SymbolDeclarations {
|
||||
visibility_constraints: &mut VisibilityConstraintsBuilder,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
for existing in &mut self.visibility_constraints {
|
||||
*existing = visibility_constraints.add_and_constraint(*existing, constraint);
|
||||
for declaration in &mut self.live_declarations {
|
||||
declaration.visibility_constraint = visibility_constraints
|
||||
.add_and_constraint(declaration.visibility_constraint, constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an iterator over live declarations for this symbol.
|
||||
pub(super) fn iter(&self) -> DeclarationIdIterator {
|
||||
DeclarationIdIterator {
|
||||
declarations: self.live_declarations.iter(),
|
||||
visibility_constraints: self.visibility_constraints.iter(),
|
||||
pub(super) fn iter(&self) -> LiveDeclarationsIterator<'_> {
|
||||
self.live_declarations.iter()
|
||||
}
|
||||
|
||||
/// Iterate over the IDs of each currently live declaration for this symbol
|
||||
fn iter_declarations(&self) -> impl Iterator<Item = ScopedDefinitionId> + '_ {
|
||||
self.iter().map(|lb| lb.declaration)
|
||||
}
|
||||
|
||||
fn simplify_visibility_constraints(&mut self, other: SymbolDeclarations) {
|
||||
// If the set of live declarations hasn't changed, don't simplify.
|
||||
if self.live_declarations.len() != other.live_declarations.len()
|
||||
|| !self.iter_declarations().eq(other.iter_declarations())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (declaration, other_declaration) in self
|
||||
.live_declarations
|
||||
.iter_mut()
|
||||
.zip(other.live_declarations)
|
||||
{
|
||||
declaration.visibility_constraint = other_declaration.visibility_constraint;
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
|
||||
let a = std::mem::take(self);
|
||||
self.live_declarations = a.live_declarations.clone();
|
||||
self.live_declarations.union(&b.live_declarations);
|
||||
|
||||
// Invariant: These zips are well-formed since we maintain an invariant that all of our
|
||||
// fields are sets/vecs with the same length.
|
||||
let a = (a.live_declarations.iter()).zip(a.visibility_constraints);
|
||||
let b = (b.live_declarations.iter()).zip(b.visibility_constraints);
|
||||
|
||||
// Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
|
||||
// the definition IDs and constraints line up correctly in the merged result. If a
|
||||
// definition is found in both `a` and `b`, we compose the constraints from the two paths
|
||||
// in an appropriate way (intersection for narrowing constraints; ternary OR for visibility
|
||||
// constraints). If a definition is found in only one path, it is used as-is.
|
||||
for zipped in a.merge_join_by(b, |(a_decl, _), (b_decl, _)| a_decl.cmp(b_decl)) {
|
||||
// the merged `live_declarations` vec remains sorted. If a definition is found in both `a`
|
||||
// and `b`, we compose the constraints from the two paths in an appropriate way
|
||||
// (intersection for narrowing constraints; ternary OR for visibility constraints). If a
|
||||
// definition is found in only one path, it is used as-is.
|
||||
let a = a.live_declarations.into_iter();
|
||||
let b = b.live_declarations.into_iter();
|
||||
for zipped in a.merge_join_by(b, |a, b| a.declaration.cmp(&b.declaration)) {
|
||||
match zipped {
|
||||
EitherOrBoth::Both((_, a_vis_constraint), (_, b_vis_constraint)) => {
|
||||
let vis_constraint = visibility_constraints
|
||||
.add_or_constraint(a_vis_constraint, b_vis_constraint);
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
EitherOrBoth::Both(a, b) => {
|
||||
let visibility_constraint = visibility_constraints
|
||||
.add_or_constraint(a.visibility_constraint, b.visibility_constraint);
|
||||
self.live_declarations.push(LiveDeclaration {
|
||||
declaration: a.declaration,
|
||||
visibility_constraint,
|
||||
});
|
||||
}
|
||||
|
||||
EitherOrBoth::Left((_, vis_constraint))
|
||||
| EitherOrBoth::Right((_, vis_constraint)) => {
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
EitherOrBoth::Left(declaration) | EitherOrBoth::Right(declaration) => {
|
||||
self.live_declarations.push(declaration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,59 +187,54 @@ impl SymbolDeclarations {
|
||||
|
||||
/// Live bindings for a single symbol at some point in control flow. Each live binding comes
|
||||
/// with a set of narrowing constraints and a visibility constraint.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) struct SymbolBindings {
|
||||
/// [`BitSet`]: which bindings (as [`ScopedDefinitionId`]) can reach the current location?
|
||||
///
|
||||
/// Invariant: Because this is a `BitSet`, it can be viewed as a _sorted_ set of definition
|
||||
/// IDs. The `constraints` and `visibility_constraints` field stores constraints for each
|
||||
/// definition. Therefore those fields must always have the same `len()` as
|
||||
/// `live_bindings`, and the elements must appear in the same order. Effectively, this means
|
||||
/// that elements must always be added in sorted order, or via a binary search that determines
|
||||
/// the correct place to insert new constraints.
|
||||
live_bindings: Bindings,
|
||||
|
||||
/// For each live binding, which [`ScopedConstraintId`] apply?
|
||||
///
|
||||
/// This is a [`smallvec::SmallVec`] which should always have one [`BitSet`] of constraints per
|
||||
/// binding in `live_bindings`.
|
||||
constraints: ConstraintsPerBinding,
|
||||
|
||||
/// For each live binding, which visibility constraint applies to it?
|
||||
visibility_constraints: VisibilityConstraintPerBinding,
|
||||
/// A list of live bindings for this symbol, sorted by their `ScopedDefinitionId`
|
||||
live_bindings: SmallVec<[LiveBinding; INLINE_DEFINITIONS_PER_SYMBOL]>,
|
||||
}
|
||||
|
||||
/// One of the live bindings for a single symbol at some point in control flow.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct LiveBinding {
|
||||
pub(super) binding: ScopedDefinitionId,
|
||||
pub(super) narrowing_constraints: Constraints,
|
||||
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
pub(super) type LiveBindingsIterator<'a> = std::slice::Iter<'a, LiveBinding>;
|
||||
|
||||
impl SymbolBindings {
|
||||
fn unbound(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
|
||||
let initial_binding = LiveBinding {
|
||||
binding: ScopedDefinitionId::UNBOUND,
|
||||
narrowing_constraints: Constraints::default(),
|
||||
visibility_constraint: scope_start_visibility,
|
||||
};
|
||||
Self {
|
||||
live_bindings: Bindings::with(ScopedDefinitionId::UNBOUND.as_u32()),
|
||||
constraints: ConstraintsPerBinding::from_iter([Constraints::default()]),
|
||||
visibility_constraints: VisibilityConstraintPerBinding::from_iter([
|
||||
scope_start_visibility,
|
||||
]),
|
||||
live_bindings: smallvec![initial_binding],
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(
|
||||
&mut self,
|
||||
binding_id: ScopedDefinitionId,
|
||||
binding: ScopedDefinitionId,
|
||||
visibility_constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
// The new binding replaces all previous live bindings in this path, and has no
|
||||
// constraints.
|
||||
self.live_bindings = Bindings::with(binding_id.into());
|
||||
self.constraints = ConstraintsPerBinding::with_capacity(1);
|
||||
self.constraints.push(Constraints::default());
|
||||
|
||||
self.visibility_constraints = VisibilityConstraintPerBinding::with_capacity(1);
|
||||
self.visibility_constraints.push(visibility_constraint);
|
||||
self.live_bindings.clear();
|
||||
self.live_bindings.push(LiveBinding {
|
||||
binding,
|
||||
narrowing_constraints: Constraints::default(),
|
||||
visibility_constraint,
|
||||
});
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
pub(super) fn record_constraint(&mut self, constraint_id: ScopedConstraintId) {
|
||||
for bitset in &mut self.constraints {
|
||||
bitset.insert(constraint_id.into());
|
||||
for binding in &mut self.live_bindings {
|
||||
binding.narrowing_constraints.insert(constraint_id.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,71 +244,67 @@ impl SymbolBindings {
|
||||
visibility_constraints: &mut VisibilityConstraintsBuilder,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
for existing in &mut self.visibility_constraints {
|
||||
*existing = visibility_constraints.add_and_constraint(*existing, constraint);
|
||||
for binding in &mut self.live_bindings {
|
||||
binding.visibility_constraint = visibility_constraints
|
||||
.add_and_constraint(binding.visibility_constraint, constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over currently live bindings for this symbol
|
||||
pub(super) fn iter(&self) -> BindingIdWithConstraintsIterator {
|
||||
BindingIdWithConstraintsIterator {
|
||||
definitions: self.live_bindings.iter(),
|
||||
constraints: self.constraints.iter(),
|
||||
visibility_constraints: self.visibility_constraints.iter(),
|
||||
pub(super) fn iter(&self) -> LiveBindingsIterator<'_> {
|
||||
self.live_bindings.iter()
|
||||
}
|
||||
|
||||
/// Iterate over the IDs of each currently live binding for this symbol
|
||||
fn iter_bindings(&self) -> impl Iterator<Item = ScopedDefinitionId> + '_ {
|
||||
self.iter().map(|lb| lb.binding)
|
||||
}
|
||||
|
||||
fn simplify_visibility_constraints(&mut self, other: SymbolBindings) {
|
||||
// If the set of live bindings hasn't changed, don't simplify.
|
||||
if self.live_bindings.len() != other.live_bindings.len()
|
||||
|| !self.iter_bindings().eq(other.iter_bindings())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (binding, other_binding) in self.live_bindings.iter_mut().zip(other.live_bindings) {
|
||||
binding.visibility_constraint = other_binding.visibility_constraint;
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, mut b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
|
||||
let mut a = std::mem::take(self);
|
||||
self.live_bindings = a.live_bindings.clone();
|
||||
self.live_bindings.union(&b.live_bindings);
|
||||
|
||||
// Invariant: These zips are well-formed since we maintain an invariant that all of our
|
||||
// fields are sets/vecs with the same length.
|
||||
//
|
||||
// Performance: We iterate over the `constraints` smallvecs via mut reference, because the
|
||||
// individual elements are `BitSet`s (currently 24 bytes in size), and we don't want to
|
||||
// move them by value multiple times during iteration. By iterating by reference, we only
|
||||
// have to copy single pointers around. In the loop below, the `std::mem::take` calls
|
||||
// specify precisely where we want to move them into the merged `constraints` smallvec.
|
||||
//
|
||||
// We don't need a similar optimization for `visibility_constraints`, since those elements
|
||||
// are 32-bit IndexVec IDs, and so are already cheap to move/copy.
|
||||
let a = (a.live_bindings.iter())
|
||||
.zip(a.constraints.iter_mut())
|
||||
.zip(a.visibility_constraints);
|
||||
let b = (b.live_bindings.iter())
|
||||
.zip(b.constraints.iter_mut())
|
||||
.zip(b.visibility_constraints);
|
||||
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
|
||||
let a = std::mem::take(self);
|
||||
|
||||
// Invariant: merge_join_by consumes the two iterators in sorted order, which ensures that
|
||||
// the definition IDs and constraints line up correctly in the merged result. If a
|
||||
// definition is found in both `a` and `b`, we compose the constraints from the two paths
|
||||
// in an appropriate way (intersection for narrowing constraints; ternary OR for visibility
|
||||
// constraints). If a definition is found in only one path, it is used as-is.
|
||||
for zipped in a.merge_join_by(b, |((a_def, _), _), ((b_def, _), _)| a_def.cmp(b_def)) {
|
||||
// the merged `live_bindings` vec remains sorted. If a definition is found in both `a` and
|
||||
// `b`, we compose the constraints from the two paths in an appropriate way (intersection
|
||||
// for narrowing constraints; ternary OR for visibility constraints). If a definition is
|
||||
// found in only one path, it is used as-is.
|
||||
let a = a.live_bindings.into_iter();
|
||||
let b = b.live_bindings.into_iter();
|
||||
for zipped in a.merge_join_by(b, |a, b| a.binding.cmp(&b.binding)) {
|
||||
match zipped {
|
||||
EitherOrBoth::Both(
|
||||
((_, a_constraints), a_vis_constraint),
|
||||
((_, b_constraints), b_vis_constraint),
|
||||
) => {
|
||||
EitherOrBoth::Both(a, b) => {
|
||||
// If the same definition is visible through both paths, any constraint
|
||||
// that applies on only one path is irrelevant to the resulting type from
|
||||
// unioning the two paths, so we intersect the constraints.
|
||||
let constraints = a_constraints;
|
||||
constraints.intersect(b_constraints);
|
||||
self.constraints.push(std::mem::take(constraints));
|
||||
let mut narrowing_constraints = a.narrowing_constraints;
|
||||
narrowing_constraints.intersect(&b.narrowing_constraints);
|
||||
|
||||
// For visibility constraints, we merge them using a ternary OR operation:
|
||||
let vis_constraint = visibility_constraints
|
||||
.add_or_constraint(a_vis_constraint, b_vis_constraint);
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
let visibility_constraint = visibility_constraints
|
||||
.add_or_constraint(a.visibility_constraint, b.visibility_constraint);
|
||||
|
||||
self.live_bindings.push(LiveBinding {
|
||||
binding: a.binding,
|
||||
narrowing_constraints,
|
||||
visibility_constraint,
|
||||
});
|
||||
}
|
||||
|
||||
EitherOrBoth::Left(((_, constraints), vis_constraint))
|
||||
| EitherOrBoth::Right(((_, constraints), vis_constraint)) => {
|
||||
self.constraints.push(std::mem::take(constraints));
|
||||
self.visibility_constraints.push(vis_constraint);
|
||||
EitherOrBoth::Left(binding) | EitherOrBoth::Right(binding) => {
|
||||
self.live_bindings.push(binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,14 +354,14 @@ impl SymbolState {
|
||||
.record_visibility_constraint(visibility_constraints, constraint);
|
||||
}
|
||||
|
||||
/// Simplifies this snapshot to have the same visibility constraints as a previous point in the
|
||||
/// control flow, but only if the set of live bindings or declarations for this symbol hasn't
|
||||
/// changed.
|
||||
pub(super) fn simplify_visibility_constraints(&mut self, snapshot_state: SymbolState) {
|
||||
if self.bindings.live_bindings == snapshot_state.bindings.live_bindings {
|
||||
self.bindings.visibility_constraints = snapshot_state.bindings.visibility_constraints;
|
||||
}
|
||||
if self.declarations.live_declarations == snapshot_state.declarations.live_declarations {
|
||||
self.declarations.visibility_constraints =
|
||||
snapshot_state.declarations.visibility_constraints;
|
||||
}
|
||||
self.bindings
|
||||
.simplify_visibility_constraints(snapshot_state.bindings);
|
||||
self.declarations
|
||||
.simplify_visibility_constraints(snapshot_state.declarations);
|
||||
}
|
||||
|
||||
/// Record a newly-encountered declaration of this symbol.
|
||||
@@ -414,98 +389,6 @@ impl SymbolState {
|
||||
}
|
||||
}
|
||||
|
||||
/// A single binding (as [`ScopedDefinitionId`]) with an iterator of its applicable
|
||||
/// narrowing constraints ([`ScopedConstraintId`]) and a corresponding visibility
|
||||
/// visibility constraint ([`ScopedVisibilityConstraintId`]).
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BindingIdWithConstraints<'map> {
|
||||
pub(super) definition: ScopedDefinitionId,
|
||||
pub(super) constraint_ids: ConstraintIdIterator<'map>,
|
||||
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BindingIdWithConstraintsIterator<'map> {
|
||||
definitions: BindingsIterator<'map>,
|
||||
constraints: ConstraintsIterator<'map>,
|
||||
visibility_constraints: VisibilityConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'map> Iterator for BindingIdWithConstraintsIterator<'map> {
|
||||
type Item = BindingIdWithConstraints<'map>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (
|
||||
self.definitions.next(),
|
||||
self.constraints.next(),
|
||||
self.visibility_constraints.next(),
|
||||
) {
|
||||
(None, None, None) => None,
|
||||
(Some(def), Some(constraints), Some(visibility_constraint_id)) => {
|
||||
Some(BindingIdWithConstraints {
|
||||
definition: ScopedDefinitionId::from_u32(def),
|
||||
constraint_ids: ConstraintIdIterator {
|
||||
wrapped: constraints.iter(),
|
||||
},
|
||||
visibility_constraint: *visibility_constraint_id,
|
||||
})
|
||||
}
|
||||
// SAFETY: see above.
|
||||
_ => unreachable!("definitions and constraints length mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for BindingIdWithConstraintsIterator<'_> {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ConstraintIdIterator<'a> {
|
||||
wrapped: BitSetIterator<'a, INLINE_CONSTRAINT_BLOCKS>,
|
||||
}
|
||||
|
||||
impl Iterator for ConstraintIdIterator<'_> {
|
||||
type Item = ScopedConstraintId;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.wrapped.next().map(ScopedConstraintId::from_u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for ConstraintIdIterator<'_> {}
|
||||
|
||||
/// A single declaration (as [`ScopedDefinitionId`]) with a corresponding visibility
|
||||
/// visibility constraint ([`ScopedVisibilityConstraintId`]).
|
||||
#[derive(Debug)]
|
||||
pub(super) struct DeclarationIdWithConstraint {
|
||||
pub(super) definition: ScopedDefinitionId,
|
||||
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
pub(super) struct DeclarationIdIterator<'map> {
|
||||
pub(crate) declarations: DeclarationsIterator<'map>,
|
||||
pub(crate) visibility_constraints: VisibilityConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
impl Iterator for DeclarationIdIterator<'_> {
|
||||
type Item = DeclarationIdWithConstraint;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.declarations.next(), self.visibility_constraints.next()) {
|
||||
(None, None) => None,
|
||||
(Some(declaration), Some(&visibility_constraint)) => {
|
||||
Some(DeclarationIdWithConstraint {
|
||||
definition: ScopedDefinitionId::from_u32(declaration),
|
||||
visibility_constraint,
|
||||
})
|
||||
}
|
||||
// SAFETY: see above.
|
||||
_ => unreachable!("declarations and visibility_constraints length mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for DeclarationIdIterator<'_> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -515,16 +398,16 @@ mod tests {
|
||||
let actual = symbol
|
||||
.bindings()
|
||||
.iter()
|
||||
.map(|def_id_with_constraints| {
|
||||
let def_id = def_id_with_constraints.definition;
|
||||
.map(|live_binding| {
|
||||
let def_id = live_binding.binding;
|
||||
let def = if def_id == ScopedDefinitionId::UNBOUND {
|
||||
"unbound".into()
|
||||
} else {
|
||||
def_id.as_u32().to_string()
|
||||
};
|
||||
let constraints = def_id_with_constraints
|
||||
.constraint_ids
|
||||
.map(ScopedConstraintId::as_u32)
|
||||
let constraints = live_binding
|
||||
.narrowing_constraints
|
||||
.iter()
|
||||
.map(|idx| idx.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
@@ -540,14 +423,14 @@ mod tests {
|
||||
.declarations()
|
||||
.iter()
|
||||
.map(
|
||||
|DeclarationIdWithConstraint {
|
||||
definition,
|
||||
|LiveDeclaration {
|
||||
declaration,
|
||||
visibility_constraint: _,
|
||||
}| {
|
||||
if definition == ScopedDefinitionId::UNBOUND {
|
||||
if *declaration == ScopedDefinitionId::UNBOUND {
|
||||
"undeclared".into()
|
||||
} else {
|
||||
definition.as_u32().to_string()
|
||||
declaration.as_u32().to_string()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::num::NonZeroUsize;
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use crate::module_resolver::{resolve_module, KnownModule};
|
||||
use crate::semantic_index::global_scope;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::symbol::Symbol;
|
||||
use crate::types::imported_symbol;
|
||||
use crate::Db;
|
||||
|
||||
/// Lookup the type of `symbol` in a given known module
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the given known module cannot be resolved for some reason
|
||||
pub(crate) fn known_module_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
known_module: KnownModule,
|
||||
symbol: &str,
|
||||
) -> Symbol<'db> {
|
||||
resolve_module(db, &known_module.name())
|
||||
.map(|module| imported_symbol(db, &module, symbol))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing` module namespace.
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason.
|
||||
#[inline]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
known_module_symbol(db, KnownModule::Typing, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing_extensions` module namespace.
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the `typing_extensions` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn typing_extensions_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
known_module_symbol(db, KnownModule::TypingExtensions, symbol)
|
||||
}
|
||||
|
||||
/// Get the scope of a core stdlib module.
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing the core module in question.
|
||||
fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option<ScopeId<'_>> {
|
||||
resolve_module(db, &core_module.name()).map(|module| global_scope(db, module.file()))
|
||||
}
|
||||
|
||||
/// Get the `builtins` module scope.
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`.
|
||||
pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
|
||||
core_module_scope(db, KnownModule::Builtins)
|
||||
}
|
||||
@@ -325,6 +325,7 @@ impl<'a> CheckSuppressionsContext<'a> {
|
||||
range,
|
||||
severity,
|
||||
file: self.file,
|
||||
secondary_messages: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
use crate::{
|
||||
types::{todo_type, Type, UnionType},
|
||||
Db,
|
||||
use ruff_db::files::File;
|
||||
|
||||
use crate::module_resolver::file_to_module;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId};
|
||||
use crate::semantic_index::{global_scope, use_def_map, DeclarationWithConstraint};
|
||||
use crate::semantic_index::{
|
||||
symbol_table, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
|
||||
};
|
||||
use crate::types::{
|
||||
binding_type, declaration_type, narrowing_constraint, todo_type, IntersectionBuilder,
|
||||
KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType,
|
||||
};
|
||||
use crate::{resolve_module, Db, KnownModule, Module, Program};
|
||||
|
||||
pub(crate) use implicit_globals::module_type_implicit_global_symbol;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Boundness {
|
||||
@@ -166,6 +178,661 @@ impl<'db> LookupError<'db> {
|
||||
/// In the future, we could possibly consider removing `Symbol` and using this type everywhere instead.
|
||||
pub(crate) type LookupResult<'db> = Result<Type<'db>, LookupError<'db>>;
|
||||
|
||||
/// Infer the public type of a symbol (its type as seen from outside its scope) in the given
|
||||
/// `scope`.
|
||||
pub(crate) fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
|
||||
symbol_impl(db, scope, name, RequiresExplicitReExport::No)
|
||||
}
|
||||
|
||||
/// Infers the public type of an explicit module-global symbol as seen from within the same file.
|
||||
///
|
||||
/// Note that all global scopes also include various "implicit globals" such as `__name__`,
|
||||
/// `__doc__` and `__file__`. This function **does not** consider those symbols; it will return
|
||||
/// `Symbol::Unbound` for them. Use the (currently test-only) `global_symbol` query to also include
|
||||
/// those additional symbols.
|
||||
///
|
||||
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
|
||||
pub(crate) fn explicit_global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, file),
|
||||
name,
|
||||
RequiresExplicitReExport::No,
|
||||
)
|
||||
}
|
||||
|
||||
/// Infers the public type of an explicit module-global symbol as seen from within the same file.
|
||||
///
|
||||
/// Unlike [`explicit_global_symbol`], this function also considers various "implicit globals"
|
||||
/// such as `__name__`, `__doc__` and `__file__`. These are looked up as attributes on `types.ModuleType`
|
||||
/// rather than being looked up as symbols explicitly defined/declared in the global scope.
|
||||
///
|
||||
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
explicit_global_symbol(db, file, name)
|
||||
.or_fall_back_to(db, || module_type_implicit_global_symbol(db, name))
|
||||
}
|
||||
|
||||
/// Infers the public type of an imported symbol.
|
||||
pub(crate) fn imported_symbol<'db>(db: &'db dyn Db, module: &Module, name: &str) -> Symbol<'db> {
|
||||
// If it's not found in the global scope, check if it's present as an instance on
|
||||
// `types.ModuleType` or `builtins.object`.
|
||||
//
|
||||
// We do a more limited version of this in `module_type_implicit_global_symbol`,
|
||||
// but there are two crucial differences here:
|
||||
// - If a member is looked up as an attribute, `__init__` is also available on the module, but
|
||||
// it isn't available as a global from inside the module
|
||||
// - If a member is looked up as an attribute, members on `builtins.object` are also available
|
||||
// (because `types.ModuleType` inherits from `object`); these attributes are also not
|
||||
// available as globals from inside the module.
|
||||
//
|
||||
// The same way as in `module_type_implicit_global_symbol`, however, we need to be careful to
|
||||
// ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with
|
||||
// dynamic imports; we shouldn't use it for `ModuleLiteral` types where we know exactly which
|
||||
// module we're dealing with.
|
||||
external_symbol_impl(db, module.file(), name).or_fall_back_to(db, || {
|
||||
if name == "__getattr__" {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the builtins namespace.
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason.
|
||||
///
|
||||
/// Note that this function is only intended for use in the context of the builtins *namespace*
|
||||
/// and should not be used when a symbol is being explicitly imported from the `builtins` module
|
||||
/// (e.g. `from builtins import int`).
|
||||
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
resolve_module(db, &KnownModule::Builtins.name())
|
||||
.map(|module| {
|
||||
external_symbol_impl(db, module.file(), symbol).or_fall_back_to(db, || {
|
||||
// We're looking up in the builtins namespace and not the module, so we should
|
||||
// do the normal lookup in `types.ModuleType` and not the special one as in
|
||||
// `imported_symbol`.
|
||||
module_type_implicit_global_symbol(db, symbol)
|
||||
})
|
||||
})
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in a given known module.
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the given known module cannot be resolved for some reason.
|
||||
pub(crate) fn known_module_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
known_module: KnownModule,
|
||||
symbol: &str,
|
||||
) -> Symbol<'db> {
|
||||
resolve_module(db, &known_module.name())
|
||||
.map(|module| imported_symbol(db, &module, symbol))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing` module namespace.
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason.
|
||||
#[inline]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
known_module_symbol(db, KnownModule::Typing, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing_extensions` module namespace.
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the `typing_extensions` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn typing_extensions_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
known_module_symbol(db, KnownModule::TypingExtensions, symbol)
|
||||
}
|
||||
|
||||
/// Get the `builtins` module scope.
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`.
|
||||
pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
|
||||
core_module_scope(db, KnownModule::Builtins)
|
||||
}
|
||||
|
||||
/// Get the scope of a core stdlib module.
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing the core module in question.
|
||||
fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option<ScopeId<'_>> {
|
||||
resolve_module(db, &core_module.name()).map(|module| global_scope(db, module.file()))
|
||||
}
|
||||
|
||||
/// Infer the combined type from an iterator of bindings, and return it
|
||||
/// together with boundness information in a [`Symbol`].
|
||||
///
|
||||
/// The type will be a union if there are multiple bindings with different types.
|
||||
pub(super) fn symbol_from_bindings<'db>(
|
||||
db: &'db dyn Db,
|
||||
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
|
||||
) -> Symbol<'db> {
|
||||
symbol_from_bindings_impl(db, bindings_with_constraints, RequiresExplicitReExport::No)
|
||||
}
|
||||
|
||||
/// Build a declared type from a [`DeclarationsIterator`].
|
||||
///
|
||||
/// If there is only one declaration, or all declarations declare the same type, returns
|
||||
/// `Ok(..)`. If there are conflicting declarations, returns an `Err(..)` variant with
|
||||
/// a union of the declared types as well as a list of all conflicting types.
|
||||
///
|
||||
/// This function also returns declaredness information (see [`Symbol`]) and a set of
|
||||
/// [`TypeQualifiers`] that have been specified on the declaration(s).
|
||||
pub(crate) fn symbol_from_declarations<'db>(
|
||||
db: &'db dyn Db,
|
||||
declarations: DeclarationsIterator<'_, 'db>,
|
||||
) -> SymbolFromDeclarationsResult<'db> {
|
||||
symbol_from_declarations_impl(db, declarations, RequiresExplicitReExport::No)
|
||||
}
|
||||
|
||||
/// The result of looking up a declared type from declarations; see [`symbol_from_declarations`].
|
||||
pub(crate) type SymbolFromDeclarationsResult<'db> =
|
||||
Result<SymbolAndQualifiers<'db>, (TypeAndQualifiers<'db>, Box<[Type<'db>]>)>;
|
||||
|
||||
/// A type with declaredness information, and a set of type qualifiers.
|
||||
///
|
||||
/// This is used to represent the result of looking up the declared type. Consider this
|
||||
/// example:
|
||||
/// ```py
|
||||
/// class C:
|
||||
/// if flag:
|
||||
/// variable: ClassVar[int]
|
||||
/// ```
|
||||
/// If we look up the declared type of `variable` in the scope of class `C`, we will get
|
||||
/// the type `int`, a "declaredness" of [`Boundness::PossiblyUnbound`], and the information
|
||||
/// that this comes with a [`CLASS_VAR`] type qualifier.
|
||||
///
|
||||
/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SymbolAndQualifiers<'db>(pub(crate) Symbol<'db>, pub(crate) TypeQualifiers);
|
||||
|
||||
impl SymbolAndQualifiers<'_> {
|
||||
/// Constructor that creates a [`SymbolAndQualifiers`] instance with a [`TodoType`] type
|
||||
/// and no qualifiers.
|
||||
///
|
||||
/// [`TodoType`]: crate::types::TodoType
|
||||
pub(crate) fn todo(message: &'static str) -> Self {
|
||||
Self(Symbol::todo(message), TypeQualifiers::empty())
|
||||
}
|
||||
|
||||
/// Returns `true` if the symbol has a `ClassVar` type qualifier.
|
||||
pub(crate) fn is_class_var(&self) -> bool {
|
||||
self.1.contains(TypeQualifiers::CLASS_VAR)
|
||||
}
|
||||
|
||||
/// Returns `true` if the symbol has a `Final` type qualifier.
|
||||
pub(crate) fn is_final(&self) -> bool {
|
||||
self.1.contains(TypeQualifiers::FINAL)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Symbol<'db>> for SymbolAndQualifiers<'db> {
|
||||
fn from(symbol: Symbol<'db>) -> Self {
|
||||
SymbolAndQualifiers(symbol, TypeQualifiers::empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of [`symbol`].
|
||||
fn symbol_impl<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
name: &str,
|
||||
requires_explicit_reexport: RequiresExplicitReExport,
|
||||
) -> Symbol<'db> {
|
||||
#[salsa::tracked]
|
||||
fn symbol_by_id<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
symbol_id: ScopedSymbolId,
|
||||
requires_explicit_reexport: RequiresExplicitReExport,
|
||||
) -> Symbol<'db> {
|
||||
let use_def = use_def_map(db, scope);
|
||||
|
||||
// If the symbol is declared, the public type is based on declarations; otherwise, it's based
|
||||
// on inference from bindings.
|
||||
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
let declared = symbol_from_declarations_impl(db, declarations, requires_explicit_reexport);
|
||||
let is_final = declared.as_ref().is_ok_and(SymbolAndQualifiers::is_final);
|
||||
let declared = declared.map(|SymbolAndQualifiers(symbol, _)| symbol);
|
||||
|
||||
match declared {
|
||||
// Symbol is declared, trust the declared type
|
||||
Ok(symbol @ Symbol::Type(_, Boundness::Bound)) => symbol,
|
||||
// Symbol is possibly declared
|
||||
Ok(Symbol::Type(declared_ty, Boundness::PossiblyUnbound)) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport);
|
||||
|
||||
match inferred {
|
||||
// Symbol is possibly undeclared and definitely unbound
|
||||
Symbol::Unbound => {
|
||||
// TODO: We probably don't want to report `Bound` here. This requires a bit of
|
||||
// design work though as we might want a different behavior for stubs and for
|
||||
// normal modules.
|
||||
Symbol::Type(declared_ty, Boundness::Bound)
|
||||
}
|
||||
// Symbol is possibly undeclared and (possibly) bound
|
||||
Symbol::Type(inferred_ty, boundness) => Symbol::Type(
|
||||
UnionType::from_elements(db, [inferred_ty, declared_ty]),
|
||||
boundness,
|
||||
),
|
||||
}
|
||||
}
|
||||
// Symbol is undeclared, return the union of `Unknown` with the inferred type
|
||||
Ok(Symbol::Unbound) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport);
|
||||
|
||||
// `__slots__` is a symbol with special behavior in Python's runtime. It can be
|
||||
// modified externally, but those changes do not take effect. We therefore issue
|
||||
// a diagnostic if we see it being modified externally. In type inference, we
|
||||
// can assign a "narrow" type to it even if it is not *declared*. This means, we
|
||||
// do not have to call [`widen_type_for_undeclared_public_symbol`].
|
||||
let is_considered_non_modifiable =
|
||||
is_final || symbol_table(db, scope).symbol(symbol_id).name() == "__slots__";
|
||||
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, is_considered_non_modifiable)
|
||||
}
|
||||
// Symbol has conflicting declared types
|
||||
Err((declared_ty, _)) => {
|
||||
// Intentionally ignore conflicting declared types; that's not our problem,
|
||||
// it's the problem of the module we are importing from.
|
||||
Symbol::bound(declared_ty.inner_type())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (ticket: https://github.com/astral-sh/ruff/issues/14297) Our handling of boundness
|
||||
// currently only depends on bindings, and ignores declarations. This is inconsistent, since
|
||||
// we only look at bindings if the symbol may be undeclared. Consider the following example:
|
||||
// ```py
|
||||
// x: int
|
||||
//
|
||||
// if flag:
|
||||
// y: int
|
||||
// else
|
||||
// y = 3
|
||||
// ```
|
||||
// If we import from this module, we will currently report `x` as a definitely-bound symbol
|
||||
// (even though it has no bindings at all!) but report `y` as possibly-unbound (even though
|
||||
// every path has either a binding or a declaration for it.)
|
||||
}
|
||||
|
||||
let _span = tracing::trace_span!("symbol", ?name).entered();
|
||||
|
||||
// We don't need to check for `typing_extensions` here, because `typing_extensions.TYPE_CHECKING`
|
||||
// is just a re-export of `typing.TYPE_CHECKING`.
|
||||
if name == "TYPE_CHECKING"
|
||||
&& file_to_module(db, scope.file(db))
|
||||
.is_some_and(|module| module.is_known(KnownModule::Typing))
|
||||
{
|
||||
return Symbol::bound(Type::BooleanLiteral(true));
|
||||
}
|
||||
if name == "platform"
|
||||
&& file_to_module(db, scope.file(db))
|
||||
.is_some_and(|module| module.is_known(KnownModule::Sys))
|
||||
{
|
||||
match Program::get(db).python_platform(db) {
|
||||
crate::PythonPlatform::Identifier(platform) => {
|
||||
return Symbol::bound(Type::string_literal(db, platform.as_str()));
|
||||
}
|
||||
crate::PythonPlatform::All => {
|
||||
// Fall through to the looked up type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
symbol_table(db, scope)
|
||||
.symbol_id_by_name(name)
|
||||
.map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
/// Implementation of [`symbol_from_bindings`].
|
||||
///
|
||||
/// ## Implementation Note
|
||||
/// This function gets called cross-module. It, therefore, shouldn't
|
||||
/// access any AST nodes from the file containing the declarations.
|
||||
fn symbol_from_bindings_impl<'db>(
|
||||
db: &'db dyn Db,
|
||||
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
|
||||
requires_explicit_reexport: RequiresExplicitReExport,
|
||||
) -> Symbol<'db> {
|
||||
let constraints = bindings_with_constraints.constraints;
|
||||
let visibility_constraints = bindings_with_constraints.visibility_constraints;
|
||||
let mut bindings_with_constraints = bindings_with_constraints.peekable();
|
||||
|
||||
let is_non_exported = |binding: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !binding.is_reexported(db)
|
||||
};
|
||||
|
||||
let unbound_visibility = match bindings_with_constraints.peek() {
|
||||
Some(BindingWithConstraints {
|
||||
binding,
|
||||
visibility_constraint,
|
||||
narrowing_constraints: _,
|
||||
}) if binding.map_or(true, is_non_exported) => {
|
||||
visibility_constraints.evaluate(db, constraints, *visibility_constraint)
|
||||
}
|
||||
_ => Truthiness::AlwaysFalse,
|
||||
};
|
||||
|
||||
let mut types = bindings_with_constraints.filter_map(
|
||||
|BindingWithConstraints {
|
||||
binding,
|
||||
narrowing_constraints,
|
||||
visibility_constraint,
|
||||
}| {
|
||||
let binding = binding?;
|
||||
|
||||
if is_non_exported(binding) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let static_visibility =
|
||||
visibility_constraints.evaluate(db, constraints, visibility_constraint);
|
||||
|
||||
if static_visibility.is_always_false() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut constraint_tys = narrowing_constraints
|
||||
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
|
||||
.peekable();
|
||||
|
||||
let binding_ty = binding_type(db, binding);
|
||||
if constraint_tys.peek().is_some() {
|
||||
let intersection_ty = constraint_tys
|
||||
.fold(
|
||||
IntersectionBuilder::new(db).add_positive(binding_ty),
|
||||
IntersectionBuilder::add_positive,
|
||||
)
|
||||
.build();
|
||||
Some(intersection_ty)
|
||||
} else {
|
||||
Some(binding_ty)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(first) = types.next() {
|
||||
let boundness = match unbound_visibility {
|
||||
Truthiness::AlwaysTrue => {
|
||||
unreachable!("If we have at least one binding, the scope-start should not be definitely visible")
|
||||
}
|
||||
Truthiness::AlwaysFalse => Boundness::Bound,
|
||||
Truthiness::Ambiguous => Boundness::PossiblyUnbound,
|
||||
};
|
||||
|
||||
if let Some(second) = types.next() {
|
||||
Symbol::Type(
|
||||
UnionType::from_elements(db, [first, second].into_iter().chain(types)),
|
||||
boundness,
|
||||
)
|
||||
} else {
|
||||
Symbol::Type(first, boundness)
|
||||
}
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of [`symbol_from_declarations`].
|
||||
///
|
||||
/// ## Implementation Note
|
||||
/// This function gets called cross-module. It, therefore, shouldn't
|
||||
/// access any AST nodes from the file containing the declarations.
|
||||
fn symbol_from_declarations_impl<'db>(
|
||||
db: &'db dyn Db,
|
||||
declarations: DeclarationsIterator<'_, 'db>,
|
||||
requires_explicit_reexport: RequiresExplicitReExport,
|
||||
) -> SymbolFromDeclarationsResult<'db> {
|
||||
let constraints = declarations.constraints;
|
||||
let visibility_constraints = declarations.visibility_constraints;
|
||||
let mut declarations = declarations.peekable();
|
||||
|
||||
let is_non_exported = |declaration: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !declaration.is_reexported(db)
|
||||
};
|
||||
|
||||
let undeclared_visibility = match declarations.peek() {
|
||||
Some(DeclarationWithConstraint {
|
||||
declaration,
|
||||
visibility_constraint,
|
||||
}) if declaration.map_or(true, is_non_exported) => {
|
||||
visibility_constraints.evaluate(db, constraints, *visibility_constraint)
|
||||
}
|
||||
_ => Truthiness::AlwaysFalse,
|
||||
};
|
||||
|
||||
let mut types = declarations.filter_map(
|
||||
|DeclarationWithConstraint {
|
||||
declaration,
|
||||
visibility_constraint,
|
||||
}| {
|
||||
let declaration = declaration?;
|
||||
|
||||
if is_non_exported(declaration) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let static_visibility =
|
||||
visibility_constraints.evaluate(db, constraints, visibility_constraint);
|
||||
|
||||
if static_visibility.is_always_false() {
|
||||
None
|
||||
} else {
|
||||
Some(declaration_type(db, declaration))
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(first) = types.next() {
|
||||
let mut conflicting: Vec<Type<'db>> = vec![];
|
||||
let declared_ty = if let Some(second) = types.next() {
|
||||
let ty_first = first.inner_type();
|
||||
let mut qualifiers = first.qualifiers();
|
||||
|
||||
let mut builder = UnionBuilder::new(db).add(ty_first);
|
||||
for other in std::iter::once(second).chain(types) {
|
||||
let other_ty = other.inner_type();
|
||||
if !ty_first.is_equivalent_to(db, other_ty) {
|
||||
conflicting.push(other_ty);
|
||||
}
|
||||
builder = builder.add(other_ty);
|
||||
qualifiers = qualifiers.union(other.qualifiers());
|
||||
}
|
||||
TypeAndQualifiers::new(builder.build(), qualifiers)
|
||||
} else {
|
||||
first
|
||||
};
|
||||
if conflicting.is_empty() {
|
||||
let boundness = match undeclared_visibility {
|
||||
Truthiness::AlwaysTrue => {
|
||||
unreachable!("If we have at least one declaration, the scope-start should not be definitely visible")
|
||||
}
|
||||
Truthiness::AlwaysFalse => Boundness::Bound,
|
||||
Truthiness::Ambiguous => Boundness::PossiblyUnbound,
|
||||
};
|
||||
|
||||
Ok(SymbolAndQualifiers(
|
||||
Symbol::Type(declared_ty.inner_type(), boundness),
|
||||
declared_ty.qualifiers(),
|
||||
))
|
||||
} else {
|
||||
Err((
|
||||
declared_ty,
|
||||
std::iter::once(first.inner_type())
|
||||
.chain(conflicting)
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Ok(Symbol::Unbound.into())
|
||||
}
|
||||
}
|
||||
|
||||
mod implicit_globals {
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::semantic_index::{self, symbol_table};
|
||||
use crate::types::KnownClass;
|
||||
|
||||
use super::Symbol;
|
||||
|
||||
/// Looks up the type of an "implicit global symbol". Returns [`Symbol::Unbound`] if
|
||||
/// `name` is not present as an implicit symbol in module-global namespaces.
|
||||
///
|
||||
/// Implicit global symbols are symbols such as `__doc__`, `__name__`, and `__file__`
|
||||
/// that are implicitly defined in every module's global scope. Because their type is
|
||||
/// always the same, we simply look these up as instance attributes on `types.ModuleType`.
|
||||
///
|
||||
/// Note that this function should only be used as a fallback if a symbol is being looked
|
||||
/// up in the global scope **from within the same file**. If the symbol is being looked up
|
||||
/// from outside the file (e.g. via imports), use [`super::imported_symbol`] (or fallback logic
|
||||
/// like the logic used in that function) instead. The reason is that this function returns
|
||||
/// [`Symbol::Unbound`] for `__init__` and `__dict__` (which cannot be found in globals if
|
||||
/// the lookup is being done from the same file) -- but these symbols *are* available in the
|
||||
/// global scope if they're being imported **from a different file**.
|
||||
pub(crate) fn module_type_implicit_global_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
) -> Symbol<'db> {
|
||||
// In general we wouldn't check to see whether a symbol exists on a class before doing the
|
||||
// `.member()` call on the instance type -- we'd just do the `.member`() call on the instance
|
||||
// type, since it has the same end result. The reason to only call `.member()` on `ModuleType`
|
||||
// when absolutely necessary is that this function is used in a very hot path (name resolution
|
||||
// in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation.
|
||||
if module_type_symbols(db)
|
||||
.iter()
|
||||
.any(|module_type_member| &**module_type_member == name)
|
||||
{
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
}
|
||||
}
|
||||
|
||||
/// An internal micro-optimisation for `module_type_implicit_global_symbol`.
|
||||
///
|
||||
/// This function returns a list of the symbols that typeshed declares in the
|
||||
/// body scope of the stub for the class `types.ModuleType`.
|
||||
///
|
||||
/// The returned list excludes the attributes `__dict__` and `__init__`. These are very
|
||||
/// special members that can be accessed as attributes on the module when imported,
|
||||
/// but cannot be accessed as globals *inside* the module.
|
||||
///
|
||||
/// The list also excludes `__getattr__`. `__getattr__` is even more special: it doesn't
|
||||
/// exist at runtime, but typeshed includes it to reduce false positives associated with
|
||||
/// functions that dynamically import modules and return `Instance(types.ModuleType)`.
|
||||
/// We should ignore it for any known module-literal type.
|
||||
///
|
||||
/// Conceptually this function could be a `Set` rather than a list,
|
||||
/// but the number of symbols declared in this scope is likely to be very small,
|
||||
/// so the cost of hashing the names is likely to be more expensive than it's worth.
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> {
|
||||
let Some(module_type) = KnownClass::ModuleType
|
||||
.to_class_literal(db)
|
||||
.into_class_literal()
|
||||
else {
|
||||
// The most likely way we get here is if a user specified a `--custom-typeshed-dir`
|
||||
// without a `types.pyi` stub in the `stdlib/` directory
|
||||
return smallvec::SmallVec::default();
|
||||
};
|
||||
|
||||
let module_type_scope = module_type.body_scope(db);
|
||||
let module_type_symbol_table = symbol_table(db, module_type_scope);
|
||||
|
||||
module_type_symbol_table
|
||||
.symbols()
|
||||
.filter(|symbol| symbol.is_declared())
|
||||
.map(semantic_index::symbol::Symbol::name)
|
||||
.filter(|symbol_name| {
|
||||
!matches!(&***symbol_name, "__dict__" | "__getattr__" | "__init__")
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::setup_db;
|
||||
|
||||
#[test]
|
||||
fn module_type_symbols_includes_declared_types_but_not_referenced_types() {
|
||||
let db = setup_db();
|
||||
let symbol_names = module_type_symbols(&db);
|
||||
|
||||
let dunder_name_symbol_name = ast::name::Name::new_static("__name__");
|
||||
assert!(symbol_names.contains(&dunder_name_symbol_name));
|
||||
|
||||
let property_symbol_name = ast::name::Name::new_static("property");
|
||||
assert!(!symbol_names.contains(&property_symbol_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of looking up a module-global symbol as seen from outside the file (e.g. via
|
||||
/// imports).
|
||||
///
|
||||
/// This will take into account whether the definition of the symbol is being explicitly
|
||||
/// re-exported from a stub file or not.
|
||||
fn external_symbol_impl<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, file),
|
||||
name,
|
||||
if file.is_stub(db.upcast()) {
|
||||
RequiresExplicitReExport::Yes
|
||||
} else {
|
||||
RequiresExplicitReExport::No
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
enum RequiresExplicitReExport {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
impl RequiresExplicitReExport {
|
||||
const fn is_yes(self) -> bool {
|
||||
matches!(self, RequiresExplicitReExport::Yes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes a possibly-widened type `Unknown | T_inferred` from the inferred type `T_inferred`
|
||||
/// of a symbol, unless the type is a known-instance type (e.g. `typing.Any`) or the symbol is
|
||||
/// considered non-modifiable (e.g. when the symbol is `@Final`). We need this for public uses
|
||||
/// of symbols that have no declared type.
|
||||
fn widen_type_for_undeclared_public_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
inferred: Symbol<'db>,
|
||||
is_considered_non_modifiable: bool,
|
||||
) -> Symbol<'db> {
|
||||
// We special-case known-instance types here since symbols like `typing.Any` are typically
|
||||
// not declared in the stubs (e.g. `Any = object()`), but we still want to treat them as
|
||||
// such.
|
||||
let is_known_instance = inferred
|
||||
.ignore_possibly_unbound()
|
||||
.is_some_and(|ty| matches!(ty, Type::KnownInstance(_)));
|
||||
|
||||
if is_considered_non_modifiable || is_known_instance {
|
||||
inferred
|
||||
} else {
|
||||
inferred.map_type(|ty| UnionType::from_elements(db, [Type::unknown(), ty]))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -222,4 +889,37 @@ mod tests {
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Symbol<'db>) {
|
||||
assert!(matches!(
|
||||
symbol,
|
||||
Symbol::Type(Type::Instance(_), Boundness::Bound)
|
||||
));
|
||||
assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_builtin_globals() {
|
||||
let db = setup_db();
|
||||
assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_typing_globals() {
|
||||
let db = setup_db();
|
||||
assert_bound_string_symbol(&db, typing_symbol(&db, "__name__"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_typing_extensions_globals() {
|
||||
let db = setup_db();
|
||||
assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_sys_globals() {
|
||||
let db = setup_db();
|
||||
assert_bound_string_symbol(&db, known_module_symbol(&db, KnownModule::Sys, "__name__"));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user