Compare commits
54 Commits
dcreager/v
...
dcreager/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81cfa8ea0a | ||
|
|
6f957475ef | ||
|
|
8235062718 | ||
|
|
b8b4a192c7 | ||
|
|
4dc2ca50ca | ||
|
|
78e1df037c | ||
|
|
5e163e2a5e | ||
|
|
530aa2429b | ||
|
|
d0719e127b | ||
|
|
ce3d84ba1b | ||
|
|
8762a3a868 | ||
|
|
824a5d7d3b | ||
|
|
e2b8b36b7a | ||
|
|
94947cbf65 | ||
|
|
7dccb6a98c | ||
|
|
948f3f856c | ||
|
|
3af0b31de3 | ||
|
|
7df7be5c7d | ||
|
|
2d2841e20d | ||
|
|
14fbc2b167 | ||
|
|
351121c5c5 | ||
|
|
64bcc8db2f | ||
|
|
b0f01ba514 | ||
|
|
3a9341f7be | ||
|
|
739c94f95a | ||
|
|
af8587eabf | ||
|
|
41207ec901 | ||
|
|
bc6e8b58ce | ||
|
|
e4d6b54a16 | ||
|
|
17ee2a28ba | ||
|
|
de77b29798 | ||
|
|
f473f6b6e5 | ||
|
|
736c4ab05a | ||
|
|
8289432252 | ||
|
|
808c94d509 | ||
|
|
b95d22c08e | ||
|
|
f3e66dd503 | ||
|
|
6516db7835 | ||
|
|
03c873765e | ||
|
|
c90707875e | ||
|
|
8e20e589f1 | ||
|
|
113e32b956 | ||
|
|
fdc18eefc3 | ||
|
|
5f40651ae7 | ||
|
|
ea031a3b39 | ||
|
|
93b64daa4a | ||
|
|
74376375e4 | ||
|
|
30f52d8cf5 | ||
|
|
77bc32b9b9 | ||
|
|
1a368b0bf9 | ||
|
|
134435415e | ||
|
|
bc6e105c18 | ||
|
|
6bd413df6c | ||
|
|
85bd961fd3 |
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -19,6 +19,10 @@
|
||||
|
||||
# ty
|
||||
/crates/ty* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
|
||||
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
|
||||
/crates/ruff_db/ @carljm @MichaReiser @sharkdp @dcreager
|
||||
/crates/ty_project/ @carljm @MichaReiser @sharkdp @dcreager
|
||||
/crates/ty_server/ @carljm @MichaReiser @sharkdp @dcreager
|
||||
/crates/ty/ @carljm @MichaReiser @sharkdp @dcreager
|
||||
/crates/ty_wasm/ @carljm @MichaReiser @sharkdp @dcreager
|
||||
/scripts/ty_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
|
||||
/crates/ty_python_semantic @carljm @AlexWaygood @sharkdp @dcreager
|
||||
|
||||
8
.github/workflows/build-docker.yml
vendored
8
.github/workflows/build-docker.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
with:
|
||||
images: ${{ env.RUFF_BASE_IMG }}
|
||||
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
with:
|
||||
images: ${{ env.RUFF_BASE_IMG }}
|
||||
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
# ghcr.io prefers index level annotations
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
|
||||
@@ -266,7 +266,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
|
||||
with:
|
||||
|
||||
18
.github/workflows/ci.yaml
vendored
18
.github/workflows/ci.yaml
vendored
@@ -240,11 +240,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -298,11 +298,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -429,7 +429,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@808dcb1b503398677d089d3216c51ac7cc11e7ab # v1.14.2
|
||||
uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
|
||||
with:
|
||||
tool: cargo-fuzz@0.11.2
|
||||
- name: "Install cargo-fuzz"
|
||||
@@ -682,7 +682,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@808dcb1b503398677d089d3216c51ac7cc11e7ab # v1.14.2
|
||||
- uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
@@ -903,7 +903,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -936,7 +936,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
|
||||
2
.github/workflows/mypy_primer.yaml
vendored
2
.github/workflows/mypy_primer.yaml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
|
||||
18
.github/workflows/typing_conformance.yaml
vendored
18
.github/workflows/typing_conformance.yaml
vendored
@@ -24,6 +24,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: 1
|
||||
CONFORMANCE_SUITE_COMMIT: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
|
||||
|
||||
jobs:
|
||||
typing_conformance:
|
||||
@@ -40,13 +41,10 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: python/typing
|
||||
ref: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
|
||||
ref: ${{ env.CONFORMANCE_SUITE_COMMIT }}
|
||||
path: typing
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
@@ -64,14 +62,13 @@ jobs:
|
||||
cd ruff
|
||||
|
||||
echo "new commit"
|
||||
git checkout -b new_commit "${{ github.event.pull_request.head.sha }}"
|
||||
git rev-list --format=%s --max-count=1 new_commit
|
||||
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
|
||||
cargo build --release --bin ty
|
||||
mv target/release/ty ty-new
|
||||
|
||||
echo "old commit (merge base)"
|
||||
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
|
||||
git checkout -b old_commit "$MERGE_BASE"
|
||||
echo "old commit (merge base)"
|
||||
git rev-list --format=%s --max-count=1 old_commit
|
||||
cargo build --release --bin ty
|
||||
mv target/release/ty ty-old
|
||||
@@ -95,6 +92,7 @@ jobs:
|
||||
fi
|
||||
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
echo "${CONFORMANCE_SUITE_COMMIT}" > conformance-suite-commit
|
||||
|
||||
- name: Upload diff
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
@@ -107,3 +105,9 @@ jobs:
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
|
||||
- name: Upload conformance suite commit
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: conformance-suite-commit
|
||||
path: conformance-suite-commit
|
||||
|
||||
@@ -32,6 +32,14 @@ jobs:
|
||||
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: Download typing conformance suite commit
|
||||
with:
|
||||
name: conformance-suite-commit
|
||||
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: "Download typing_conformance results"
|
||||
id: download-typing_conformance_diff
|
||||
@@ -61,7 +69,14 @@ jobs:
|
||||
# subsequent runs
|
||||
echo '<!-- generated-comment typing_conformance_diagnostics_diff -->' >> comment.txt
|
||||
|
||||
echo '## Diagnostic diff on typing conformance tests' >> comment.txt
|
||||
if [[ -f conformance-suite-commit ]]
|
||||
then
|
||||
echo "## Diagnostic diff on [typing conformance tests](https://github.com/python/typing/tree/$(<conformance-suite-commit)/conformance)" >> comment.txt
|
||||
else
|
||||
echo "conformance-suite-commit file not found"
|
||||
echo "## Diagnostic diff on typing conformance tests" >> comment.txt
|
||||
fi
|
||||
|
||||
if [ -s "pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff" ]; then
|
||||
echo '<details>' >> comment.txt
|
||||
echo '<summary>Changes were detected when running ty on typing conformance tests</summary>' >> comment.txt
|
||||
|
||||
@@ -81,7 +81,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.5
|
||||
rev: v0.12.7
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff-check
|
||||
|
||||
16
Cargo.lock
generated
16
Cargo.lock
generated
@@ -2050,9 +2050,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.1.0"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"fsevent-sys",
|
||||
@@ -3441,7 +3441,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa?rev=f3dc2f30f9a250618161e35600a00de7fe744953#f3dc2f30f9a250618161e35600a00de7fe744953"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3454,7 +3454,6 @@ dependencies = [
|
||||
"inventory",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"rayon",
|
||||
"rustc-hash",
|
||||
"salsa-macro-rules",
|
||||
"salsa-macros",
|
||||
@@ -3466,12 +3465,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa?rev=f3dc2f30f9a250618161e35600a00de7fe744953#f3dc2f30f9a250618161e35600a00de7fe744953"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa?rev=f3dc2f30f9a250618161e35600a00de7fe744953#f3dc2f30f9a250618161e35600a00de7fe744953"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3568,9 +3567,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.141"
|
||||
version = "1.0.142"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
|
||||
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -4187,6 +4186,7 @@ dependencies = [
|
||||
"argfile",
|
||||
"clap",
|
||||
"clap_complete_command",
|
||||
"clearscreen",
|
||||
"colored 3.0.0",
|
||||
"crossbeam",
|
||||
"ctrlc",
|
||||
|
||||
@@ -141,7 +141,12 @@ regex-automata = { version = "0.4.9" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
rustc-stable-hash = { version = "0.1.2" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa", rev = "f3dc2f30f9a250618161e35600a00de7fe744953" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d66fe331d546216132ace503512b94d5c68d2c50", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
"inventory",
|
||||
] }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
@@ -22,11 +22,17 @@ exit_code: 1
|
||||
"description": "`os` imported but unused",
|
||||
"fingerprint": "4dbad37161e65c72",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
},
|
||||
"path": "input.py"
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 8,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 10,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
@@ -35,11 +41,17 @@ exit_code: 1
|
||||
"description": "Undefined name `y`",
|
||||
"fingerprint": "7af59862a085230",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 2,
|
||||
"end": 2
|
||||
},
|
||||
"path": "input.py"
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 5,
|
||||
"line": 2
|
||||
},
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
@@ -48,11 +60,17 @@ exit_code: 1
|
||||
"description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
|
||||
"fingerprint": "e558cec859bb66e8",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 3,
|
||||
"end": 3
|
||||
},
|
||||
"path": "input.py"
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 1,
|
||||
"line": 3
|
||||
},
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
|
||||
@@ -89,3 +89,14 @@ print(1)
|
||||
# ///
|
||||
#
|
||||
# Foobar
|
||||
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/19713
|
||||
|
||||
# mypy: ignore-errors
|
||||
# pyright: ignore-errors
|
||||
# pyrefly: ignore-errors
|
||||
# ty: ignore[unresolved-import]
|
||||
# pyrefly: ignore[unused-import]
|
||||
|
||||
print(1)
|
||||
|
||||
@@ -162,3 +162,86 @@ except Exception:
|
||||
exception("An error occurred")
|
||||
else:
|
||||
exception("An error occurred")
|
||||
|
||||
# Test tuple exceptions
|
||||
try:
|
||||
pass
|
||||
except (Exception,):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (Exception, ValueError):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (ValueError, Exception):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (ValueError, Exception) as e:
|
||||
print(e)
|
||||
|
||||
try:
|
||||
pass
|
||||
except (BaseException, TypeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (TypeError, BaseException):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (Exception, BaseException):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (BaseException, Exception):
|
||||
pass
|
||||
|
||||
# Test nested tuples
|
||||
try:
|
||||
pass
|
||||
except ((Exception, ValueError), TypeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (ValueError, (BaseException, TypeError)):
|
||||
pass
|
||||
|
||||
# Test valid tuple exceptions (should not trigger)
|
||||
try:
|
||||
pass
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (OSError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (OSError, FileNotFoundError) as e:
|
||||
print(e)
|
||||
|
||||
try:
|
||||
pass
|
||||
except (Exception, ValueError):
|
||||
critical("...", exc_info=True)
|
||||
|
||||
try:
|
||||
pass
|
||||
except (Exception, ValueError):
|
||||
raise
|
||||
|
||||
try:
|
||||
pass
|
||||
except (Exception, ValueError) as e:
|
||||
raise e
|
||||
|
||||
@@ -129,4 +129,35 @@ print(" x ".rsplit(maxsplit=0))
|
||||
print(" x ".rsplit(maxsplit=0))
|
||||
print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
print(" x ".rsplit(maxsplit=0))
|
||||
print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
|
||||
r"""simple@example.com
|
||||
very.common@example.com
|
||||
FirstName.LastName@EasierReading.org
|
||||
x@example.com
|
||||
long.email-address-with-hyphens@and.subdomains.example.com
|
||||
user.name+tag+sorting@example.com
|
||||
name/surname@example.com
|
||||
xample@s.example
|
||||
" "@example.org
|
||||
"john..doe"@example.org
|
||||
mailhost!username@example.org
|
||||
"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
|
||||
user%example.com@example.org
|
||||
user-@example.org
|
||||
I❤️CHOCOLATE@example.com
|
||||
this\ still\"not\\allowed@example.com
|
||||
stellyamburrr985@example.com
|
||||
Abc.123@example.com
|
||||
user+mailbox/department=shipping@example.com
|
||||
!#$%&'*+-/=?^_`.{|}~@example.com
|
||||
"Abc@def"@example.com
|
||||
"Fred\ Bloggs"@example.com
|
||||
"Joe.\\Blow"@example.com""".split("\n")
|
||||
|
||||
|
||||
r"""first
|
||||
'no need' to escape
|
||||
"swap" quote style
|
||||
"use' ugly triple quotes""".split("\n")
|
||||
|
||||
@@ -61,22 +61,17 @@ impl Serialize for SerializedMessages<'_> {
|
||||
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
|
||||
|
||||
for diagnostic in self.diagnostics {
|
||||
let start_location = diagnostic.expect_ruff_start_location();
|
||||
let end_location = diagnostic.expect_ruff_end_location();
|
||||
|
||||
let filename = diagnostic.expect_ruff_filename();
|
||||
let lines = if self.context.is_notebook(&filename) {
|
||||
|
||||
let (start_location, end_location) = if self.context.is_notebook(&filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
json!({
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
})
|
||||
Default::default()
|
||||
} else {
|
||||
json!({
|
||||
"begin": start_location.line,
|
||||
"end": end_location.line
|
||||
})
|
||||
(
|
||||
diagnostic.expect_ruff_start_location(),
|
||||
diagnostic.expect_ruff_end_location(),
|
||||
)
|
||||
};
|
||||
|
||||
let path = self.project_dir.as_ref().map_or_else(
|
||||
@@ -111,8 +106,11 @@ impl Serialize for SerializedMessages<'_> {
|
||||
"fingerprint": format!("{:x}", message_fingerprint),
|
||||
"location": {
|
||||
"path": path,
|
||||
"lines": lines
|
||||
}
|
||||
"positions": {
|
||||
"begin": start_location,
|
||||
"end": end_location,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
|
||||
@@ -8,11 +8,17 @@ expression: redact_fingerprint(&content)
|
||||
"description": "`os` imported but unused",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
},
|
||||
"path": "fib.py"
|
||||
"path": "fib.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 8,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 10,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
@@ -21,11 +27,17 @@ expression: redact_fingerprint(&content)
|
||||
"description": "Local variable `x` is assigned to but never used",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 6,
|
||||
"end": 6
|
||||
},
|
||||
"path": "fib.py"
|
||||
"path": "fib.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 5,
|
||||
"line": 6
|
||||
},
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
@@ -34,11 +46,17 @@ expression: redact_fingerprint(&content)
|
||||
"description": "Undefined name `a`",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
},
|
||||
"path": "undef.py"
|
||||
"path": "undef.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 4,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 5,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
|
||||
@@ -8,11 +8,17 @@ expression: redact_fingerprint(&content)
|
||||
"description": "Expected one or more symbol names after import",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 2
|
||||
},
|
||||
"path": "syntax_errors.py"
|
||||
"path": "syntax_errors.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 15,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 1,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
@@ -21,11 +27,17 @@ expression: redact_fingerprint(&content)
|
||||
"description": "Expected ')', found newline",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 3,
|
||||
"end": 4
|
||||
},
|
||||
"path": "syntax_errors.py"
|
||||
"path": "syntax_errors.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 12,
|
||||
"line": 3
|
||||
},
|
||||
"end": {
|
||||
"column": 1,
|
||||
"line": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ static ALLOWLIST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
(?:
|
||||
# Case-sensitive
|
||||
pyright
|
||||
| pyrefly
|
||||
| mypy:
|
||||
| type:\s*ignore
|
||||
| SPDX-License-Identifier:
|
||||
|
||||
@@ -75,6 +75,22 @@ impl Violation for BlindExcept {
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_blind_exception<'a>(
|
||||
semantic: &'a SemanticModel,
|
||||
expr: &'a Expr,
|
||||
) -> Option<(&'a str, ruff_text_size::TextRange)> {
|
||||
match expr {
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
|
||||
.iter()
|
||||
.find_map(|elt| contains_blind_exception(semantic, elt)),
|
||||
_ => {
|
||||
let builtin_exception_type = semantic.resolve_builtin_symbol(expr)?;
|
||||
matches!(builtin_exception_type, "BaseException" | "Exception")
|
||||
.then(|| (builtin_exception_type, expr.range()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// BLE001
|
||||
pub(crate) fn blind_except(
|
||||
checker: &Checker,
|
||||
@@ -87,12 +103,9 @@ pub(crate) fn blind_except(
|
||||
};
|
||||
|
||||
let semantic = checker.semantic();
|
||||
let Some(builtin_exception_type) = semantic.resolve_builtin_symbol(type_) else {
|
||||
let Some((builtin_exception_type, range)) = contains_blind_exception(semantic, type_) else {
|
||||
return;
|
||||
};
|
||||
if !matches!(builtin_exception_type, "BaseException" | "Exception") {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the exception is re-raised, don't flag an error.
|
||||
let mut visitor = ReraiseVisitor::new(name);
|
||||
@@ -110,9 +123,9 @@ pub(crate) fn blind_except(
|
||||
|
||||
checker.report_diagnostic(
|
||||
BlindExcept {
|
||||
name: builtin_exception_type.to_string(),
|
||||
name: builtin_exception_type.into(),
|
||||
},
|
||||
type_.range(),
|
||||
range,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -147,3 +147,93 @@ BLE.py:131:8: BLE001 Do not catch blind exception: `Exception`
|
||||
| ^^^^^^^^^ BLE001
|
||||
132 | critical("...", exc_info=None)
|
||||
|
|
||||
|
||||
BLE.py:169:9: BLE001 Do not catch blind exception: `Exception`
|
||||
|
|
||||
167 | try:
|
||||
168 | pass
|
||||
169 | except (Exception,):
|
||||
| ^^^^^^^^^ BLE001
|
||||
170 | pass
|
||||
|
|
||||
|
||||
BLE.py:174:9: BLE001 Do not catch blind exception: `Exception`
|
||||
|
|
||||
172 | try:
|
||||
173 | pass
|
||||
174 | except (Exception, ValueError):
|
||||
| ^^^^^^^^^ BLE001
|
||||
175 | pass
|
||||
|
|
||||
|
||||
BLE.py:179:21: BLE001 Do not catch blind exception: `Exception`
|
||||
|
|
||||
177 | try:
|
||||
178 | pass
|
||||
179 | except (ValueError, Exception):
|
||||
| ^^^^^^^^^ BLE001
|
||||
180 | pass
|
||||
|
|
||||
|
||||
BLE.py:184:21: BLE001 Do not catch blind exception: `Exception`
|
||||
|
|
||||
182 | try:
|
||||
183 | pass
|
||||
184 | except (ValueError, Exception) as e:
|
||||
| ^^^^^^^^^ BLE001
|
||||
185 | print(e)
|
||||
|
|
||||
|
||||
BLE.py:189:9: BLE001 Do not catch blind exception: `BaseException`
|
||||
|
|
||||
187 | try:
|
||||
188 | pass
|
||||
189 | except (BaseException, TypeError):
|
||||
| ^^^^^^^^^^^^^ BLE001
|
||||
190 | pass
|
||||
|
|
||||
|
||||
BLE.py:194:20: BLE001 Do not catch blind exception: `BaseException`
|
||||
|
|
||||
192 | try:
|
||||
193 | pass
|
||||
194 | except (TypeError, BaseException):
|
||||
| ^^^^^^^^^^^^^ BLE001
|
||||
195 | pass
|
||||
|
|
||||
|
||||
BLE.py:199:9: BLE001 Do not catch blind exception: `Exception`
|
||||
|
|
||||
197 | try:
|
||||
198 | pass
|
||||
199 | except (Exception, BaseException):
|
||||
| ^^^^^^^^^ BLE001
|
||||
200 | pass
|
||||
|
|
||||
|
||||
BLE.py:204:9: BLE001 Do not catch blind exception: `BaseException`
|
||||
|
|
||||
202 | try:
|
||||
203 | pass
|
||||
204 | except (BaseException, Exception):
|
||||
| ^^^^^^^^^^^^^ BLE001
|
||||
205 | pass
|
||||
|
|
||||
|
||||
BLE.py:210:10: BLE001 Do not catch blind exception: `Exception`
|
||||
|
|
||||
208 | try:
|
||||
209 | pass
|
||||
210 | except ((Exception, ValueError), TypeError):
|
||||
| ^^^^^^^^^ BLE001
|
||||
211 | pass
|
||||
|
|
||||
|
||||
BLE.py:215:22: BLE001 Do not catch blind exception: `BaseException`
|
||||
|
|
||||
213 | try:
|
||||
214 | pass
|
||||
215 | except (ValueError, (BaseException, TypeError)):
|
||||
| ^^^^^^^^^^^^^ BLE001
|
||||
216 | pass
|
||||
|
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::StringFlags;
|
||||
use ruff_python_ast::{
|
||||
Expr, ExprCall, ExprContext, ExprList, ExprUnaryOp, StringLiteral, StringLiteralFlags,
|
||||
StringLiteralValue, UnaryOp, str::TripleQuotes,
|
||||
@@ -116,26 +117,50 @@ pub(crate) fn split_static_string(
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_flags(elt: &str, flags: StringLiteralFlags) -> StringLiteralFlags {
|
||||
// In the ideal case we can wrap the element in _single_ quotes of the same
|
||||
// style. For example, both of these are okay:
|
||||
//
|
||||
// ```python
|
||||
// """itemA
|
||||
// itemB
|
||||
// itemC""".split() # -> ["itemA", "itemB", "itemC"]
|
||||
// ```
|
||||
//
|
||||
// ```python
|
||||
// r"""itemA
|
||||
// 'single'quoted
|
||||
// """.split() # -> [r"itemA",r"'single'quoted'"]
|
||||
// ```
|
||||
if !flags.prefix().is_raw() || !elt.contains(flags.quote_style().as_char()) {
|
||||
flags.with_triple_quotes(TripleQuotes::No)
|
||||
}
|
||||
// If we have a raw string containing a quotation mark of the same style,
|
||||
// then we have to swap the style of quotation marks used
|
||||
else if !elt.contains(flags.quote_style().opposite().as_char()) {
|
||||
flags
|
||||
.with_quote_style(flags.quote_style().opposite())
|
||||
.with_triple_quotes(TripleQuotes::No)
|
||||
} else
|
||||
// If both types of quotes are used in the raw, triple-quoted string, then
|
||||
// we are forced to either add escapes or keep the triple quotes. We opt for
|
||||
// the latter.
|
||||
{
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
|
||||
Expr::List(ExprList {
|
||||
elts: elts
|
||||
.iter()
|
||||
.map(|elt| {
|
||||
let element_flags = replace_flags(elt, flags);
|
||||
Expr::from(StringLiteral {
|
||||
value: Box::from(*elt),
|
||||
range: TextRange::default(),
|
||||
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
|
||||
// intentionally omit the triple quote flag, if set, to avoid strange
|
||||
// replacements like
|
||||
//
|
||||
// ```python
|
||||
// """
|
||||
// itemA
|
||||
// itemB
|
||||
// itemC
|
||||
// """.split() # -> ["""itemA""", """itemB""", """itemC"""]
|
||||
// ```
|
||||
flags: flags.with_triple_quotes(TripleQuotes::No),
|
||||
flags: element_flags,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
|
||||
@@ -1226,6 +1226,7 @@ SIM905.py:130:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
130 |+print([" x"])
|
||||
131 131 | print(" x ".rsplit(maxsplit=0))
|
||||
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
133 133 |
|
||||
|
||||
SIM905.py:131:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
|
||||
@@ -1244,6 +1245,8 @@ SIM905.py:131:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
131 |-print(" x ".rsplit(maxsplit=0))
|
||||
131 |+print([" x"])
|
||||
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
133 133 |
|
||||
134 134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
|
||||
|
||||
SIM905.py:132:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
|
||||
@@ -1251,6 +1254,8 @@ SIM905.py:132:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
131 | print(" x ".rsplit(maxsplit=0))
|
||||
132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
|
||||
133 |
|
||||
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
|
||||
|
|
||||
= help: Replace with list literal
|
||||
|
||||
@@ -1260,3 +1265,88 @@ SIM905.py:132:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
131 131 | print(" x ".rsplit(maxsplit=0))
|
||||
132 |-print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
132 |+print([" x"])
|
||||
133 133 |
|
||||
134 134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
|
||||
135 135 | r"""simple@example.com
|
||||
|
||||
SIM905.py:135:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
|
||||
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
|
||||
135 | / r"""simple@example.com
|
||||
136 | | very.common@example.com
|
||||
137 | | FirstName.LastName@EasierReading.org
|
||||
138 | | x@example.com
|
||||
139 | | long.email-address-with-hyphens@and.subdomains.example.com
|
||||
140 | | user.name+tag+sorting@example.com
|
||||
141 | | name/surname@example.com
|
||||
142 | | xample@s.example
|
||||
143 | | " "@example.org
|
||||
144 | | "john..doe"@example.org
|
||||
145 | | mailhost!username@example.org
|
||||
146 | | "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
|
||||
147 | | user%example.com@example.org
|
||||
148 | | user-@example.org
|
||||
149 | | I❤️CHOCOLATE@example.com
|
||||
150 | | this\ still\"not\\allowed@example.com
|
||||
151 | | stellyamburrr985@example.com
|
||||
152 | | Abc.123@example.com
|
||||
153 | | user+mailbox/department=shipping@example.com
|
||||
154 | | !#$%&'*+-/=?^_`.{|}~@example.com
|
||||
155 | | "Abc@def"@example.com
|
||||
156 | | "Fred\ Bloggs"@example.com
|
||||
157 | | "Joe.\\Blow"@example.com""".split("\n")
|
||||
| |_______________________________________^ SIM905
|
||||
|
|
||||
= help: Replace with list literal
|
||||
|
||||
ℹ Safe fix
|
||||
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
133 133 |
|
||||
134 134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
|
||||
135 |-r"""simple@example.com
|
||||
136 |-very.common@example.com
|
||||
137 |-FirstName.LastName@EasierReading.org
|
||||
138 |-x@example.com
|
||||
139 |-long.email-address-with-hyphens@and.subdomains.example.com
|
||||
140 |-user.name+tag+sorting@example.com
|
||||
141 |-name/surname@example.com
|
||||
142 |-xample@s.example
|
||||
143 |-" "@example.org
|
||||
144 |-"john..doe"@example.org
|
||||
145 |-mailhost!username@example.org
|
||||
146 |-"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
|
||||
147 |-user%example.com@example.org
|
||||
148 |-user-@example.org
|
||||
149 |-I❤️CHOCOLATE@example.com
|
||||
150 |-this\ still\"not\\allowed@example.com
|
||||
151 |-stellyamburrr985@example.com
|
||||
152 |-Abc.123@example.com
|
||||
153 |-user+mailbox/department=shipping@example.com
|
||||
154 |-!#$%&'*+-/=?^_`.{|}~@example.com
|
||||
155 |-"Abc@def"@example.com
|
||||
156 |-"Fred\ Bloggs"@example.com
|
||||
157 |-"Joe.\\Blow"@example.com""".split("\n")
|
||||
135 |+[r"simple@example.com", r"very.common@example.com", r"FirstName.LastName@EasierReading.org", r"x@example.com", r"long.email-address-with-hyphens@and.subdomains.example.com", r"user.name+tag+sorting@example.com", r"name/surname@example.com", r"xample@s.example", r'" "@example.org', r'"john..doe"@example.org', r"mailhost!username@example.org", r'"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com', r"user%example.com@example.org", r"user-@example.org", r"I❤️CHOCOLATE@example.com", r'this\ still\"not\\allowed@example.com', r"stellyamburrr985@example.com", r"Abc.123@example.com", r"user+mailbox/department=shipping@example.com", r"!#$%&'*+-/=?^_`.{|}~@example.com", r'"Abc@def"@example.com', r'"Fred\ Bloggs"@example.com', r'"Joe.\\Blow"@example.com']
|
||||
158 136 |
|
||||
159 137 |
|
||||
160 138 | r"""first
|
||||
|
||||
SIM905.py:160:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
|
|
||||
160 | / r"""first
|
||||
161 | | 'no need' to escape
|
||||
162 | | "swap" quote style
|
||||
163 | | "use' ugly triple quotes""".split("\n")
|
||||
| |_______________________________________^ SIM905
|
||||
|
|
||||
= help: Replace with list literal
|
||||
|
||||
ℹ Safe fix
|
||||
157 157 | "Joe.\\Blow"@example.com""".split("\n")
|
||||
158 158 |
|
||||
159 159 |
|
||||
160 |-r"""first
|
||||
161 |-'no need' to escape
|
||||
162 |-"swap" quote style
|
||||
163 |-"use' ugly triple quotes""".split("\n")
|
||||
160 |+[r"first", r"'no need' to escape", r'"swap" quote style', r""""use' ugly triple quotes"""]
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::checkers::ast::Checker;
|
||||
/// Checks for type annotations where `None` is not at the end of an union.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Type annotation unions are associative, meaning that the order of the elements
|
||||
/// Type annotation unions are commutative, meaning that the order of the elements
|
||||
/// does not matter. The `None` literal represents the absence of a value. For
|
||||
/// readability, it's preferred to write the more informative type expressions first.
|
||||
///
|
||||
|
||||
@@ -33,6 +33,7 @@ pub(in crate::server) enum BackgroundSchedule {
|
||||
/// while local tasks have exclusive access and can modify it as they please. Keep in mind that
|
||||
/// local tasks will **block** the main event loop, so only use local tasks if you **need**
|
||||
/// mutable state access or you need the absolute lowest latency possible.
|
||||
#[must_use]
|
||||
pub(in crate::server) enum Task {
|
||||
Background(BackgroundTaskBuilder),
|
||||
Sync(SyncTask),
|
||||
|
||||
@@ -25,6 +25,7 @@ anyhow = { workspace = true }
|
||||
argfile = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help", "string", "env"] }
|
||||
clap_complete_command = { workspace = true }
|
||||
clearscreen = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
crossbeam = { workspace = true }
|
||||
ctrlc = { version = "3.4.4" }
|
||||
|
||||
118
crates/ty/docs/rules.md
generated
118
crates/ty/docs/rules.md
generated
@@ -36,7 +36,7 @@ def test(): -> "int":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L100)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L99)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L144)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L143)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -88,7 +88,7 @@ f(int) # error
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L170)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L169)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -117,7 +117,7 @@ a = 1
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L195)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L194)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -147,7 +147,7 @@ class C(A, B): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L221)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L220)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -177,7 +177,7 @@ class B(A): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -202,7 +202,7 @@ class B(A, A): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L307)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L306)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L449)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L448)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -334,7 +334,7 @@ class C(A, B): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L473)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L472)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L339)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L338)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -445,7 +445,7 @@ an atypical memory layout.
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L493)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L492)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L533)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L532)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -496,7 +496,7 @@ a: int = ''
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1537)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1536)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L555)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L554)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -550,7 +550,7 @@ class A(42): ... # error: [invalid-base]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L606)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L605)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -575,7 +575,7 @@ with 1:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L627)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L626)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -602,7 +602,7 @@ a: str
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L649)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -644,7 +644,7 @@ except ZeroDivisionError:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L686)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L685)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -675,7 +675,7 @@ class C[U](Generic[T]): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L712)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L711)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -708,7 +708,7 @@ def f(t: TypeVar("U")): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L761)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L760)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -740,7 +740,7 @@ class B(metaclass=f): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L787)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -788,7 +788,7 @@ def foo(x: int) -> int: ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L831)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L830)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -812,7 +812,7 @@ def f(a: int = ''): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L421)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L420)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -844,7 +844,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L851)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L850)
|
||||
</small>
|
||||
|
||||
Checks for `raise` statements that raise non-exceptions or use invalid
|
||||
@@ -891,7 +891,7 @@ def g():
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L514)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L513)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -914,7 +914,7 @@ def func() -> int:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L894)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L893)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -968,7 +968,7 @@ TODO #14889
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L740)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -993,7 +993,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L933)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L932)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1021,7 +1021,7 @@ TYPE_CHECKING = ''
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L957)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1049,7 +1049,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1009)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1008)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1081,7 +1081,7 @@ f(10) # Error
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L980)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1113,7 +1113,7 @@ class C:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1037)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1036)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1146,7 +1146,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1066)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1065)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1169,7 +1169,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1084)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1196,7 +1196,7 @@ func("string") # error: [no-matching-overload]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1108)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1107)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1218,7 +1218,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1126)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1125)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1242,7 +1242,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1177)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1296,7 +1296,7 @@ def test(): -> "int":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1513)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1512)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1324,7 +1324,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1268)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1267)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1351,7 +1351,7 @@ class B(A): ... # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1313)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1376,7 +1376,7 @@ f("foo") # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1291)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1402,7 +1402,7 @@ def _(x: int):
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1334)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1333)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1446,7 +1446,7 @@ class A:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1391)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1390)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1471,7 +1471,7 @@ f(x=1, y=2) # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1497,7 +1497,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1434)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1433)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1520,7 +1520,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1452)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1543,7 +1543,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1146)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1145)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1578,7 +1578,7 @@ b1 < b2 < b1 # exception raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1604,7 +1604,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1494)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1493)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1627,7 +1627,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L265)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L264)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1680,7 +1680,7 @@ a = 20 / 0 # type: ignore
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1197)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1706,7 +1706,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L118)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L117)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1736,7 +1736,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1220)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1219)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1766,7 +1766,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1565)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1791,7 +1791,7 @@ cast(int, f()) # Redundant
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1373)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1372)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1842,7 +1842,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1586)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1585)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1896,7 +1896,7 @@ def g():
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L573)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L572)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1933,7 +1933,7 @@ class D(C): ... # error: [unsupported-base]
|
||||
<small>
|
||||
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L247)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L246)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1955,7 +1955,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
||||
<small>
|
||||
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1245)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
|
||||
@@ -22,12 +22,13 @@ use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::max_parallelism;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use salsa::Database;
|
||||
use ty_project::metadata::options::ProjectOptionsOverrides;
|
||||
use ty_project::watch::ProjectWatcher;
|
||||
use ty_project::{Db, watch};
|
||||
use ty_project::{CollectReporter, Db, watch};
|
||||
use ty_project::{ProjectDatabase, ProjectMetadata};
|
||||
use ty_server::run_server;
|
||||
|
||||
@@ -238,11 +239,6 @@ impl MainLoop {
|
||||
})?;
|
||||
|
||||
self.watcher = Some(ProjectWatcher::new(watcher, db));
|
||||
|
||||
// Do not show progress bars with `--watch`, indicatif does not seem to
|
||||
// handle cancelling independent progress bars very well.
|
||||
// TODO(zanieb): We can probably use `MultiProgress` to handle this case in the future.
|
||||
self.printer = self.printer.with_no_progress();
|
||||
self.run(db)?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
@@ -265,6 +261,9 @@ impl MainLoop {
|
||||
let mut revision = 0u64;
|
||||
|
||||
while let Ok(message) = self.receiver.recv() {
|
||||
if self.watcher.is_some() {
|
||||
Printer::clear_screen()?;
|
||||
}
|
||||
match message {
|
||||
MainLoopMessage::CheckWorkspace => {
|
||||
let db = db.clone();
|
||||
@@ -273,9 +272,13 @@ impl MainLoop {
|
||||
// Spawn a new task that checks the project. This needs to be done in a separate thread
|
||||
// to prevent blocking the main loop here.
|
||||
rayon::spawn(move || {
|
||||
let mut reporter = IndicatifReporter::from(self.printer);
|
||||
let bar = reporter.bar.clone();
|
||||
|
||||
match salsa::Cancelled::catch(|| {
|
||||
let mut reporter = IndicatifReporter::from(self.printer);
|
||||
db.check_with_reporter(&mut reporter)
|
||||
db.check_with_reporter(&mut reporter);
|
||||
reporter.bar.finish();
|
||||
reporter.collector.into_sorted(&db)
|
||||
}) {
|
||||
Ok(result) => {
|
||||
// Send the result back to the main loop for printing.
|
||||
@@ -284,6 +287,7 @@ impl MainLoop {
|
||||
.unwrap();
|
||||
}
|
||||
Err(cancelled) => {
|
||||
bar.finish_and_clear();
|
||||
tracing::debug!("Check has been cancelled: {cancelled:?}");
|
||||
}
|
||||
}
|
||||
@@ -380,9 +384,7 @@ impl MainLoop {
|
||||
}
|
||||
MainLoopMessage::Exit => {
|
||||
// Cancel any pending queries and wait for them to complete.
|
||||
// TODO: Don't use Salsa internal APIs
|
||||
// [Zulip-Thread](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries)
|
||||
let _ = db.zalsa_mut();
|
||||
db.trigger_cancellation();
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
}
|
||||
@@ -395,54 +397,52 @@ impl MainLoop {
|
||||
}
|
||||
|
||||
/// A progress reporter for `ty check`.
|
||||
enum IndicatifReporter {
|
||||
/// A constructed reporter that is not yet ready, contains the target for the progress bar.
|
||||
Pending(indicatif::ProgressDrawTarget),
|
||||
struct IndicatifReporter {
|
||||
collector: CollectReporter,
|
||||
|
||||
/// A reporter that is ready, containing a progress bar to report to.
|
||||
///
|
||||
/// Initialization of the bar is deferred to [`ty_project::ProgressReporter::set_files`] so we
|
||||
/// do not initialize the bar too early as it may take a while to collect the number of files to
|
||||
/// process and we don't want to display an empty "0/0" bar.
|
||||
Initialized(indicatif::ProgressBar),
|
||||
bar: indicatif::ProgressBar,
|
||||
|
||||
printer: Printer,
|
||||
}
|
||||
|
||||
impl From<Printer> for IndicatifReporter {
|
||||
fn from(printer: Printer) -> Self {
|
||||
Self::Pending(printer.progress_target())
|
||||
Self {
|
||||
bar: indicatif::ProgressBar::hidden(),
|
||||
collector: CollectReporter::default(),
|
||||
printer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ty_project::ProgressReporter for IndicatifReporter {
|
||||
fn set_files(&mut self, files: usize) {
|
||||
let target = match std::mem::replace(
|
||||
self,
|
||||
IndicatifReporter::Pending(indicatif::ProgressDrawTarget::hidden()),
|
||||
) {
|
||||
Self::Pending(target) => target,
|
||||
Self::Initialized(_) => panic!("The progress reporter should only be initialized once"),
|
||||
};
|
||||
self.collector.set_files(files);
|
||||
|
||||
let bar = indicatif::ProgressBar::with_draw_target(Some(files as u64), target);
|
||||
bar.set_style(
|
||||
self.bar.set_length(files as u64);
|
||||
self.bar.set_message("Checking");
|
||||
self.bar.set_style(
|
||||
indicatif::ProgressStyle::with_template(
|
||||
"{msg:8.dim} {bar:60.green/dim} {pos}/{len} files",
|
||||
)
|
||||
.unwrap()
|
||||
.progress_chars("--"),
|
||||
);
|
||||
bar.set_message("Checking");
|
||||
*self = Self::Initialized(bar);
|
||||
self.bar.set_draw_target(self.printer.progress_target());
|
||||
}
|
||||
|
||||
fn report_file(&self, _file: &ruff_db::files::File) {
|
||||
match self {
|
||||
IndicatifReporter::Initialized(progress_bar) => {
|
||||
progress_bar.inc(1);
|
||||
}
|
||||
IndicatifReporter::Pending(_) => {
|
||||
panic!("`report_file` called before `set_files`")
|
||||
}
|
||||
}
|
||||
fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
|
||||
self.collector.report_checked_file(db, file, diagnostics);
|
||||
self.bar.inc(1);
|
||||
}
|
||||
|
||||
fn report_diagnostics(&mut self, db: &dyn Db, diagnostics: Vec<Diagnostic>) {
|
||||
self.collector.report_diagnostics(db, diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::io::StdoutLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use indicatif::ProgressDrawTarget;
|
||||
|
||||
use crate::logging::VerbosityLevel;
|
||||
@@ -11,14 +12,6 @@ pub(crate) struct Printer {
|
||||
}
|
||||
|
||||
impl Printer {
|
||||
#[must_use]
|
||||
pub(crate) fn with_no_progress(self) -> Self {
|
||||
Self {
|
||||
verbosity: self.verbosity,
|
||||
no_progress: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn with_verbosity(self, verbosity: VerbosityLevel) -> Self {
|
||||
Self {
|
||||
@@ -109,6 +102,11 @@ impl Printer {
|
||||
pub(crate) fn stream_for_details(self) -> Stdout {
|
||||
self.stdout_general()
|
||||
}
|
||||
|
||||
pub(crate) fn clear_screen() -> Result<()> {
|
||||
clearscreen::clear()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@@ -724,3 +724,231 @@ fn invalid_exclude_pattern() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that ty works correctly with Bazel's symlinked file structure
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn bazel_symlinked_files() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
// Original source files in the project
|
||||
(
|
||||
"main.py",
|
||||
r#"
|
||||
import library
|
||||
|
||||
result = library.process_data()
|
||||
print(undefined_var) # error: unresolved-reference
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"library.py",
|
||||
r#"
|
||||
def process_data():
|
||||
return missing_value # error: unresolved-reference
|
||||
"#,
|
||||
),
|
||||
// Another source file that won't be symlinked
|
||||
(
|
||||
"other.py",
|
||||
r#"
|
||||
print(other_undefined) # error: unresolved-reference
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
// Create Bazel-style symlinks pointing to the actual source files
|
||||
// Bazel typically creates symlinks in bazel-out/k8-fastbuild/bin/ that point to actual sources
|
||||
std::fs::create_dir_all(case.project_dir.join("bazel-out/k8-fastbuild/bin"))?;
|
||||
|
||||
// Use absolute paths to ensure the symlinks work correctly
|
||||
case.write_symlink(
|
||||
case.project_dir.join("main.py"),
|
||||
"bazel-out/k8-fastbuild/bin/main.py",
|
||||
)?;
|
||||
case.write_symlink(
|
||||
case.project_dir.join("library.py"),
|
||||
"bazel-out/k8-fastbuild/bin/library.py",
|
||||
)?;
|
||||
|
||||
// Change to the bazel-out directory and run ty from there
|
||||
// The symlinks should be followed and errors should be found
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir.join("bazel-out/k8-fastbuild/bin")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `missing_value` used when not defined
|
||||
--> library.py:3:12
|
||||
|
|
||||
2 | def process_data():
|
||||
3 | return missing_value # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
error[unresolved-reference]: Name `undefined_var` used when not defined
|
||||
--> main.py:5:7
|
||||
|
|
||||
4 | result = library.process_data()
|
||||
5 | print(undefined_var) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// Test that when checking a specific symlinked file from the bazel-out directory, it works correctly
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir.join("bazel-out/k8-fastbuild/bin")).arg("main.py"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `undefined_var` used when not defined
|
||||
--> main.py:5:7
|
||||
|
|
||||
4 | result = library.process_data()
|
||||
5 | print(undefined_var) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that exclude patterns match on symlink source names, not target names
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn exclude_symlink_source_not_target() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
// Target files with generic names
|
||||
(
|
||||
"src/module.py",
|
||||
r#"
|
||||
def process():
|
||||
return undefined_var # error: unresolved-reference
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"src/utils.py",
|
||||
r#"
|
||||
def helper():
|
||||
return missing_value # error: unresolved-reference
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"regular.py",
|
||||
r#"
|
||||
print(regular_undefined) # error: unresolved-reference
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
// Create symlinks with names that differ from their targets
|
||||
// This simulates build systems that rename files during symlinking
|
||||
case.write_symlink("src/module.py", "generated_module.py")?;
|
||||
case.write_symlink("src/utils.py", "generated_utils.py")?;
|
||||
|
||||
// Exclude pattern should match on the symlink name (generated_*), not the target name
|
||||
assert_cmd_snapshot!(case.command().arg("--exclude").arg("generated_*.py"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `regular_undefined` used when not defined
|
||||
--> regular.py:2:7
|
||||
|
|
||||
2 | print(regular_undefined) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
error[unresolved-reference]: Name `undefined_var` used when not defined
|
||||
--> src/module.py:3:12
|
||||
|
|
||||
2 | def process():
|
||||
3 | return undefined_var # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
error[unresolved-reference]: Name `missing_value` used when not defined
|
||||
--> src/utils.py:3:12
|
||||
|
|
||||
2 | def helper():
|
||||
3 | return missing_value # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 3 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// Exclude pattern on target path should not affect symlinks with different names
|
||||
assert_cmd_snapshot!(case.command().arg("--exclude").arg("src/*.py"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `undefined_var` used when not defined
|
||||
--> generated_module.py:3:12
|
||||
|
|
||||
2 | def process():
|
||||
3 | return undefined_var # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
error[unresolved-reference]: Name `missing_value` used when not defined
|
||||
--> generated_utils.py:3:12
|
||||
|
|
||||
2 | def helper():
|
||||
3 | return missing_value # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
error[unresolved-reference]: Name `regular_undefined` used when not defined
|
||||
--> regular.py:2:7
|
||||
|
|
||||
2 | print(regular_undefined) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 3 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// Test that explicitly passing a symlink always checks it, even if excluded
|
||||
assert_cmd_snapshot!(case.command().arg("--exclude").arg("generated_*.py").arg("generated_module.py"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `undefined_var` used when not defined
|
||||
--> generated_module.py:3:12
|
||||
|
|
||||
2 | def process():
|
||||
3 | return undefined_var # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -863,10 +863,18 @@ fn overrides_unknown_rules() -> anyhow::Result<()> {
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^
|
||||
|
|
||||
info: rule `division-by-zero` was selected in the configuration file
|
||||
|
||||
warning[unknown-rule]: Unknown lint rule `division-by-zer`
|
||||
--> pyproject.toml:10:1
|
||||
|
|
||||
@@ -876,14 +884,6 @@ fn overrides_unknown_rules() -> anyhow::Result<()> {
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^
|
||||
|
|
||||
info: rule `division-by-zero` was selected in the configuration file
|
||||
|
||||
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
--> tests/test_main.py:2:5
|
||||
|
|
||||
@@ -896,7 +896,7 @@ fn overrides_unknown_rules() -> anyhow::Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
"#);
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1233,28 +1233,28 @@ quux.<CURSOR>
|
||||
baz :: Unknown | Literal[3]
|
||||
foo :: Unknown | Literal[1]
|
||||
__annotations__ :: dict[str, Any]
|
||||
__class__ :: type
|
||||
__delattr__ :: bound method object.__delattr__(name: str, /) -> None
|
||||
__class__ :: type[Quux]
|
||||
__delattr__ :: bound method Quux.__delattr__(name: str, /) -> None
|
||||
__dict__ :: dict[str, Any]
|
||||
__dir__ :: bound method object.__dir__() -> Iterable[str]
|
||||
__dir__ :: bound method Quux.__dir__() -> Iterable[str]
|
||||
__doc__ :: str | None
|
||||
__eq__ :: bound method object.__eq__(value: object, /) -> bool
|
||||
__format__ :: bound method object.__format__(format_spec: str, /) -> str
|
||||
__getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
|
||||
__getstate__ :: bound method object.__getstate__() -> object
|
||||
__hash__ :: bound method object.__hash__() -> int
|
||||
__eq__ :: bound method Quux.__eq__(value: object, /) -> bool
|
||||
__format__ :: bound method Quux.__format__(format_spec: str, /) -> str
|
||||
__getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any
|
||||
__getstate__ :: bound method Quux.__getstate__() -> object
|
||||
__hash__ :: bound method Quux.__hash__() -> int
|
||||
__init__ :: bound method Quux.__init__() -> Unknown
|
||||
__init_subclass__ :: bound method object.__init_subclass__() -> None
|
||||
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
|
||||
__module__ :: str
|
||||
__ne__ :: bound method object.__ne__(value: object, /) -> bool
|
||||
__new__ :: bound method object.__new__() -> Self@object
|
||||
__reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method object.__repr__() -> str
|
||||
__setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
|
||||
__sizeof__ :: bound method object.__sizeof__() -> int
|
||||
__str__ :: bound method object.__str__() -> str
|
||||
__subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
|
||||
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
||||
__new__ :: bound method Quux.__new__() -> Self@object
|
||||
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method Quux.__repr__() -> str
|
||||
__setattr__ :: bound method Quux.__setattr__(name: str, value: Any, /) -> None
|
||||
__sizeof__ :: bound method Quux.__sizeof__() -> int
|
||||
__str__ :: bound method Quux.__str__() -> str
|
||||
__subclasshook__ :: bound method type[Quux].__subclasshook__(subclass: type, /) -> bool
|
||||
");
|
||||
}
|
||||
|
||||
@@ -1278,28 +1278,28 @@ quux.b<CURSOR>
|
||||
baz :: Unknown | Literal[3]
|
||||
foo :: Unknown | Literal[1]
|
||||
__annotations__ :: dict[str, Any]
|
||||
__class__ :: type
|
||||
__delattr__ :: bound method object.__delattr__(name: str, /) -> None
|
||||
__class__ :: type[Quux]
|
||||
__delattr__ :: bound method Quux.__delattr__(name: str, /) -> None
|
||||
__dict__ :: dict[str, Any]
|
||||
__dir__ :: bound method object.__dir__() -> Iterable[str]
|
||||
__dir__ :: bound method Quux.__dir__() -> Iterable[str]
|
||||
__doc__ :: str | None
|
||||
__eq__ :: bound method object.__eq__(value: object, /) -> bool
|
||||
__format__ :: bound method object.__format__(format_spec: str, /) -> str
|
||||
__getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
|
||||
__getstate__ :: bound method object.__getstate__() -> object
|
||||
__hash__ :: bound method object.__hash__() -> int
|
||||
__eq__ :: bound method Quux.__eq__(value: object, /) -> bool
|
||||
__format__ :: bound method Quux.__format__(format_spec: str, /) -> str
|
||||
__getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any
|
||||
__getstate__ :: bound method Quux.__getstate__() -> object
|
||||
__hash__ :: bound method Quux.__hash__() -> int
|
||||
__init__ :: bound method Quux.__init__() -> Unknown
|
||||
__init_subclass__ :: bound method object.__init_subclass__() -> None
|
||||
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
|
||||
__module__ :: str
|
||||
__ne__ :: bound method object.__ne__(value: object, /) -> bool
|
||||
__new__ :: bound method object.__new__() -> Self@object
|
||||
__reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method object.__repr__() -> str
|
||||
__setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
|
||||
__sizeof__ :: bound method object.__sizeof__() -> int
|
||||
__str__ :: bound method object.__str__() -> str
|
||||
__subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
|
||||
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
||||
__new__ :: bound method Quux.__new__() -> Self@object
|
||||
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method Quux.__repr__() -> str
|
||||
__setattr__ :: bound method Quux.__setattr__(name: str, value: Any, /) -> None
|
||||
__sizeof__ :: bound method Quux.__sizeof__() -> int
|
||||
__str__ :: bound method Quux.__str__() -> str
|
||||
__subclasshook__ :: bound method type[Quux].__subclasshook__(subclass: type, /) -> bool
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,14 @@ use std::{cmp, fmt};
|
||||
|
||||
pub use self::changes::ChangeResult;
|
||||
use crate::metadata::settings::file_settings;
|
||||
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
|
||||
use crate::{CollectReporter, DEFAULT_LINT_REGISTRY};
|
||||
use crate::{ProgressReporter, Project, ProjectMetadata};
|
||||
use ruff_db::Db as SourceDb;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use salsa::{Event, Setter};
|
||||
use salsa::{Database, Event, Setter};
|
||||
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use ty_python_semantic::{Db as SemanticDb, Program};
|
||||
|
||||
@@ -34,7 +33,7 @@ pub struct ProjectDatabase {
|
||||
// or the "trick" to get a mutable `Arc` in `Self::system_mut` is no longer guaranteed to work.
|
||||
system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
|
||||
|
||||
// IMPORTANT: This field must be the last because we use `zalsa_mut` (drops all other storage references)
|
||||
// IMPORTANT: This field must be the last because we use `trigger_cancellation` (drops all other storage references)
|
||||
// to drop all other references to the database, which gives us exclusive access to other `Arc`s stored on this db.
|
||||
// However, for this to work it's important that the `storage` is dropped AFTER any `Arc` that
|
||||
// we try to mutably borrow using `Arc::get_mut` (like `system`).
|
||||
@@ -87,7 +86,9 @@ impl ProjectDatabase {
|
||||
///
|
||||
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
|
||||
pub fn check(&self) -> Vec<Diagnostic> {
|
||||
self.project().check(self, &mut DummyReporter)
|
||||
let mut collector = CollectReporter::default();
|
||||
self.project().check(self, &mut collector);
|
||||
collector.into_sorted(self)
|
||||
}
|
||||
|
||||
/// Checks the files in the project and its dependencies, using the given reporter.
|
||||
@@ -95,8 +96,8 @@ impl ProjectDatabase {
|
||||
/// Use [`set_check_mode`] to update the check mode.
|
||||
///
|
||||
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
|
||||
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
|
||||
self.project().check(self, reporter)
|
||||
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) {
|
||||
self.project().check(self, reporter);
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
@@ -114,12 +115,11 @@ impl ProjectDatabase {
|
||||
///
|
||||
/// WARNING: Triggers a new revision, canceling other database handles. This can lead to deadlock.
|
||||
pub fn system_mut(&mut self) -> &mut dyn System {
|
||||
// TODO: Use a more official method to cancel other queries.
|
||||
// https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries
|
||||
let _ = self.zalsa_mut();
|
||||
self.trigger_cancellation();
|
||||
|
||||
Arc::get_mut(&mut self.system)
|
||||
.expect("ref count should be 1 because `zalsa_mut` drops all other DB references.")
|
||||
Arc::get_mut(&mut self.system).expect(
|
||||
"ref count should be 1 because `trigger_cancellation` drops all other DB references.",
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a [`SalsaMemoryDump`] that can be use to dump Salsa memory usage information
|
||||
|
||||
@@ -58,17 +58,16 @@ impl IndexedFiles {
|
||||
///
|
||||
/// The changes are automatically written back to the database once the view is dropped.
|
||||
pub(super) fn indexed_mut(db: &mut dyn Db, project: Project) -> Option<IndexedMut> {
|
||||
// Calling `zalsa_mut` cancels all pending salsa queries. This ensures that there are no pending
|
||||
// reads to the file set.
|
||||
// TODO: Use a non-internal API instead https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries
|
||||
let _ = db.as_dyn_database_mut().zalsa_mut();
|
||||
// Calling `trigger_cancellation` cancels all pending salsa queries. This ensures that there are no pending
|
||||
// reads to the file set (this `db` is the only alive db).
|
||||
db.trigger_cancellation();
|
||||
|
||||
// Replace the state with lazy. The `IndexedMut` guard restores the state
|
||||
// to `State::Indexed` or sets a new `PackageFiles` when it gets dropped to ensure the state
|
||||
// is restored to how it has been before replacing the value.
|
||||
//
|
||||
// It isn't necessary to hold on to the lock after this point:
|
||||
// * The above call to `zalsa_mut` guarantees that there's exactly **one** DB reference.
|
||||
// * The above call to `trigger_cancellation` guarantees that there's exactly **one** DB reference.
|
||||
// * `Indexed` has a `'db` lifetime, and this method requires a `&mut db`.
|
||||
// This means that there can't be any pending reference to `Indexed` because Rust
|
||||
// doesn't allow borrowing `db` as mutable (to call this method) and immutable (`Indexed<'db>`) at the same time.
|
||||
|
||||
@@ -127,17 +127,46 @@ pub trait ProgressReporter: Send + Sync {
|
||||
/// Initialize the reporter with the number of files.
|
||||
fn set_files(&mut self, files: usize);
|
||||
|
||||
/// Report the completion of a given file.
|
||||
fn report_file(&self, file: &File);
|
||||
/// Report the completion of checking a given file along with its diagnostics.
|
||||
fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]);
|
||||
|
||||
/// Reports settings or IO related diagnostics. The diagnostics
|
||||
/// can belong to different files or no file at all.
|
||||
/// But it's never a file for which [`Self::report_checked_file`] gets called.
|
||||
fn report_diagnostics(&mut self, db: &dyn Db, diagnostics: Vec<Diagnostic>);
|
||||
}
|
||||
|
||||
/// A no-op implementation of [`ProgressReporter`].
|
||||
/// Reporter that collects all diagnostics into a `Vec`.
|
||||
#[derive(Default)]
|
||||
pub struct DummyReporter;
|
||||
pub struct CollectReporter(std::sync::Mutex<Vec<Diagnostic>>);
|
||||
|
||||
impl ProgressReporter for DummyReporter {
|
||||
impl CollectReporter {
|
||||
pub fn into_sorted(self, db: &dyn Db) -> Vec<Diagnostic> {
|
||||
let mut diagnostics = self.0.into_inner().unwrap();
|
||||
diagnostics.sort_by(|left, right| {
|
||||
left.rendering_sort_key(db)
|
||||
.cmp(&right.rendering_sort_key(db))
|
||||
});
|
||||
diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgressReporter for CollectReporter {
|
||||
fn set_files(&mut self, _files: usize) {}
|
||||
fn report_file(&self, _file: &File) {}
|
||||
fn report_checked_file(&self, _db: &dyn Db, _file: File, diagnostics: &[Diagnostic]) {
|
||||
if diagnostics.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.extend(diagnostics.iter().map(Clone::clone));
|
||||
}
|
||||
|
||||
fn report_diagnostics(&mut self, _db: &dyn Db, diagnostics: Vec<Diagnostic>) {
|
||||
self.0.get_mut().unwrap().extend(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
@@ -225,11 +254,7 @@ impl Project {
|
||||
}
|
||||
|
||||
/// Checks the project and its dependencies according to the project's check mode.
|
||||
pub(crate) fn check(
|
||||
self,
|
||||
db: &ProjectDatabase,
|
||||
reporter: &mut dyn ProgressReporter,
|
||||
) -> Vec<Diagnostic> {
|
||||
pub(crate) fn check(self, db: &ProjectDatabase, reporter: &mut dyn ProgressReporter) {
|
||||
let project_span = tracing::debug_span!("Project::check");
|
||||
let _span = project_span.enter();
|
||||
|
||||
@@ -239,12 +264,11 @@ impl Project {
|
||||
name = self.name(db)
|
||||
);
|
||||
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
diagnostics.extend(
|
||||
self.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(OptionDiagnostic::to_diagnostic),
|
||||
);
|
||||
let mut diagnostics: Vec<Diagnostic> = self
|
||||
.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(OptionDiagnostic::to_diagnostic)
|
||||
.collect();
|
||||
|
||||
let files = ProjectFiles::new(db, self);
|
||||
reporter.set_files(files.len());
|
||||
@@ -256,19 +280,19 @@ impl Project {
|
||||
.map(IOErrorDiagnostic::to_diagnostic),
|
||||
);
|
||||
|
||||
reporter.report_diagnostics(db, diagnostics);
|
||||
|
||||
let open_files = self.open_files(db);
|
||||
let check_start = ruff_db::Instant::now();
|
||||
let file_diagnostics = std::sync::Mutex::new(vec![]);
|
||||
|
||||
{
|
||||
let db = db.clone();
|
||||
let file_diagnostics = &file_diagnostics;
|
||||
let project_span = &project_span;
|
||||
let reporter = &reporter;
|
||||
|
||||
rayon::scope(move |scope| {
|
||||
for file in &files {
|
||||
let db = db.clone();
|
||||
let reporter = &*reporter;
|
||||
scope.spawn(move |_| {
|
||||
let check_file_span =
|
||||
tracing::debug_span!(parent: project_span, "check_file", ?file);
|
||||
@@ -276,10 +300,7 @@ impl Project {
|
||||
|
||||
match check_file_impl(&db, file) {
|
||||
Ok(diagnostics) => {
|
||||
file_diagnostics
|
||||
.lock()
|
||||
.unwrap()
|
||||
.extend(diagnostics.iter().map(Clone::clone));
|
||||
reporter.report_checked_file(&db, file, diagnostics);
|
||||
|
||||
// This is outside `check_file_impl` to avoid that opening or closing
|
||||
// a file invalidates the `check_file_impl` query of every file!
|
||||
@@ -295,28 +316,22 @@ impl Project {
|
||||
}
|
||||
}
|
||||
Err(io_error) => {
|
||||
file_diagnostics.lock().unwrap().push(io_error.clone());
|
||||
reporter.report_checked_file(
|
||||
&db,
|
||||
file,
|
||||
std::slice::from_ref(io_error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reporter.report_file(&file);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
"Checking all files took {:.3}s",
|
||||
check_start.elapsed().as_secs_f64(),
|
||||
);
|
||||
|
||||
let mut file_diagnostics = file_diagnostics.into_inner().unwrap();
|
||||
file_diagnostics.sort_by(|left, right| {
|
||||
left.rendering_sort_key(db)
|
||||
.cmp(&right.rendering_sort_key(db))
|
||||
});
|
||||
diagnostics.extend(file_diagnostics);
|
||||
diagnostics
|
||||
}
|
||||
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
||||
|
||||
@@ -173,21 +173,30 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
Ok(entry) => {
|
||||
// Skip excluded directories unless they were explicitly passed to the walker
|
||||
// (which is the case passed to `ty check <paths>`).
|
||||
if entry.file_type().is_directory() && entry.depth() > 0 {
|
||||
return match self.filter.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown) {
|
||||
IncludeResult::Included => WalkState::Continue,
|
||||
IncludeResult::Excluded => {
|
||||
tracing::debug!("Skipping directory '{path}' because it is excluded by a default or `src.exclude` pattern", path=entry.path());
|
||||
WalkState::Skip
|
||||
},
|
||||
IncludeResult::NotIncluded => {
|
||||
tracing::debug!("Skipping directory `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI", path=entry.path());
|
||||
WalkState::Skip
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if entry.file_type().is_file() {
|
||||
if entry.file_type().is_directory() {
|
||||
if entry.depth() > 0 {
|
||||
let directory_included = self
|
||||
.filter
|
||||
.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown);
|
||||
return match directory_included {
|
||||
IncludeResult::Included => WalkState::Continue,
|
||||
IncludeResult::Excluded => {
|
||||
tracing::debug!(
|
||||
"Skipping directory '{path}' because it is excluded by a default or `src.exclude` pattern",
|
||||
path=entry.path()
|
||||
);
|
||||
WalkState::Skip
|
||||
},
|
||||
IncludeResult::NotIncluded => {
|
||||
tracing::debug!(
|
||||
"Skipping directory `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI",
|
||||
path=entry.path()
|
||||
);
|
||||
WalkState::Skip
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Ignore any non python files to avoid creating too many entries in `Files`.
|
||||
if entry
|
||||
.path()
|
||||
@@ -201,14 +210,23 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
// For all files, except the ones that were explicitly passed to the walker (CLI),
|
||||
// check if they're included in the project.
|
||||
if entry.depth() > 0 {
|
||||
match self.filter.is_file_included(entry.path(), GlobFilterCheckMode::TopDown) {
|
||||
match self
|
||||
.filter
|
||||
.is_file_included(entry.path(), GlobFilterCheckMode::TopDown)
|
||||
{
|
||||
IncludeResult::Included => {},
|
||||
IncludeResult::Excluded => {
|
||||
tracing::debug!("Ignoring file `{path}` because it is excluded by a default or `src.exclude` pattern.", path=entry.path());
|
||||
tracing::debug!(
|
||||
"Ignoring file `{path}` because it is excluded by a default or `src.exclude` pattern.",
|
||||
path=entry.path()
|
||||
);
|
||||
return WalkState::Continue;
|
||||
},
|
||||
IncludeResult::NotIncluded => {
|
||||
tracing::debug!("Ignoring file `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI.", path=entry.path());
|
||||
tracing::debug!(
|
||||
"Ignoring file `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI.",
|
||||
path=entry.path()
|
||||
);
|
||||
return WalkState::Continue;
|
||||
},
|
||||
}
|
||||
|
||||
@@ -18,5 +18,5 @@ def append_int(*args: *Ts) -> tuple[*Ts, int]:
|
||||
return (*args, 1)
|
||||
|
||||
# TODO should be tuple[Literal[True], Literal["a"], int]
|
||||
reveal_type(append_int(True, "a")) # revealed: @Todo(PEP 646)
|
||||
reveal_type(append_int(True, "a")) # revealed: tuple[@Todo(PEP 646), ...]
|
||||
```
|
||||
|
||||
@@ -17,6 +17,7 @@ Alias: TypeAlias = int
|
||||
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
|
||||
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
|
||||
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
|
||||
return args
|
||||
|
||||
def g() -> TypeGuard[int]: ...
|
||||
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
|
||||
|
||||
@@ -59,7 +59,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
|
||||
reveal_type(e) # revealed: tuple[str, ...]
|
||||
|
||||
reveal_type(f) # revealed: tuple[str, *tuple[int, ...], bytes]
|
||||
reveal_type(g) # revealed: @Todo(PEP 646)
|
||||
reveal_type(g) # revealed: tuple[@Todo(PEP 646), ...]
|
||||
|
||||
reveal_type(h) # revealed: tuple[list[int], list[int]]
|
||||
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||
|
||||
@@ -1916,7 +1916,7 @@ d = True
|
||||
reveal_type(d.__class__) # revealed: <class 'bool'>
|
||||
|
||||
e = (42, 42)
|
||||
reveal_type(e.__class__) # revealed: <class 'tuple[Literal[42], Literal[42]]'>
|
||||
reveal_type(e.__class__) # revealed: type[tuple[Literal[42], Literal[42]]]
|
||||
|
||||
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
|
||||
reveal_type(a.__class__) # revealed: type[int]
|
||||
|
||||
@@ -350,6 +350,25 @@ def _(target: None | Foo):
|
||||
reveal_type(y) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
## `as` patterns
|
||||
|
||||
```py
|
||||
def _(target: int | str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case 1 as x:
|
||||
y = 2
|
||||
reveal_type(x) # revealed: @Todo(`match` pattern definition types)
|
||||
case "foo" as x:
|
||||
y = 3
|
||||
reveal_type(x) # revealed: @Todo(`match` pattern definition types)
|
||||
case _:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## Guard with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
|
||||
@@ -338,3 +338,32 @@ def no_invalid_return_diagnostic_here_either[T](x: A[T]) -> ASub[T]:
|
||||
# is null and void (and therefore we don't emit a diagnostic)
|
||||
return x
|
||||
```
|
||||
|
||||
## More `match` pattern types
|
||||
|
||||
### `as` patterns
|
||||
|
||||
```py
|
||||
from typing import assert_never
|
||||
|
||||
def as_pattern_exhaustive(subject: int | str):
|
||||
match subject:
|
||||
case int() as x:
|
||||
pass
|
||||
case str() as y:
|
||||
pass
|
||||
case _:
|
||||
no_diagnostic_here
|
||||
|
||||
assert_never(subject)
|
||||
|
||||
def as_pattern_non_exhaustive(subject: int | str):
|
||||
match subject:
|
||||
case int() as x:
|
||||
pass
|
||||
case _:
|
||||
this_should_be_an_error # error: [unresolved-reference]
|
||||
|
||||
# this diagnostic is correct: the inferred type of `subject` is `str`
|
||||
assert_never(subject) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
@@ -208,6 +208,36 @@ static_assert(has_member(Answer, "YES"))
|
||||
static_assert(has_member(Answer, "__members__"))
|
||||
```
|
||||
|
||||
### TypedDicts
|
||||
|
||||
```py
|
||||
from ty_extensions import has_member, static_assert
|
||||
from typing import TypedDict
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
age: int | None
|
||||
|
||||
static_assert(not has_member(Person, "name"))
|
||||
static_assert(has_member(Person, "keys"))
|
||||
static_assert(has_member(Person, "__total__"))
|
||||
|
||||
def _(person: Person):
|
||||
static_assert(not has_member(person, "name"))
|
||||
static_assert(not has_member(person, "__total__"))
|
||||
static_assert(has_member(person, "keys"))
|
||||
|
||||
# type(person) is `dict` at runtime, so `__total__` is not available:
|
||||
static_assert(not has_member(type(person), "name"))
|
||||
static_assert(not has_member(type(person), "__total__"))
|
||||
static_assert(has_member(type(person), "keys"))
|
||||
|
||||
def _(t_person: type[Person]):
|
||||
static_assert(not has_member(t_person, "name"))
|
||||
static_assert(has_member(t_person, "__total__"))
|
||||
static_assert(has_member(t_person, "keys"))
|
||||
```
|
||||
|
||||
### Unions
|
||||
|
||||
For unions, `ide_support::all_members` only returns members that are available on all elements of
|
||||
|
||||
@@ -182,16 +182,19 @@ class ContextManagerThatMightNotRunToCompletion:
|
||||
with ContextManagerThatMightNotRunToCompletion() as L:
|
||||
U = ...
|
||||
|
||||
match 42:
|
||||
def get_object() -> object:
|
||||
pass
|
||||
|
||||
match get_object():
|
||||
case {"something": M}:
|
||||
...
|
||||
case [*N]:
|
||||
...
|
||||
case [O]:
|
||||
...
|
||||
case P | Q: # error: [invalid-syntax] "name capture `P` makes remaining patterns unreachable"
|
||||
case I(foo=R):
|
||||
...
|
||||
case object(foo=R):
|
||||
case P | Q:
|
||||
...
|
||||
|
||||
match 56:
|
||||
|
||||
@@ -346,7 +346,10 @@ l: list[str | Literal[1] | None] = [None]
|
||||
|
||||
def f(x: str | Literal[1] | None):
|
||||
class C:
|
||||
if x is not None: # TODO: should be an unresolved-reference error
|
||||
# If we try to access a variable in a class before it has been defined,
|
||||
# the lookup will fall back to global.
|
||||
# error: [unresolved-reference]
|
||||
if x is not None:
|
||||
def _():
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: str | None
|
||||
@@ -360,11 +363,12 @@ def f(x: str | Literal[1] | None):
|
||||
x = None
|
||||
|
||||
def _():
|
||||
# No narrowing is performed on unresolved references.
|
||||
# error: [unresolved-reference]
|
||||
if x is not None:
|
||||
def _():
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Never
|
||||
reveal_type(x) # revealed: None
|
||||
x = None
|
||||
|
||||
def f(const: str | Literal[1] | None):
|
||||
@@ -372,8 +376,7 @@ def f(const: str | Literal[1] | None):
|
||||
if const is not None:
|
||||
def _():
|
||||
if const != 1:
|
||||
# TODO: should be `str`
|
||||
reveal_type(const) # revealed: str | None
|
||||
reveal_type(const) # revealed: str
|
||||
|
||||
class D:
|
||||
if const != 1:
|
||||
|
||||
@@ -262,3 +262,28 @@ def f():
|
||||
global __file__ # allowed, implicit global
|
||||
global int # error: [unresolved-global] "Invalid global declaration of `int`: `int` has no declarations or bindings in the global scope"
|
||||
```
|
||||
|
||||
## References to variables before they are defined within a class scope are considered global
|
||||
|
||||
If we try to access a variable in a class before it has been defined, the lookup will fall back to
|
||||
global.
|
||||
|
||||
```py
|
||||
x: str = "a"
|
||||
|
||||
def f(x: int, y: int):
|
||||
class C:
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
class D:
|
||||
x = None
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
class E:
|
||||
reveal_type(x) # revealed: str
|
||||
x = None
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(y) # revealed: Unknown
|
||||
y = None
|
||||
```
|
||||
|
||||
@@ -119,6 +119,28 @@ def _(empty: EmptyTupleSubclass, single_element: SingleElementTupleSubclass, mix
|
||||
mixed.__class__()
|
||||
```
|
||||
|
||||
## Meta-type of tuple instances
|
||||
|
||||
The type `tuple[str, int]` does not only have exact instances of `tuple` as its inhabitants: its
|
||||
inhabitants also include any instances of subclasses of `tuple[str, int]`. As such, the meta-type of
|
||||
`tuple[str, int]` should be `type[tuple[str, int]]` rather than `<class 'tuple[str, int]'>`. The
|
||||
former accurately reflects the fact that given an instance of `tuple[str, int]`, we do not know
|
||||
exactly what the `__class__` of that instance will be: we only know that it will be a subclass of
|
||||
`tuple[str, int]`. The latter would be incorrectly precise: it would imply that all instances of
|
||||
`tuple[str, int]` have the runtime object `tuple` as their `__class__`, which isn't true.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
def f(x: tuple[int, ...], y: tuple[str, str], z: tuple[int, *tuple[str, ...], bytes]):
|
||||
reveal_type(type(x)) # revealed: type[tuple[int, ...]]
|
||||
reveal_type(type(y)) # revealed: type[tuple[str, str]]
|
||||
reveal_type(type(z)) # revealed: type[tuple[int, *tuple[str, ...], bytes]]
|
||||
```
|
||||
|
||||
## Subtyping relationships
|
||||
|
||||
The type `tuple[S1, S2]` is a subtype of `tuple[T1, T2]` if and only if `S1` is a subtype of `T1`
|
||||
|
||||
@@ -1052,8 +1052,7 @@ class FooLegacy(Generic[T]):
|
||||
class Bar[T, **P]:
|
||||
def __call__(self): ...
|
||||
|
||||
# TODO: should not error
|
||||
class BarLegacy(Generic[T, P]): # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
|
||||
class BarLegacy(Generic[T, P]):
|
||||
def __call__(self): ...
|
||||
|
||||
static_assert(is_assignable_to(Foo, Callable[..., Any]))
|
||||
@@ -1064,9 +1063,7 @@ static_assert(is_assignable_to(BarLegacy, Callable[..., Any]))
|
||||
class Spam[T]: ...
|
||||
class SpamLegacy(Generic[T]): ...
|
||||
class Eggs[T, **P]: ...
|
||||
|
||||
# TODO: should not error
|
||||
class EggsLegacy(Generic[T, P]): ... # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
|
||||
class EggsLegacy(Generic[T, P]): ...
|
||||
|
||||
static_assert(not is_assignable_to(Spam, Callable[..., Any]))
|
||||
static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any]))
|
||||
|
||||
@@ -205,3 +205,54 @@ reveal_type(bool(AmbiguousEnum2.YES)) # revealed: bool
|
||||
reveal_type(bool(CustomLenEnum.NO)) # revealed: bool
|
||||
reveal_type(bool(CustomLenEnum.YES)) # revealed: bool
|
||||
```
|
||||
|
||||
## TypedDict
|
||||
|
||||
It may be feasible to infer `Literal[True]` for some `TypedDict` types, if `{}` can definitely be
|
||||
excluded as a possible value. We currently do not attempt to do this.
|
||||
|
||||
If `{}` is the *only* possible value, we could infer `Literal[False]`. This might only be possible
|
||||
if something like <https://peps.python.org/pep-0728> is accepted, a `TypedDict` has no defined
|
||||
items, and `closed=True` is used.
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, Literal, NotRequired
|
||||
|
||||
class Normal(TypedDict):
|
||||
a: str
|
||||
b: int
|
||||
|
||||
def _(n: Normal) -> None:
|
||||
# Could be `Literal[True]`
|
||||
reveal_type(bool(n)) # revealed: bool
|
||||
|
||||
class OnlyFalsyItems(TypedDict):
|
||||
wrong: Literal[False]
|
||||
|
||||
def _(n: OnlyFalsyItems) -> None:
|
||||
# Could be `Literal[True]` (it does not matter if all items are falsy)
|
||||
reveal_type(bool(n)) # revealed: bool
|
||||
|
||||
class Empty(TypedDict):
|
||||
pass
|
||||
|
||||
def _(e: Empty) -> None:
|
||||
# This should be `bool`. `Literal[False]` would be wrong, as `Empty` can be subclassed.
|
||||
reveal_type(bool(e)) # revealed: bool
|
||||
|
||||
class AllKeysOptional(TypedDict, total=False):
|
||||
a: str
|
||||
b: int
|
||||
|
||||
def _(a: AllKeysOptional) -> None:
|
||||
# This should be `bool`. `Literal[True]` would be wrong as `{}` is a valid value.
|
||||
reveal_type(bool(a)) # revealed: bool
|
||||
|
||||
class AllKeysNotRequired(TypedDict):
|
||||
a: NotRequired[str]
|
||||
b: NotRequired[int]
|
||||
|
||||
def _(a: AllKeysNotRequired) -> None:
|
||||
# This should be `bool`. `Literal[True]` would be wrong as `{}` is a valid value.
|
||||
reveal_type(bool(a)) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -38,6 +38,29 @@ c.d = 2
|
||||
c.e = 2
|
||||
```
|
||||
|
||||
## From stubs
|
||||
|
||||
This is a regression test for a bug where we did not properly keep track of type qualifiers when
|
||||
accessed from stub files.
|
||||
|
||||
`module.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import ClassVar
|
||||
|
||||
class C:
|
||||
a: ClassVar[int]
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from module import C
|
||||
|
||||
c = C()
|
||||
c.a = 2 # error: [invalid-attribute-access]
|
||||
```
|
||||
|
||||
## Conflicting type qualifiers
|
||||
|
||||
We currently ignore conflicting qualifiers and simply union them, which is more conservative than
|
||||
|
||||
@@ -60,6 +60,8 @@ Assignments to keys are also validated:
|
||||
```py
|
||||
# TODO: this should be an error
|
||||
alice["name"] = None
|
||||
# TODO: this should be an error
|
||||
bob["name"] = None
|
||||
```
|
||||
|
||||
Assignments to non-existing keys are disallowed:
|
||||
@@ -67,6 +69,8 @@ Assignments to non-existing keys are disallowed:
|
||||
```py
|
||||
# TODO: this should be an error
|
||||
alice["extra"] = True
|
||||
# TODO: this should be an error
|
||||
bob["extra"] = True
|
||||
```
|
||||
|
||||
## Structural assignability
|
||||
@@ -123,7 +127,7 @@ dangerous(alice)
|
||||
reveal_type(alice["name"]) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Types of keys and values
|
||||
## Methods on `TypedDict`
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
@@ -133,14 +137,19 @@ class Person(TypedDict):
|
||||
age: int | None
|
||||
|
||||
def _(p: Person) -> None:
|
||||
reveal_type(p.keys()) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(p.values()) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(p.keys()) # revealed: dict_keys[str, object]
|
||||
reveal_type(p.values()) # revealed: dict_values[str, object]
|
||||
|
||||
reveal_type(p.setdefault("name", "Alice")) # revealed: @Todo(Support for `TypedDict`)
|
||||
|
||||
reveal_type(p.get("name")) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(p.get("name", "Unknown")) # revealed: @Todo(Support for `TypedDict`)
|
||||
```
|
||||
|
||||
## Unlike normal classes
|
||||
|
||||
`TypedDict` types are not like normal classes. The "attributes" can not be accessed. Neither on the
|
||||
class itself, nor on inhabitants of the type defined by the class:
|
||||
`TypedDict` types do not act like normal classes. For example, calling `type(..)` on an inhabitant
|
||||
of a `TypedDict` type will return `dict`:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
@@ -149,11 +158,28 @@ class Person(TypedDict):
|
||||
name: str
|
||||
age: int | None
|
||||
|
||||
# TODO: this should be an error
|
||||
def _(p: Person) -> None:
|
||||
reveal_type(type(p)) # revealed: <class 'dict[str, object]'>
|
||||
|
||||
reveal_type(p.__class__) # revealed: <class 'dict[str, object]'>
|
||||
```
|
||||
|
||||
Also, the "attributes" on the class definition can not be accessed. Neither on the class itself, nor
|
||||
on inhabitants of the type defined by the class:
|
||||
|
||||
```py
|
||||
# error: [unresolved-attribute] "Type `<class 'Person'>` has no attribute `name`"
|
||||
Person.name
|
||||
|
||||
# TODO: this should be an error
|
||||
Person(name="Alice", age=30).name
|
||||
def _(P: type[Person]):
|
||||
# error: [unresolved-attribute] "Type `type[Person]` has no attribute `name`"
|
||||
P.name
|
||||
|
||||
def _(p: Person) -> None:
|
||||
# error: [unresolved-attribute] "Type `Person` has no attribute `name`"
|
||||
p.name
|
||||
|
||||
type(p).name # error: [unresolved-attribute] "Type `<class 'dict[str, object]'>` has no attribute `name`"
|
||||
```
|
||||
|
||||
## Special properties
|
||||
@@ -167,9 +193,39 @@ class Person(TypedDict):
|
||||
name: str
|
||||
age: int | None
|
||||
|
||||
reveal_type(Person.__total__) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(Person.__required_keys__) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(Person.__optional_keys__) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(Person.__total__) # revealed: bool
|
||||
reveal_type(Person.__required_keys__) # revealed: frozenset[str]
|
||||
reveal_type(Person.__optional_keys__) # revealed: frozenset[str]
|
||||
```
|
||||
|
||||
These attributes can not be accessed on inhabitants:
|
||||
|
||||
```py
|
||||
def _(person: Person) -> None:
|
||||
person.__total__ # error: [unresolved-attribute]
|
||||
person.__required_keys__ # error: [unresolved-attribute]
|
||||
person.__optional_keys__ # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
Also, they can not be accessed on `type(person)`, as that would be `dict` at runtime:
|
||||
|
||||
```py
|
||||
def _(person: Person) -> None:
|
||||
type(person).__total__ # error: [unresolved-attribute]
|
||||
type(person).__required_keys__ # error: [unresolved-attribute]
|
||||
type(person).__optional_keys__ # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
But they *can* be accessed on `type[Person]`, because this function would accept the class object
|
||||
`Person` as an argument:
|
||||
|
||||
```py
|
||||
def accepts_typed_dict_class(t_person: type[Person]) -> None:
|
||||
reveal_type(t_person.__total__) # revealed: bool
|
||||
reveal_type(t_person.__required_keys__) # revealed: frozenset[str]
|
||||
reveal_type(t_person.__optional_keys__) # revealed: frozenset[str]
|
||||
|
||||
accepts_typed_dict_class(Person)
|
||||
```
|
||||
|
||||
## Subclassing
|
||||
@@ -272,6 +328,9 @@ msg = Message(id=1, content="Hello")
|
||||
OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True)
|
||||
|
||||
reveal_type(Message.__required_keys__) # revealed: @Todo(Support for `TypedDict`)
|
||||
|
||||
# TODO: this should be an error
|
||||
msg.content
|
||||
```
|
||||
|
||||
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
|
||||
|
||||
@@ -356,7 +356,9 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
for nested_symbol in self.place_tables[popped_scope_id].symbols() {
|
||||
// For the same reason, symbols declared as nonlocal or global are not recorded.
|
||||
// Also, if the enclosing scope allows its members to be modified from elsewhere, the snapshot will not be recorded.
|
||||
if self.scopes[enclosing_scope_id].visibility().is_public() {
|
||||
// (In the case of class scopes, class variables can be modified from elsewhere, but this has no effect in nested scopes,
|
||||
// as class variables are not visible to them)
|
||||
if self.scopes[enclosing_scope_id].kind().is_module() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -838,6 +840,13 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
.collect();
|
||||
PatternPredicateKind::Or(predicates)
|
||||
}
|
||||
ast::Pattern::MatchAs(pattern) => PatternPredicateKind::As(
|
||||
pattern
|
||||
.pattern
|
||||
.as_ref()
|
||||
.map(|p| Box::new(self.predicate_kind(p))),
|
||||
pattern.name.as_ref().map(|name| name.id.clone()),
|
||||
),
|
||||
_ => PatternPredicateKind::Unsupported,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_index::{Idx, IndexVec};
|
||||
use ruff_python_ast::Singleton;
|
||||
use ruff_python_ast::{Singleton, name::Name};
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
@@ -136,6 +136,7 @@ pub(crate) enum PatternPredicateKind<'db> {
|
||||
Value(Expression<'db>),
|
||||
Or(Vec<PatternPredicateKind<'db>>),
|
||||
Class(Expression<'db>, ClassPatternKind),
|
||||
As(Option<Box<PatternPredicateKind<'db>>>, Option<Name>),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
|
||||
@@ -340,6 +340,10 @@ fn pattern_kind_to_type<'db>(db: &'db dyn Db, kind: &PatternPredicateKind<'db>)
|
||||
PatternPredicateKind::Or(predicates) => {
|
||||
UnionType::from_elements(db, predicates.iter().map(|p| pattern_kind_to_type(db, p)))
|
||||
}
|
||||
PatternPredicateKind::As(pattern, _) => pattern
|
||||
.as_deref()
|
||||
.map(|p| pattern_kind_to_type(db, p))
|
||||
.unwrap_or_else(|| Type::object(db)),
|
||||
PatternPredicateKind::Unsupported => Type::Never,
|
||||
}
|
||||
}
|
||||
@@ -761,6 +765,10 @@ impl ReachabilityConstraints {
|
||||
}
|
||||
})
|
||||
}
|
||||
PatternPredicateKind::As(pattern, _) => pattern
|
||||
.as_deref()
|
||||
.map(|p| Self::analyze_single_pattern_predicate_kind(db, p, subject_ty))
|
||||
.unwrap_or(Truthiness::AlwaysTrue),
|
||||
PatternPredicateKind::Unsupported => Truthiness::Ambiguous,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +251,10 @@ impl ScopeKind {
|
||||
matches!(self, ScopeKind::Class)
|
||||
}
|
||||
|
||||
pub(crate) const fn is_module(self) -> bool {
|
||||
matches!(self, ScopeKind::Module)
|
||||
}
|
||||
|
||||
pub(crate) const fn is_type_parameter(self) -> bool {
|
||||
matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ bitflags! {
|
||||
const IS_DECLARED = 1 << 2;
|
||||
const MARKED_GLOBAL = 1 << 3;
|
||||
const MARKED_NONLOCAL = 1 << 4;
|
||||
/// true if the symbol is assigned more than once, or if it is assigned even though it is already in use
|
||||
const IS_REASSIGNED = 1 << 5;
|
||||
}
|
||||
}
|
||||
@@ -92,7 +93,7 @@ impl Symbol {
|
||||
}
|
||||
|
||||
pub(super) fn mark_bound(&mut self) {
|
||||
if self.is_bound() {
|
||||
if self.is_bound() || self.is_used() {
|
||||
self.insert_flags(SymbolFlags::IS_REASSIGNED);
|
||||
}
|
||||
|
||||
|
||||
@@ -223,7 +223,8 @@ impl<'db> Completion<'db> {
|
||||
Type::NominalInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_) => CompletionKind::Struct,
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypedDict(_) => CompletionKind::Struct,
|
||||
Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::TypeIs(_)
|
||||
|
||||
@@ -90,6 +90,7 @@ mod string_annotation;
|
||||
mod subclass_of;
|
||||
mod tuple;
|
||||
mod type_ordering;
|
||||
mod unification;
|
||||
mod unpacker;
|
||||
mod visitor;
|
||||
|
||||
@@ -606,6 +607,8 @@ pub enum Type<'db> {
|
||||
BoundSuper(BoundSuperType<'db>),
|
||||
/// A subtype of `bool` that allows narrowing in both positive and negative cases.
|
||||
TypeIs(TypeIsType<'db>),
|
||||
/// A type that represents an inhabitant of a `TypedDict`.
|
||||
TypedDict(TypedDictType<'db>),
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
@@ -667,6 +670,10 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::Dynamic(_))
|
||||
}
|
||||
|
||||
pub(crate) const fn is_typed_dict(&self) -> bool {
|
||||
matches!(self, Type::TypedDict(..))
|
||||
}
|
||||
|
||||
/// Returns the top materialization (or upper bound materialization) of this type, which is the
|
||||
/// most general form of the type that is fully static.
|
||||
#[must_use]
|
||||
@@ -780,6 +787,10 @@ impl<'db> Type<'db> {
|
||||
Type::TypeIs(type_is) => {
|
||||
type_is.with_type(db, type_is.return_type(db).materialize(db, variance))
|
||||
}
|
||||
Type::TypedDict(_) => {
|
||||
// TODO: Materialization of gradual TypedDicts
|
||||
*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,6 +801,13 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn into_subclass_of(self) -> Option<SubclassOfType<'db>> {
|
||||
match self {
|
||||
Type::SubclassOf(subclass_of) => Some(subclass_of),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn expect_class_literal(self) -> ClassLiteral<'db> {
|
||||
self.into_class_literal()
|
||||
@@ -1073,6 +1091,12 @@ impl<'db> Type<'db> {
|
||||
// Always normalize single-member enums to their class instance (`Literal[Single.VALUE]` => `Single`)
|
||||
enum_literal.enum_class_instance(db)
|
||||
}
|
||||
|
||||
Type::TypedDict(_) => {
|
||||
// TODO: Normalize TypedDicts
|
||||
self
|
||||
}
|
||||
|
||||
Type::LiteralString
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
@@ -1121,7 +1145,8 @@ impl<'db> Type<'db> {
|
||||
| Type::AlwaysTruthy
|
||||
| Type::PropertyInstance(_)
|
||||
// might inherit `Any`, but subtyping is still reflexive
|
||||
| Type::ClassLiteral(_) => true,
|
||||
| Type::ClassLiteral(_)
|
||||
=> true,
|
||||
Type::Dynamic(_)
|
||||
| Type::NominalInstance(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
@@ -1133,7 +1158,8 @@ impl<'db> Type<'db> {
|
||||
| Type::Tuple(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeIs(_) => false,
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1196,7 +1222,8 @@ impl<'db> Type<'db> {
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::TypeIs(_) => None,
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => None,
|
||||
|
||||
// TODO
|
||||
Type::MethodWrapper(_)
|
||||
@@ -1290,6 +1317,11 @@ impl<'db> Type<'db> {
|
||||
field.default_type(db).has_relation_to(db, right, relation)
|
||||
}
|
||||
|
||||
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
|
||||
// TODO: Implement assignability and subtyping for TypedDict
|
||||
relation.is_assignability()
|
||||
}
|
||||
|
||||
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
|
||||
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
|
||||
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
|
||||
@@ -1754,6 +1786,11 @@ impl<'db> Type<'db> {
|
||||
|
||||
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => false,
|
||||
|
||||
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
|
||||
// TODO: Implement disjointness for TypedDict
|
||||
false
|
||||
}
|
||||
|
||||
// A typevar is never disjoint from itself, since all occurrences of the typevar must
|
||||
// be specialized to the same type. (This is an important difference between typevars
|
||||
// and `Any`!) Different typevars might be disjoint, depending on their bounds and
|
||||
@@ -2367,6 +2404,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
Type::AlwaysTruthy | Type::AlwaysFalsy => false,
|
||||
Type::TypeIs(type_is) => type_is.is_bound(db),
|
||||
Type::TypedDict(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2455,7 +2493,8 @@ impl<'db> Type<'db> {
|
||||
| Type::Callable(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::DataclassDecorator(_)
|
||||
| Type::DataclassTransformer(_) => false,
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::TypedDict(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2495,6 +2534,10 @@ impl<'db> Type<'db> {
|
||||
|
||||
Type::Dynamic(_) | Type::Never => Some(Place::bound(self).into()),
|
||||
|
||||
Type::ClassLiteral(class) if class.is_typed_dict(db) => {
|
||||
Some(class.typed_dict_member(db, None, name, policy))
|
||||
}
|
||||
|
||||
Type::ClassLiteral(class) => {
|
||||
match (class.known(db), name) {
|
||||
(Some(KnownClass::FunctionType), "__get__") => Some(
|
||||
@@ -2524,6 +2567,10 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
Type::GenericAlias(alias) if alias.is_typed_dict(db) => {
|
||||
Some(alias.origin(db).typed_dict_member(db, None, name, policy))
|
||||
}
|
||||
|
||||
Type::GenericAlias(alias) => {
|
||||
Some(ClassType::from(*alias).class_member(db, name, policy))
|
||||
}
|
||||
@@ -2575,7 +2622,8 @@ impl<'db> Type<'db> {
|
||||
| Type::NominalInstance(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::TypeIs(_) => None,
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2732,6 +2780,8 @@ impl<'db> Type<'db> {
|
||||
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => {
|
||||
Place::Unbound.into()
|
||||
}
|
||||
|
||||
Type::TypedDict(_) => Place::Unbound.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3063,7 +3113,7 @@ impl<'db> Type<'db> {
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
tracing::trace!("member_lookup_with_policy: {}.{}", self.display(db), name);
|
||||
if name == "__class__" {
|
||||
return Place::bound(self.to_meta_type(db)).into();
|
||||
return Place::bound(self.dunder_class(db)).into();
|
||||
}
|
||||
|
||||
let name_str = name.as_str();
|
||||
@@ -3221,7 +3271,8 @@ impl<'db> Type<'db> {
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy
|
||||
| Type::TypeIs(..) => {
|
||||
| Type::TypeIs(..)
|
||||
| Type::TypedDict(_) => {
|
||||
let fallback = self.instance_member(db, name_str);
|
||||
|
||||
let result = self.invoke_descriptor_protocol(
|
||||
@@ -3279,6 +3330,12 @@ impl<'db> Type<'db> {
|
||||
.into()
|
||||
};
|
||||
|
||||
if result.is_class_var() && self.is_typed_dict() {
|
||||
// `ClassVar`s on `TypedDictFallback` can not be accessed on inhabitants of `SomeTypedDict`.
|
||||
// They can only be accessed on `SomeTypedDict` directly.
|
||||
return Place::Unbound.into();
|
||||
}
|
||||
|
||||
match result {
|
||||
member @ PlaceAndQualifiers {
|
||||
place: Place::Type(_, Boundness::Bound),
|
||||
@@ -3507,6 +3564,12 @@ impl<'db> Type<'db> {
|
||||
| Type::LiteralString
|
||||
| Type::TypeIs(_) => Truthiness::Ambiguous,
|
||||
|
||||
Type::TypedDict(_) => {
|
||||
// TODO: We could do better here, but it's unclear if this is important.
|
||||
// See existing `TypedDict`-related tests in `truthiness.md`
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
@@ -3801,7 +3864,7 @@ impl<'db> Type<'db> {
|
||||
db,
|
||||
[
|
||||
KnownClass::Str.to_instance(db),
|
||||
TupleType::homogeneous(db, KnownClass::Str.to_instance(db)),
|
||||
Type::homogeneous_tuple(db, KnownClass::Str.to_instance(db)),
|
||||
],
|
||||
)),
|
||||
Parameter::positional_only(Some(Name::new_static("start")))
|
||||
@@ -4114,7 +4177,7 @@ impl<'db> Type<'db> {
|
||||
Parameter::positional_only(Some(Name::new_static("name")))
|
||||
.with_annotated_type(str_instance),
|
||||
Parameter::positional_only(Some(Name::new_static("bases")))
|
||||
.with_annotated_type(TupleType::homogeneous(
|
||||
.with_annotated_type(Type::homogeneous_tuple(
|
||||
db,
|
||||
type_instance,
|
||||
)),
|
||||
@@ -4304,7 +4367,7 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(Type::any())
|
||||
.type_form(),
|
||||
Parameter::keyword_only(Name::new_static("type_params"))
|
||||
.with_annotated_type(TupleType::homogeneous(
|
||||
.with_annotated_type(Type::homogeneous_tuple(
|
||||
db,
|
||||
UnionType::from_elements(
|
||||
db,
|
||||
@@ -4315,7 +4378,7 @@ impl<'db> Type<'db> {
|
||||
],
|
||||
),
|
||||
))
|
||||
.with_default_type(TupleType::empty(db)),
|
||||
.with_default_type(Type::empty_tuple(db)),
|
||||
]),
|
||||
None,
|
||||
),
|
||||
@@ -4401,7 +4464,7 @@ impl<'db> Type<'db> {
|
||||
CallableBinding::from_overloads(
|
||||
self,
|
||||
[
|
||||
Signature::new(Parameters::empty(), Some(TupleType::empty(db))),
|
||||
Signature::new(Parameters::empty(), Some(Type::empty_tuple(db))),
|
||||
Signature::new(
|
||||
Parameters::new([Parameter::positional_only(Some(
|
||||
Name::new_static("iterable"),
|
||||
@@ -4409,7 +4472,7 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(
|
||||
KnownClass::Iterable.to_specialized_instance(db, [object]),
|
||||
)]),
|
||||
Some(TupleType::homogeneous(db, object)),
|
||||
Some(Type::homogeneous_tuple(db, object)),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -4545,7 +4608,8 @@ impl<'db> Type<'db> {
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::TypeIs(_) => CallableBinding::not_callable(self).into(),
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => CallableBinding::not_callable(self).into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5160,7 +5224,8 @@ impl<'db> Type<'db> {
|
||||
| Type::BoundSuper(_)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy
|
||||
| Type::TypeIs(_) => None,
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5199,10 +5264,16 @@ impl<'db> Type<'db> {
|
||||
KnownClass::Float.to_instance(db),
|
||||
],
|
||||
),
|
||||
_ if class.is_typed_dict(db) => {
|
||||
Type::TypedDict(TypedDictType::new(db, ClassType::NonGeneric(*class)))
|
||||
}
|
||||
_ => Type::instance(db, class.default_specialization(db)),
|
||||
};
|
||||
Ok(ty)
|
||||
}
|
||||
Type::GenericAlias(alias) if alias.is_typed_dict(db) => Ok(Type::TypedDict(
|
||||
TypedDictType::new(db, ClassType::from(*alias)),
|
||||
)),
|
||||
Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))),
|
||||
|
||||
Type::SubclassOf(_)
|
||||
@@ -5228,7 +5299,8 @@ impl<'db> Type<'db> {
|
||||
| Type::BoundSuper(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::TypeIs(_) => Err(InvalidTypeExpressionError {
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec_inline![
|
||||
InvalidTypeExpression::InvalidType(*self, scope_id)
|
||||
],
|
||||
@@ -5267,7 +5339,7 @@ impl<'db> Type<'db> {
|
||||
|
||||
// We treat `typing.Type` exactly the same as `builtins.type`:
|
||||
SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)),
|
||||
SpecialFormType::Tuple => Ok(TupleType::homogeneous(db, Type::unknown())),
|
||||
SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())),
|
||||
|
||||
// Legacy `typing` aliases
|
||||
SpecialFormType::List => Ok(KnownClass::List.to_instance(db)),
|
||||
@@ -5458,7 +5530,7 @@ impl<'db> Type<'db> {
|
||||
Type::Union(UnionType::new(db, elements))
|
||||
};
|
||||
|
||||
TupleType::from_elements(
|
||||
Type::heterogeneous_tuple(
|
||||
db,
|
||||
[
|
||||
Type::IntLiteral(python_version.major.into()),
|
||||
@@ -5472,6 +5544,9 @@ impl<'db> Type<'db> {
|
||||
|
||||
/// Given a type that is assumed to represent an instance of a class,
|
||||
/// return a type that represents that class itself.
|
||||
///
|
||||
/// Note: the return type of `type(obj)` is subtly different from this.
|
||||
/// See `Self::dunder_class` for more details.
|
||||
#[must_use]
|
||||
pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
match self {
|
||||
@@ -5496,10 +5571,8 @@ impl<'db> Type<'db> {
|
||||
Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db),
|
||||
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
|
||||
Type::Tuple(tuple) => tuple
|
||||
.to_class_type(db)
|
||||
.map(Type::from)
|
||||
.unwrap_or_else(Type::unknown),
|
||||
|
||||
.to_subclass_of(db)
|
||||
.unwrap_or_else(SubclassOfType::subclass_of_unknown),
|
||||
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
|
||||
None => KnownClass::Type.to_instance(db),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db),
|
||||
@@ -5532,9 +5605,27 @@ impl<'db> Type<'db> {
|
||||
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
|
||||
Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db),
|
||||
Type::ProtocolInstance(protocol) => protocol.to_meta_type(db),
|
||||
Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class(db)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the type of the `__class__` attribute of this type.
|
||||
///
|
||||
/// For most types, this is equivalent to the meta type of this type. For `TypedDict` types,
|
||||
/// this returns `type[dict[str, object]]` instead, because inhabitants of a `TypedDict` are
|
||||
/// instances of `dict` at runtime.
|
||||
#[must_use]
|
||||
pub fn dunder_class(self, db: &'db dyn Db) -> Type<'db> {
|
||||
if self.is_typed_dict() {
|
||||
return KnownClass::Dict
|
||||
.to_specialized_class_type(db, [KnownClass::Str.to_instance(db), Type::object(db)])
|
||||
.map(Type::from)
|
||||
.unwrap_or_else(Type::unknown);
|
||||
}
|
||||
|
||||
self.to_meta_type(db)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn apply_optional_specialization(
|
||||
self,
|
||||
@@ -5632,6 +5723,10 @@ impl<'db> Type<'db> {
|
||||
Type::GenericAlias(generic.apply_type_mapping(db, type_mapping))
|
||||
}
|
||||
|
||||
Type::TypedDict(typed_dict) => {
|
||||
Type::TypedDict(typed_dict.apply_type_mapping(db, type_mapping))
|
||||
}
|
||||
|
||||
Type::SubclassOf(subclass_of) => Type::SubclassOf(
|
||||
subclass_of.apply_type_mapping(db, type_mapping),
|
||||
),
|
||||
@@ -5791,7 +5886,8 @@ impl<'db> Type<'db> {
|
||||
| Type::EnumLiteral(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::SpecialForm(_)
|
||||
| Type::KnownInstance(_) => {}
|
||||
| Type::KnownInstance(_)
|
||||
| Type::TypedDict(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5906,6 +6002,10 @@ impl<'db> Type<'db> {
|
||||
Protocol::Synthesized(_) => None,
|
||||
},
|
||||
|
||||
Type::TypedDict(typed_dict) => {
|
||||
Some(TypeDefinition::Class(typed_dict.defining_class(db).definition(db)))
|
||||
}
|
||||
|
||||
Self::Union(_) | Self::Intersection(_) => None,
|
||||
|
||||
// These types have no definition
|
||||
@@ -6250,9 +6350,6 @@ pub enum DynamicType {
|
||||
/// A special Todo-variant for type aliases declared using `typing.TypeAlias`.
|
||||
/// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions.
|
||||
TodoTypeAlias,
|
||||
/// A special Todo-variant for classes inheriting from `TypedDict`.
|
||||
/// A temporary variant to avoid false positives while we wait for full support.
|
||||
TodoTypedDict,
|
||||
}
|
||||
|
||||
impl DynamicType {
|
||||
@@ -6284,13 +6381,6 @@ impl std::fmt::Display for DynamicType {
|
||||
f.write_str("@Todo")
|
||||
}
|
||||
}
|
||||
DynamicType::TodoTypedDict => {
|
||||
if cfg!(debug_assertions) {
|
||||
f.write_str("@Todo(Support for `TypedDict`)")
|
||||
} else {
|
||||
f.write_str("@Todo")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8581,6 +8671,21 @@ pub(super) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>
|
||||
}
|
||||
|
||||
impl<'db> IntersectionType<'db> {
|
||||
/// Create an intersection from a list of elements
|
||||
/// (which may be eagerly simplified into a different variant of [`Type`] altogether).
|
||||
pub fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<Type<'db>>,
|
||||
{
|
||||
elements
|
||||
.into_iter()
|
||||
.fold(IntersectionBuilder::new(db), |builder, element| {
|
||||
builder.add_positive(element.into())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Return a new `IntersectionType` instance with the positive and negative types sorted
|
||||
/// according to a canonical ordering, and other normalizations applied to each element as applicable.
|
||||
///
|
||||
@@ -8745,6 +8850,10 @@ impl<'db> IntersectionType<'db> {
|
||||
self.positive(db).iter().copied()
|
||||
}
|
||||
|
||||
pub fn iter_negative(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {
|
||||
self.negative(db).iter().copied()
|
||||
}
|
||||
|
||||
pub fn has_one_element(&self, db: &'db dyn Db) -> bool {
|
||||
(self.positive(db).len() + self.negative(db).len()) == 1
|
||||
}
|
||||
@@ -8825,6 +8934,40 @@ impl<'db> EnumLiteralType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Type that represents the set of all inhabitants (`dict` instances) that conform to
|
||||
/// a given `TypedDict` schema.
|
||||
#[salsa::interned(debug)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct TypedDictType<'db> {
|
||||
/// A reference to the class (inheriting from `typing.TypedDict`) that specifies the
|
||||
/// schema of this `TypedDict`.
|
||||
defining_class: ClassType<'db>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
impl get_size2::GetSize for TypedDictType<'_> {}
|
||||
|
||||
impl<'db> TypedDictType<'db> {
|
||||
pub(crate) fn apply_type_mapping<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
type_mapping: &TypeMapping<'a, 'db>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
db,
|
||||
self.defining_class(db).apply_type_mapping(db, type_mapping),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
db: &'db dyn Db,
|
||||
typed_dict: TypedDictType<'db>,
|
||||
visitor: &mut V,
|
||||
) {
|
||||
visitor.visit_type(db, typed_dict.defining_class(db).into());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum BoundSuperError<'db> {
|
||||
InvalidPivotClassType {
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_python_ast as ast;
|
||||
use crate::Db;
|
||||
use crate::types::KnownClass;
|
||||
use crate::types::enums::enum_member_literals;
|
||||
use crate::types::tuple::{TupleLength, TupleSpec, TupleType};
|
||||
use crate::types::tuple::{TupleLength, TupleSpec};
|
||||
|
||||
use super::Type;
|
||||
|
||||
@@ -246,7 +246,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
|
||||
}
|
||||
})
|
||||
.multi_cartesian_product()
|
||||
.map(|types| TupleType::from_elements(db, types))
|
||||
.map(|types| Type::heterogeneous_tuple(db, types))
|
||||
.collect::<Vec<_>>();
|
||||
if expanded.len() == 1 {
|
||||
// There are no elements in the tuple type that can be expanded.
|
||||
@@ -306,17 +306,17 @@ mod tests {
|
||||
let false_ty = Type::BooleanLiteral(false);
|
||||
|
||||
// Empty tuple
|
||||
let empty_tuple = TupleType::empty(&db);
|
||||
let empty_tuple = Type::empty_tuple(&db);
|
||||
let expanded = expand_type(&db, empty_tuple);
|
||||
assert!(expanded.is_none());
|
||||
|
||||
// None of the elements can be expanded.
|
||||
let tuple_type1 = TupleType::from_elements(&db, [int_ty, str_ty]);
|
||||
let tuple_type1 = Type::heterogeneous_tuple(&db, [int_ty, str_ty]);
|
||||
let expanded = expand_type(&db, tuple_type1);
|
||||
assert!(expanded.is_none());
|
||||
|
||||
// All elements can be expanded.
|
||||
let tuple_type2 = TupleType::from_elements(
|
||||
let tuple_type2 = Type::heterogeneous_tuple(
|
||||
&db,
|
||||
[
|
||||
bool_ty,
|
||||
@@ -324,18 +324,18 @@ mod tests {
|
||||
],
|
||||
);
|
||||
let expected_types = [
|
||||
TupleType::from_elements(&db, [true_ty, int_ty]),
|
||||
TupleType::from_elements(&db, [true_ty, str_ty]),
|
||||
TupleType::from_elements(&db, [true_ty, bytes_ty]),
|
||||
TupleType::from_elements(&db, [false_ty, int_ty]),
|
||||
TupleType::from_elements(&db, [false_ty, str_ty]),
|
||||
TupleType::from_elements(&db, [false_ty, bytes_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, int_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, bytes_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, int_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, bytes_ty]),
|
||||
];
|
||||
let expanded = expand_type(&db, tuple_type2).unwrap();
|
||||
assert_eq!(expanded, expected_types);
|
||||
|
||||
// Mixed set of elements where some can be expanded while others cannot be.
|
||||
let tuple_type3 = TupleType::from_elements(
|
||||
let tuple_type3 = Type::heterogeneous_tuple(
|
||||
&db,
|
||||
[
|
||||
bool_ty,
|
||||
@@ -345,21 +345,21 @@ mod tests {
|
||||
],
|
||||
);
|
||||
let expected_types = [
|
||||
TupleType::from_elements(&db, [true_ty, int_ty, str_ty, str_ty]),
|
||||
TupleType::from_elements(&db, [true_ty, int_ty, bytes_ty, str_ty]),
|
||||
TupleType::from_elements(&db, [false_ty, int_ty, str_ty, str_ty]),
|
||||
TupleType::from_elements(&db, [false_ty, int_ty, bytes_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, int_ty, str_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, int_ty, bytes_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, int_ty, str_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, int_ty, bytes_ty, str_ty]),
|
||||
];
|
||||
let expanded = expand_type(&db, tuple_type3).unwrap();
|
||||
assert_eq!(expanded, expected_types);
|
||||
|
||||
// Variable-length tuples are not expanded.
|
||||
let variable_length_tuple = TupleType::mixed(
|
||||
let variable_length_tuple = Type::tuple(TupleType::mixed(
|
||||
&db,
|
||||
[bool_ty],
|
||||
int_ty,
|
||||
[UnionType::from_elements(&db, [str_ty, bytes_ty]), str_ty],
|
||||
);
|
||||
));
|
||||
let expanded = expand_type(&db, variable_length_tuple);
|
||||
assert!(expanded.is_none());
|
||||
}
|
||||
|
||||
@@ -390,9 +390,9 @@ impl<'db> Bindings<'db> {
|
||||
);
|
||||
}
|
||||
Some("__constraints__") => {
|
||||
overload.set_return_type(TupleType::from_elements(
|
||||
overload.set_return_type(Type::heterogeneous_tuple(
|
||||
db,
|
||||
typevar.constraints(db).into_iter().flatten().copied(),
|
||||
typevar.constraints(db).into_iter().flatten(),
|
||||
));
|
||||
}
|
||||
Some("__default__") => {
|
||||
@@ -674,7 +674,7 @@ impl<'db> Bindings<'db> {
|
||||
Some(names) => {
|
||||
let mut names = names.iter().collect::<Vec<_>>();
|
||||
names.sort();
|
||||
TupleType::from_elements(
|
||||
Type::heterogeneous_tuple(
|
||||
db,
|
||||
names.iter().map(|name| {
|
||||
Type::string_literal(db, name.as_str())
|
||||
@@ -694,7 +694,7 @@ impl<'db> Bindings<'db> {
|
||||
let return_ty = match ty {
|
||||
Type::ClassLiteral(class) => {
|
||||
if let Some(metadata) = enums::enum_metadata(db, *class) {
|
||||
TupleType::from_elements(
|
||||
Type::heterogeneous_tuple(
|
||||
db,
|
||||
metadata
|
||||
.members
|
||||
@@ -714,7 +714,7 @@ impl<'db> Bindings<'db> {
|
||||
|
||||
Some(KnownFunction::AllMembers) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(TupleType::from_elements(
|
||||
overload.set_return_type(Type::heterogeneous_tuple(
|
||||
db,
|
||||
ide_support::all_members(db, *ty)
|
||||
.into_iter()
|
||||
@@ -1010,7 +1010,7 @@ impl<'db> Bindings<'db> {
|
||||
|
||||
Some(KnownClass::Type) if overload_index == 0 => {
|
||||
if let [Some(arg)] = overload.parameter_types() {
|
||||
overload.set_return_type(arg.to_meta_type(db));
|
||||
overload.set_return_type(arg.dunder_class(db));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1458,7 +1458,7 @@ impl<'db> CallableBinding<'db> {
|
||||
}
|
||||
|
||||
let top_materialized_argument_type =
|
||||
TupleType::from_elements(db, top_materialized_argument_types);
|
||||
Type::heterogeneous_tuple(db, top_materialized_argument_types);
|
||||
|
||||
// A flag to indicate whether we've found the overload that makes the remaining overloads
|
||||
// unmatched for the given argument types.
|
||||
@@ -1494,7 +1494,7 @@ impl<'db> CallableBinding<'db> {
|
||||
parameter_types.push(UnionType::from_elements(db, current_parameter_types));
|
||||
}
|
||||
if top_materialized_argument_type
|
||||
.is_assignable_to(db, TupleType::from_elements(db, parameter_types))
|
||||
.is_assignable_to(db, Type::heterogeneous_tuple(db, parameter_types))
|
||||
{
|
||||
filter_remaining_overloads = true;
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ use crate::types::function::{DataclassTransformerParams, KnownFunction};
|
||||
use crate::types::generics::{GenericContext, Specialization, walk_specialization};
|
||||
use crate::types::infer::nearest_enclosing_class;
|
||||
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
|
||||
use crate::types::tuple::{TupleSpec, TupleType};
|
||||
use crate::types::tuple::TupleSpec;
|
||||
use crate::types::{
|
||||
BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams,
|
||||
DeprecatedInstance, DynamicType, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation,
|
||||
DeprecatedInstance, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation,
|
||||
TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type,
|
||||
infer_definition_types,
|
||||
infer_definition_types, todo_type,
|
||||
};
|
||||
use crate::{
|
||||
Db, FxIndexMap, FxOrderSet, Program,
|
||||
@@ -112,6 +112,21 @@ fn try_mro_cycle_initial<'db>(
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_typed_dict_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &bool,
|
||||
_count: u32,
|
||||
_self: ClassLiteral<'db>,
|
||||
) -> salsa::CycleRecoveryAction<bool> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn is_typed_dict_cycle_initial<'db>(_db: &'db dyn Db, _self: ClassLiteral<'db>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn try_metaclass_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Result<(Type<'db>, Option<DataclassTransformerParams>), MetaclassError<'db>>,
|
||||
@@ -140,6 +155,8 @@ pub(crate) enum CodeGeneratorKind {
|
||||
DataclassLike,
|
||||
/// Classes inheriting from `typing.NamedTuple`
|
||||
NamedTuple,
|
||||
/// Classes inheriting from `typing.TypedDict`
|
||||
TypedDict,
|
||||
}
|
||||
|
||||
impl CodeGeneratorKind {
|
||||
@@ -148,6 +165,8 @@ impl CodeGeneratorKind {
|
||||
Some(CodeGeneratorKind::DataclassLike)
|
||||
} else if CodeGeneratorKind::NamedTuple.matches(db, class) {
|
||||
Some(CodeGeneratorKind::NamedTuple)
|
||||
} else if CodeGeneratorKind::TypedDict.matches(db, class) {
|
||||
Some(CodeGeneratorKind::TypedDict)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -165,6 +184,7 @@ impl CodeGeneratorKind {
|
||||
base.into_class_literal()
|
||||
.is_some_and(|c| c.is_known(db, KnownClass::NamedTuple))
|
||||
}),
|
||||
Self::TypedDict => class.is_typed_dict(db),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,6 +258,10 @@ impl<'db> GenericAlias<'db> {
|
||||
// look in `self.tuple`.
|
||||
self.specialization(db).find_legacy_typevars(db, typevars);
|
||||
}
|
||||
|
||||
pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
|
||||
self.origin(db).is_typed_dict(db)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<GenericAlias<'db>> for Type<'db> {
|
||||
@@ -268,6 +292,10 @@ pub enum ClassType<'db> {
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> ClassType<'db> {
|
||||
pub(super) const fn is_not_generic(self) -> bool {
|
||||
matches!(self, Self::NonGeneric(_))
|
||||
}
|
||||
|
||||
pub(super) fn normalized_impl(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
@@ -419,15 +447,6 @@ impl<'db> ClassType<'db> {
|
||||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
// TODO: remove this branch once we have proper support for TypedDicts.
|
||||
if self.is_known(db, KnownClass::Dict)
|
||||
&& other
|
||||
.iter_mro(db)
|
||||
.any(|b| matches!(b, ClassBase::Dynamic(DynamicType::TodoTypedDict)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
self.iter_mro(db).any(|base| {
|
||||
match base {
|
||||
ClassBase::Dynamic(_) => match relation {
|
||||
@@ -451,6 +470,11 @@ impl<'db> ClassType<'db> {
|
||||
(ClassType::Generic(_), ClassType::NonGeneric(_))
|
||||
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
|
||||
},
|
||||
|
||||
ClassBase::TypedDict => {
|
||||
// TODO: Implement subclassing and assignability for TypedDicts.
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -778,7 +802,7 @@ impl<'db> ClassType<'db> {
|
||||
|
||||
overload_signatures.push(synthesize_getitem_overload_signature(
|
||||
KnownClass::Slice.to_instance(db),
|
||||
TupleType::homogeneous(db, all_elements_unioned),
|
||||
Type::homogeneous_tuple(db, all_elements_unioned),
|
||||
));
|
||||
|
||||
let getitem_signature =
|
||||
@@ -841,7 +865,8 @@ impl<'db> ClassType<'db> {
|
||||
// - an unspecialized tuple
|
||||
// - a tuple with no minimum length
|
||||
if specialization.is_none_or(|spec| spec.tuple(db).len().minimum() == 0) {
|
||||
iterable_parameter = iterable_parameter.with_default_type(TupleType::empty(db));
|
||||
iterable_parameter =
|
||||
iterable_parameter.with_default_type(Type::empty_tuple(db));
|
||||
}
|
||||
|
||||
let parameters = Parameters::new([
|
||||
@@ -868,6 +893,11 @@ impl<'db> ClassType<'db> {
|
||||
/// See [`Type::instance_member`] for more details.
|
||||
pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
||||
let (class_literal, specialization) = self.class_literal(db);
|
||||
|
||||
if class_literal.is_typed_dict(db) {
|
||||
return Place::Unbound.into();
|
||||
}
|
||||
|
||||
class_literal
|
||||
.instance_member(db, specialization, name)
|
||||
.map_type(|ty| ty.apply_optional_specialization(db, specialization))
|
||||
@@ -1452,6 +1482,22 @@ impl<'db> ClassLiteral<'db> {
|
||||
.contains(&ClassBase::Class(other))
|
||||
}
|
||||
|
||||
/// Return `true` if this class constitutes a typed dict specification (inherits from
|
||||
/// `typing.TypedDict`, either directly or indirectly).
|
||||
#[salsa::tracked(
|
||||
cycle_fn=is_typed_dict_cycle_recover,
|
||||
cycle_initial=is_typed_dict_cycle_initial,
|
||||
heap_size=get_size2::heap_size
|
||||
)]
|
||||
pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
|
||||
if let Some(known) = self.known(db) {
|
||||
return known.is_typed_dict_subclass();
|
||||
}
|
||||
|
||||
self.iter_mro(db, None)
|
||||
.any(|base| matches!(base, ClassBase::TypedDict))
|
||||
}
|
||||
|
||||
/// Return the explicit `metaclass` of this class, if one is defined.
|
||||
///
|
||||
/// ## Note
|
||||
@@ -1533,7 +1579,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
}
|
||||
} else {
|
||||
let name = Type::string_literal(db, self.name(db));
|
||||
let bases = TupleType::from_elements(db, self.explicit_bases(db).iter().copied());
|
||||
let bases = Type::heterogeneous_tuple(db, self.explicit_bases(db));
|
||||
let namespace = KnownClass::Dict
|
||||
.to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()]);
|
||||
|
||||
@@ -1624,8 +1670,8 @@ impl<'db> ClassLiteral<'db> {
|
||||
policy: MemberLookupPolicy,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
if name == "__mro__" {
|
||||
let tuple_elements = self.iter_mro(db, specialization).map(Type::from);
|
||||
return Place::bound(TupleType::from_elements(db, tuple_elements)).into();
|
||||
let tuple_elements = self.iter_mro(db, specialization);
|
||||
return Place::bound(Type::heterogeneous_tuple(db, tuple_elements)).into();
|
||||
}
|
||||
|
||||
self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization))
|
||||
@@ -1687,6 +1733,12 @@ impl<'db> ClassLiteral<'db> {
|
||||
)
|
||||
});
|
||||
}
|
||||
ClassBase::TypedDict => {
|
||||
return KnownClass::TypedDictFallback
|
||||
.to_class_literal(db)
|
||||
.find_name_in_mro_with_policy(db, name, policy)
|
||||
.expect("Will return Some() when called on class literal");
|
||||
}
|
||||
}
|
||||
if lookup_result.is_ok() {
|
||||
break;
|
||||
@@ -1787,7 +1839,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
|
||||
/// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or
|
||||
/// a synthesized `__new__` method for a `NamedTuple`.
|
||||
fn own_synthesized_member(
|
||||
pub(super) fn own_synthesized_member(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
@@ -1971,10 +2023,118 @@ impl<'db> ClassLiteral<'db> {
|
||||
}
|
||||
None
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "__setitem__") => {
|
||||
// TODO: synthesize a set of overloads with precise types
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key"))),
|
||||
Parameter::positional_only(Some(Name::new_static("value"))),
|
||||
]),
|
||||
Some(Type::none(db)),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "__getitem__") => {
|
||||
// TODO: synthesize a set of overloads with precise types
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key"))),
|
||||
]),
|
||||
Some(todo_type!("Support for `TypedDict`")),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "get") => {
|
||||
// TODO: synthesize a set of overloads with precise types
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key"))),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_default_type(Type::unknown()),
|
||||
]),
|
||||
Some(todo_type!("Support for `TypedDict`")),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "pop") => {
|
||||
// TODO: synthesize a set of overloads with precise types.
|
||||
// Required keys should be forbidden to be popped.
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key"))),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_default_type(Type::unknown()),
|
||||
]),
|
||||
Some(todo_type!("Support for `TypedDict`")),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "setdefault") => {
|
||||
// TODO: synthesize a set of overloads with precise types
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key"))),
|
||||
Parameter::positional_only(Some(Name::new_static("default"))),
|
||||
]),
|
||||
Some(todo_type!("Support for `TypedDict`")),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "update") => {
|
||||
// TODO: synthesize a set of overloads with precise types
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::variadic(Name::new_static("args")),
|
||||
Parameter::keyword_variadic(Name::new_static("kwargs")),
|
||||
]),
|
||||
Some(Type::none(db)),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Member lookup for classes that inherit from `typing.TypedDict`.
|
||||
///
|
||||
/// This is implemented as a separate method because the item definitions on a `TypedDict`-based
|
||||
/// class are *not* accessible as class members. Instead, this mostly defers to `TypedDictFallback`,
|
||||
/// unless `name` corresponds to one of the specialized synthetic members like `__getitem__`.
|
||||
pub(crate) fn typed_dict_member(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
name: &str,
|
||||
policy: MemberLookupPolicy,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
if let Some(member) = self.own_synthesized_member(db, specialization, name) {
|
||||
Place::bound(member).into()
|
||||
} else {
|
||||
KnownClass::TypedDictFallback
|
||||
.to_class_literal(db)
|
||||
.find_name_in_mro_with_policy(db, name, policy)
|
||||
.expect("`find_name_in_mro_with_policy` will return `Some()` when called on class literal")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of all annotated attributes defined in this class, or any of its superclasses.
|
||||
///
|
||||
/// See [`ClassLiteral::own_fields`] for more details.
|
||||
@@ -2105,6 +2265,10 @@ impl<'db> ClassLiteral<'db> {
|
||||
specialization: Option<Specialization<'db>>,
|
||||
name: &str,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
if self.is_typed_dict(db) {
|
||||
return Place::Unbound.into();
|
||||
}
|
||||
|
||||
let mut union = UnionBuilder::new(db);
|
||||
let mut union_qualifiers = TypeQualifiers::empty();
|
||||
|
||||
@@ -2142,6 +2306,9 @@ impl<'db> ClassLiteral<'db> {
|
||||
union = union.add(ty);
|
||||
}
|
||||
}
|
||||
ClassBase::TypedDict => {
|
||||
return Place::bound(todo_type!("Support for `TypedDict`")).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2857,6 +3024,7 @@ pub enum KnownClass {
|
||||
InitVar,
|
||||
// _typeshed._type_checker_internals
|
||||
NamedTupleFallback,
|
||||
TypedDictFallback,
|
||||
}
|
||||
|
||||
impl KnownClass {
|
||||
@@ -2953,7 +3121,8 @@ impl KnownClass {
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback => Truthiness::Ambiguous,
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback => Truthiness::Ambiguous,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3035,6 +3204,7 @@ impl KnownClass {
|
||||
| Self::SupportsIndex
|
||||
| Self::NamedTuple
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback
|
||||
| Self::Counter
|
||||
| Self::DefaultDict
|
||||
| Self::OrderedDict
|
||||
@@ -3118,7 +3288,85 @@ impl KnownClass {
|
||||
| KnownClass::Field
|
||||
| KnownClass::KwOnly
|
||||
| KnownClass::InitVar
|
||||
| KnownClass::NamedTupleFallback => false,
|
||||
| KnownClass::NamedTupleFallback
|
||||
| KnownClass::TypedDictFallback => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this class is a (true) subclass of `typing.TypedDict`.
|
||||
pub(crate) const fn is_typed_dict_subclass(self) -> bool {
|
||||
match self {
|
||||
KnownClass::Bool
|
||||
| KnownClass::Object
|
||||
| KnownClass::Bytes
|
||||
| KnownClass::Bytearray
|
||||
| KnownClass::Type
|
||||
| KnownClass::Int
|
||||
| KnownClass::Float
|
||||
| KnownClass::Complex
|
||||
| KnownClass::Str
|
||||
| KnownClass::List
|
||||
| KnownClass::Tuple
|
||||
| KnownClass::Set
|
||||
| KnownClass::FrozenSet
|
||||
| KnownClass::Dict
|
||||
| KnownClass::Slice
|
||||
| KnownClass::Property
|
||||
| KnownClass::BaseException
|
||||
| KnownClass::Exception
|
||||
| KnownClass::BaseExceptionGroup
|
||||
| KnownClass::ExceptionGroup
|
||||
| KnownClass::Staticmethod
|
||||
| KnownClass::Classmethod
|
||||
| KnownClass::Awaitable
|
||||
| KnownClass::Generator
|
||||
| KnownClass::Deprecated
|
||||
| KnownClass::Super
|
||||
| KnownClass::Enum
|
||||
| KnownClass::EnumType
|
||||
| KnownClass::Auto
|
||||
| KnownClass::Member
|
||||
| KnownClass::Nonmember
|
||||
| KnownClass::ABCMeta
|
||||
| KnownClass::GenericAlias
|
||||
| KnownClass::ModuleType
|
||||
| KnownClass::FunctionType
|
||||
| KnownClass::MethodType
|
||||
| KnownClass::MethodWrapperType
|
||||
| KnownClass::WrapperDescriptorType
|
||||
| KnownClass::UnionType
|
||||
| KnownClass::GeneratorType
|
||||
| KnownClass::AsyncGeneratorType
|
||||
| KnownClass::CoroutineType
|
||||
| KnownClass::NoneType
|
||||
| KnownClass::Any
|
||||
| KnownClass::StdlibAlias
|
||||
| KnownClass::SpecialForm
|
||||
| KnownClass::TypeVar
|
||||
| KnownClass::ParamSpec
|
||||
| KnownClass::ParamSpecArgs
|
||||
| KnownClass::ParamSpecKwargs
|
||||
| KnownClass::TypeVarTuple
|
||||
| KnownClass::TypeAliasType
|
||||
| KnownClass::NoDefaultType
|
||||
| KnownClass::NamedTuple
|
||||
| KnownClass::NewType
|
||||
| KnownClass::SupportsIndex
|
||||
| KnownClass::Iterable
|
||||
| KnownClass::Iterator
|
||||
| KnownClass::ChainMap
|
||||
| KnownClass::Counter
|
||||
| KnownClass::DefaultDict
|
||||
| KnownClass::Deque
|
||||
| KnownClass::OrderedDict
|
||||
| KnownClass::VersionInfo
|
||||
| KnownClass::EllipsisType
|
||||
| KnownClass::NotImplementedType
|
||||
| KnownClass::Field
|
||||
| KnownClass::KwOnly
|
||||
| KnownClass::InitVar
|
||||
| KnownClass::NamedTupleFallback
|
||||
| KnownClass::TypedDictFallback => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3206,7 +3454,8 @@ impl KnownClass {
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback => false,
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3302,6 +3551,7 @@ impl KnownClass {
|
||||
Self::KwOnly => "KW_ONLY",
|
||||
Self::InitVar => "InitVar",
|
||||
Self::NamedTupleFallback => "NamedTupleFallback",
|
||||
Self::TypedDictFallback => "TypedDictFallback",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3558,7 +3808,7 @@ impl KnownClass {
|
||||
| Self::Deque
|
||||
| Self::OrderedDict => KnownModule::Collections,
|
||||
Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses,
|
||||
Self::NamedTupleFallback => KnownModule::TypeCheckerInternals,
|
||||
Self::NamedTupleFallback | Self::TypedDictFallback => KnownModule::TypeCheckerInternals,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3635,7 +3885,8 @@ impl KnownClass {
|
||||
| Self::InitVar
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::NamedTupleFallback => false,
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3714,7 +3965,8 @@ impl KnownClass {
|
||||
| Self::InitVar
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::NamedTupleFallback => false,
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3804,6 +4056,7 @@ impl KnownClass {
|
||||
"KW_ONLY" => Self::KwOnly,
|
||||
"InitVar" => Self::InitVar,
|
||||
"NamedTupleFallback" => Self::NamedTupleFallback,
|
||||
"TypedDictFallback" => Self::TypedDictFallback,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
@@ -3868,6 +4121,7 @@ impl KnownClass {
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback
|
||||
| Self::Awaitable
|
||||
| Self::Generator => module == self.canonical_module(db),
|
||||
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
|
||||
@@ -4422,22 +4676,41 @@ mod tests {
|
||||
fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() {
|
||||
let mut db = setup_db();
|
||||
|
||||
for class in KnownClass::iter() {
|
||||
let version_added = match class {
|
||||
KnownClass::UnionType => PythonVersion::PY310,
|
||||
KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => PythonVersion::PY311,
|
||||
KnownClass::GenericAlias => PythonVersion::PY39,
|
||||
KnownClass::KwOnly => PythonVersion::PY310,
|
||||
KnownClass::Member | KnownClass::Nonmember => PythonVersion::PY311,
|
||||
_ => PythonVersion::PY37,
|
||||
};
|
||||
// First, collect the `KnownClass` variants
|
||||
// and sort them according to the version they were added in.
|
||||
// This makes the test far faster as it minimizes the number of times
|
||||
// we need to change the Python version in the loop.
|
||||
let mut classes: Vec<(KnownClass, PythonVersion)> = KnownClass::iter()
|
||||
.map(|class| {
|
||||
let version_added = match class {
|
||||
KnownClass::UnionType => PythonVersion::PY310,
|
||||
KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => {
|
||||
PythonVersion::PY311
|
||||
}
|
||||
KnownClass::GenericAlias => PythonVersion::PY39,
|
||||
KnownClass::KwOnly => PythonVersion::PY310,
|
||||
KnownClass::Member | KnownClass::Nonmember => PythonVersion::PY311,
|
||||
_ => PythonVersion::PY37,
|
||||
};
|
||||
(class, version_added)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Program::get(&db)
|
||||
.set_python_version_with_source(&mut db)
|
||||
.to(PythonVersionWithSource {
|
||||
version: version_added,
|
||||
source: PythonVersionSource::default(),
|
||||
});
|
||||
classes.sort_unstable_by_key(|(_, version)| *version);
|
||||
|
||||
let program = Program::get(&db);
|
||||
let mut current_version = program.python_version(&db);
|
||||
|
||||
for (class, version_added) in classes {
|
||||
if version_added != current_version {
|
||||
program
|
||||
.set_python_version_with_source(&mut db)
|
||||
.to(PythonVersionWithSource {
|
||||
version: version_added,
|
||||
source: PythonVersionSource::default(),
|
||||
});
|
||||
current_version = version_added;
|
||||
}
|
||||
|
||||
assert_ne!(
|
||||
class.to_instance(&db),
|
||||
|
||||
@@ -24,6 +24,7 @@ pub enum ClassBase<'db> {
|
||||
/// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`,
|
||||
/// `Protocol[T]`, or bare `Protocol`.
|
||||
Generic,
|
||||
TypedDict,
|
||||
}
|
||||
|
||||
impl<'db> ClassBase<'db> {
|
||||
@@ -39,7 +40,7 @@ impl<'db> ClassBase<'db> {
|
||||
match self {
|
||||
Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()),
|
||||
Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)),
|
||||
Self::Protocol | Self::Generic => self,
|
||||
Self::Protocol | Self::Generic | Self::TypedDict => self,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +52,11 @@ impl<'db> ClassBase<'db> {
|
||||
ClassBase::Dynamic(
|
||||
DynamicType::Todo(_)
|
||||
| DynamicType::TodoPEP695ParamSpec
|
||||
| DynamicType::TodoTypeAlias
|
||||
| DynamicType::TodoTypedDict,
|
||||
| DynamicType::TodoTypeAlias,
|
||||
) => "@Todo",
|
||||
ClassBase::Protocol => "Protocol",
|
||||
ClassBase::Generic => "Generic",
|
||||
ClassBase::TypedDict => "TypedDict",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +159,8 @@ impl<'db> ClassBase<'db> {
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::TypeIs(_) => None,
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => None,
|
||||
|
||||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic),
|
||||
@@ -234,7 +236,7 @@ impl<'db> ClassBase<'db> {
|
||||
SpecialFormType::OrderedDict => {
|
||||
Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db))
|
||||
}
|
||||
SpecialFormType::TypedDict => Some(Self::Dynamic(DynamicType::TodoTypedDict)),
|
||||
SpecialFormType::TypedDict => Some(Self::TypedDict),
|
||||
SpecialFormType::Callable => {
|
||||
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
|
||||
}
|
||||
@@ -245,14 +247,14 @@ impl<'db> ClassBase<'db> {
|
||||
pub(super) fn into_class(self) -> Option<ClassType<'db>> {
|
||||
match self {
|
||||
Self::Class(class) => Some(class),
|
||||
Self::Dynamic(_) | Self::Generic | Self::Protocol => None,
|
||||
Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
|
||||
match self {
|
||||
Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)),
|
||||
Self::Dynamic(_) | Self::Generic | Self::Protocol => self,
|
||||
Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => self,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +278,10 @@ impl<'db> ClassBase<'db> {
|
||||
.try_mro(db, specialization)
|
||||
.is_err_and(MroError::is_cycle)
|
||||
}
|
||||
ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::Protocol => false,
|
||||
ClassBase::Dynamic(_)
|
||||
| ClassBase::Generic
|
||||
| ClassBase::Protocol
|
||||
| ClassBase::TypedDict => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +293,9 @@ impl<'db> ClassBase<'db> {
|
||||
) -> impl Iterator<Item = ClassBase<'db>> {
|
||||
match self {
|
||||
ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic),
|
||||
ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self),
|
||||
ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::TypedDict => {
|
||||
ClassBaseMroIterator::length_2(db, self)
|
||||
}
|
||||
ClassBase::Class(class) => {
|
||||
ClassBaseMroIterator::from_class(db, class, additional_specialization)
|
||||
}
|
||||
@@ -309,6 +316,7 @@ impl<'db> From<ClassBase<'db>> for Type<'db> {
|
||||
ClassBase::Class(class) => class.into(),
|
||||
ClassBase::Protocol => Type::SpecialForm(SpecialFormType::Protocol),
|
||||
ClassBase::Generic => Type::SpecialForm(SpecialFormType::Generic),
|
||||
ClassBase::TypedDict => Type::SpecialForm(SpecialFormType::TypedDict),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ use crate::types::string_annotation::{
|
||||
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
RAW_STRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use crate::types::tuple::TupleType;
|
||||
use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
|
||||
use crate::util::diagnostics::format_enumeration;
|
||||
use crate::{Db, FxIndexMap, Module, ModuleName, Program, declare_lint};
|
||||
@@ -2430,7 +2429,7 @@ pub(crate) fn report_invalid_or_unsupported_base(
|
||||
return;
|
||||
}
|
||||
|
||||
let tuple_of_types = TupleType::homogeneous(db, instance_of_type);
|
||||
let tuple_of_types = Type::homogeneous_tuple(db, instance_of_type);
|
||||
|
||||
let explain_mro_entries = |diagnostic: &mut LintDiagnosticGuard| {
|
||||
diagnostic.info(
|
||||
|
||||
@@ -234,6 +234,9 @@ impl Display for DisplayRepresentation<'_> {
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
Type::TypedDict(typed_dict) => {
|
||||
f.write_str(typed_dict.defining_class(self.db).name(self.db))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,7 +975,8 @@ fn is_instance_truthiness<'db>(
|
||||
| Type::TypeIs(..)
|
||||
| Type::Callable(..)
|
||||
| Type::Dynamic(..)
|
||||
| Type::Never => {
|
||||
| Type::Never
|
||||
| Type::TypedDict(_) => {
|
||||
// We could probably try to infer more precise types in some of these cases, but it's unclear
|
||||
// if it's worth the effort.
|
||||
Truthiness::Ambiguous
|
||||
|
||||
@@ -242,7 +242,7 @@ impl<'db> GenericContext<'db> {
|
||||
|
||||
/// Returns a tuple type of the typevars introduced by this generic context.
|
||||
pub(crate) fn as_tuple(self, db: &'db dyn Db) -> Type<'db> {
|
||||
TupleType::from_elements(
|
||||
Type::heterogeneous_tuple(
|
||||
db,
|
||||
self.variables(db)
|
||||
.iter()
|
||||
|
||||
@@ -95,7 +95,19 @@ impl<'db> AllMembers<'db> {
|
||||
|
||||
Type::NominalInstance(instance) => {
|
||||
let (class_literal, _specialization) = instance.class.class_literal(db);
|
||||
self.extend_with_instance_members(db, class_literal);
|
||||
self.extend_with_instance_members(db, ty, class_literal);
|
||||
}
|
||||
|
||||
Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => {
|
||||
self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db));
|
||||
}
|
||||
|
||||
Type::GenericAlias(generic_alias) if generic_alias.is_typed_dict(db) => {
|
||||
self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db));
|
||||
}
|
||||
|
||||
Type::SubclassOf(subclass_of_type) if subclass_of_type.is_typed_dict(db) => {
|
||||
self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db));
|
||||
}
|
||||
|
||||
Type::ClassLiteral(class_literal) => {
|
||||
@@ -143,6 +155,11 @@ impl<'db> AllMembers<'db> {
|
||||
Type::ClassLiteral(class_literal) => {
|
||||
self.extend_with_class_members(db, ty, class_literal);
|
||||
}
|
||||
Type::SubclassOf(subclass_of) => {
|
||||
if let Some(class) = subclass_of.subclass_of().into_class() {
|
||||
self.extend_with_class_members(db, ty, class.class_literal(db).0);
|
||||
}
|
||||
}
|
||||
Type::GenericAlias(generic_alias) => {
|
||||
let class_literal = generic_alias.origin(db);
|
||||
self.extend_with_class_members(db, ty, class_literal);
|
||||
@@ -150,6 +167,18 @@ impl<'db> AllMembers<'db> {
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Type::TypedDict(_) => {
|
||||
if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) {
|
||||
self.extend_with_class_members(db, ty, class_literal);
|
||||
}
|
||||
|
||||
if let Type::ClassLiteral(class) =
|
||||
KnownClass::TypedDictFallback.to_class_literal(db)
|
||||
{
|
||||
self.extend_with_instance_members(db, ty, class);
|
||||
}
|
||||
}
|
||||
|
||||
Type::ModuleLiteral(literal) => {
|
||||
self.extend_with_type(db, KnownClass::ModuleType.to_instance(db));
|
||||
let module = literal.module(db);
|
||||
@@ -260,13 +289,17 @@ impl<'db> AllMembers<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_with_instance_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) {
|
||||
fn extend_with_instance_members(
|
||||
&mut self,
|
||||
db: &'db dyn Db,
|
||||
ty: Type<'db>,
|
||||
class_literal: ClassLiteral<'db>,
|
||||
) {
|
||||
for parent in class_literal
|
||||
.iter_mro(db, None)
|
||||
.filter_map(ClassBase::into_class)
|
||||
.map(|class| class.class_literal(db).0)
|
||||
{
|
||||
let parent_instance = Type::instance(db, parent.default_specialization(db));
|
||||
let class_body_scope = parent.body_scope(db);
|
||||
let file = class_body_scope.file(db);
|
||||
let index = semantic_index(db, file);
|
||||
@@ -276,7 +309,7 @@ impl<'db> AllMembers<'db> {
|
||||
let Some(name) = place_expr.as_instance_attribute() else {
|
||||
continue;
|
||||
};
|
||||
let result = parent_instance.member(db, name.as_str());
|
||||
let result = ty.member(db, name.as_str());
|
||||
let Some(ty) = result.place.ignore_possibly_unbound() else {
|
||||
continue;
|
||||
};
|
||||
@@ -293,7 +326,7 @@ impl<'db> AllMembers<'db> {
|
||||
// member, e.g., `SomeClass.__delattr__` is not a bound
|
||||
// method, but `instance_of_SomeClass.__delattr__` is.
|
||||
for Member { name, .. } in all_declarations_and_bindings(db, class_body_scope) {
|
||||
let result = parent_instance.member(db, name.as_str());
|
||||
let result = ty.member(db, name.as_str());
|
||||
let Some(ty) = result.place.ignore_possibly_unbound() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -694,7 +694,7 @@ enum IntersectionOn {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum DeclaredAndInferredType<'db> {
|
||||
/// We know that both the declared and inferred types are the same.
|
||||
AreTheSame(Type<'db>),
|
||||
AreTheSame(TypeAndQualifiers<'db>),
|
||||
/// Declared and inferred types might be different, we need to check assignability.
|
||||
MightBeDifferent {
|
||||
declared_ty: TypeAndQualifiers<'db>,
|
||||
@@ -702,6 +702,12 @@ enum DeclaredAndInferredType<'db> {
|
||||
},
|
||||
}
|
||||
|
||||
impl<'db> DeclaredAndInferredType<'db> {
|
||||
fn are_the_same_type(ty: Type<'db>) -> Self {
|
||||
Self::AreTheSame(ty.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder to infer all types in a region.
|
||||
///
|
||||
/// A builder is used by creating it with [`new()`](TypeInferenceBuilder::new), and then calling
|
||||
@@ -2132,7 +2138,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
);
|
||||
|
||||
let (declared_ty, inferred_ty) = match *declared_and_inferred_ty {
|
||||
DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty),
|
||||
DeclaredAndInferredType::AreTheSame(type_and_qualifiers) => {
|
||||
(type_and_qualifiers, type_and_qualifiers.inner_type())
|
||||
}
|
||||
DeclaredAndInferredType::MightBeDifferent {
|
||||
declared_ty,
|
||||
inferred_ty,
|
||||
@@ -2191,7 +2199,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
node,
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(Type::unknown()),
|
||||
&DeclaredAndInferredType::are_the_same_type(Type::unknown()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2658,7 +2666,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
function.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(inferred_ty),
|
||||
&DeclaredAndInferredType::are_the_same_type(inferred_ty),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2818,7 +2826,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
.as_ref()
|
||||
.is_some_and(|d| d.is_ellipsis_literal_expr())
|
||||
{
|
||||
DeclaredAndInferredType::AreTheSame(declared_ty)
|
||||
DeclaredAndInferredType::are_the_same_type(declared_ty)
|
||||
} else {
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
@@ -2831,10 +2839,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
declared_ty.display(self.db())
|
||||
));
|
||||
}
|
||||
DeclaredAndInferredType::AreTheSame(declared_ty)
|
||||
DeclaredAndInferredType::are_the_same_type(declared_ty)
|
||||
}
|
||||
} else {
|
||||
DeclaredAndInferredType::AreTheSame(declared_ty)
|
||||
DeclaredAndInferredType::are_the_same_type(declared_ty)
|
||||
};
|
||||
self.add_declaration_with_binding(
|
||||
parameter.into(),
|
||||
@@ -2868,19 +2876,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
todo_type!("PEP 646")
|
||||
} else {
|
||||
let annotated_type = self.file_expression_type(annotation);
|
||||
TupleType::homogeneous(self.db(), annotated_type)
|
||||
Type::homogeneous_tuple(self.db(), annotated_type)
|
||||
};
|
||||
|
||||
self.add_declaration_with_binding(
|
||||
parameter.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(ty),
|
||||
&DeclaredAndInferredType::are_the_same_type(ty),
|
||||
);
|
||||
} else {
|
||||
self.add_binding(
|
||||
parameter.into(),
|
||||
definition,
|
||||
TupleType::homogeneous(self.db(), Type::unknown()),
|
||||
Type::homogeneous_tuple(self.db(), Type::unknown()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2906,7 +2914,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
parameter.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(ty),
|
||||
&DeclaredAndInferredType::are_the_same_type(ty),
|
||||
);
|
||||
} else {
|
||||
self.add_binding(
|
||||
@@ -3004,7 +3012,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
class_node.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(class_ty),
|
||||
&DeclaredAndInferredType::are_the_same_type(class_ty),
|
||||
);
|
||||
|
||||
// if there are type parameters, then the keywords and bases are within that scope
|
||||
@@ -3078,7 +3086,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
type_alias.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(type_alias_ty),
|
||||
&DeclaredAndInferredType::are_the_same_type(type_alias_ty),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3293,7 +3301,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
)
|
||||
} else if node_ty.is_assignable_to(
|
||||
self.db(),
|
||||
TupleType::homogeneous(self.db(), type_base_exception),
|
||||
Type::homogeneous_tuple(self.db(), type_base_exception),
|
||||
) {
|
||||
extract_tuple_specialization(self.db(), node_ty)
|
||||
.unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db()))
|
||||
@@ -3303,7 +3311,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.db(),
|
||||
[
|
||||
type_base_exception,
|
||||
TupleType::homogeneous(self.db(), type_base_exception),
|
||||
Type::homogeneous_tuple(self.db(), type_base_exception),
|
||||
],
|
||||
),
|
||||
) {
|
||||
@@ -3433,7 +3441,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
node.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(ty),
|
||||
&DeclaredAndInferredType::are_the_same_type(ty),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3453,7 +3461,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
node.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(pep_695_todo),
|
||||
&DeclaredAndInferredType::are_the_same_type(pep_695_todo),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3473,7 +3481,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
node.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(pep_695_todo),
|
||||
&DeclaredAndInferredType::are_the_same_type(pep_695_todo),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3584,7 +3592,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
fn infer_nested_match_pattern(&mut self, pattern: &ast::Pattern) {
|
||||
match pattern {
|
||||
ast::Pattern::MatchValue(match_value) => {
|
||||
self.infer_expression(&match_value.value);
|
||||
self.infer_maybe_standalone_expression(&match_value.value);
|
||||
}
|
||||
ast::Pattern::MatchSequence(match_sequence) => {
|
||||
for pattern in &match_sequence.patterns {
|
||||
@@ -3619,7 +3627,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
for keyword in &arguments.keywords {
|
||||
self.infer_nested_match_pattern(&keyword.pattern);
|
||||
}
|
||||
self.infer_expression(cls);
|
||||
self.infer_maybe_standalone_expression(cls);
|
||||
}
|
||||
ast::Pattern::MatchAs(match_as) => {
|
||||
if let Some(pattern) = &match_as.pattern {
|
||||
@@ -3906,7 +3914,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
| Type::TypeVar(..)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy
|
||||
| Type::TypeIs(_) => {
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => {
|
||||
// First, try to call the `__setattr__` dunder method. If this is present/defined, overrides
|
||||
// assigning the attributed by the normal mechanism.
|
||||
let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy(
|
||||
@@ -4557,7 +4566,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
target.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(declared.inner_type()),
|
||||
&DeclaredAndInferredType::AreTheSame(declared),
|
||||
);
|
||||
} else {
|
||||
self.add_declaration(target.into(), definition, declared);
|
||||
@@ -4875,7 +4884,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
alias.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(binding_ty),
|
||||
&DeclaredAndInferredType::are_the_same_type(binding_ty),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5124,7 +5133,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.add_declaration_with_binding(
|
||||
alias.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(submodule_type),
|
||||
&DeclaredAndInferredType::are_the_same_type(submodule_type),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -5611,7 +5620,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// consuming the whole iterator).
|
||||
let element_types: Vec<_> = elts.iter().map(|elt| self.infer_expression(elt)).collect();
|
||||
|
||||
TupleType::from_elements(self.db(), element_types)
|
||||
Type::heterogeneous_tuple(self.db(), element_types)
|
||||
}
|
||||
|
||||
fn infer_list_expression(&mut self, list: &ast::ExprList) -> Type<'db> {
|
||||
@@ -6069,7 +6078,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// the `try_call` path below.
|
||||
// TODO: it should be possible to move these special cases into the `try_call_constructor`
|
||||
// path instead, or even remove some entirely once we support overloads fully.
|
||||
if !matches!(
|
||||
let has_special_cased_constructor = matches!(
|
||||
class.known(self.db()),
|
||||
Some(
|
||||
KnownClass::Bool
|
||||
@@ -6083,20 +6092,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
| KnownClass::TypeAliasType
|
||||
| KnownClass::Deprecated
|
||||
)
|
||||
)
|
||||
|
||||
// Constructor calls to `tuple` and subclasses of `tuple` are handled in `Type::Bindings`,
|
||||
// but constructor calls to `tuple[int]`, `tuple[int, ...]`, `tuple[int, *tuple[str, ...]]` (etc.)
|
||||
// are handled by the default constructor-call logic (we synthesize a `__new__` method for them
|
||||
// in `ClassType::own_class_member()`).
|
||||
&& (callable_type.is_generic_alias() || !class.is_known(self.db(), KnownClass::Tuple))
|
||||
) || (
|
||||
// Constructor calls to `tuple` and subclasses of `tuple` are handled in `Type::Bindings`,
|
||||
// but constructor calls to `tuple[int]`, `tuple[int, ...]`, `tuple[int, *tuple[str, ...]]` (etc.)
|
||||
// are handled by the default constructor-call logic (we synthesize a `__new__` method for them
|
||||
// in `ClassType::own_class_member()`).
|
||||
class.is_known(self.db(), KnownClass::Tuple) && class.is_not_generic()
|
||||
);
|
||||
|
||||
// temporary special-casing for all subclasses of `enum.Enum`
|
||||
// until we support the functional syntax for creating enum classes
|
||||
&& KnownClass::Enum
|
||||
.to_class_literal(self.db())
|
||||
.to_class_type(self.db())
|
||||
.is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class))
|
||||
if !has_special_cased_constructor
|
||||
&& KnownClass::Enum
|
||||
.to_class_literal(self.db())
|
||||
.to_class_type(self.db())
|
||||
.is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class))
|
||||
{
|
||||
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
|
||||
self.infer_argument_types(arguments, &mut call_arguments, &argument_forms);
|
||||
@@ -6555,6 +6565,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
if let Some(use_id) = use_id {
|
||||
constraint_keys.push((file_scope_id, ConstraintKey::UseId(use_id)));
|
||||
}
|
||||
let local_is_unbound = local_scope_place.is_unbound();
|
||||
|
||||
let place = PlaceAndQualifiers::from(local_scope_place).or_fall_back_to(db, || {
|
||||
let has_bindings_in_this_scope = match place_table.place_id(place_expr) {
|
||||
@@ -6573,7 +6584,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let mut is_nonlocal_binding = false;
|
||||
if let Some(symbol) = place_expr.as_symbol() {
|
||||
if let Some(symbol_id) = place_table.symbol_id(symbol.name()) {
|
||||
if self.skip_non_global_scopes(file_scope_id, symbol_id) {
|
||||
// If we try to access a variable in a class before it has been defined,
|
||||
// the lookup will fall back to global.
|
||||
let fallback_to_global = local_is_unbound
|
||||
&& has_bindings_in_this_scope
|
||||
&& scope.node(db).scope_kind().is_class();
|
||||
if self.skip_non_global_scopes(file_scope_id, symbol_id) || fallback_to_global {
|
||||
return global_symbol(self.db(), self.file(), symbol.name()).map_type(
|
||||
|ty| {
|
||||
self.narrow_place_with_applicable_constraints(
|
||||
@@ -7142,7 +7158,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::TypeIs(_),
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_),
|
||||
) => {
|
||||
let unary_dunder_method = match op {
|
||||
ast::UnaryOp::Invert => "__invert__",
|
||||
@@ -7267,8 +7284,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
todo @ Type::Dynamic(
|
||||
DynamicType::Todo(_)
|
||||
| DynamicType::TodoPEP695ParamSpec
|
||||
| DynamicType::TodoTypeAlias
|
||||
| DynamicType::TodoTypedDict,
|
||||
| DynamicType::TodoTypeAlias,
|
||||
),
|
||||
_,
|
||||
_,
|
||||
@@ -7278,8 +7294,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
todo @ Type::Dynamic(
|
||||
DynamicType::Todo(_)
|
||||
| DynamicType::TodoPEP695ParamSpec
|
||||
| DynamicType::TodoTypeAlias
|
||||
| DynamicType::TodoTypedDict,
|
||||
| DynamicType::TodoTypeAlias,
|
||||
),
|
||||
_,
|
||||
) => Some(todo),
|
||||
@@ -7467,7 +7482,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::TypeIs(_),
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_),
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::Callable(..)
|
||||
@@ -7496,7 +7512,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::TypeIs(_),
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_),
|
||||
op,
|
||||
) => {
|
||||
// We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from
|
||||
@@ -8478,6 +8495,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
let tuple_generic_alias = |db: &'db dyn Db, tuple: Option<TupleType<'db>>| {
|
||||
let tuple =
|
||||
tuple.unwrap_or_else(|| TupleType::homogeneous(db, Type::unknown()).unwrap());
|
||||
tuple
|
||||
.to_class_type(db)
|
||||
.map(Type::from)
|
||||
.unwrap_or_else(Type::unknown)
|
||||
};
|
||||
|
||||
// HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the
|
||||
// subscript inference logic and treat this as an explicit specialization.
|
||||
// TODO: Move this logic into a custom callable, and update `find_name_in_mro` to return
|
||||
@@ -8486,9 +8512,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// special cases, too.
|
||||
if let Type::ClassLiteral(class) = value_ty {
|
||||
if class.is_tuple(self.db()) {
|
||||
return self
|
||||
.infer_tuple_type_expression(slice)
|
||||
.to_meta_type(self.db());
|
||||
return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice));
|
||||
}
|
||||
if let Some(generic_context) = class.generic_context(self.db()) {
|
||||
return self.infer_explicit_class_specialization(
|
||||
@@ -8500,9 +8524,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty {
|
||||
return self
|
||||
.infer_tuple_type_expression(slice)
|
||||
.to_meta_type(self.db());
|
||||
return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice));
|
||||
}
|
||||
|
||||
let slice_ty = self.infer_expression(slice);
|
||||
@@ -8525,7 +8547,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
);
|
||||
self.store_expression_type(
|
||||
slice_node,
|
||||
TupleType::from_elements(self.db(), arguments.iter_types()),
|
||||
Type::heterogeneous_tuple(self.db(), arguments.iter_types()),
|
||||
);
|
||||
arguments
|
||||
}
|
||||
@@ -8557,7 +8579,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
|
||||
fn infer_subscript_expression_types(
|
||||
&self,
|
||||
value_node: &ast::Expr,
|
||||
value_node: &'ast ast::Expr,
|
||||
value_ty: Type<'db>,
|
||||
slice_ty: Type<'db>,
|
||||
expr_context: ExprContext,
|
||||
@@ -8617,7 +8639,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
};
|
||||
|
||||
if let Ok(new_elements) = tuple.py_slice(db, start, stop, step) {
|
||||
TupleType::from_elements(db, new_elements)
|
||||
Type::heterogeneous_tuple(db, new_elements)
|
||||
} else {
|
||||
report_slice_step_size_zero(context, value_node.into());
|
||||
Type::unknown()
|
||||
@@ -8726,7 +8748,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
.map(|context| {
|
||||
Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context))
|
||||
})
|
||||
.unwrap_or_else(Type::unknown),
|
||||
.unwrap_or_else(GenericContextError::into_type),
|
||||
// TODO: emit a diagnostic
|
||||
TupleSpec::Variable(_) => Type::unknown(),
|
||||
})
|
||||
@@ -8739,7 +8761,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
LegacyGenericBase::Protocol,
|
||||
)
|
||||
.map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context)))
|
||||
.unwrap_or_else(Type::unknown),
|
||||
.unwrap_or_else(GenericContextError::into_type),
|
||||
),
|
||||
|
||||
(Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)), _) => {
|
||||
@@ -8758,7 +8780,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
.map(|context| {
|
||||
Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context))
|
||||
})
|
||||
.unwrap_or_else(Type::unknown),
|
||||
.unwrap_or_else(GenericContextError::into_type),
|
||||
// TODO: emit a diagnostic
|
||||
TupleSpec::Variable(_) => Type::unknown(),
|
||||
})
|
||||
@@ -8771,7 +8793,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
LegacyGenericBase::Generic,
|
||||
)
|
||||
.map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context)))
|
||||
.unwrap_or_else(Type::unknown),
|
||||
.unwrap_or_else(GenericContextError::into_type),
|
||||
),
|
||||
|
||||
(Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_)), _) => {
|
||||
@@ -8940,11 +8962,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
value_node: &ast::Expr,
|
||||
typevars: &[Type<'db>],
|
||||
origin: LegacyGenericBase,
|
||||
) -> Option<GenericContext<'db>> {
|
||||
let typevars: Option<FxOrderSet<_>> = typevars
|
||||
) -> Result<GenericContext<'db>, GenericContextError> {
|
||||
let typevars: Result<FxOrderSet<_>, GenericContextError> = typevars
|
||||
.iter()
|
||||
.map(|typevar| match typevar {
|
||||
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Some(*typevar),
|
||||
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Ok(*typevar),
|
||||
Type::NominalInstance(NominalInstanceType { class, .. })
|
||||
if matches!(
|
||||
class.known(self.db()),
|
||||
Some(KnownClass::TypeVarTuple | KnownClass::ParamSpec)
|
||||
) =>
|
||||
{
|
||||
Err(GenericContextError::NotYetSupported)
|
||||
}
|
||||
_ => {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node)
|
||||
@@ -8954,7 +8984,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
typevar.display(self.db()),
|
||||
));
|
||||
}
|
||||
None
|
||||
Err(GenericContextError::InvalidArgument)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -9644,7 +9674,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
let hinted_type = if list.len() == 1 {
|
||||
KnownClass::List.to_specialized_instance(db, inner_types)
|
||||
} else {
|
||||
TupleType::from_elements(db, inner_types)
|
||||
Type::heterogeneous_tuple(db, inner_types)
|
||||
};
|
||||
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
@@ -9676,7 +9706,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
Type::Dynamic(DynamicType::Todo(_) | DynamicType::Unknown)
|
||||
)
|
||||
}) {
|
||||
let hinted_type = TupleType::from_elements(self.db(), inner_types);
|
||||
let hinted_type = Type::heterogeneous_tuple(self.db(), inner_types);
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Did you mean `{}`?",
|
||||
hinted_type.display(self.db()),
|
||||
@@ -9895,7 +9925,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
) -> Type<'db> {
|
||||
match value_ty {
|
||||
Type::ClassLiteral(class_literal) => match class_literal.known(self.db()) {
|
||||
Some(KnownClass::Tuple) => self.infer_tuple_type_expression(slice),
|
||||
Some(KnownClass::Tuple) => Type::tuple(self.infer_tuple_type_expression(slice)),
|
||||
Some(KnownClass::Type) => self.infer_subclass_of_type_expression(slice),
|
||||
_ => self.infer_subscript_type_expression(subscript, value_ty),
|
||||
},
|
||||
@@ -9920,7 +9950,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
}
|
||||
|
||||
/// Given the slice of a `tuple[]` annotation, return the type that the annotation represents
|
||||
fn infer_tuple_type_expression(&mut self, tuple_slice: &ast::Expr) -> Type<'db> {
|
||||
fn infer_tuple_type_expression(&mut self, tuple_slice: &ast::Expr) -> Option<TupleType<'db>> {
|
||||
/// In most cases, if a subelement of the tuple is inferred as `Todo`,
|
||||
/// we should only infer `Todo` for that specific subelement.
|
||||
/// Certain specific AST nodes can however change the meaning of the entire tuple,
|
||||
@@ -9961,7 +9991,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
self.infer_expression(ellipsis);
|
||||
let result =
|
||||
TupleType::homogeneous(self.db(), self.infer_type_expression(element));
|
||||
self.store_expression_type(tuple_slice, result);
|
||||
self.store_expression_type(tuple_slice, Type::tuple(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -9988,15 +10018,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
}
|
||||
|
||||
let ty = if return_todo {
|
||||
todo_type!("PEP 646")
|
||||
TupleType::homogeneous(self.db(), todo_type!("PEP 646"))
|
||||
} else {
|
||||
Type::tuple(TupleType::new(self.db(), element_types))
|
||||
TupleType::new(self.db(), element_types)
|
||||
};
|
||||
|
||||
// Here, we store the type for the inner `int, str` tuple-expression,
|
||||
// while the type for the outer `tuple[int, str]` slice-expression is
|
||||
// stored in the surrounding `infer_type_expression` call:
|
||||
self.store_expression_type(tuple_slice, ty);
|
||||
self.store_expression_type(tuple_slice, Type::tuple(ty));
|
||||
|
||||
ty
|
||||
}
|
||||
@@ -10004,7 +10034,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
let single_element_ty = self.infer_type_expression(single_element);
|
||||
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty, self)
|
||||
{
|
||||
todo_type!("PEP 646")
|
||||
TupleType::homogeneous(self.db(), todo_type!("PEP 646"))
|
||||
} else {
|
||||
TupleType::from_elements(self.db(), std::iter::once(single_element_ty))
|
||||
}
|
||||
@@ -10668,7 +10698,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
Type::unknown()
|
||||
}
|
||||
SpecialFormType::Type => self.infer_subclass_of_type_expression(arguments_slice),
|
||||
SpecialFormType::Tuple => self.infer_tuple_type_expression(arguments_slice),
|
||||
SpecialFormType::Tuple => {
|
||||
Type::tuple(self.infer_tuple_type_expression(arguments_slice))
|
||||
}
|
||||
SpecialFormType::Generic | SpecialFormType::Protocol => {
|
||||
self.infer_expression(arguments_slice);
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
|
||||
@@ -10852,6 +10884,24 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GenericContextError {
|
||||
/// It's invalid to subscript `Generic` or `Protocol` with this type
|
||||
InvalidArgument,
|
||||
/// It's valid to subscribe `Generic` or `Protocol` with this type,
|
||||
/// but the type is not yet supported.
|
||||
NotYetSupported,
|
||||
}
|
||||
|
||||
impl GenericContextError {
|
||||
const fn into_type<'db>(self) -> Type<'db> {
|
||||
match self {
|
||||
GenericContextError::InvalidArgument => Type::unknown(),
|
||||
GenericContextError::NotYetSupported => todo_type!("ParamSpecs and TypeVarTuples"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The deferred state of a specific expression in an inference region.
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
enum DeferredExpressionState {
|
||||
|
||||
@@ -19,7 +19,7 @@ impl<'db> Type<'db> {
|
||||
match (class, class.known(db)) {
|
||||
(_, Some(KnownClass::Any)) => Self::Dynamic(DynamicType::Any),
|
||||
(ClassType::NonGeneric(_), Some(KnownClass::Tuple)) => {
|
||||
TupleType::homogeneous(db, Type::unknown())
|
||||
Type::tuple(TupleType::homogeneous(db, Type::unknown()))
|
||||
}
|
||||
(ClassType::Generic(alias), Some(KnownClass::Tuple)) => {
|
||||
Self::tuple(TupleType::new(db, alias.specialization(db).tuple(db)))
|
||||
@@ -125,15 +125,17 @@ impl<'db> NominalInstanceType<'db> {
|
||||
}
|
||||
|
||||
pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool {
|
||||
self.class.known(db).is_some_and(KnownClass::is_singleton)
|
||||
|| is_single_member_enum(db, self.class.class_literal(db).0)
|
||||
self.class
|
||||
.known(db)
|
||||
.map(KnownClass::is_singleton)
|
||||
.unwrap_or_else(|| is_single_member_enum(db, self.class.class_literal(db).0))
|
||||
}
|
||||
|
||||
pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool {
|
||||
self.class
|
||||
.known(db)
|
||||
.is_some_and(KnownClass::is_single_valued)
|
||||
|| is_single_member_enum(db, self.class.class_literal(db).0)
|
||||
.map(KnownClass::is_single_valued)
|
||||
.unwrap_or_else(|| is_single_member_enum(db, self.class.class_literal(db).0))
|
||||
}
|
||||
|
||||
pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
|
||||
@@ -269,7 +269,10 @@ impl<'db> Mro<'db> {
|
||||
continue;
|
||||
}
|
||||
match base {
|
||||
ClassBase::Class(_) | ClassBase::Generic | ClassBase::Protocol => {
|
||||
ClassBase::Class(_)
|
||||
| ClassBase::Generic
|
||||
| ClassBase::Protocol
|
||||
| ClassBase::TypedDict => {
|
||||
errors.push(DuplicateBaseError {
|
||||
duplicate_base: base,
|
||||
first_index: *first_index,
|
||||
|
||||
@@ -256,7 +256,8 @@ impl ClassInfoConstraintFunction {
|
||||
| Type::KnownInstance(_)
|
||||
| Type::TypeIs(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::DataclassTransformer(_) => None,
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::TypedDict(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,6 +411,9 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
|
||||
PatternPredicateKind::Or(predicates) => {
|
||||
self.evaluate_match_pattern_or(subject, predicates, is_positive)
|
||||
}
|
||||
PatternPredicateKind::As(pattern, _) => pattern
|
||||
.as_deref()
|
||||
.and_then(|p| self.evaluate_pattern_predicate_kind(p, subject, is_positive)),
|
||||
PatternPredicateKind::Unsupported => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,13 +195,13 @@ impl Ty {
|
||||
}
|
||||
Ty::FixedLengthTuple(tys) => {
|
||||
let elements = tys.into_iter().map(|ty| ty.into_type(db));
|
||||
TupleType::from_elements(db, elements)
|
||||
Type::heterogeneous_tuple(db, elements)
|
||||
}
|
||||
Ty::VariableLengthTuple(prefix, variable, suffix) => {
|
||||
let prefix = prefix.into_iter().map(|ty| ty.into_type(db));
|
||||
let variable = variable.into_type(db);
|
||||
let suffix = suffix.into_iter().map(|ty| ty.into_type(db));
|
||||
TupleType::mixed(db, prefix, variable, suffix)
|
||||
Type::tuple(TupleType::mixed(db, prefix, variable, suffix))
|
||||
}
|
||||
Ty::SubclassOfAny => SubclassOfType::subclass_of_any(),
|
||||
Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from(
|
||||
|
||||
@@ -196,6 +196,12 @@ impl<'db> SubclassOfType<'db> {
|
||||
SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
|
||||
self.subclass_of
|
||||
.into_class()
|
||||
.is_some_and(|class| class.class_literal(db).0.is_typed_dict(db))
|
||||
}
|
||||
}
|
||||
|
||||
/// An enumeration of the different kinds of `type[]` types that a [`SubclassOfType`] can represent:
|
||||
|
||||
@@ -22,8 +22,8 @@ use std::hash::Hash;
|
||||
|
||||
use itertools::{Either, EitherOrBoth, Itertools};
|
||||
|
||||
use crate::types::Truthiness;
|
||||
use crate::types::class::{ClassType, KnownClass};
|
||||
use crate::types::{SubclassOfType, Truthiness};
|
||||
use crate::types::{
|
||||
Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, TypeVarVariance,
|
||||
UnionBuilder, UnionType, cyclic::PairVisitor,
|
||||
@@ -151,6 +151,25 @@ impl<'db> Type<'db> {
|
||||
};
|
||||
Self::Tuple(tuple)
|
||||
}
|
||||
|
||||
pub(crate) fn homogeneous_tuple(db: &'db dyn Db, element: Type<'db>) -> Self {
|
||||
Type::tuple(TupleType::homogeneous(db, element))
|
||||
}
|
||||
|
||||
pub(crate) fn heterogeneous_tuple<I, T>(db: &'db dyn Db, elements: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<Type<'db>>,
|
||||
{
|
||||
Type::tuple(TupleType::from_elements(
|
||||
db,
|
||||
elements.into_iter().map(Into::into),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn empty_tuple(db: &'db dyn Db) -> Self {
|
||||
Type::Tuple(TupleType::empty(db))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> TupleType<'db> {
|
||||
@@ -180,18 +199,16 @@ impl<'db> TupleType<'db> {
|
||||
Some(TupleType::new_internal(db, tuple_key))
|
||||
}
|
||||
|
||||
pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> {
|
||||
Type::tuple(TupleType::new(
|
||||
db,
|
||||
TupleSpec::from(FixedLengthTuple::empty()),
|
||||
))
|
||||
pub(crate) fn empty(db: &'db dyn Db) -> Self {
|
||||
TupleType::new(db, TupleSpec::from(FixedLengthTuple::empty()))
|
||||
.expect("TupleType::new() should always return `Some` for an empty `TupleSpec`")
|
||||
}
|
||||
|
||||
pub(crate) fn from_elements(
|
||||
db: &'db dyn Db,
|
||||
types: impl IntoIterator<Item = Type<'db>>,
|
||||
) -> Type<'db> {
|
||||
Type::tuple(TupleType::new(db, TupleSpec::from_elements(types)))
|
||||
) -> Option<Self> {
|
||||
TupleType::new(db, TupleSpec::from_elements(types))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -200,15 +217,12 @@ impl<'db> TupleType<'db> {
|
||||
prefix: impl IntoIterator<Item = Type<'db>>,
|
||||
variable: Type<'db>,
|
||||
suffix: impl IntoIterator<Item = Type<'db>>,
|
||||
) -> Type<'db> {
|
||||
Type::tuple(TupleType::new(
|
||||
db,
|
||||
VariableLengthTuple::mixed(prefix, variable, suffix),
|
||||
))
|
||||
) -> Option<Self> {
|
||||
TupleType::new(db, VariableLengthTuple::mixed(prefix, variable, suffix))
|
||||
}
|
||||
|
||||
pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Type<'db> {
|
||||
Type::tuple(TupleType::new(db, TupleSpec::homogeneous(element)))
|
||||
pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Option<Self> {
|
||||
TupleType::new(db, TupleSpec::homogeneous(element))
|
||||
}
|
||||
|
||||
pub(crate) fn to_class_type(self, db: &'db dyn Db) -> Option<ClassType<'db>> {
|
||||
@@ -224,6 +238,11 @@ impl<'db> TupleType<'db> {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
self.to_class_type(db)
|
||||
.map(|class| SubclassOfType::from(db, class))
|
||||
}
|
||||
|
||||
/// Return a normalized version of `self`.
|
||||
///
|
||||
/// See [`Type::normalized`] for more details.
|
||||
|
||||
@@ -166,6 +166,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||
(ClassBase::Generic, _) => Ordering::Less,
|
||||
(_, ClassBase::Generic) => Ordering::Greater,
|
||||
|
||||
(ClassBase::TypedDict, _) => Ordering::Less,
|
||||
(_, ClassBase::TypedDict) => Ordering::Greater,
|
||||
|
||||
(ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => {
|
||||
dynamic_elements_ordering(left, right)
|
||||
}
|
||||
@@ -234,6 +237,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||
|
||||
unreachable!("Two equal, normalized intersections should share the same Salsa ID")
|
||||
}
|
||||
|
||||
(Type::TypedDict(left), Type::TypedDict(right)) => left.cmp(right),
|
||||
(Type::TypedDict(_), _) => Ordering::Less,
|
||||
(_, Type::TypedDict(_)) => Ordering::Greater,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,9 +264,6 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
|
||||
|
||||
(DynamicType::TodoTypeAlias, _) => Ordering::Less,
|
||||
(_, DynamicType::TodoTypeAlias) => Ordering::Greater,
|
||||
|
||||
(DynamicType::TodoTypedDict, _) => Ordering::Less,
|
||||
(_, DynamicType::TodoTypedDict) => Ordering::Greater,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
438
crates/ty_python_semantic/src/types/unification.rs
Normal file
438
crates/ty_python_semantic/src/types/unification.rs
Normal file
@@ -0,0 +1,438 @@
|
||||
//! Implements the _tallying_ algorithm from [[POPL2015][]], which finds the unification of a
|
||||
//! set of subtyping constraints.
|
||||
//!
|
||||
//! [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
|
||||
// XXX
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::Db;
|
||||
use crate::types::{
|
||||
IntersectionBuilder, IntersectionType, Type, TypeVarInstance, UnionBuilder, UnionType,
|
||||
};
|
||||
|
||||
/// A constraint that the type `s` must be a subtype of the type `t`. Tallying will find all
|
||||
/// substitutions of any type variables in `s` and `t` that ensure that this subtyping holds.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)]
|
||||
pub(crate) struct Constraint<'db> {
|
||||
pub(crate) lower: Type<'db>,
|
||||
pub(crate) typevar: TypeVarInstance<'db>,
|
||||
pub(crate) upper: Type<'db>,
|
||||
}
|
||||
|
||||
impl<'db> Constraint<'db> {
|
||||
/// Returns whether this constraint subsumes `other` — if it constrains the same typevar and
|
||||
/// has tighter bounds.
|
||||
fn subsumes(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.typevar == other.typevar
|
||||
&& other.lower.is_assignable_to(db, self.lower)
|
||||
&& self.upper.is_assignable_to(db, other.upper)
|
||||
}
|
||||
|
||||
/// Merges another constraint into this one. Panics if the two constraints have different
|
||||
/// typevars.
|
||||
fn merge(&mut self, db: &'db dyn Db, other: Self) {
|
||||
debug_assert!(self.typevar == other.typevar);
|
||||
self.lower = UnionType::from_elements(db, [self.lower, other.lower]);
|
||||
self.upper = IntersectionType::from_elements(db, [self.upper, other.upper]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of merged constraints. We guarantee that no constraint in the set subsumes another, and
|
||||
/// that no two constraints in the set have the same typevar.
|
||||
///
|
||||
/// This is denoted _C_ in [[POPL2015][]].
|
||||
///
|
||||
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
#[derive(Clone, Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)]
|
||||
pub(crate) struct ConstraintSet<'db> {
|
||||
constraints: Vec<Constraint<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> ConstraintSet<'db> {
|
||||
/// Returns an empty constraint set
|
||||
fn empty() -> Self {
|
||||
Self {
|
||||
constraints: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a singleton constraint set.
|
||||
pub(crate) fn singleton(constraint: Constraint<'db>) -> Self {
|
||||
Self {
|
||||
constraints: vec![constraint],
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a new constraint to this set, ensuring that no constraint in the set subsumes another,
|
||||
/// and that no two constraints in the set have the same typevar.
|
||||
fn add(&mut self, db: &'db dyn Db, constraint: Constraint<'db>) {
|
||||
for existing in &mut self.constraints {
|
||||
if constraint.typevar == existing.typevar {
|
||||
existing.merge(db, constraint);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.constraints.push(constraint);
|
||||
}
|
||||
|
||||
/// Combines two constraint sets, merging any constraints that share the same typevar.
|
||||
fn combine(&mut self, db: &'db dyn Db, other: &Self) {
|
||||
for constraint in &other.constraints {
|
||||
self.add(db, *constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this constraint set subsumes `other` — if every constraint in `other` is
|
||||
/// subsumed by some constraint in `self`.
|
||||
fn subsumes(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
other.constraints.iter().all(|other_constraint| {
|
||||
self.constraints
|
||||
.iter()
|
||||
.any(|self_constraint| self_constraint.subsumes(db, *other_constraint))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of constraint sets.
|
||||
///
|
||||
/// This is denoted _𝒮_ in [[POPL2015][]].
|
||||
///
|
||||
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
#[derive(Clone, Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)]
|
||||
pub(crate) struct ConstraintSetSet<'db> {
|
||||
sets: Vec<ConstraintSet<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> ConstraintSetSet<'db> {
|
||||
/// Returns the set of constraint sets that is unsolvable.
|
||||
pub(crate) fn never() -> Self {
|
||||
Self { sets: vec![] }
|
||||
}
|
||||
|
||||
/// Returns a singleton set of constraint sets.
|
||||
pub(crate) fn singleton(constraint_set: ConstraintSet<'db>) -> Self {
|
||||
Self {
|
||||
sets: vec![constraint_set],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the set of constraint sets that is always satisfied.
|
||||
pub(crate) fn always() -> Self {
|
||||
Self::singleton(ConstraintSet::empty())
|
||||
}
|
||||
|
||||
/// Adds a new constraint set to this set, ensuring that no constraint set in the set subsumes
|
||||
/// another.
|
||||
fn add(&mut self, db: &'db dyn Db, constraint_set: ConstraintSet<'db>) {
|
||||
for existing in &mut self.sets {
|
||||
// If there is an existing constraint set that subsumes (or is subsumed by) the new
|
||||
// one, we want to keep the _subsumed_ constraint set.
|
||||
if constraint_set.subsumes(db, existing) {
|
||||
return;
|
||||
} else if existing.subsumes(db, &constraint_set) {
|
||||
*existing = constraint_set;
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.sets.push(constraint_set);
|
||||
}
|
||||
|
||||
/// Intersects two sets of constraint sets.
|
||||
///
|
||||
/// This is the ⊓ operator from [[POPL2015][]], Definition 3.5.
|
||||
///
|
||||
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
fn intersect(&self, db: &'db dyn Db, other: &Self) -> Self {
|
||||
let mut result = Self::never();
|
||||
for self_set in &self.sets {
|
||||
for other_set in &other.sets {
|
||||
let mut new_set = self_set.clone();
|
||||
new_set.combine(db, other_set);
|
||||
result.add(db, new_set);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Union two sets of constraint sets.
|
||||
///
|
||||
/// This is the ⊔ operator from [[POPL2015][]], Definition 3.5.
|
||||
///
|
||||
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
fn union(&mut self, db: &'db dyn Db, other: Self) {
|
||||
for set in other.sets {
|
||||
self.add(db, set);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the distributed intersection of an iterator of sets of constraint sets.
|
||||
fn distributed_intersection(db: &'db dyn Db, sets: impl IntoIterator<Item = Self>) -> Self {
|
||||
sets.into_iter()
|
||||
.fold(Self::always(), |sets, element| element.intersect(db, &sets))
|
||||
}
|
||||
|
||||
/// Calculate the distributed union of an iterator of sets of constraint sets.
|
||||
fn distributed_union(db: &'db dyn Db, sets: impl IntoIterator<Item = Self>) -> Self {
|
||||
let mut result = Self::never();
|
||||
for set in sets {
|
||||
result.union(db, set);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Constraint<'db>> for ConstraintSetSet<'db> {
|
||||
fn from(constraint: Constraint<'db>) -> ConstraintSetSet<'db> {
|
||||
ConstraintSetSet::singleton(ConstraintSet::singleton(constraint))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<ConstraintSet<'db>> for ConstraintSetSet<'db> {
|
||||
fn from(constraint_set: ConstraintSet<'db>) -> ConstraintSetSet<'db> {
|
||||
ConstraintSetSet::singleton(constraint_set)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a set of normalized constraint sets that is equivalent to the constraint that `ty` is
|
||||
/// (a subtype of) `Never`.
|
||||
///
|
||||
/// This is a combination of the "Constraint normalization" and "Constraint merging" steps from
|
||||
/// [[POPL2015][]], §3.2.1.
|
||||
///
|
||||
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
pub(crate) fn normalized_constraints_from_type<'db>(
|
||||
db: &'db dyn Db,
|
||||
ty: Type<'db>,
|
||||
) -> ConstraintSetSet<'db> {
|
||||
normalized_constraints_from_type_inner(db, ty, ())
|
||||
}
|
||||
|
||||
#[salsa::tracked(
|
||||
cycle_fn=normalized_constraints_from_type_recover,
|
||||
cycle_initial=normalized_constraints_from_type_initial,
|
||||
heap_size=get_size2::heap_size,
|
||||
)]
|
||||
fn normalized_constraints_from_type_inner<'db>(
|
||||
db: &'db dyn Db,
|
||||
ty: Type<'db>,
|
||||
_unused: (),
|
||||
) -> ConstraintSetSet<'db> {
|
||||
match ty {
|
||||
// These atomic types are always inhabited by at least one value, and can
|
||||
// therefore never be a subtype of `Never`.
|
||||
Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::Never
|
||||
| Type::LiteralString
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::EnumLiteral(_)
|
||||
| Type::DataclassDecorator(_)
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::SpecialForm(_) => ConstraintSetSet::never(),
|
||||
|
||||
Type::Union(union) => {
|
||||
// Figure 3, step 6
|
||||
// A union is a subtype of Never only if every element is.
|
||||
ConstraintSetSet::distributed_union(
|
||||
db,
|
||||
(union.iter(db)).map(|element| normalized_constraints_from_type(db, *element)),
|
||||
)
|
||||
}
|
||||
|
||||
Type::Intersection(intersection) => {
|
||||
// Figure 3, step 2
|
||||
// If an intersection contains any (positive or negative) top-level type
|
||||
// variables, extract out and isolate the smallest one (according to our
|
||||
// arbitrary ordering).
|
||||
let smallest_positive = find_smallest_typevar(intersection.iter_positive(db));
|
||||
let smallest_negative = find_smallest_typevar(intersection.iter_negative(db));
|
||||
let constraint = match (smallest_positive, smallest_negative) {
|
||||
(Some(typevar), None) => Some(Constraint {
|
||||
lower: Type::Never,
|
||||
typevar,
|
||||
upper: remove_positive_typevar(db, intersection, typevar).negate(db),
|
||||
}),
|
||||
|
||||
(Some(typevar), Some(negative)) if typevar < negative => Some(Constraint {
|
||||
lower: Type::Never,
|
||||
typevar,
|
||||
upper: remove_positive_typevar(db, intersection, typevar).negate(db),
|
||||
}),
|
||||
|
||||
(_, Some(typevar)) => Some(Constraint {
|
||||
lower: remove_negative_typevar(db, intersection, typevar),
|
||||
typevar,
|
||||
upper: Type::object(db),
|
||||
}),
|
||||
|
||||
(None, None) => None,
|
||||
};
|
||||
if let Some(constraint) = constraint {
|
||||
return constraint.into();
|
||||
}
|
||||
|
||||
// Figure 3, step 3
|
||||
// If all (positive and negative) elements of the intersection are atomic
|
||||
// types (and therefore cannot contain any typevars), fall back on an
|
||||
// assignability check: if the intersection of the positive elements is
|
||||
// assignable to the union of the negative elements, then the overall
|
||||
// intersection is empty.
|
||||
let mut all_atomic = true;
|
||||
let mut positive_atomic = IntersectionBuilder::new(db);
|
||||
let mut negative_atomic = UnionBuilder::new(db);
|
||||
for positive in intersection.iter_positive(db) {
|
||||
if !all_atomic {
|
||||
break;
|
||||
}
|
||||
if !positive.is_atomic() {
|
||||
all_atomic = false;
|
||||
break;
|
||||
}
|
||||
positive_atomic = positive_atomic.add_positive(positive);
|
||||
}
|
||||
for negative in intersection.iter_negative(db) {
|
||||
if !all_atomic {
|
||||
break;
|
||||
}
|
||||
if !negative.is_atomic() {
|
||||
all_atomic = false;
|
||||
break;
|
||||
}
|
||||
negative_atomic = negative_atomic.add(negative);
|
||||
}
|
||||
if all_atomic {
|
||||
let positive_atomic = positive_atomic.build();
|
||||
let negative_atomic = negative_atomic.build();
|
||||
if positive_atomic.is_assignable_to(db, negative_atomic) {
|
||||
return ConstraintSetSet::always();
|
||||
} else {
|
||||
return ConstraintSetSet::never();
|
||||
};
|
||||
}
|
||||
|
||||
// Figure 3, step 4
|
||||
// If all (positive and negative) elements of the intersection are callable, decompose
|
||||
// the corresponding arrow type.
|
||||
let positive_callable: Option<Vec<_>> = (intersection.iter_positive(db))
|
||||
.map(|ty| ty.into_callable(db))
|
||||
.collect();
|
||||
let negative_callable: Option<Vec<_>> = (intersection.iter_negative(db))
|
||||
.map(|ty| ty.into_callable(db))
|
||||
.collect();
|
||||
if let (Some(positive), Some(negative)) = (positive_callable, negative_callable) {
|
||||
ConstraintSetSet::distributed_union(
|
||||
db,
|
||||
negative.into_iter().map(|negative| {
|
||||
let norm_negative = normalized_constraints_from_type(db);
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Do we need to handle non-uniform intersections? For now, assume
|
||||
// the intersection is not empty.
|
||||
ConstraintSetSet::never()
|
||||
}
|
||||
|
||||
Type::TypeVar(typevar) => {
|
||||
// Figure 3, step 2
|
||||
// (special case where P' = {typevar}, and P = N = N' = ø)
|
||||
let constraint = Constraint {
|
||||
lower: Type::Never,
|
||||
typevar,
|
||||
upper: Type::object(db),
|
||||
};
|
||||
constraint.into()
|
||||
}
|
||||
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_constraints_from_type_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &ConstraintSetSet<'db>,
|
||||
_count: u32,
|
||||
_ty: Type<'db>,
|
||||
_unused: (),
|
||||
) -> salsa::CycleRecoveryAction<ConstraintSetSet<'db>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn normalized_constraints_from_type_initial<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_ty: Type<'db>,
|
||||
_unused: (),
|
||||
) -> ConstraintSetSet<'db> {
|
||||
// Figure 3, step 1: The coinductive base case, for if we encounter this type again recursively
|
||||
// while we are in the middle of calculating a result for it.
|
||||
ConstraintSetSet::always()
|
||||
}
|
||||
|
||||
/// Returns the “smallest” top-level typevar in an iterator of types.
|
||||
///
|
||||
/// “Smallest” is with respect to the ≼ total order mentioned in [[POPL2015][]], §3.2.1. “Any will
|
||||
/// do”, so we just compare Salsa IDs.
|
||||
///
|
||||
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
fn find_smallest_typevar<'db>(
|
||||
types: impl IntoIterator<Item = Type<'db>>,
|
||||
) -> Option<TypeVarInstance<'db>> {
|
||||
types
|
||||
.into_iter()
|
||||
.filter_map(|ty| match ty {
|
||||
Type::TypeVar(typevar) => Some(typevar),
|
||||
_ => None,
|
||||
})
|
||||
.min()
|
||||
}
|
||||
|
||||
/// Removes a top-level positive typevar from an intersection.
|
||||
///
|
||||
/// This is the `single` function from [[POPL2015][]], §3.2.1, for the `k ∈ P'` case.
|
||||
///
|
||||
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
fn remove_positive_typevar<'db>(
|
||||
db: &'db dyn Db,
|
||||
intersection: IntersectionType<'db>,
|
||||
typevar: TypeVarInstance<'db>,
|
||||
) -> Type<'db> {
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
for positive in intersection.iter_positive(db) {
|
||||
if positive != Type::TypeVar(typevar) {
|
||||
builder = builder.add_positive(positive);
|
||||
}
|
||||
}
|
||||
for negative in intersection.iter_negative(db) {
|
||||
builder = builder.add_negative(negative);
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
||||
/// Removes a top-level negative typevar from an intersection.
|
||||
///
|
||||
/// This is the `single` function from [[POPL2015][]], §3.2.1, for the `k ∈ N'` case.
|
||||
///
|
||||
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
fn remove_negative_typevar<'db>(
|
||||
db: &'db dyn Db,
|
||||
intersection: IntersectionType<'db>,
|
||||
typevar: TypeVarInstance<'db>,
|
||||
) -> Type<'db> {
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
for positive in intersection.iter_positive(db) {
|
||||
builder = builder.add_positive(positive);
|
||||
}
|
||||
for negative in intersection.iter_negative(db) {
|
||||
if negative != Type::TypeVar(typevar) {
|
||||
builder = builder.add_negative(negative);
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
BoundMethodType, BoundSuperType, CallableType, GenericAlias, IntersectionType,
|
||||
KnownInstanceType, MethodWrapperKind, NominalInstanceType, PropertyInstanceType,
|
||||
ProtocolInstanceType, SubclassOfType, Type, TypeAliasType, TypeIsType, TypeVarInstance,
|
||||
UnionType,
|
||||
TypedDictType, UnionType,
|
||||
class::walk_generic_alias,
|
||||
function::{FunctionType, walk_function_type},
|
||||
instance::{walk_nominal_instance_type, walk_protocol_instance_type},
|
||||
@@ -12,7 +12,8 @@ use crate::{
|
||||
tuple::{TupleType, walk_tuple_type},
|
||||
walk_bound_method_type, walk_bound_super_type, walk_callable_type, walk_intersection_type,
|
||||
walk_known_instance_type, walk_method_wrapper_type, walk_property_instance_type,
|
||||
walk_type_alias_type, walk_type_var_type, walk_typeis_type, walk_union,
|
||||
walk_type_alias_type, walk_type_var_type, walk_typed_dict_type, walk_typeis_type,
|
||||
walk_union,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -107,6 +108,10 @@ pub(crate) trait TypeVisitor<'db> {
|
||||
fn visit_type_alias_type(&mut self, db: &'db dyn Db, type_alias: TypeAliasType<'db>) {
|
||||
walk_type_alias_type(db, type_alias, self);
|
||||
}
|
||||
|
||||
fn visit_typed_dict_type(&mut self, db: &'db dyn Db, typed_dict: TypedDictType<'db>) {
|
||||
walk_typed_dict_type(db, typed_dict, self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration of types that may contain other types, such as unions, intersections, and generics.
|
||||
@@ -128,6 +133,7 @@ enum NonAtomicType<'db> {
|
||||
TypeIs(TypeIsType<'db>),
|
||||
TypeVar(TypeVarInstance<'db>),
|
||||
ProtocolInstance(ProtocolInstanceType<'db>),
|
||||
TypedDict(TypedDictType<'db>),
|
||||
}
|
||||
|
||||
enum TypeKind<'db> {
|
||||
@@ -190,10 +196,19 @@ impl<'db> From<Type<'db>> for TypeKind<'db> {
|
||||
}
|
||||
Type::TypeVar(type_var) => TypeKind::NonAtomic(NonAtomicType::TypeVar(type_var)),
|
||||
Type::TypeIs(type_is) => TypeKind::NonAtomic(NonAtomicType::TypeIs(type_is)),
|
||||
Type::TypedDict(typed_dict) => {
|
||||
TypeKind::NonAtomic(NonAtomicType::TypedDict(typed_dict))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<'_> {
|
||||
pub(crate) fn is_atomic(self) -> bool {
|
||||
matches!(TypeKind::from(self), TypeKind::Atomic)
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>(
|
||||
db: &'db dyn Db,
|
||||
non_atomic_type: NonAtomicType<'db>,
|
||||
@@ -226,6 +241,7 @@ fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>(
|
||||
NonAtomicType::ProtocolInstance(protocol) => {
|
||||
visitor.visit_protocol_instance_type(db, protocol);
|
||||
}
|
||||
NonAtomicType::TypedDict(typed_dict) => visitor.visit_typed_dict_type(db, typed_dict),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use lsp_server::Connection;
|
||||
use ruff_db::system::{OsSystem, SystemPathBuf};
|
||||
|
||||
pub use crate::logging::{LogLevel, init_logging};
|
||||
pub use crate::server::Server;
|
||||
pub use crate::server::{PartialWorkspaceProgress, PartialWorkspaceProgressParams, Server};
|
||||
pub use crate::session::{ClientOptions, DiagnosticMode};
|
||||
pub use document::{NotebookDocument, PositionEncoding, TextDocument};
|
||||
pub(crate) use session::{DocumentQuery, Session};
|
||||
@@ -47,7 +47,7 @@ pub fn run_server() -> anyhow::Result<()> {
|
||||
// This is to complement the `LSPSystem` if the document is not available in the index.
|
||||
let fallback_system = Arc::new(OsSystem::new(cwd));
|
||||
|
||||
let server_result = Server::new(worker_threads, connection, fallback_system, true)
|
||||
let server_result = Server::new(worker_threads, connection, fallback_system, false)
|
||||
.context("Failed to start server")?
|
||||
.run();
|
||||
|
||||
|
||||
@@ -29,10 +29,10 @@ pub(crate) use main_loop::{
|
||||
Action, ConnectionSender, Event, MainLoopReceiver, MainLoopSender, SendRequest,
|
||||
};
|
||||
pub(crate) type Result<T> = std::result::Result<T, api::Error>;
|
||||
pub use api::{PartialWorkspaceProgress, PartialWorkspaceProgressParams};
|
||||
|
||||
pub struct Server {
|
||||
connection: Connection,
|
||||
client_capabilities: ClientCapabilities,
|
||||
worker_threads: NonZeroUsize,
|
||||
main_loop_receiver: MainLoopReceiver,
|
||||
main_loop_sender: MainLoopSender,
|
||||
@@ -44,7 +44,7 @@ impl Server {
|
||||
worker_threads: NonZeroUsize,
|
||||
connection: Connection,
|
||||
native_system: Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>,
|
||||
initialize_logging: bool,
|
||||
in_test: bool,
|
||||
) -> crate::Result<Self> {
|
||||
let (id, init_value) = connection.initialize_start()?;
|
||||
let init_params: InitializeParams = serde_json::from_value(init_value)?;
|
||||
@@ -81,7 +81,7 @@ impl Server {
|
||||
let (main_loop_sender, main_loop_receiver) = crossbeam::channel::bounded(32);
|
||||
let client = Client::new(main_loop_sender.clone(), connection.sender.clone());
|
||||
|
||||
if initialize_logging {
|
||||
if !in_test {
|
||||
crate::logging::init_logging(
|
||||
global_options.tracing.log_level.unwrap_or_default(),
|
||||
global_options.tracing.log_file.as_deref(),
|
||||
@@ -160,8 +160,8 @@ impl Server {
|
||||
global_options,
|
||||
workspaces,
|
||||
native_system,
|
||||
in_test,
|
||||
)?,
|
||||
client_capabilities,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use self::traits::{NotificationHandler, RequestHandler};
|
||||
use super::{Result, schedule::BackgroundSchedule};
|
||||
use crate::session::client::Client;
|
||||
pub(crate) use diagnostics::publish_settings_diagnostics;
|
||||
pub use requests::{PartialWorkspaceProgress, PartialWorkspaceProgressParams};
|
||||
use ruff_db::panic::PanicError;
|
||||
|
||||
/// Processes a request from the client to the server.
|
||||
@@ -218,13 +219,11 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
let result = ruff_db::panic::catch_unwind(|| {
|
||||
if let Err(error) = ruff_db::panic::catch_unwind(|| {
|
||||
let snapshot = snapshot;
|
||||
R::run(snapshot.0, client, params)
|
||||
});
|
||||
|
||||
if let Some(response) = request_result_to_response::<R>(&id, client, result, retry) {
|
||||
respond::<R>(&id, response, client);
|
||||
R::handle_request(&id, snapshot.0, client, params);
|
||||
}) {
|
||||
panic_response::<R>(&id, client, &error, retry);
|
||||
}
|
||||
})
|
||||
}))
|
||||
@@ -287,58 +286,50 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
let result = ruff_db::panic::catch_unwind(|| {
|
||||
R::run_with_snapshot(&db, snapshot, client, params)
|
||||
});
|
||||
|
||||
if let Some(response) = request_result_to_response::<R>(&id, client, result, retry) {
|
||||
respond::<R>(&id, response, client);
|
||||
if let Err(error) = ruff_db::panic::catch_unwind(|| {
|
||||
R::handle_request(&id, &db, snapshot, client, params);
|
||||
}) {
|
||||
panic_response::<R>(&id, client, &error, retry);
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn request_result_to_response<R>(
|
||||
fn panic_response<R>(
|
||||
id: &RequestId,
|
||||
client: &Client,
|
||||
result: std::result::Result<
|
||||
Result<<<R as RequestHandler>::RequestType as Request>::Result>,
|
||||
PanicError,
|
||||
>,
|
||||
error: &PanicError,
|
||||
request: Option<lsp_server::Request>,
|
||||
) -> Option<Result<<<R as RequestHandler>::RequestType as Request>::Result>>
|
||||
where
|
||||
) where
|
||||
R: traits::RetriableRequestHandler,
|
||||
{
|
||||
match result {
|
||||
Ok(response) => Some(response),
|
||||
Err(error) => {
|
||||
// Check if the request was canceled due to some modifications to the salsa database.
|
||||
if error.payload.downcast_ref::<salsa::Cancelled>().is_some() {
|
||||
// If the query supports retry, re-queue the request.
|
||||
// The query is still likely to succeed if the user modified any other document.
|
||||
if let Some(request) = request {
|
||||
tracing::trace!(
|
||||
"request id={} method={} was cancelled by salsa, re-queueing for retry",
|
||||
request.id,
|
||||
request.method
|
||||
);
|
||||
client.retry(request);
|
||||
} else {
|
||||
tracing::trace!(
|
||||
"request id={} was cancelled by salsa, sending content modified",
|
||||
id
|
||||
);
|
||||
respond_silent_error(id.clone(), client, R::salsa_cancellation_error());
|
||||
}
|
||||
None
|
||||
} else {
|
||||
Some(Err(Error {
|
||||
code: lsp_server::ErrorCode::InternalError,
|
||||
error: anyhow!("request handler {error}"),
|
||||
}))
|
||||
}
|
||||
// Check if the request was canceled due to some modifications to the salsa database.
|
||||
if error.payload.downcast_ref::<salsa::Cancelled>().is_some() {
|
||||
// If the query supports retry, re-queue the request.
|
||||
// The query is still likely to succeed if the user modified any other document.
|
||||
if let Some(request) = request {
|
||||
tracing::trace!(
|
||||
"request id={} method={} was cancelled by salsa, re-queueing for retry",
|
||||
request.id,
|
||||
request.method
|
||||
);
|
||||
client.retry(request);
|
||||
} else {
|
||||
tracing::trace!(
|
||||
"request id={} was cancelled by salsa, sending content modified",
|
||||
id
|
||||
);
|
||||
respond_silent_error(id.clone(), client, R::salsa_cancellation_error());
|
||||
}
|
||||
} else {
|
||||
respond::<R>(
|
||||
id,
|
||||
Err(Error {
|
||||
code: lsp_server::ErrorCode::InternalError,
|
||||
error: anyhow!("request handler {error}"),
|
||||
}),
|
||||
client,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +342,13 @@ fn sync_notification_task<N: traits::SyncNotificationHandler>(
|
||||
if let Err(err) = N::run(session, client, params) {
|
||||
tracing::error!("An error occurred while running {id}: {err}");
|
||||
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's a pending workspace diagnostic long-polling request,
|
||||
// resume it, but only if the session revision changed (e.g. because some document changed).
|
||||
session.resume_suspended_workspace_diagnostic_request(client);
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -26,17 +26,29 @@ pub(super) struct Diagnostics<'a> {
|
||||
}
|
||||
|
||||
impl Diagnostics<'_> {
|
||||
pub(super) fn result_id_from_hash(diagnostics: &[ruff_db::diagnostic::Diagnostic]) -> String {
|
||||
/// Computes the result ID for `diagnostics`.
|
||||
///
|
||||
/// Returns `None` if there are no diagnostics.
|
||||
pub(super) fn result_id_from_hash(
|
||||
diagnostics: &[ruff_db::diagnostic::Diagnostic],
|
||||
) -> Option<String> {
|
||||
if diagnostics.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Generate result ID based on raw diagnostic content only
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
// Hash the length first to ensure different numbers of diagnostics produce different hashes
|
||||
diagnostics.hash(&mut hasher);
|
||||
|
||||
format!("{:x}", hasher.finish())
|
||||
Some(format!("{:x}", hasher.finish()))
|
||||
}
|
||||
|
||||
pub(super) fn result_id(&self) -> String {
|
||||
/// Computes the result ID for the diagnostics.
|
||||
///
|
||||
/// Returns `None` if there are no diagnostics.
|
||||
pub(super) fn result_id(&self) -> Option<String> {
|
||||
Self::result_id_from_hash(&self.items)
|
||||
}
|
||||
|
||||
|
||||
@@ -83,33 +83,27 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut project_changed = false;
|
||||
|
||||
for (root, changes) in events_by_db {
|
||||
tracing::debug!("Applying changes to `{root}`");
|
||||
|
||||
let result = session.apply_changes(&AnySystemPath::System(root.clone()), changes);
|
||||
session.apply_changes(&AnySystemPath::System(root.clone()), changes);
|
||||
publish_settings_diagnostics(session, client, root);
|
||||
|
||||
project_changed |= result.project_changed();
|
||||
}
|
||||
|
||||
let client_capabilities = session.client_capabilities();
|
||||
|
||||
if project_changed {
|
||||
if client_capabilities.supports_workspace_diagnostic_refresh() {
|
||||
client.send_request::<types::request::WorkspaceDiagnosticRefresh>(
|
||||
session,
|
||||
(),
|
||||
|_, ()| {},
|
||||
);
|
||||
} else {
|
||||
for key in session.text_document_keys() {
|
||||
publish_diagnostics(session, &key, client);
|
||||
}
|
||||
if client_capabilities.supports_workspace_diagnostic_refresh() {
|
||||
client.send_request::<types::request::WorkspaceDiagnosticRefresh>(
|
||||
session,
|
||||
(),
|
||||
|_, ()| {},
|
||||
);
|
||||
} else {
|
||||
for key in session.text_document_keys() {
|
||||
publish_diagnostics(session, &key, client);
|
||||
}
|
||||
// TODO: always publish diagnostics for notebook files (since they don't use pull diagnostics)
|
||||
}
|
||||
// TODO: always publish diagnostics for notebook files (since they don't use pull diagnostics)
|
||||
|
||||
if client_capabilities.supports_inlay_hint_refresh() {
|
||||
client.send_request::<types::request::InlayHintRefreshRequest>(session, (), |_, ()| {});
|
||||
|
||||
@@ -33,3 +33,5 @@ pub(super) use shutdown::ShutdownHandler;
|
||||
pub(super) use signature_help::SignatureHelpRequestHandler;
|
||||
pub(super) use workspace_diagnostic::WorkspaceDiagnosticRequestHandler;
|
||||
pub(super) use workspace_symbols::WorkspaceSymbolRequestHandler;
|
||||
|
||||
pub use workspace_diagnostic::{PartialWorkspaceProgress, PartialWorkspaceProgressParams};
|
||||
|
||||
@@ -28,7 +28,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: CompletionParams,
|
||||
) -> crate::server::Result<Option<CompletionResponse>> {
|
||||
|
||||
@@ -29,11 +29,11 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: DocumentDiagnosticParams,
|
||||
) -> Result<DocumentDiagnosticReportResult> {
|
||||
let diagnostics = compute_diagnostics(db, &snapshot);
|
||||
let diagnostics = compute_diagnostics(db, snapshot);
|
||||
|
||||
let Some(diagnostics) = diagnostics else {
|
||||
return Ok(DocumentDiagnosticReportResult::Report(
|
||||
@@ -43,23 +43,26 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
|
||||
|
||||
let result_id = diagnostics.result_id();
|
||||
|
||||
let report = if params.previous_result_id.as_deref() == Some(&result_id) {
|
||||
DocumentDiagnosticReport::Unchanged(RelatedUnchangedDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport {
|
||||
result_id,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: Some(result_id),
|
||||
// SAFETY: Pull diagnostic requests are only called for text documents, not for
|
||||
// notebook documents.
|
||||
items: diagnostics.to_lsp_diagnostics(db).expect_text_document(),
|
||||
},
|
||||
})
|
||||
let report = match result_id {
|
||||
Some(new_id) if Some(&new_id) == params.previous_result_id.as_ref() => {
|
||||
DocumentDiagnosticReport::Unchanged(RelatedUnchangedDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport {
|
||||
result_id: new_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
new_id => {
|
||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: new_id,
|
||||
// SAFETY: Pull diagnostic requests are only called for text documents, not for
|
||||
// notebook documents.
|
||||
items: diagnostics.to_lsp_diagnostics(db).expect_text_document(),
|
||||
},
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DocumentDiagnosticReportResult::Report(report))
|
||||
|
||||
@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: DocumentHighlightParams,
|
||||
) -> crate::server::Result<Option<Vec<DocumentHighlight>>> {
|
||||
|
||||
@@ -28,7 +28,7 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: DocumentSymbolParams,
|
||||
) -> crate::server::Result<Option<lsp_types::DocumentSymbolResponse>> {
|
||||
|
||||
@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: GotoDeclarationParams,
|
||||
) -> crate::server::Result<Option<GotoDefinitionResponse>> {
|
||||
|
||||
@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for GotoDefinitionRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: GotoDefinitionParams,
|
||||
) -> crate::server::Result<Option<GotoDefinitionResponse>> {
|
||||
|
||||
@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for ReferencesRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: ReferenceParams,
|
||||
) -> crate::server::Result<Option<Vec<Location>>> {
|
||||
|
||||
@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: GotoTypeDefinitionParams,
|
||||
) -> crate::server::Result<Option<GotoDefinitionResponse>> {
|
||||
|
||||
@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: HoverParams,
|
||||
) -> crate::server::Result<Option<lsp_types::Hover>> {
|
||||
|
||||
@@ -25,7 +25,7 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: InlayHintParams,
|
||||
) -> crate::server::Result<Option<Vec<lsp_types::InlayHint>>> {
|
||||
|
||||
@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: SelectionRangeParams,
|
||||
) -> crate::server::Result<Option<Vec<LspSelectionRange>>> {
|
||||
|
||||
@@ -22,7 +22,7 @@ impl BackgroundDocumentRequestHandler for SemanticTokensRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
_params: SemanticTokensParams,
|
||||
) -> crate::server::Result<Option<SemanticTokensResult>> {
|
||||
|
||||
@@ -24,7 +24,7 @@ impl BackgroundDocumentRequestHandler for SemanticTokensRangeRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: SemanticTokensRangeParams,
|
||||
) -> crate::server::Result<Option<SemanticTokensRangeResult>> {
|
||||
|
||||
@@ -2,6 +2,8 @@ use crate::Session;
|
||||
use crate::server::api::traits::{RequestHandler, SyncRequestHandler};
|
||||
use crate::session::client::Client;
|
||||
|
||||
use lsp_types::{WorkspaceDiagnosticReport, WorkspaceDiagnosticReportResult};
|
||||
|
||||
pub(crate) struct ShutdownHandler;
|
||||
|
||||
impl RequestHandler for ShutdownHandler {
|
||||
@@ -9,9 +11,23 @@ impl RequestHandler for ShutdownHandler {
|
||||
}
|
||||
|
||||
impl SyncRequestHandler for ShutdownHandler {
|
||||
fn run(session: &mut Session, _client: &Client, _params: ()) -> crate::server::Result<()> {
|
||||
tracing::debug!("Received shutdown request, waiting for shutdown notification");
|
||||
fn run(session: &mut Session, client: &Client, _params: ()) -> crate::server::Result<()> {
|
||||
tracing::debug!("Received shutdown request, waiting for exit notification");
|
||||
|
||||
// Respond to any pending workspace diagnostic requests
|
||||
if let Some(suspended_workspace_request) =
|
||||
session.take_suspended_workspace_diagnostic_request()
|
||||
{
|
||||
client.respond(
|
||||
&suspended_workspace_request.id,
|
||||
Ok(WorkspaceDiagnosticReportResult::Report(
|
||||
WorkspaceDiagnosticReport::default(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
session.set_shutdown_requested(true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
snapshot: &DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: SignatureHelpParams,
|
||||
) -> crate::server::Result<Option<SignatureHelp>> {
|
||||
|
||||
@@ -1,25 +1,96 @@
|
||||
use lsp_types::request::WorkspaceDiagnosticRequest;
|
||||
use lsp_types::{
|
||||
FullDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport, Url,
|
||||
WorkspaceDiagnosticParams, WorkspaceDiagnosticReport, WorkspaceDiagnosticReportResult,
|
||||
WorkspaceDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport,
|
||||
WorkspaceUnchangedDocumentDiagnosticReport,
|
||||
};
|
||||
use ruff_db::files::File;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use ty_project::ProgressReporter;
|
||||
|
||||
use crate::server::Result;
|
||||
use crate::PositionEncoding;
|
||||
use crate::server::api::diagnostics::{Diagnostics, to_lsp_diagnostic};
|
||||
use crate::server::api::traits::{
|
||||
BackgroundRequestHandler, RequestHandler, RetriableRequestHandler,
|
||||
};
|
||||
use crate::server::lazy_work_done_progress::LazyWorkDoneProgress;
|
||||
use crate::session::SessionSnapshot;
|
||||
use crate::server::{Action, Result};
|
||||
use crate::session::client::Client;
|
||||
use crate::system::file_to_url;
|
||||
use crate::session::index::Index;
|
||||
use crate::session::{SessionSnapshot, SuspendedWorkspaceDiagnosticRequest};
|
||||
use crate::system::{AnySystemPath, file_to_url};
|
||||
use lsp_server::RequestId;
|
||||
use lsp_types::request::WorkspaceDiagnosticRequest;
|
||||
use lsp_types::{
|
||||
FullDocumentDiagnosticReport, PreviousResultId, ProgressToken,
|
||||
UnchangedDocumentDiagnosticReport, Url, WorkspaceDiagnosticParams, WorkspaceDiagnosticReport,
|
||||
WorkspaceDiagnosticReportPartialResult, WorkspaceDiagnosticReportResult,
|
||||
WorkspaceDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport,
|
||||
WorkspaceUnchangedDocumentDiagnosticReport, notification::Notification,
|
||||
};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::File;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Instant;
|
||||
use ty_project::{Db, ProgressReporter};
|
||||
|
||||
/// Handler for [Workspace diagnostics](workspace-diagnostics)
|
||||
///
|
||||
/// Workspace diagnostics are special in many ways compared to other request handlers.
|
||||
/// This is mostly due to the fact that computing them is expensive. Because of that,
|
||||
/// the LSP supports multiple optimizations of which we all make use:
|
||||
///
|
||||
/// ## Partial results
|
||||
///
|
||||
/// Many clients support partial results. They allow a server
|
||||
/// to send multiple responses (in the form of `$/progress` notifications) for
|
||||
/// the same request. We use partial results to stream the results for
|
||||
/// changed files. This has the obvious benefit is that users
|
||||
/// don't need to wait for the entire check to complete before they see any diagnostics.
|
||||
/// The other benefit of "chunking" the work also helps client to incrementally
|
||||
/// update (and repaint) the diagnostics instead of all at once.
|
||||
/// We did see lags in VS code for projects with 10k+ diagnostics before implementing
|
||||
/// this improvement.
|
||||
///
|
||||
/// ## Result IDs
|
||||
///
|
||||
/// The server can compute a result id for every file which the client
|
||||
/// sends back in the next pull or workspace diagnostic request. The way we use
|
||||
/// the result id is that we compute a fingerprint of the file's diagnostics (a hash)
|
||||
/// and compare it with the result id sent by the server. We know that
|
||||
/// the diagnostics for a file are unchanged (the client still has the most recent review)
|
||||
/// if the ids compare equal.
|
||||
///
|
||||
/// Result IDs are also useful to identify files for which ty no longer emits
|
||||
/// any diagnostics. For example, file A contained a syntax error that has now been fixed
|
||||
/// by the user. The client will send us a result id for file A but we won't match it with
|
||||
/// any new diagnostics because all errors in the file were fixed. The fact that we can't
|
||||
/// match up the result ID tells us that we need to clear the diagnostics on the client
|
||||
/// side by sending an empty diagnostic report (report without any diagnostics). We'll set the
|
||||
/// result id to `None` so that the client stops sending us a result id for this file.
|
||||
///
|
||||
/// Sending unchanged instead of the full diagnostics for files that haven't changed
|
||||
/// helps reduce the data that's sent from the server to the client and it also enables long-polling
|
||||
/// (see the next section).
|
||||
///
|
||||
/// ## Long polling
|
||||
///
|
||||
/// As of today (1st of August 2025), VS code's LSP client automatically schedules a
|
||||
/// workspace diagnostic request every two seconds because it doesn't know *when* to pull
|
||||
/// for new workspace diagnostics (it doesn't know what actions invalidate the diagnostics).
|
||||
/// However, running the workspace diagnostics every two seconds is wasting a lot of CPU cycles (and battery life as a result)
|
||||
/// if the user's only browsing the project (it requires ty to iterate over all files).
|
||||
/// That's why we implement long polling (as recommended in the LSP) for workspace diagnostics.
|
||||
///
|
||||
/// The basic idea of long-polling is that the server doesn't respond if there are no diagnostics
|
||||
/// or all diagnostics are unchanged. Instead, the server keeps the request open (it doesn't respond)
|
||||
/// and only responses when the diagnostics change. This puts the server in full control of when
|
||||
/// to recheck a workspace and a client can simply wait for the response to come in.
|
||||
///
|
||||
/// One challenge with long polling for ty's server architecture is that we can't just keep
|
||||
/// the background thread running because holding on to the [`ProjectDatabase`] references
|
||||
/// prevents notifications from acquiring the exclusive db lock (or the long polling background thread
|
||||
/// panics if a notification tries to do so). What we do instead is that this request handler
|
||||
/// doesn't send a response if there are no diagnostics or all are unchanged and it
|
||||
/// sets a "[snapshot](SuspendedWorkspaceDiagnosticRequest)" of the workspace diagnostic request on the [`Session`].
|
||||
/// The second part to this is in the notification request handling. ty retries the
|
||||
/// suspended workspace diagnostic request (if any) after every notification if the notification
|
||||
/// changed the [`Session`]'s state.
|
||||
///
|
||||
/// [workspace-diagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_diagnostic)
|
||||
pub(crate) struct WorkspaceDiagnosticRequestHandler;
|
||||
|
||||
impl RequestHandler for WorkspaceDiagnosticRequestHandler {
|
||||
@@ -28,7 +99,7 @@ impl RequestHandler for WorkspaceDiagnosticRequestHandler {
|
||||
|
||||
impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
||||
fn run(
|
||||
snapshot: SessionSnapshot,
|
||||
snapshot: &SessionSnapshot,
|
||||
client: &Client,
|
||||
params: WorkspaceDiagnosticParams,
|
||||
) -> Result<WorkspaceDiagnosticReportResult> {
|
||||
@@ -41,12 +112,12 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
||||
));
|
||||
}
|
||||
|
||||
// Create a map of previous result IDs for efficient lookup
|
||||
let mut previous_results: BTreeMap<_, _> = params
|
||||
.previous_result_ids
|
||||
.into_iter()
|
||||
.map(|prev| (prev.uri, prev.value))
|
||||
.collect();
|
||||
let writer = ResponseWriter::new(
|
||||
params.partial_result_params.partial_result_token,
|
||||
params.previous_result_ids,
|
||||
snapshot,
|
||||
client,
|
||||
);
|
||||
|
||||
// Use the work done progress token from the client request, if provided
|
||||
// Note: neither VS Code nor Zed currently support this,
|
||||
@@ -58,103 +129,57 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
||||
"Checking",
|
||||
snapshot.resolved_client_capabilities(),
|
||||
);
|
||||
|
||||
// Collect all diagnostics from all projects with their database references
|
||||
let mut items = Vec::new();
|
||||
let mut reporter = WorkspaceDiagnosticsProgressReporter::new(work_done_progress, writer);
|
||||
|
||||
for db in snapshot.projects() {
|
||||
let diagnostics = db.check_with_reporter(
|
||||
&mut WorkspaceDiagnosticsProgressReporter::new(work_done_progress.clone()),
|
||||
);
|
||||
db.check_with_reporter(&mut reporter);
|
||||
}
|
||||
|
||||
// Group diagnostics by URL
|
||||
let mut diagnostics_by_url: BTreeMap<Url, Vec<_>> = BTreeMap::default();
|
||||
Ok(reporter.into_final_report())
|
||||
}
|
||||
|
||||
for diagnostic in diagnostics {
|
||||
if let Some(span) = diagnostic.primary_span() {
|
||||
let file = span.expect_ty_file();
|
||||
let Some(url) = file_to_url(db, file) else {
|
||||
tracing::debug!("Failed to convert file to URL at {}", file.path(db));
|
||||
continue;
|
||||
};
|
||||
diagnostics_by_url.entry(url).or_default().push(diagnostic);
|
||||
}
|
||||
}
|
||||
fn handle_request(
|
||||
id: &RequestId,
|
||||
snapshot: SessionSnapshot,
|
||||
client: &Client,
|
||||
params: WorkspaceDiagnosticParams,
|
||||
) {
|
||||
let result = Self::run(&snapshot, client, params.clone());
|
||||
|
||||
items.reserve(diagnostics_by_url.len());
|
||||
// Test if this is a no-op result, in which case we should long-poll the request and
|
||||
// only respond once some diagnostics have changed to get the latest result ids.
|
||||
//
|
||||
// Bulk response: This the simple case. Simply test if all diagnostics are unchanged (or empty)
|
||||
// Streaming: This trickier but follows the same principle.
|
||||
// * If the server sent any partial results, then `result` is a `Partial` result (in which
|
||||
// case we shouldn't do any long polling because some diagnostics changed).
|
||||
// * If this is a full report, then check if all items are unchanged (or empty), the same as for
|
||||
// the non-streaming case.
|
||||
if let Ok(WorkspaceDiagnosticReportResult::Report(full)) = &result {
|
||||
let all_unchanged = full
|
||||
.items
|
||||
.iter()
|
||||
.all(|item| matches!(item, WorkspaceDocumentDiagnosticReport::Unchanged(_)));
|
||||
|
||||
// Convert to workspace diagnostic report format
|
||||
for (url, file_diagnostics) in diagnostics_by_url {
|
||||
let version = index
|
||||
.key_from_url(url.clone())
|
||||
.ok()
|
||||
.and_then(|key| index.make_document_ref(key).ok())
|
||||
.map(|doc| i64::from(doc.version()));
|
||||
let result_id = Diagnostics::result_id_from_hash(&file_diagnostics);
|
||||
if all_unchanged {
|
||||
tracing::debug!(
|
||||
"Suspending workspace diagnostic request, all diagnostics are unchanged or the project has no diagnostics"
|
||||
);
|
||||
|
||||
// Check if this file's diagnostics have changed since the previous request
|
||||
if let Some(previous_result_id) = previous_results.remove(&url) {
|
||||
if previous_result_id == result_id {
|
||||
// Diagnostics haven't changed, return unchanged report
|
||||
items.push(WorkspaceDocumentDiagnosticReport::Unchanged(
|
||||
WorkspaceUnchangedDocumentDiagnosticReport {
|
||||
uri: url,
|
||||
version,
|
||||
unchanged_document_diagnostic_report:
|
||||
UnchangedDocumentDiagnosticReport { result_id },
|
||||
},
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert diagnostics to LSP format
|
||||
let lsp_diagnostics = file_diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| {
|
||||
to_lsp_diagnostic(db, &diagnostic, snapshot.position_encoding())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Diagnostics have changed or this is the first request, return full report
|
||||
items.push(WorkspaceDocumentDiagnosticReport::Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: url,
|
||||
version,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: Some(result_id),
|
||||
items: lsp_diagnostics,
|
||||
},
|
||||
client.queue_action(Action::SuspendWorkspaceDiagnostics(Box::new(
|
||||
SuspendedWorkspaceDiagnosticRequest {
|
||||
id: id.clone(),
|
||||
params: serde_json::to_value(¶ms).unwrap(),
|
||||
revision: snapshot.revision(),
|
||||
},
|
||||
));
|
||||
)));
|
||||
|
||||
// Don't respond, keep the request open (long polling).
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle files that had diagnostics in previous request but no longer have any
|
||||
// Any remaining entries in previous_results are files that were fixed
|
||||
for (previous_url, _previous_result_id) in previous_results {
|
||||
// This file had diagnostics before but doesn't now, so we need to report it as having no diagnostics
|
||||
let version = index
|
||||
.key_from_url(previous_url.clone())
|
||||
.ok()
|
||||
.and_then(|key| index.make_document_ref(key).ok())
|
||||
.map(|doc| i64::from(doc.version()));
|
||||
|
||||
items.push(WorkspaceDocumentDiagnosticReport::Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: previous_url,
|
||||
version,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None, // No result ID needed for empty diagnostics
|
||||
items: vec![], // No diagnostics
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
Ok(WorkspaceDiagnosticReportResult::Report(
|
||||
WorkspaceDiagnosticReport { items },
|
||||
))
|
||||
client.respond(id, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,21 +196,32 @@ impl RetriableRequestHandler for WorkspaceDiagnosticRequestHandler {
|
||||
}
|
||||
}
|
||||
|
||||
struct WorkspaceDiagnosticsProgressReporter {
|
||||
/// ty progress reporter that streams the diagnostics to the client
|
||||
/// and sends progress reports (checking X/Y files).
|
||||
///
|
||||
/// Diagnostics are only streamed if the client sends a partial result token.
|
||||
struct WorkspaceDiagnosticsProgressReporter<'a> {
|
||||
total_files: usize,
|
||||
checked_files: AtomicUsize,
|
||||
work_done: LazyWorkDoneProgress,
|
||||
response: std::sync::Mutex<ResponseWriter<'a>>,
|
||||
}
|
||||
|
||||
impl WorkspaceDiagnosticsProgressReporter {
|
||||
fn new(work_done: LazyWorkDoneProgress) -> Self {
|
||||
impl<'a> WorkspaceDiagnosticsProgressReporter<'a> {
|
||||
fn new(work_done: LazyWorkDoneProgress, response: ResponseWriter<'a>) -> Self {
|
||||
Self {
|
||||
total_files: 0,
|
||||
checked_files: AtomicUsize::new(0),
|
||||
work_done,
|
||||
response: std::sync::Mutex::new(response),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_final_report(self) -> WorkspaceDiagnosticReportResult {
|
||||
let writer = self.response.into_inner().unwrap();
|
||||
writer.into_final_report()
|
||||
}
|
||||
|
||||
fn report_progress(&self) {
|
||||
let checked = self.checked_files.load(Ordering::Relaxed);
|
||||
let total = self.total_files;
|
||||
@@ -207,18 +243,371 @@ impl WorkspaceDiagnosticsProgressReporter {
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgressReporter for WorkspaceDiagnosticsProgressReporter {
|
||||
impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> {
|
||||
fn set_files(&mut self, files: usize) {
|
||||
self.total_files += files;
|
||||
self.report_progress();
|
||||
}
|
||||
|
||||
fn report_file(&self, _file: &File) {
|
||||
fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
|
||||
let checked = self.checked_files.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
|
||||
if checked % 10 == 0 || checked == self.total_files {
|
||||
// Report progress every 10 files or when all files are checked
|
||||
if checked % 100 == 0 || checked == self.total_files {
|
||||
// Report progress every 100 files or when all files are checked
|
||||
self.report_progress();
|
||||
}
|
||||
|
||||
// Another thread might have panicked at this point because of a salsa cancellation which
|
||||
// poisoned the result. If the response is poisoned, just don't report and wait for our thread
|
||||
// to unwind with a salsa cancellation next.
|
||||
let Ok(mut response) = self.response.lock() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Don't report empty diagnostics. We clear previous diagnostics in `into_response`
|
||||
// which also handles the case where a file no longer has diagnostics because
|
||||
// it's no longer part of the project.
|
||||
if !diagnostics.is_empty() {
|
||||
response.write_diagnostics_for_file(db, file, diagnostics);
|
||||
}
|
||||
|
||||
response.maybe_flush();
|
||||
}
|
||||
|
||||
fn report_diagnostics(&mut self, db: &dyn Db, diagnostics: Vec<Diagnostic>) {
|
||||
let mut by_file: BTreeMap<File, Vec<Diagnostic>> = BTreeMap::new();
|
||||
|
||||
for diagnostic in diagnostics {
|
||||
if let Some(file) = diagnostic.primary_span().map(|span| span.expect_ty_file()) {
|
||||
by_file.entry(file).or_default().push(diagnostic);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Ignoring diagnostic without a file: {diagnostic}",
|
||||
diagnostic = diagnostic.primary_message()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let response = self.response.get_mut().unwrap();
|
||||
|
||||
for (file, diagnostics) in by_file {
|
||||
response.write_diagnostics_for_file(db, file, &diagnostics);
|
||||
}
|
||||
response.maybe_flush();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ResponseWriter<'a> {
|
||||
mode: ReportingMode,
|
||||
index: &'a Index,
|
||||
position_encoding: PositionEncoding,
|
||||
// It's important that we use `AnySystemPath` over `Url` here because
|
||||
// `file_to_url` isn't guaranteed to return the exact same URL as the one provided
|
||||
// by the client.
|
||||
previous_result_ids: FxHashMap<AnySystemPath, (Url, String)>,
|
||||
}
|
||||
|
||||
impl<'a> ResponseWriter<'a> {
|
||||
fn new(
|
||||
partial_result_token: Option<ProgressToken>,
|
||||
previous_result_ids: Vec<PreviousResultId>,
|
||||
snapshot: &'a SessionSnapshot,
|
||||
client: &Client,
|
||||
) -> Self {
|
||||
let index = snapshot.index();
|
||||
let position_encoding = snapshot.position_encoding();
|
||||
|
||||
let mode = if let Some(token) = partial_result_token {
|
||||
ReportingMode::Streaming(Streaming {
|
||||
first: true,
|
||||
client: client.clone(),
|
||||
token,
|
||||
is_test: snapshot.in_test(),
|
||||
last_flush: Instant::now(),
|
||||
changed: Vec::new(),
|
||||
unchanged: Vec::with_capacity(previous_result_ids.len()),
|
||||
})
|
||||
} else {
|
||||
ReportingMode::Bulk(Vec::new())
|
||||
};
|
||||
|
||||
let previous_result_ids = previous_result_ids
|
||||
.into_iter()
|
||||
.filter_map(|prev| {
|
||||
Some((
|
||||
AnySystemPath::try_from_url(&prev.uri).ok()?,
|
||||
(prev.uri, prev.value),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
mode,
|
||||
index,
|
||||
position_encoding,
|
||||
previous_result_ids,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_diagnostics_for_file(&mut self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
|
||||
let Some(url) = file_to_url(db, file) else {
|
||||
tracing::debug!("Failed to convert file path to URL at {}", file.path(db));
|
||||
return;
|
||||
};
|
||||
|
||||
let version = self
|
||||
.index
|
||||
.key_from_url(url.clone())
|
||||
.ok()
|
||||
.and_then(|key| self.index.make_document_ref(key).ok())
|
||||
.map(|doc| i64::from(doc.version()));
|
||||
|
||||
let result_id = Diagnostics::result_id_from_hash(diagnostics);
|
||||
|
||||
let previous_result_id = AnySystemPath::try_from_url(&url)
|
||||
.ok()
|
||||
.and_then(|path| self.previous_result_ids.remove(&path))
|
||||
.map(|(_url, id)| id);
|
||||
|
||||
let report = match result_id {
|
||||
Some(new_id) if Some(&new_id) == previous_result_id.as_ref() => {
|
||||
WorkspaceDocumentDiagnosticReport::Unchanged(
|
||||
WorkspaceUnchangedDocumentDiagnosticReport {
|
||||
uri: url,
|
||||
version,
|
||||
unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport {
|
||||
result_id: new_id,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
new_id => {
|
||||
let lsp_diagnostics = diagnostics
|
||||
.iter()
|
||||
.map(|diagnostic| to_lsp_diagnostic(db, diagnostic, self.position_encoding))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
WorkspaceDocumentDiagnosticReport::Full(WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: url,
|
||||
version,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: new_id,
|
||||
items: lsp_diagnostics,
|
||||
},
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
self.write_report(report);
|
||||
}
|
||||
|
||||
fn write_report(&mut self, report: WorkspaceDocumentDiagnosticReport) {
|
||||
match &mut self.mode {
|
||||
ReportingMode::Streaming(streaming) => {
|
||||
streaming.write_report(report);
|
||||
}
|
||||
ReportingMode::Bulk(all) => {
|
||||
all.push(report);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Flush any pending reports if streaming diagnostics.
|
||||
///
|
||||
/// Note: The flush is throttled when streaming.
|
||||
fn maybe_flush(&mut self) {
|
||||
match &mut self.mode {
|
||||
ReportingMode::Streaming(streaming) => streaming.maybe_flush(),
|
||||
ReportingMode::Bulk(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the final response after all files have been processed.
|
||||
///
|
||||
/// The result can be a partial or full report depending on whether the server's streaming
|
||||
/// diagnostics and if it already sent some diagnostics.
|
||||
fn into_final_report(mut self) -> WorkspaceDiagnosticReportResult {
|
||||
let mut items = Vec::new();
|
||||
|
||||
// Handle files that had diagnostics in previous request but no longer have any
|
||||
// Any remaining entries in previous_results are files that were fixed
|
||||
for (previous_url, previous_result_id) in self.previous_result_ids.into_values() {
|
||||
// This file had diagnostics before but doesn't now, so we need to report it as having no diagnostics
|
||||
let version = self
|
||||
.index
|
||||
.key_from_url(previous_url.clone())
|
||||
.ok()
|
||||
.and_then(|key| self.index.make_document_ref(key).ok())
|
||||
.map(|doc| i64::from(doc.version()));
|
||||
|
||||
let new_result_id = Diagnostics::result_id_from_hash(&[]);
|
||||
|
||||
let report = match new_result_id {
|
||||
Some(new_id) if new_id == previous_result_id => {
|
||||
WorkspaceDocumentDiagnosticReport::Unchanged(
|
||||
WorkspaceUnchangedDocumentDiagnosticReport {
|
||||
uri: previous_url,
|
||||
version,
|
||||
unchanged_document_diagnostic_report:
|
||||
UnchangedDocumentDiagnosticReport { result_id: new_id },
|
||||
},
|
||||
)
|
||||
}
|
||||
new_id => {
|
||||
WorkspaceDocumentDiagnosticReport::Full(WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: previous_url,
|
||||
version,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: new_id,
|
||||
items: vec![], // No diagnostics
|
||||
},
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
items.push(report);
|
||||
}
|
||||
|
||||
match &mut self.mode {
|
||||
ReportingMode::Streaming(streaming) => {
|
||||
items.extend(
|
||||
std::mem::take(&mut streaming.changed)
|
||||
.into_iter()
|
||||
.map(WorkspaceDocumentDiagnosticReport::Full),
|
||||
);
|
||||
items.extend(
|
||||
std::mem::take(&mut streaming.unchanged)
|
||||
.into_iter()
|
||||
.map(WorkspaceDocumentDiagnosticReport::Unchanged),
|
||||
);
|
||||
}
|
||||
ReportingMode::Bulk(all) => {
|
||||
all.extend(items);
|
||||
items = std::mem::take(all);
|
||||
}
|
||||
}
|
||||
|
||||
self.mode.create_result(items)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ReportingMode {
|
||||
/// Streams the diagnostics to the client as they are computed (file by file).
|
||||
/// Requires that the client provides a partial result token.
|
||||
Streaming(Streaming),
|
||||
|
||||
/// For clients that don't support streaming diagnostics. Collects all workspace
|
||||
/// diagnostics and sends them in the `workspace/diagnostic` response.
|
||||
Bulk(Vec<WorkspaceDocumentDiagnosticReport>),
|
||||
}
|
||||
|
||||
impl ReportingMode {
|
||||
fn create_result(
|
||||
&mut self,
|
||||
items: Vec<WorkspaceDocumentDiagnosticReport>,
|
||||
) -> WorkspaceDiagnosticReportResult {
|
||||
match self {
|
||||
ReportingMode::Streaming(streaming) => streaming.create_result(items),
|
||||
ReportingMode::Bulk(..) => {
|
||||
WorkspaceDiagnosticReportResult::Report(WorkspaceDiagnosticReport { items })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Streaming {
|
||||
first: bool,
|
||||
client: Client,
|
||||
/// The partial result token.
|
||||
token: ProgressToken,
|
||||
/// Throttles the flush reports to not happen more than once every 100ms.
|
||||
last_flush: Instant,
|
||||
is_test: bool,
|
||||
/// The reports for files with changed diagnostics.
|
||||
/// The implementation uses batching to avoid too many
|
||||
/// requests for large projects (can slow down the entire
|
||||
/// analysis).
|
||||
changed: Vec<WorkspaceFullDocumentDiagnosticReport>,
|
||||
/// All the unchanged reports. Don't stream them,
|
||||
/// since nothing has changed.
|
||||
unchanged: Vec<WorkspaceUnchangedDocumentDiagnosticReport>,
|
||||
}
|
||||
|
||||
impl Streaming {
|
||||
fn write_report(&mut self, report: WorkspaceDocumentDiagnosticReport) {
|
||||
match report {
|
||||
WorkspaceDocumentDiagnosticReport::Full(full) => {
|
||||
self.changed.push(full);
|
||||
}
|
||||
WorkspaceDocumentDiagnosticReport::Unchanged(unchanged) => {
|
||||
self.unchanged.push(unchanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_flush(&mut self) {
|
||||
if self.changed.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Flush every ~50ms or whenever we have two items and this is a test run.
|
||||
let should_flush = if self.is_test {
|
||||
self.changed.len() >= 2
|
||||
} else {
|
||||
self.last_flush.elapsed().as_millis() >= 50
|
||||
};
|
||||
if !should_flush {
|
||||
return;
|
||||
}
|
||||
|
||||
let items = self
|
||||
.changed
|
||||
.drain(..)
|
||||
.map(WorkspaceDocumentDiagnosticReport::Full)
|
||||
.collect();
|
||||
|
||||
let report = self.create_result(items);
|
||||
self.client
|
||||
.send_notification::<PartialWorkspaceProgress>(PartialWorkspaceProgressParams {
|
||||
token: self.token.clone(),
|
||||
value: report,
|
||||
});
|
||||
self.last_flush = Instant::now();
|
||||
}
|
||||
|
||||
fn create_result(
|
||||
&mut self,
|
||||
items: Vec<WorkspaceDocumentDiagnosticReport>,
|
||||
) -> WorkspaceDiagnosticReportResult {
|
||||
// As per the LSP spec:
|
||||
// > partial result: The first literal send need to be a WorkspaceDiagnosticReport followed
|
||||
// > by `n` WorkspaceDiagnosticReportPartialResult literals defined as follows:
|
||||
if self.first {
|
||||
self.first = false;
|
||||
WorkspaceDiagnosticReportResult::Report(WorkspaceDiagnosticReport { items })
|
||||
} else {
|
||||
WorkspaceDiagnosticReportResult::Partial(WorkspaceDiagnosticReportPartialResult {
|
||||
items,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `$/progress` notification for partial workspace diagnostics.
|
||||
///
|
||||
/// This type is missing in `lsp_types`. That's why we define it here.
|
||||
pub struct PartialWorkspaceProgress;
|
||||
|
||||
impl Notification for PartialWorkspaceProgress {
|
||||
type Params = PartialWorkspaceProgressParams;
|
||||
const METHOD: &'static str = "$/progress";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct PartialWorkspaceProgressParams {
|
||||
pub token: ProgressToken,
|
||||
pub value: WorkspaceDiagnosticReportResult,
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ impl RequestHandler for WorkspaceSymbolRequestHandler {
|
||||
|
||||
impl BackgroundRequestHandler for WorkspaceSymbolRequestHandler {
|
||||
fn run(
|
||||
snapshot: SessionSnapshot,
|
||||
snapshot: &SessionSnapshot,
|
||||
_client: &Client,
|
||||
params: WorkspaceSymbolParams,
|
||||
) -> crate::server::Result<Option<WorkspaceSymbolResponse>> {
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
//! See the `./requests` and `./notifications` directories for concrete implementations of these
|
||||
//! traits in action.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::session::client::Client;
|
||||
use crate::session::{DocumentSnapshot, Session, SessionSnapshot};
|
||||
use lsp_server::RequestId;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use lsp_types::Url;
|
||||
use lsp_types::notification::Notification;
|
||||
@@ -91,12 +91,36 @@ pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler {
|
||||
params: &<<Self as RequestHandler>::RequestType as Request>::Params,
|
||||
) -> Cow<Url>;
|
||||
|
||||
/// Processes the request parameters and returns the LSP request result.
|
||||
///
|
||||
/// This is the main method that handlers implement. It takes the request parameters
|
||||
/// from the client and computes the appropriate response data for the LSP request.
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: &DocumentSnapshot,
|
||||
client: &Client,
|
||||
params: <<Self as RequestHandler>::RequestType as Request>::Params,
|
||||
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
|
||||
|
||||
/// Handles the entire request lifecycle and sends the response to the client.
|
||||
///
|
||||
/// It allows handlers to customize how the server sends the response to the client.
|
||||
fn handle_request(
|
||||
id: &RequestId,
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
client: &Client,
|
||||
params: <<Self as RequestHandler>::RequestType as Request>::Params,
|
||||
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
|
||||
) {
|
||||
let result = Self::run_with_snapshot(db, &snapshot, client, params);
|
||||
|
||||
if let Err(err) = &result {
|
||||
tracing::error!("An error occurred with request ID {id}: {err}");
|
||||
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
||||
}
|
||||
|
||||
client.respond(id, result);
|
||||
}
|
||||
}
|
||||
|
||||
/// A request handler that can be run on a background thread.
|
||||
@@ -106,11 +130,34 @@ pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler {
|
||||
/// operations that require access to the entire session state, such as fetching workspace
|
||||
/// diagnostics.
|
||||
pub(super) trait BackgroundRequestHandler: RetriableRequestHandler {
|
||||
/// Processes the request parameters and returns the LSP request result.
|
||||
///
|
||||
/// This is the main method that handlers implement. It takes the request parameters
|
||||
/// from the client and computes the appropriate response data for the LSP request.
|
||||
fn run(
|
||||
snapshot: SessionSnapshot,
|
||||
snapshot: &SessionSnapshot,
|
||||
client: &Client,
|
||||
params: <<Self as RequestHandler>::RequestType as Request>::Params,
|
||||
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
|
||||
|
||||
/// Handles the request lifecycle and sends the response to the client.
|
||||
///
|
||||
/// It allows handlers to customize how the server sends the response to the client.
|
||||
fn handle_request(
|
||||
id: &RequestId,
|
||||
snapshot: SessionSnapshot,
|
||||
client: &Client,
|
||||
params: <<Self as RequestHandler>::RequestType as Request>::Params,
|
||||
) {
|
||||
let result = Self::run(&snapshot, client, params);
|
||||
|
||||
if let Err(err) = &result {
|
||||
tracing::error!("An error occurred with request ID {id}: {err}");
|
||||
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
||||
}
|
||||
|
||||
client.respond(id, result);
|
||||
}
|
||||
}
|
||||
|
||||
/// A supertrait for any server notification handler.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::server::schedule::Scheduler;
|
||||
use crate::server::{Server, api};
|
||||
use crate::session::ClientOptions;
|
||||
use crate::session::client::{Client, ClientResponseHandler};
|
||||
use crate::session::{ClientOptions, SuspendedWorkspaceDiagnosticRequest};
|
||||
use anyhow::anyhow;
|
||||
use crossbeam::select;
|
||||
use lsp_server::Message;
|
||||
@@ -49,7 +49,8 @@ impl Server {
|
||||
|
||||
if self.session.is_shutdown_requested() {
|
||||
tracing::warn!(
|
||||
"Received request after server shutdown was requested, discarding"
|
||||
"Received request `{}` after server shutdown was requested, discarding",
|
||||
&req.method
|
||||
);
|
||||
client.respond_err(
|
||||
req.id,
|
||||
@@ -130,7 +131,8 @@ impl Server {
|
||||
.incoming()
|
||||
.is_pending(&request.id)
|
||||
{
|
||||
api::request(request);
|
||||
let task = api::request(request);
|
||||
scheduler.dispatch(task, &mut self.session, client);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Request {}/{} was cancelled, not retrying",
|
||||
@@ -142,6 +144,13 @@ impl Server {
|
||||
|
||||
Action::SendRequest(request) => client.send_request_raw(&self.session, request),
|
||||
|
||||
Action::SuspendWorkspaceDiagnostics(suspended_request) => {
|
||||
self.session.set_suspended_workspace_diagnostics_request(
|
||||
*suspended_request,
|
||||
&client,
|
||||
);
|
||||
}
|
||||
|
||||
Action::InitializeWorkspaces(workspaces_with_options) => {
|
||||
self.session
|
||||
.initialize_workspaces(workspaces_with_options, &client);
|
||||
@@ -227,11 +236,9 @@ impl Server {
|
||||
);
|
||||
|
||||
let fs_watcher = self
|
||||
.client_capabilities
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|workspace| workspace.did_change_watched_files?.dynamic_registration)
|
||||
.unwrap_or_default();
|
||||
.session
|
||||
.client_capabilities()
|
||||
.supports_did_change_watched_files_dynamic_registration();
|
||||
|
||||
if fs_watcher {
|
||||
let registration = lsp_types::Registration {
|
||||
@@ -306,6 +313,8 @@ pub(crate) enum Action {
|
||||
/// Send a request from the server to the client.
|
||||
SendRequest(SendRequest),
|
||||
|
||||
SuspendWorkspaceDiagnostics(Box<SuspendedWorkspaceDiagnosticRequest>),
|
||||
|
||||
/// Initialize the workspace after the server received
|
||||
/// the options from the client.
|
||||
InitializeWorkspaces(Vec<(Url, ClientOptions)>),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user