Compare commits
28 Commits
0.9.10
...
david/mypy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71e07fbae3 | ||
|
|
0af4985067 | ||
|
|
da069aa00c | ||
|
|
ec9ee93d68 | ||
|
|
a73548d0ca | ||
|
|
c60e8a037a | ||
|
|
f19cb86c5d | ||
|
|
36d12cea47 | ||
|
|
a4396b3e6b | ||
|
|
acfb920863 | ||
|
|
08c48b15af | ||
|
|
a7095c4196 | ||
|
|
e79f9171cf | ||
|
|
e517b44a0a | ||
|
|
1275665ecb | ||
|
|
f2b41306d0 | ||
|
|
b02a42d99a | ||
|
|
79443d71eb | ||
|
|
ca974706dd | ||
|
|
b6c7ba4f8e | ||
|
|
c970b794d0 | ||
|
|
335b264fe2 | ||
|
|
0361021863 | ||
|
|
24c8b1242e | ||
|
|
820a31af5d | ||
|
|
a18d8bfa7d | ||
|
|
348c196cb3 | ||
|
|
6d6e524b90 |
93
.github/workflows/mypy_primer.yaml
vendored
Normal file
93
.github/workflows/mypy_primer.yaml
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
name: Run mypy_primer
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "crates/red_knot*/**"
|
||||
- "crates/ruff_db"
|
||||
- "crates/ruff_python_ast"
|
||||
- "crates/ruff_python_parser"
|
||||
- ".github/workflows/mypy_primer.yaml"
|
||||
- ".github/workflows/mypy_primer_comment.yaml"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
|
||||
jobs:
|
||||
mypy_primer:
|
||||
name: Run mypy_primer
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: ruff
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
- name: Install Rust toolchain
|
||||
run: rustup show
|
||||
|
||||
- name: Install mypy_primer
|
||||
run: |
|
||||
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
|
||||
|
||||
- name: Run mypy_primer
|
||||
shell: bash
|
||||
run: |
|
||||
cd ruff
|
||||
|
||||
echo "new commit"
|
||||
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
|
||||
|
||||
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
|
||||
git checkout -b base_commit "$MERGE_BASE"
|
||||
echo "base commit"
|
||||
git rev-list --format=%s --max-count=1 base_commit
|
||||
|
||||
cd ..
|
||||
|
||||
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
|
||||
uvx mypy_primer \
|
||||
--repo ruff \
|
||||
--type-checker knot \
|
||||
--old base_commit \
|
||||
--new "$GITHUB_SHA" \
|
||||
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow)$' \
|
||||
--output concise \
|
||||
--debug > mypy_primer.diff || [ $? -eq 1 ]
|
||||
|
||||
# Output diff with ANSI color codes
|
||||
cat mypy_primer.diff
|
||||
|
||||
# Remove ANSI color codes before uploading
|
||||
sed -ie 's/\x1b\[[0-9;]*m//g' mypy_primer.diff
|
||||
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- name: Upload diff
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mypy_primer_diff
|
||||
path: mypy_primer.diff
|
||||
|
||||
- name: Upload pr-number
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
97
.github/workflows/mypy_primer_comment.yaml
vendored
Normal file
97
.github/workflows/mypy_primer_comment.yaml
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
name: PR comment (mypy_primer)
|
||||
|
||||
on: # zizmor: ignore[dangerous-triggers]
|
||||
workflow_run:
|
||||
workflows: [Run mypy_primer]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
workflow_run_id:
|
||||
description: The mypy_primer workflow that triggers the workflow run
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
name: Download PR number
|
||||
with:
|
||||
name: pr-number
|
||||
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Parse pull request number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [[ -f pr-number ]]
|
||||
then
|
||||
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
name: "Download mypy_primer results"
|
||||
id: download-mypy_primer_diff
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
with:
|
||||
name: mypy_primer_diff
|
||||
workflow: mypy_primer.yaml
|
||||
pr: ${{ steps.pr-number.outputs.pr-number }}
|
||||
path: pr/mypy_primer_diff
|
||||
workflow_conclusion: completed
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Generate comment content
|
||||
id: generate-comment
|
||||
if: steps.download-mypy_primer_diff.outputs.found_artifact == 'true'
|
||||
run: |
|
||||
# Guard against malicious mypy_primer results that symlink to a secret
|
||||
# file on this runner
|
||||
if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]]
|
||||
then
|
||||
echo "Error: mypy_primer.diff cannot be a symlink"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Note this identifier is used to find the comment to update on
|
||||
# subsequent runs
|
||||
echo '<!-- generated-comment mypy_primer -->' >> comment.txt
|
||||
|
||||
echo '## `mypy_primer` results' >> comment.txt
|
||||
if [ -s "pr/mypy_primer_diff/mypy_primer.diff" ]; then
|
||||
echo '<details>' >> comment.txt
|
||||
echo '<summary>Changes were detected when running on open source projects</summary>' >> comment.txt
|
||||
echo '' >> comment.txt
|
||||
echo '```diff' >> comment.txt
|
||||
cat pr/mypy_primer_diff/mypy_primer.diff >> comment.txt
|
||||
echo '```' >> comment.txt
|
||||
echo '</details>' >> comment.txt
|
||||
else
|
||||
echo 'No ecosystem changes detected ✅' >> comment.txt
|
||||
fi
|
||||
|
||||
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
|
||||
cat comment.txt >> "$GITHUB_OUTPUT"
|
||||
echo 'EOF' >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find existing comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
if: steps.generate-comment.outcome == 'success'
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ steps.pr-number.outputs.pr-number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: "<!-- generated-comment mypy_primer -->"
|
||||
|
||||
- name: Create or update comment
|
||||
if: steps.find-comment.outcome == 'success'
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ steps.pr-number.outputs.pr-number }}
|
||||
body-path: comment.txt
|
||||
edit-mode: replace
|
||||
140
Cargo.lock
generated
140
Cargo.lock
generated
@@ -124,9 +124,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.96"
|
||||
version = "1.0.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
|
||||
[[package]]
|
||||
name = "argfile"
|
||||
@@ -399,7 +399,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -416,7 +416,7 @@ checksum = "8c41dc435a7b98e4608224bbf65282309f5403719df9113621b30f8b6f74e2f4"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"terminfo",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"which",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -690,7 +690,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -701,7 +701,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -771,7 +771,7 @@ dependencies = [
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -803,7 +803,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1295,7 +1295,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1387,9 +1387,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.5"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
@@ -1460,7 +1460,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1602,7 +1602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bf66548c351bcaed792ef3e2b430cc840fbde504e09da6b29ed114ca60dcd4b"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2081,7 +2081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
@@ -2105,7 +2105,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2174,7 +2174,7 @@ checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2243,9 +2243,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.93"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
|
||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2275,7 +2275,7 @@ dependencies = [
|
||||
"newtype-uuid",
|
||||
"quick-xml",
|
||||
"strip-ansi-escapes",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2310,9 +2310,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.38"
|
||||
version = "1.0.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
|
||||
checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -2453,7 +2453,7 @@ dependencies = [
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2499,7 +2499,7 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -2551,7 +2551,7 @@ dependencies = [
|
||||
"serde",
|
||||
"smallvec",
|
||||
"tempfile",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
]
|
||||
|
||||
@@ -2707,7 +2707,7 @@ dependencies = [
|
||||
"strum",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"tikv-jemallocator",
|
||||
"toml",
|
||||
"tracing",
|
||||
@@ -2790,7 +2790,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
@@ -2945,7 +2945,7 @@ dependencies = [
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"test-case",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"typed-arena",
|
||||
"unicode-normalization",
|
||||
@@ -2962,7 +2962,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2979,7 +2979,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"test-case",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -3053,7 +3053,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -3189,7 +3189,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -3359,7 +3359,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -3393,7 +3393,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3416,9 +3416,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.218"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -3436,13 +3436,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.218"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3453,14 +3453,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.139"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3476,7 +3476,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3517,7 +3517,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3648,7 +3648,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3664,9 +3664,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.98"
|
||||
version = "2.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3681,7 +3681,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3753,7 +3753,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3764,7 +3764,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
@@ -3779,11 +3779,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.11"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.11",
|
||||
"thiserror-impl 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3794,18 +3794,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.11"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3936,7 +3936,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4087,9 +4087,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -4203,7 +4203,7 @@ checksum = "d28dd23acb5f2fa7bd2155ab70b960e770596b3bb6395119b40476c3655dfba4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4325,7 +4325,7 @@ dependencies = [
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -4360,7 +4360,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -4395,7 +4395,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4510,7 +4510,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4521,7 +4521,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4759,7 +4759,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4790,7 +4790,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4801,7 +4801,7 @@ checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4821,7 +4821,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4844,7 +4844,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -216,6 +216,17 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
||||
self.visit_body(&for_stmt.orelse);
|
||||
return;
|
||||
}
|
||||
Stmt::With(with_stmt) => {
|
||||
for item in &with_stmt.items {
|
||||
if let Some(target) = &item.optional_vars {
|
||||
self.visit_target(target);
|
||||
}
|
||||
self.visit_expr(&item.context_expr);
|
||||
}
|
||||
|
||||
self.visit_body(&with_stmt.body);
|
||||
return;
|
||||
}
|
||||
Stmt::AnnAssign(_)
|
||||
| Stmt::Return(_)
|
||||
| Stmt::Delete(_)
|
||||
@@ -223,7 +234,6 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
||||
| Stmt::TypeAlias(_)
|
||||
| Stmt::While(_)
|
||||
| Stmt::If(_)
|
||||
| Stmt::With(_)
|
||||
| Stmt::Match(_)
|
||||
| Stmt::Raise(_)
|
||||
| Stmt::Try(_)
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
# Callable
|
||||
|
||||
References:
|
||||
|
||||
- <https://typing.readthedocs.io/en/latest/spec/callables.html#callable>
|
||||
|
||||
TODO: Use `collections.abc` as importing from `typing` is deprecated but this requires support for
|
||||
`*` imports. See: <https://docs.python.org/3/library/typing.html#deprecated-aliases>.
|
||||
|
||||
## Invalid forms
|
||||
|
||||
The `Callable` special form requires _exactly_ two arguments where the first argument is either a
|
||||
parameter type list, parameter specification, `typing.Concatenate`, or `...` and the second argument
|
||||
is the return type. Here, we explore various invalid forms.
|
||||
|
||||
### Empty
|
||||
|
||||
A bare `Callable` without any type arguments:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### Invalid parameter type argument
|
||||
|
||||
When it's not a list:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
def _(c: Callable[int, str]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
Or, when it's a literal type:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
def _(c: Callable[42, str]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
Or, when one of the parameter type is invalid in the list:
|
||||
|
||||
```py
|
||||
def _(c: Callable[[int, 42, str, False], None]):
|
||||
# revealed: (int, @Todo(number literal in type expression), str, @Todo(boolean literal in type expression), /) -> None
|
||||
reveal_type(c)
|
||||
```
|
||||
|
||||
### Missing return type
|
||||
|
||||
Using a parameter list:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
def _(c: Callable[[int, str]]):
|
||||
reveal_type(c) # revealed: (int, str, /) -> Unknown
|
||||
```
|
||||
|
||||
Or, an ellipsis:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
def _(c: Callable[...]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### More than two arguments
|
||||
|
||||
We can't reliably infer the callable type if there are more then 2 arguments because we don't know
|
||||
which argument corresponds to either the parameters or the return type.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
def _(c: Callable[[int], str, str]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
## Simple
|
||||
|
||||
A simple `Callable` with multiple parameters and a return type:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable[[int, str], int]):
|
||||
reveal_type(c) # revealed: (int, str, /) -> int
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
A nested `Callable` as one of the parameter types:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable[[Callable[[int], str]], int]):
|
||||
reveal_type(c) # revealed: ((int, /) -> str, /) -> int
|
||||
```
|
||||
|
||||
And, as the return type:
|
||||
|
||||
```py
|
||||
def _(c: Callable[[int, str], Callable[[int], int]]):
|
||||
reveal_type(c) # revealed: (int, str, /) -> (int, /) -> int
|
||||
```
|
||||
|
||||
## Gradual form
|
||||
|
||||
The `Callable` special form supports the use of `...` in place of the list of parameter types. This
|
||||
is a [gradual form] indicating that the type is consistent with any input signature:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def gradual_form(c: Callable[..., str]):
|
||||
reveal_type(c) # revealed: (...) -> str
|
||||
```
|
||||
|
||||
## Using `typing.Concatenate`
|
||||
|
||||
Using `Concatenate` as the first argument to `Callable`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Callable, Concatenate
|
||||
|
||||
def _(c: Callable[Concatenate[int, str, ...], int]):
|
||||
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
|
||||
```
|
||||
|
||||
And, as one of the parameter types:
|
||||
|
||||
```py
|
||||
def _(c: Callable[[Concatenate[int, str, ...], int], int]):
|
||||
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
|
||||
```
|
||||
|
||||
## Using `typing.ParamSpec`
|
||||
|
||||
Using a `ParamSpec` in a `Callable` annotation:
|
||||
|
||||
```py
|
||||
from typing_extensions import Callable
|
||||
|
||||
# TODO: Not an error; remove once `ParamSpec` is supported
|
||||
# error: [invalid-type-form]
|
||||
def _[**P1](c: Callable[P1, int]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
And, using the legacy syntax:
|
||||
|
||||
```py
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
P2 = ParamSpec("P2")
|
||||
|
||||
# TODO: Not an error; remove once `ParamSpec` is supported
|
||||
# error: [invalid-type-form]
|
||||
def _(c: Callable[P2, int]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
## Using `typing.Unpack`
|
||||
|
||||
Using the unpack operator (`*`):
|
||||
|
||||
```py
|
||||
from typing_extensions import Callable, TypeVarTuple
|
||||
|
||||
Ts = TypeVarTuple("Ts")
|
||||
|
||||
def _(c: Callable[[int, *Ts], int]):
|
||||
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
|
||||
```
|
||||
|
||||
And, using the legacy syntax using `Unpack`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Unpack
|
||||
|
||||
def _(c: Callable[[int, Unpack[Ts]], int]):
|
||||
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
|
||||
```
|
||||
|
||||
[gradual form]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-gradual-form
|
||||
@@ -73,12 +73,12 @@ qux = (foo, bar)
|
||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||
|
||||
# TODO: Infer "LiteralString"
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(overloaded method)
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
template: LiteralString = "{}, {}"
|
||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||
# TODO: Infer `LiteralString`
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(overloaded method)
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
@@ -70,8 +70,7 @@ import typing
|
||||
|
||||
class ListSubclass(typing.List): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[ListSubclass], Literal[list], Unknown, Literal[object]]
|
||||
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
|
||||
reveal_type(ListSubclass.__mro__)
|
||||
|
||||
class DictSubclass(typing.Dict): ...
|
||||
|
||||
@@ -29,6 +29,8 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(kwargs) # revealed: dict
|
||||
|
||||
# TODO: not an error; remove once `call` is implemented for `Callable`
|
||||
# error: [call-non-callable]
|
||||
return callback(42, *args, **kwargs)
|
||||
|
||||
class Foo:
|
||||
|
||||
@@ -75,8 +75,7 @@ def _(flag: bool):
|
||||
|
||||
f = Foo()
|
||||
|
||||
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
|
||||
# that `Foo.__iadd__` may be unbound as additional context.
|
||||
# error: [unsupported-operator] "Operator `+=` is unsupported between objects of type `Foo` and `Literal["Hello, world!"]`"
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | Unknown
|
||||
|
||||
@@ -155,7 +155,9 @@ reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None
|
||||
|
||||
reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None
|
||||
|
||||
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: str | None
|
||||
# TODO: This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API,
|
||||
# which is planned in https://github.com/astral-sh/ruff/issues/14297
|
||||
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None
|
||||
|
||||
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
|
||||
```
|
||||
@@ -356,9 +358,25 @@ class C:
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# TODO: Should be `Unknown | int | None`
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.x) # revealed: Unknown
|
||||
reveal_type(c_instance.x) # revealed: Unknown | int | None
|
||||
```
|
||||
|
||||
#### Attributes defined in `with` statements, but with unpacking
|
||||
|
||||
```py
|
||||
class ContextManager:
|
||||
def __enter__(self) -> tuple[int | None, int]: ...
|
||||
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
|
||||
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
with ContextManager() as (self.x, self.y):
|
||||
pass
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.x) # revealed: Unknown | int | None
|
||||
reveal_type(c_instance.y) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
#### Attributes defined in comprehensions
|
||||
@@ -704,8 +722,91 @@ reveal_type(Derived().declared_in_body) # revealed: int | None
|
||||
reveal_type(Derived().defined_in_init) # revealed: str | None
|
||||
```
|
||||
|
||||
## Accessing attributes on class objects
|
||||
|
||||
When accessing attributes on class objects, they are always looked up on the type of the class
|
||||
object first, i.e. on the metaclass:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Meta1:
|
||||
attr: Literal["metaclass value"] = "metaclass value"
|
||||
|
||||
class C1(metaclass=Meta1): ...
|
||||
|
||||
reveal_type(C1.attr) # revealed: Literal["metaclass value"]
|
||||
```
|
||||
|
||||
However, the metaclass attribute only takes precedence over a class-level attribute if it is a data
|
||||
descriptor. If it is a non-data descriptor or a normal attribute, the class-level attribute is used
|
||||
instead (see the [descriptor protocol tests] for data/non-data descriptor attributes):
|
||||
|
||||
```py
|
||||
class Meta2:
|
||||
attr: str = "metaclass value"
|
||||
|
||||
class C2(metaclass=Meta2):
|
||||
attr: Literal["class value"] = "class value"
|
||||
|
||||
reveal_type(C2.attr) # revealed: Literal["class value"]
|
||||
```
|
||||
|
||||
If the class-level attribute is only partially defined, we union the metaclass attribute with the
|
||||
class-level attribute:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Meta3:
|
||||
attr1 = "metaclass value"
|
||||
attr2: Literal["metaclass value"] = "metaclass value"
|
||||
|
||||
class C3(metaclass=Meta3):
|
||||
if flag:
|
||||
attr1 = "class value"
|
||||
# TODO: Neither mypy nor pyright show an error here, but we could consider emitting a conflicting-declaration diagnostic here.
|
||||
attr2: Literal["class value"] = "class value"
|
||||
|
||||
reveal_type(C3.attr1) # revealed: Unknown | Literal["metaclass value", "class value"]
|
||||
reveal_type(C3.attr2) # revealed: Literal["metaclass value", "class value"]
|
||||
```
|
||||
|
||||
If the *metaclass* attribute is only partially defined, we emit a `possibly-unbound-attribute`
|
||||
diagnostic:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Meta4:
|
||||
if flag:
|
||||
attr1: str = "metaclass value"
|
||||
|
||||
class C4(metaclass=Meta4): ...
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(C4.attr1) # revealed: str
|
||||
```
|
||||
|
||||
Finally, if both the metaclass attribute and the class-level attribute are only partially defined,
|
||||
we union them and emit a `possibly-unbound-attribute` diagnostic:
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class Meta5:
|
||||
if flag1:
|
||||
attr1 = "metaclass value"
|
||||
|
||||
class C5(metaclass=Meta5):
|
||||
if flag2:
|
||||
attr1 = "class value"
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(C5.attr1) # revealed: Unknown | Literal["metaclass value", "class value"]
|
||||
```
|
||||
|
||||
## Union of attributes
|
||||
|
||||
If the (meta)class is a union type or if the attribute on the (meta) class has a union type, we
|
||||
infer those union types accordingly:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
@@ -716,14 +817,35 @@ def _(flag: bool):
|
||||
class C1:
|
||||
x = 2
|
||||
|
||||
reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
|
||||
|
||||
class C2:
|
||||
if flag:
|
||||
x = 3
|
||||
else:
|
||||
x = 4
|
||||
|
||||
reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Unknown | Literal[3, 4]
|
||||
|
||||
if flag:
|
||||
class Meta3(type):
|
||||
x = 5
|
||||
|
||||
else:
|
||||
class Meta3(type):
|
||||
x = 6
|
||||
|
||||
class C3(metaclass=Meta3): ...
|
||||
reveal_type(C3.x) # revealed: Unknown | Literal[5, 6]
|
||||
|
||||
class Meta4(type):
|
||||
if flag:
|
||||
x = 7
|
||||
else:
|
||||
x = 8
|
||||
|
||||
class C4(metaclass=Meta4): ...
|
||||
reveal_type(C4.x) # revealed: Unknown | Literal[7, 8]
|
||||
```
|
||||
|
||||
## Inherited class attributes
|
||||
@@ -883,7 +1005,7 @@ def _(flag: bool):
|
||||
self.x = 1
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(Foo().x) # revealed: int
|
||||
reveal_type(Foo().x) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
#### Possibly unbound
|
||||
@@ -1105,8 +1227,8 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
|
||||
bools are instances of that class:
|
||||
|
||||
```py
|
||||
reveal_type(True.__and__) # revealed: @Todo(overloaded method)
|
||||
reveal_type(False.__or__) # revealed: @Todo(overloaded method)
|
||||
reveal_type(True.__and__) # revealed: <bound method `__and__` of `Literal[True]`>
|
||||
reveal_type(False.__or__) # revealed: <bound method `__or__` of `Literal[False]`>
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
@@ -1262,6 +1384,7 @@ reveal_type(C.a_none) # revealed: None
|
||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||
[pyright's documentation] on this topic.
|
||||
|
||||
[descriptor protocol tests]: descriptor_protocol.md
|
||||
[pyright's documentation]: https://microsoft.github.io/pyright/#/type-concepts-advanced?id=class-and-instance-variables
|
||||
[typing spec on `classvar`]: https://typing.readthedocs.io/en/latest/spec/class-compat.html#classvar
|
||||
[`typing.classvar`]: https://docs.python.org/3/library/typing.html#typing.ClassVar
|
||||
|
||||
@@ -10,8 +10,7 @@ reveal_type(-3 // 3) # revealed: Literal[-1]
|
||||
reveal_type(-3 / 3) # revealed: float
|
||||
reveal_type(5 % 3) # revealed: Literal[2]
|
||||
|
||||
# TODO: Should emit `unsupported-operator` but we don't understand the bases of `str`, so we think
|
||||
# it inherits `Unknown`, so we think `str.__radd__` is `Unknown` instead of nonexistent.
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[2]` and `Literal["f"]`"
|
||||
reveal_type(2 + "f") # revealed: Unknown
|
||||
|
||||
def lhs(x: int):
|
||||
|
||||
@@ -40,10 +40,21 @@ class Meta(type):
|
||||
def __getitem__(cls, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class DunderOnMetaClass(metaclass=Meta):
|
||||
class DunderOnMetaclass(metaclass=Meta):
|
||||
pass
|
||||
|
||||
reveal_type(DunderOnMetaClass[0]) # revealed: str
|
||||
reveal_type(DunderOnMetaclass[0]) # revealed: str
|
||||
```
|
||||
|
||||
If the dunder method is only present on the class itself, it will not be called:
|
||||
|
||||
```py
|
||||
class ClassWithNormalDunder:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
# error: [non-subscriptable]
|
||||
ClassWithNormalDunder[0]
|
||||
```
|
||||
|
||||
## Operating on instances
|
||||
@@ -79,13 +90,32 @@ reveal_type(this_fails[0]) # revealed: Unknown
|
||||
However, the attached dunder method *can* be called if accessed directly:
|
||||
|
||||
```py
|
||||
# TODO: `this_fails.__getitem__` is incorrectly treated as a bound method. This
|
||||
# should be fixed with https://github.com/astral-sh/ruff/issues/16367
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(this_fails.__getitem__(this_fails, 0)) # revealed: Unknown | str
|
||||
```
|
||||
|
||||
The instance-level method is also not called when the class-level method is present:
|
||||
|
||||
```py
|
||||
def external_getitem1(instance, key) -> str:
|
||||
return "a"
|
||||
|
||||
def external_getitem2(key) -> int:
|
||||
return 1
|
||||
|
||||
def _(flag: bool):
|
||||
class ThisFails:
|
||||
if flag:
|
||||
__getitem__ = external_getitem1
|
||||
|
||||
def __init__(self):
|
||||
self.__getitem__ = external_getitem2
|
||||
|
||||
this_fails = ThisFails()
|
||||
|
||||
# error: [call-possibly-unbound-method]
|
||||
reveal_type(this_fails[0]) # revealed: Unknown | str
|
||||
```
|
||||
|
||||
## When the dunder is not a method
|
||||
|
||||
A dunder can also be a non-method callable:
|
||||
@@ -126,3 +156,64 @@ class_with_descriptor_dunder = ClassWithDescriptorDunder()
|
||||
|
||||
reveal_type(class_with_descriptor_dunder[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Dunders can not be overwritten on instances
|
||||
|
||||
If we attempt to overwrite a dunder method on an instance, it does not affect the behavior of
|
||||
implicit dunder calls:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
def f(self):
|
||||
# TODO: This should emit an `invalid-assignment` diagnostic once we understand the type of `self`
|
||||
self.__getitem__ = None
|
||||
|
||||
# This is still fine, and simply calls the `__getitem__` method on the class
|
||||
reveal_type(C()[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Calling a union of dunder methods
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class C:
|
||||
if flag:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
else:
|
||||
def __getitem__(self, key: int) -> bytes:
|
||||
return key
|
||||
|
||||
c = C()
|
||||
reveal_type(c[0]) # revealed: str | bytes
|
||||
|
||||
if flag:
|
||||
class D:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
else:
|
||||
class D:
|
||||
def __getitem__(self, key: int) -> bytes:
|
||||
return key
|
||||
|
||||
d = D()
|
||||
reveal_type(d[0]) # revealed: str | bytes
|
||||
```
|
||||
|
||||
## Calling a possibly-unbound dunder method
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class C:
|
||||
if flag:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
c = C()
|
||||
# error: [call-possibly-unbound-method]
|
||||
reveal_type(c[0]) # revealed: str
|
||||
```
|
||||
|
||||
@@ -12,7 +12,7 @@ import inspect
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance, owner) -> str:
|
||||
return 1
|
||||
return "a"
|
||||
|
||||
class C:
|
||||
normal: int = 1
|
||||
@@ -59,7 +59,7 @@ import sys
|
||||
reveal_type(inspect.getattr_static(sys, "platform")) # revealed: LiteralString
|
||||
reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static]
|
||||
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[1]
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[real]
|
||||
```
|
||||
|
||||
(Implicit) instance attributes can also be accessed through `inspect.getattr_static`:
|
||||
@@ -72,6 +72,23 @@ class D:
|
||||
reveal_type(inspect.getattr_static(D(), "instance_attr")) # revealed: int
|
||||
```
|
||||
|
||||
And attributes on metaclasses can be accessed when probing the class:
|
||||
|
||||
```py
|
||||
class Meta(type):
|
||||
attr: int = 1
|
||||
|
||||
class E(metaclass=Meta): ...
|
||||
|
||||
reveal_type(inspect.getattr_static(E, "attr")) # revealed: int
|
||||
```
|
||||
|
||||
Metaclass attributes can not be added when probing an instance of the class:
|
||||
|
||||
```py
|
||||
reveal_type(inspect.getattr_static(E(), "attr", "non_existent")) # revealed: Literal["non_existent"]
|
||||
```
|
||||
|
||||
## Error cases
|
||||
|
||||
We can only infer precise types if the attribute is a literal string. In all other cases, we fall
|
||||
|
||||
@@ -255,6 +255,58 @@ method_wrapper()
|
||||
method_wrapper(C(), C, "one too many")
|
||||
```
|
||||
|
||||
## Fallback to metaclass
|
||||
|
||||
When a method is accessed on a class object, it is looked up on the metaclass if it is not found on
|
||||
the class itself. This also creates a bound method that is bound to the class object itself:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class Meta(type):
|
||||
def f(cls, arg: int) -> str:
|
||||
return "a"
|
||||
|
||||
class C(metaclass=Meta):
|
||||
pass
|
||||
|
||||
reveal_type(C.f) # revealed: <bound method `f` of `Literal[C]`>
|
||||
reveal_type(C.f(1)) # revealed: str
|
||||
```
|
||||
|
||||
The method `f` can not be accessed from an instance of the class:
|
||||
|
||||
```py
|
||||
# error: [unresolved-attribute] "Type `C` has no attribute `f`"
|
||||
C().f
|
||||
```
|
||||
|
||||
A metaclass function can be shadowed by a method on the class:
|
||||
|
||||
```py
|
||||
from typing import Any, Literal
|
||||
|
||||
class D(metaclass=Meta):
|
||||
def f(arg: int) -> Literal["a"]:
|
||||
return "a"
|
||||
|
||||
reveal_type(D.f(1)) # revealed: Literal["a"]
|
||||
```
|
||||
|
||||
If the class method is possibly unbound, we union the return types:
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
class E(metaclass=Meta):
|
||||
if flag():
|
||||
def f(arg: int) -> Any:
|
||||
return "a"
|
||||
|
||||
reveal_type(E.f(1)) # revealed: str | Any
|
||||
```
|
||||
|
||||
## `@classmethod`
|
||||
|
||||
### Basic
|
||||
@@ -371,10 +423,10 @@ class C:
|
||||
# these should all return `str`:
|
||||
|
||||
reveal_type(C.f1(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C().f1(1)) # revealed: @Todo(decorated method)
|
||||
reveal_type(C().f1(1)) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
reveal_type(C.f2(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C().f2(1)) # revealed: @Todo(decorated method)
|
||||
reveal_type(C().f2(1)) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Call `type[...]`
|
||||
|
||||
## Single class
|
||||
|
||||
### Trivial constructor
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
def _(subclass_of_c: type[C]):
|
||||
reveal_type(subclass_of_c()) # revealed: C
|
||||
```
|
||||
|
||||
### Non-trivial constructor
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, x: int): ...
|
||||
|
||||
def _(subclass_of_c: type[C]):
|
||||
reveal_type(subclass_of_c(1)) # revealed: C
|
||||
|
||||
# TODO: Those should all be errors
|
||||
reveal_type(subclass_of_c("a")) # revealed: C
|
||||
reveal_type(subclass_of_c()) # revealed: C
|
||||
reveal_type(subclass_of_c(1, 2)) # revealed: C
|
||||
```
|
||||
|
||||
## Dynamic base
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(subclass_of_any: type[Any], subclass_of_unknown: type[Unknown]):
|
||||
reveal_type(subclass_of_any()) # revealed: Any
|
||||
reveal_type(subclass_of_any("any", "args", 1, 2)) # revealed: Any
|
||||
reveal_type(subclass_of_unknown()) # revealed: Unknown
|
||||
reveal_type(subclass_of_unknown("any", "args", 1, 2)) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unions of classes
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(subclass_of_ab: type[A | B]):
|
||||
reveal_type(subclass_of_ab()) # revealed: A | B
|
||||
```
|
||||
@@ -31,16 +31,12 @@ reveal_type(c.ten) # revealed: Literal[10]
|
||||
reveal_type(C.ten) # revealed: Literal[10]
|
||||
|
||||
# These are fine:
|
||||
# TODO: This should not be an error
|
||||
c.ten = 10 # error: [invalid-assignment]
|
||||
c.ten = 10
|
||||
C.ten = 10
|
||||
|
||||
# TODO: This should be an error (as the wrong type is being implicitly passed to `Ten.__set__`),
|
||||
# but the error message is misleading.
|
||||
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Ten`"
|
||||
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`"
|
||||
c.ten = 11
|
||||
|
||||
# TODO: same as above
|
||||
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`"
|
||||
C.ten = 11
|
||||
```
|
||||
@@ -67,16 +63,14 @@ c = C()
|
||||
|
||||
reveal_type(c.flexible_int) # revealed: int | None
|
||||
|
||||
# TODO: These should not be errors
|
||||
# error: [invalid-assignment]
|
||||
c.flexible_int = 42 # okay
|
||||
# TODO: This should not be an error
|
||||
# error: [invalid-assignment]
|
||||
c.flexible_int = "42" # also okay!
|
||||
|
||||
reveal_type(c.flexible_int) # revealed: int | None
|
||||
|
||||
# TODO: This should be an error, but the message needs to be improved.
|
||||
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `flexible_int` of type `FlexibleInt`"
|
||||
# TODO: This should be an error
|
||||
c.flexible_int = None # not okay
|
||||
|
||||
reveal_type(c.flexible_int) # revealed: int | None
|
||||
@@ -84,11 +78,10 @@ reveal_type(c.flexible_int) # revealed: int | None
|
||||
|
||||
## Data and non-data descriptors
|
||||
|
||||
Descriptors that define `__set__` or `__delete__` are called *data descriptors*. An example\
|
||||
of a data descriptor is a `property` with a setter and/or a deleter.\
|
||||
Descriptors that only define `__get__`, meanwhile, are called *non-data descriptors*. Examples
|
||||
include\
|
||||
functions, `classmethod` or `staticmethod`).
|
||||
Descriptors that define `__set__` or `__delete__` are called *data descriptors*. An example of a
|
||||
data descriptor is a `property` with a setter and/or a deleter. Descriptors that only define
|
||||
`__get__`, meanwhile, are called *non-data descriptors*. Examples include functions, `classmethod`
|
||||
or `staticmethod`.
|
||||
|
||||
The precedence chain for attribute access is (1) data descriptors, (2) instance attributes, and (3)
|
||||
non-data descriptors.
|
||||
@@ -100,7 +93,7 @@ class DataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
|
||||
return "data"
|
||||
|
||||
def __set__(self, instance: int, value) -> None:
|
||||
def __set__(self, instance: object, value: int) -> None:
|
||||
pass
|
||||
|
||||
class NonDataDescriptor:
|
||||
@@ -124,12 +117,7 @@ class C:
|
||||
|
||||
c = C()
|
||||
|
||||
# TODO: This should ideally be `Unknown | Literal["data"]`.
|
||||
#
|
||||
# - Pyright also wrongly shows `int | Literal['data']` here
|
||||
# - Mypy shows Literal["data"] here, but also shows Literal["non-data"] below.
|
||||
#
|
||||
reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data", 1]
|
||||
reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data"]
|
||||
|
||||
reveal_type(c.non_data_descriptor) # revealed: Unknown | Literal["non-data", 1]
|
||||
|
||||
@@ -143,6 +131,230 @@ reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
|
||||
C.data_descriptor = "something else" # This is okay
|
||||
```
|
||||
|
||||
## Descriptor protocol for class objects
|
||||
|
||||
When attributes are accessed on a class object, the following [precedence chain] is used:
|
||||
|
||||
- Data descriptor on the metaclass
|
||||
- Data or non-data descriptor on the class
|
||||
- Class attribute
|
||||
- Non-data descriptor on the metaclass
|
||||
- Metaclass attribute
|
||||
|
||||
To verify this, we define a data and a non-data descriptor:
|
||||
|
||||
```py
|
||||
from typing import Literal, Any
|
||||
|
||||
class DataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
|
||||
return "data"
|
||||
|
||||
def __set__(self, instance: object, value: str) -> None:
|
||||
pass
|
||||
|
||||
class NonDataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
|
||||
return "non-data"
|
||||
```
|
||||
|
||||
First, we make sure that the descriptors are correctly accessed when defined on the metaclass or the
|
||||
class:
|
||||
|
||||
```py
|
||||
class Meta1(type):
|
||||
meta_data_descriptor: DataDescriptor = DataDescriptor()
|
||||
meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
|
||||
|
||||
class C1(metaclass=Meta1):
|
||||
class_data_descriptor: DataDescriptor = DataDescriptor()
|
||||
class_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
|
||||
|
||||
reveal_type(C1.meta_data_descriptor) # revealed: Literal["data"]
|
||||
reveal_type(C1.meta_non_data_descriptor) # revealed: Literal["non-data"]
|
||||
|
||||
reveal_type(C1.class_data_descriptor) # revealed: Literal["data"]
|
||||
reveal_type(C1.class_non_data_descriptor) # revealed: Literal["non-data"]
|
||||
```
|
||||
|
||||
Next, we demonstrate that a *metaclass data descriptor* takes precedence over all class-level
|
||||
attributes:
|
||||
|
||||
```py
|
||||
class Meta2(type):
|
||||
meta_data_descriptor1: DataDescriptor = DataDescriptor()
|
||||
meta_data_descriptor2: DataDescriptor = DataDescriptor()
|
||||
|
||||
class ClassLevelDataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["class level data descriptor"]:
|
||||
return "class level data descriptor"
|
||||
|
||||
def __set__(self, instance: object, value: str) -> None:
|
||||
pass
|
||||
|
||||
class C2(metaclass=Meta2):
|
||||
meta_data_descriptor1: Literal["value on class"] = "value on class"
|
||||
meta_data_descriptor2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
|
||||
|
||||
reveal_type(C2.meta_data_descriptor1) # revealed: Literal["data"]
|
||||
reveal_type(C2.meta_data_descriptor2) # revealed: Literal["data"]
|
||||
```
|
||||
|
||||
On the other hand, normal metaclass attributes and metaclass non-data descriptors are shadowed by
|
||||
class-level attributes (descriptor or not):
|
||||
|
||||
```py
|
||||
class Meta3(type):
|
||||
meta_attribute1: Literal["value on metaclass"] = "value on metaclass"
|
||||
meta_attribute2: Literal["value on metaclass"] = "value on metaclass"
|
||||
meta_non_data_descriptor1: NonDataDescriptor = NonDataDescriptor()
|
||||
meta_non_data_descriptor2: NonDataDescriptor = NonDataDescriptor()
|
||||
|
||||
class C3(metaclass=Meta3):
|
||||
meta_attribute1: Literal["value on class"] = "value on class"
|
||||
meta_attribute2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
|
||||
meta_non_data_descriptor1: Literal["value on class"] = "value on class"
|
||||
meta_non_data_descriptor2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
|
||||
|
||||
reveal_type(C3.meta_attribute1) # revealed: Literal["value on class"]
|
||||
reveal_type(C3.meta_attribute2) # revealed: Literal["class level data descriptor"]
|
||||
reveal_type(C3.meta_non_data_descriptor1) # revealed: Literal["value on class"]
|
||||
reveal_type(C3.meta_non_data_descriptor2) # revealed: Literal["class level data descriptor"]
|
||||
```
|
||||
|
||||
Finally, metaclass attributes and metaclass non-data descriptors are only accessible when they are
|
||||
not shadowed by class-level attributes:
|
||||
|
||||
```py
|
||||
class Meta4(type):
|
||||
meta_attribute: Literal["value on metaclass"] = "value on metaclass"
|
||||
meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
|
||||
|
||||
class C4(metaclass=Meta4): ...
|
||||
|
||||
reveal_type(C4.meta_attribute) # revealed: Literal["value on metaclass"]
|
||||
reveal_type(C4.meta_non_data_descriptor) # revealed: Literal["non-data"]
|
||||
```
|
||||
|
||||
When a metaclass data descriptor is possibly unbound, we union the result type of its `__get__`
|
||||
method with an underlying class level attribute, if present:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Meta5(type):
|
||||
if flag:
|
||||
meta_data_descriptor1: DataDescriptor = DataDescriptor()
|
||||
meta_data_descriptor2: DataDescriptor = DataDescriptor()
|
||||
|
||||
class C5(metaclass=Meta5):
|
||||
meta_data_descriptor1: Literal["value on class"] = "value on class"
|
||||
|
||||
reveal_type(C5.meta_data_descriptor1) # revealed: Literal["data", "value on class"]
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(C5.meta_data_descriptor2) # revealed: Literal["data"]
|
||||
```
|
||||
|
||||
When a class-level attribute is possibly unbound, we union its (descriptor protocol) type with the
|
||||
metaclass attribute (unless it's a data descriptor, which always takes precedence):
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def _(flag: bool):
|
||||
class Meta6(type):
|
||||
attribute1: DataDescriptor = DataDescriptor()
|
||||
attribute2: NonDataDescriptor = NonDataDescriptor()
|
||||
attribute3: Literal["value on metaclass"] = "value on metaclass"
|
||||
|
||||
class C6(metaclass=Meta6):
|
||||
if flag:
|
||||
attribute1: Literal["value on class"] = "value on class"
|
||||
attribute2: Literal["value on class"] = "value on class"
|
||||
attribute3: Literal["value on class"] = "value on class"
|
||||
attribute4: Literal["value on class"] = "value on class"
|
||||
|
||||
reveal_type(C6.attribute1) # revealed: Literal["data"]
|
||||
reveal_type(C6.attribute2) # revealed: Literal["non-data", "value on class"]
|
||||
reveal_type(C6.attribute3) # revealed: Literal["value on metaclass", "value on class"]
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(C6.attribute4) # revealed: Literal["value on class"]
|
||||
```
|
||||
|
||||
Finally, we can also have unions of various types of attributes:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Meta7(type):
|
||||
if flag:
|
||||
union_of_metaclass_attributes: Literal[1] = 1
|
||||
union_of_metaclass_data_descriptor_and_attribute: DataDescriptor = DataDescriptor()
|
||||
else:
|
||||
union_of_metaclass_attributes: Literal[2] = 2
|
||||
union_of_metaclass_data_descriptor_and_attribute: Literal[2] = 2
|
||||
|
||||
class C7(metaclass=Meta7):
|
||||
if flag:
|
||||
union_of_class_attributes: Literal[1] = 1
|
||||
union_of_class_data_descriptor_and_attribute: DataDescriptor = DataDescriptor()
|
||||
else:
|
||||
union_of_class_attributes: Literal[2] = 2
|
||||
union_of_class_data_descriptor_and_attribute: Literal[2] = 2
|
||||
|
||||
reveal_type(C7.union_of_metaclass_attributes) # revealed: Literal[1, 2]
|
||||
reveal_type(C7.union_of_metaclass_data_descriptor_and_attribute) # revealed: Literal["data", 2]
|
||||
reveal_type(C7.union_of_class_attributes) # revealed: Literal[1, 2]
|
||||
reveal_type(C7.union_of_class_data_descriptor_and_attribute) # revealed: Literal["data", 2]
|
||||
```
|
||||
|
||||
## Partial fall back
|
||||
|
||||
Our implementation of the descriptor protocol takes into account that symbols can be possibly
|
||||
unbound. In those cases, we fall back to lower precedence steps of the descriptor protocol and union
|
||||
all possible results accordingly. We start by defining a data and a non-data descriptor:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class DataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
|
||||
return "data"
|
||||
|
||||
def __set__(self, instance: object, value: int) -> None:
|
||||
pass
|
||||
|
||||
class NonDataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
|
||||
return "non-data"
|
||||
```
|
||||
|
||||
Then, we demonstrate that we fall back to an instance attribute if a data descriptor is possibly
|
||||
unbound:
|
||||
|
||||
```py
|
||||
def f1(flag: bool):
|
||||
class C1:
|
||||
if flag:
|
||||
attr = DataDescriptor()
|
||||
|
||||
def f(self):
|
||||
self.attr = "normal"
|
||||
|
||||
reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]
|
||||
```
|
||||
|
||||
We never treat implicit instance attributes as definitely bound, so we fall back to the non-data
|
||||
descriptor here:
|
||||
|
||||
```py
|
||||
def f2(flag: bool):
|
||||
class C2:
|
||||
def f(self):
|
||||
self.attr = "normal"
|
||||
attr = NonDataDescriptor()
|
||||
|
||||
reveal_type(C2().attr) # revealed: Unknown | Literal["non-data", "normal"]
|
||||
```
|
||||
|
||||
## Built-in `property` descriptor
|
||||
|
||||
The built-in `property` decorator creates a descriptor. The names for attribute reads/writes are
|
||||
@@ -166,18 +378,21 @@ c = C()
|
||||
|
||||
reveal_type(c._name) # revealed: str | None
|
||||
|
||||
# Should be `str`
|
||||
reveal_type(c.name) # revealed: @Todo(decorated method)
|
||||
# TODO: Should be `str`
|
||||
reveal_type(c.name) # revealed: <bound method `name` of `C`>
|
||||
|
||||
# Should be `builtins.property`
|
||||
reveal_type(C.name) # revealed: Literal[name]
|
||||
|
||||
# This is fine:
|
||||
# TODO: These should not emit errors
|
||||
# error: [invalid-assignment]
|
||||
c.name = "new"
|
||||
|
||||
# error: [invalid-assignment]
|
||||
c.name = None
|
||||
|
||||
# TODO: this should be an error
|
||||
# TODO: this should be an error, but with a proper error message
|
||||
# error: [invalid-assignment] "Object of type `Literal[42]` is not assignable to attribute `name` of type `<bound method `name` of `C`>`"
|
||||
c.name = 42
|
||||
```
|
||||
|
||||
@@ -225,8 +440,7 @@ class C:
|
||||
def __init__(self):
|
||||
self.ten: Ten = Ten()
|
||||
|
||||
# TODO: Should be Ten
|
||||
reveal_type(C().ten) # revealed: Literal[10]
|
||||
reveal_type(C().ten) # revealed: Ten
|
||||
```
|
||||
|
||||
## Descriptors distinguishing between class and instance access
|
||||
@@ -295,12 +509,20 @@ class TailoredForInstanceAccess:
|
||||
def __get__(self, instance: C, owner: type[C] | None = None) -> str:
|
||||
return "a"
|
||||
|
||||
class C:
|
||||
class TailoredForMetaclassAccess:
|
||||
def __get__(self, instance: type[C], owner: type[Meta]) -> bytes:
|
||||
return b"a"
|
||||
|
||||
class Meta(type):
|
||||
metaclass_access: TailoredForMetaclassAccess = TailoredForMetaclassAccess()
|
||||
|
||||
class C(metaclass=Meta):
|
||||
class_object_access: TailoredForClassObjectAccess = TailoredForClassObjectAccess()
|
||||
instance_access: TailoredForInstanceAccess = TailoredForInstanceAccess()
|
||||
|
||||
reveal_type(C.class_object_access) # revealed: int
|
||||
reveal_type(C().instance_access) # revealed: str
|
||||
reveal_type(C.metaclass_access) # revealed: bytes
|
||||
|
||||
# TODO: These should emit a diagnostic
|
||||
reveal_type(C().class_object_access) # revealed: TailoredForClassObjectAccess
|
||||
@@ -320,6 +542,42 @@ class C:
|
||||
|
||||
# TODO: This should be an error
|
||||
reveal_type(C.descriptor) # revealed: Descriptor
|
||||
|
||||
# TODO: This should be an error
|
||||
reveal_type(C().descriptor) # revealed: Descriptor
|
||||
```
|
||||
|
||||
## Possibly unbound descriptor attributes
|
||||
|
||||
```py
|
||||
class DataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int:
|
||||
return 1
|
||||
|
||||
def __set__(self, instance: int, value) -> None:
|
||||
pass
|
||||
|
||||
class NonDataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int:
|
||||
return 1
|
||||
|
||||
def _(flag: bool):
|
||||
class PossiblyUnbound:
|
||||
if flag:
|
||||
non_data: NonDataDescriptor = NonDataDescriptor()
|
||||
data: DataDescriptor = DataDescriptor()
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `Literal[PossiblyUnbound]` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound.non_data) # revealed: int
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound().non_data) # revealed: int
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `data` on type `Literal[PossiblyUnbound]` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound.data) # revealed: int
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound().data) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly-unbound `__get__` method
|
||||
@@ -334,13 +592,55 @@ def _(flag: bool):
|
||||
class C:
|
||||
descriptor: MaybeDescriptor = MaybeDescriptor()
|
||||
|
||||
# TODO: This should be `MaybeDescriptor | int`
|
||||
reveal_type(C.descriptor) # revealed: int
|
||||
reveal_type(C.descriptor) # revealed: int | MaybeDescriptor
|
||||
|
||||
reveal_type(C().descriptor) # revealed: int | MaybeDescriptor
|
||||
```
|
||||
|
||||
## Descriptors with non-function `__get__` callables that are descriptors themselves
|
||||
|
||||
The descriptor protocol is recursive, i.e. looking up `__get__` can involve triggering the
|
||||
descriptor protocol on the callable's `__call__` method:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class ReturnedCallable2:
|
||||
def __call__(self, descriptor: Descriptor1, instance: None, owner: type[C]) -> int:
|
||||
return 1
|
||||
|
||||
class ReturnedCallable1:
|
||||
def __call__(self, descriptor: Descriptor2, instance: Callable1, owner: type[Callable1]) -> ReturnedCallable2:
|
||||
return ReturnedCallable2()
|
||||
|
||||
class Callable3:
|
||||
def __call__(self, descriptor: Descriptor3, instance: Callable2, owner: type[Callable2]) -> ReturnedCallable1:
|
||||
return ReturnedCallable1()
|
||||
|
||||
class Descriptor3:
|
||||
__get__: Callable3 = Callable3()
|
||||
|
||||
class Callable2:
|
||||
__call__: Descriptor3 = Descriptor3()
|
||||
|
||||
class Descriptor2:
|
||||
__get__: Callable2 = Callable2()
|
||||
|
||||
class Callable1:
|
||||
__call__: Descriptor2 = Descriptor2()
|
||||
|
||||
class Descriptor1:
|
||||
__get__: Callable1 = Callable1()
|
||||
|
||||
class C:
|
||||
d: Descriptor1 = Descriptor1()
|
||||
|
||||
reveal_type(C.d) # revealed: int
|
||||
```
|
||||
|
||||
## Dunder methods
|
||||
|
||||
Dunder methods are looked up on the meta type, but we still need to invoke the descriptor protocol:
|
||||
Dunder methods are looked up on the meta-type, but we still need to invoke the descriptor protocol:
|
||||
|
||||
```py
|
||||
class SomeCallable:
|
||||
@@ -438,4 +738,5 @@ wrapper_descriptor(f, None, type(f), "one too many")
|
||||
```
|
||||
|
||||
[descriptors]: https://docs.python.org/3/howto/descriptor.html
|
||||
[precedence chain]: https://github.com/python/cpython/blob/3.13/Objects/typeobject.c#L5393-L5481
|
||||
[simple example]: https://docs.python.org/3/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# `lambda` expression
|
||||
|
||||
## No parameters
|
||||
|
||||
`lambda` expressions can be defined without any parameters.
|
||||
|
||||
```py
|
||||
reveal_type(lambda: 1) # revealed: () -> @Todo(lambda return type)
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(lambda: a) # revealed: () -> @Todo(lambda return type)
|
||||
```
|
||||
|
||||
## With parameters
|
||||
|
||||
Unlike parameters in function definition, the parameters in a `lambda` expression cannot be
|
||||
annotated.
|
||||
|
||||
```py
|
||||
reveal_type(lambda a: a) # revealed: (a) -> @Todo(lambda return type)
|
||||
reveal_type(lambda a, b: a + b) # revealed: (a, b) -> @Todo(lambda return type)
|
||||
```
|
||||
|
||||
But, it can have default values:
|
||||
|
||||
```py
|
||||
reveal_type(lambda a=1: a) # revealed: (a=Literal[1]) -> @Todo(lambda return type)
|
||||
reveal_type(lambda a, b=2: a) # revealed: (a, b=Literal[2]) -> @Todo(lambda return type)
|
||||
```
|
||||
|
||||
And, positional-only parameters:
|
||||
|
||||
```py
|
||||
reveal_type(lambda a, b, /, c: c) # revealed: (a, b, /, c) -> @Todo(lambda return type)
|
||||
```
|
||||
|
||||
And, keyword-only parameters:
|
||||
|
||||
```py
|
||||
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=Literal[2], c) -> @Todo(lambda return type)
|
||||
```
|
||||
|
||||
And, variadic parameter:
|
||||
|
||||
```py
|
||||
reveal_type(lambda *args: args) # revealed: (*args) -> @Todo(lambda return type)
|
||||
```
|
||||
|
||||
And, keyword-varidic parameter:
|
||||
|
||||
```py
|
||||
reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> @Todo(lambda return type)
|
||||
```
|
||||
|
||||
Mixing all of them together:
|
||||
|
||||
```py
|
||||
# revealed: (a, b, /, c=Literal[True], *args, *, d=Literal["default"], e=Literal[5], **kwargs) -> @Todo(lambda return type)
|
||||
reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None)
|
||||
```
|
||||
|
||||
## Parameter type
|
||||
|
||||
In addition to correctly inferring the `lambda` expression, the parameters should also be inferred
|
||||
correctly.
|
||||
|
||||
Using a parameter with no default value:
|
||||
|
||||
```py
|
||||
lambda x: reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
Using a parameter with default value:
|
||||
|
||||
```py
|
||||
lambda x=1: reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
Using a variadic paramter:
|
||||
|
||||
```py
|
||||
# TODO: should be `tuple[Unknown, ...]` (needs generics)
|
||||
lambda *args: reveal_type(args) # revealed: tuple
|
||||
```
|
||||
|
||||
Using a keyword-varidic parameter:
|
||||
|
||||
```py
|
||||
# TODO: should be `dict[str, Unknown]` (needs generics)
|
||||
lambda **kwargs: reveal_type(kwargs) # revealed: dict
|
||||
```
|
||||
|
||||
## Nested `lambda` expressions
|
||||
|
||||
Here, a `lambda` expression is used as the default value for a parameter in another `lambda`
|
||||
expression.
|
||||
|
||||
```py
|
||||
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> @Todo(lambda return type)) -> @Todo(lambda return type)
|
||||
```
|
||||
@@ -68,7 +68,7 @@ class C[T]:
|
||||
# TODO: no error
|
||||
# TODO: revealed: C[int]
|
||||
# error: [non-subscriptable]
|
||||
reveal_type(C[int]()) # revealed: Unknown
|
||||
reveal_type(C[int]()) # revealed: C
|
||||
```
|
||||
|
||||
We can infer the type parameter from a type context:
|
||||
@@ -129,18 +129,19 @@ propagate through:
|
||||
|
||||
```py
|
||||
class Base[T]:
|
||||
x: T
|
||||
x: T | None = None
|
||||
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class Sub[U](Base[U]): ...
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: int
|
||||
# TODO: revealed: int | None
|
||||
# error: [non-subscriptable]
|
||||
reveal_type(Base[int].x) # revealed: Unknown
|
||||
# TODO: revealed: int
|
||||
reveal_type(Sub[int].x) # revealed: Unknown
|
||||
reveal_type(Base[int].x) # revealed: T | None
|
||||
# TODO: revealed: int | None
|
||||
# error: [non-subscriptable]
|
||||
reveal_type(Sub[int].x) # revealed: T | None
|
||||
```
|
||||
|
||||
## Cyclic class definition
|
||||
|
||||
@@ -216,9 +216,10 @@ from typing import Iterable
|
||||
|
||||
def f[T](x: T, y: T) -> None:
|
||||
class Ok[S]: ...
|
||||
# TODO: error
|
||||
# TODO: error for reuse of typevar
|
||||
class Bad1[T]: ...
|
||||
# TODO: error
|
||||
# TODO: no non-subscriptable error, error for reuse of typevar
|
||||
# error: [non-subscriptable]
|
||||
class Bad2(Iterable[T]): ...
|
||||
```
|
||||
|
||||
@@ -229,9 +230,10 @@ from typing import Iterable
|
||||
|
||||
class C[T]:
|
||||
class Ok1[S]: ...
|
||||
# TODO: error
|
||||
# TODO: error for reuse of typevar
|
||||
class Bad1[T]: ...
|
||||
# TODO: error
|
||||
# TODO: no non-subscriptable error, error for reuse of typevar
|
||||
# error: [non-subscriptable]
|
||||
class Bad2(Iterable[T]): ...
|
||||
```
|
||||
|
||||
|
||||
@@ -91,3 +91,16 @@ match while:
|
||||
for x in foo.pass:
|
||||
pass
|
||||
```
|
||||
|
||||
## Invalid annotation
|
||||
|
||||
### `typing.Callable`
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# error: [invalid-syntax] "Expected index or slice expression"
|
||||
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
def _(c: Callable[]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
@@ -163,7 +163,7 @@ reveal_type(B.__class__) # revealed: Literal[M]
|
||||
## Non-class
|
||||
|
||||
When a class has an explicit `metaclass` that is not a class, but is a callable that accepts
|
||||
`type.__new__` arguments, we should return the meta type of its return type.
|
||||
`type.__new__` arguments, we should return the meta-type of its return type.
|
||||
|
||||
```py
|
||||
def f(*args, **kwargs) -> int: ...
|
||||
|
||||
@@ -9,7 +9,7 @@ is unbound.
|
||||
```py
|
||||
reveal_type(__name__) # revealed: str
|
||||
reveal_type(__file__) # revealed: str | None
|
||||
reveal_type(__loader__) # revealed: @Todo(instance attribute on class with dynamic base) | None
|
||||
reveal_type(__loader__) # revealed: LoaderProtocol | None
|
||||
reveal_type(__package__) # revealed: str | None
|
||||
reveal_type(__doc__) # revealed: str | None
|
||||
|
||||
@@ -151,6 +151,7 @@ typeshed = "/typeshed"
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
class object: ...
|
||||
class int: ...
|
||||
class bytes: ...
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ class Foo[T]: ...
|
||||
class Bar(Foo[Bar]): ...
|
||||
|
||||
reveal_type(Bar) # revealed: Literal[Bar]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
# TODO: Instead of `Literal[Foo]`, we might eventually want to show a type that involves the type parameter.
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Literal[object]]
|
||||
```
|
||||
|
||||
## Access to attributes declarated in stubs
|
||||
|
||||
@@ -117,7 +117,6 @@ from typing import Tuple
|
||||
|
||||
class C(Tuple): ...
|
||||
|
||||
# Runtime value: `(C, tuple, typing.Generic, object)`
|
||||
# TODO: Add `Generic` to the MRO
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[tuple], Unknown, Literal[object]]
|
||||
# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
|
||||
reveal_type(C.__mro__)
|
||||
```
|
||||
|
||||
@@ -38,16 +38,15 @@ For example, the `type: ignore` comment in this example suppresses the error of
|
||||
`"test"` and adding `"other"` to the result of the cast.
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
from typing import cast
|
||||
|
||||
y = (
|
||||
cast(int, "test" +
|
||||
# TODO: Remove the expected error after implementing `invalid-operator` for binary expressions
|
||||
# error: [unused-ignore-comment]
|
||||
2 # type: ignore
|
||||
# error: [unsupported-operator]
|
||||
cast(
|
||||
int,
|
||||
2 + "test", # type: ignore
|
||||
)
|
||||
+ "other" # TODO: expected-error[invalid-operator]
|
||||
+ "other"
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
A type is single-valued iff it is not empty and all inhabitants of it compare equal.
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, Literal, LiteralString, Never
|
||||
from typing_extensions import Any, Literal, LiteralString, Never, Callable
|
||||
from knot_extensions import is_single_valued, static_assert
|
||||
|
||||
static_assert(is_single_valued(None))
|
||||
@@ -22,4 +22,7 @@ static_assert(not is_single_valued(Any))
|
||||
static_assert(not is_single_valued(Literal[1, 2]))
|
||||
|
||||
static_assert(not is_single_valued(tuple[None, int]))
|
||||
|
||||
static_assert(not is_single_valued(Callable[..., None]))
|
||||
static_assert(not is_single_valued(Callable[[int, str], None]))
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ A type is a singleton type iff it has exactly one inhabitant.
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, Never
|
||||
from typing_extensions import Literal, Never, Callable
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(None))
|
||||
@@ -23,6 +23,9 @@ static_assert(not is_singleton(Literal[1, 2]))
|
||||
static_assert(not is_singleton(tuple[()]))
|
||||
static_assert(not is_singleton(tuple[None]))
|
||||
static_assert(not is_singleton(tuple[None, Literal[True]]))
|
||||
|
||||
static_assert(not is_singleton(Callable[..., None]))
|
||||
static_assert(not is_singleton(Callable[[int, str], None]))
|
||||
```
|
||||
|
||||
## `NoDefault`
|
||||
|
||||
@@ -383,7 +383,7 @@ static_assert(is_subtype_of(LiteralStr, type[object]))
|
||||
|
||||
static_assert(not is_subtype_of(type[str], LiteralStr))
|
||||
|
||||
# custom meta classes
|
||||
# custom metaclasses
|
||||
|
||||
type LiteralHasCustomMetaclass = TypeOf[HasCustomMetaclass]
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Unpacking
|
||||
|
||||
If there are not enough or too many values when unpacking, an error will occur and the types of
|
||||
all variables (if nested tuple unpacking fails, only the variables within the failed tuples) is
|
||||
inferred to be `Unknown`.
|
||||
|
||||
## Tuple
|
||||
|
||||
### Simple tuple
|
||||
@@ -63,8 +67,8 @@ reveal_type(c) # revealed: Literal[4]
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
|
||||
(a, b, c) = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -73,8 +77,30 @@ reveal_type(c) # revealed: Unknown
|
||||
```py
|
||||
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
|
||||
(a, b) = (1, 2, 3)
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Nested uneven unpacking (1)
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, (b, c), d) = (1, (2,), 3)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
### Nested uneven unpacking (2)
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
|
||||
(a, (b, c), d) = (1, (2, 3, 4), 5)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Literal[5]
|
||||
```
|
||||
|
||||
### Starred expression (1)
|
||||
@@ -82,10 +108,10 @@ reveal_type(b) # revealed: Literal[2]
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
|
||||
[a, *b, c, d] = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(a) # revealed: Unknown
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: Literal[2]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -135,10 +161,10 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 5 or more, got 1)"
|
||||
(a, b, c, *d, e, f) = (1,)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(d) # revealed: Unknown
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
@@ -201,8 +227,8 @@ reveal_type(b) # revealed: LiteralString
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
|
||||
a, b, c = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -211,8 +237,8 @@ reveal_type(c) # revealed: Unknown
|
||||
```py
|
||||
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
|
||||
a, b = "abc"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Starred expression (1)
|
||||
@@ -220,10 +246,19 @@ reveal_type(b) # revealed: LiteralString
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
|
||||
(a, *b, c, d) = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(a) # revealed: Unknown
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 1)"
|
||||
(a, b, *c, d) = "a"
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -274,7 +309,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, b) = "é"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -284,7 +319,7 @@ reveal_type(b) # revealed: Unknown
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, b) = "\u9e6c"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -294,7 +329,7 @@ reveal_type(b) # revealed: Unknown
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, b) = "\U0010ffff"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -388,8 +423,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
|
||||
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
|
||||
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 5)"
|
||||
a, b = arg
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: bytes | int
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Size mismatch (2)
|
||||
@@ -399,8 +434,8 @@ def _(arg: tuple[int, bytes] | tuple[int, str]):
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
|
||||
a, b, c = arg
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: bytes | str
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -542,7 +577,7 @@ for a, b in ((1, 2), ("a", "b")):
|
||||
# error: "Object of type `Literal[4]` is not iterable"
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"):
|
||||
reveal_type(a) # revealed: Unknown | Literal[3, 5] | LiteralString
|
||||
reveal_type(a) # revealed: Unknown | Literal[3, 5]
|
||||
reveal_type(b) # revealed: Unknown | Literal["a", "b"]
|
||||
```
|
||||
|
||||
@@ -578,3 +613,98 @@ def _(arg: tuple[tuple[int, str], Iterable]):
|
||||
reveal_type(a) # revealed: int | bytes
|
||||
reveal_type(b) # revealed: str | bytes
|
||||
```
|
||||
|
||||
## With statement
|
||||
|
||||
Unpacking in a `with` statement.
|
||||
|
||||
### Same types
|
||||
|
||||
```py
|
||||
class ContextManager:
|
||||
def __enter__(self) -> tuple[int, int]:
|
||||
return (1, 2)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||
pass
|
||||
|
||||
with ContextManager() as (a, b):
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int
|
||||
```
|
||||
|
||||
### Mixed types
|
||||
|
||||
```py
|
||||
class ContextManager:
|
||||
def __enter__(self) -> tuple[int, str]:
|
||||
return (1, "a")
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||
pass
|
||||
|
||||
with ContextManager() as (a, b):
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
```
|
||||
|
||||
### Nested
|
||||
|
||||
```py
|
||||
class ContextManager:
|
||||
def __enter__(self) -> tuple[int, tuple[str, bytes]]:
|
||||
return (1, ("a", b"bytes"))
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||
pass
|
||||
|
||||
with ContextManager() as (a, (b, c)):
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
reveal_type(c) # revealed: bytes
|
||||
```
|
||||
|
||||
### Starred expression
|
||||
|
||||
```py
|
||||
class ContextManager:
|
||||
def __enter__(self) -> tuple[int, int, int]:
|
||||
return (1, 2, 3)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||
pass
|
||||
|
||||
with ContextManager() as (a, *b):
|
||||
reveal_type(a) # revealed: int
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
```
|
||||
|
||||
### Unbound context manager expression
|
||||
|
||||
```py
|
||||
# TODO: should only be one diagnostic
|
||||
# error: [unresolved-reference] "Name `nonexistant` used when not defined"
|
||||
# error: [unresolved-reference] "Name `nonexistant` used when not defined"
|
||||
# error: [unresolved-reference] "Name `nonexistant` used when not defined"
|
||||
with nonexistant as (x, y):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Invalid unpacking
|
||||
|
||||
```py
|
||||
class ContextManager:
|
||||
def __enter__(self) -> tuple[int, str]:
|
||||
return (1, "a")
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
pass
|
||||
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
|
||||
with ContextManager() as (a, b, c):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -45,7 +45,7 @@ def _(flag: bool):
|
||||
```py
|
||||
class Manager: ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
@@ -56,7 +56,7 @@ with Manager():
|
||||
class Manager:
|
||||
def __exit__(self, exc_tpe, exc_value, traceback): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__enter__`"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
@@ -67,7 +67,7 @@ with Manager():
|
||||
class Manager:
|
||||
def __enter__(self): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__exit__`"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
@@ -113,8 +113,7 @@ def _(flag: bool):
|
||||
class NotAContextManager: ...
|
||||
context_expr = Manager1() if flag else NotAContextManager()
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__enter__` is possibly unbound"
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__exit__` is possibly unbound"
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound"
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
@@ -45,7 +45,7 @@ pub struct AstNodeRef<T> {
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
impl<T> AstNodeRef<T> {
|
||||
/// Creates a new `AstNodeRef` that reference `node`. The `parsed` is the [`ParsedModule`] to
|
||||
/// Creates a new `AstNodeRef` that references `node`. The `parsed` is the [`ParsedModule`] to
|
||||
/// which the `AstNodeRef` belongs.
|
||||
///
|
||||
/// ## Safety
|
||||
|
||||
@@ -22,6 +22,10 @@ pub(crate) enum AttributeAssignment<'db> {
|
||||
/// `for self.x in <iterable>`.
|
||||
Iterable { iterable: Expression<'db> },
|
||||
|
||||
/// An attribute assignment where the expression to be assigned is a context manager, for example
|
||||
/// `with <context_manager> as self.x`.
|
||||
ContextManager { context_manager: Expression<'db> },
|
||||
|
||||
/// An attribute assignment where the left-hand side is an unpacking expression,
|
||||
/// e.g. `self.x, self.y = <value>`.
|
||||
Unpack {
|
||||
|
||||
@@ -1032,6 +1032,7 @@ where
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
// SAFETY: `target` belongs to the `self.module` tree
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
@@ -1262,16 +1263,64 @@ where
|
||||
is_async,
|
||||
..
|
||||
}) => {
|
||||
for item in items {
|
||||
self.visit_expr(&item.context_expr);
|
||||
if let Some(optional_vars) = item.optional_vars.as_deref() {
|
||||
self.add_standalone_expression(&item.context_expr);
|
||||
self.push_assignment(CurrentAssignment::WithItem {
|
||||
item,
|
||||
is_async: *is_async,
|
||||
});
|
||||
for item @ ruff_python_ast::WithItem {
|
||||
range: _,
|
||||
context_expr,
|
||||
optional_vars,
|
||||
} in items
|
||||
{
|
||||
self.visit_expr(context_expr);
|
||||
if let Some(optional_vars) = optional_vars.as_deref() {
|
||||
let context_manager = self.add_standalone_expression(context_expr);
|
||||
let current_assignment = match optional_vars {
|
||||
ast::Expr::Tuple(_) | ast::Expr::List(_) => {
|
||||
Some(CurrentAssignment::WithItem {
|
||||
item,
|
||||
first: true,
|
||||
is_async: *is_async,
|
||||
unpack: Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
// SAFETY: the node `optional_vars` belongs to the `self.module` tree
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), optional_vars)
|
||||
},
|
||||
UnpackValue::ContextManager(context_manager),
|
||||
countme::Count::default(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
ast::Expr::Name(_) => Some(CurrentAssignment::WithItem {
|
||||
item,
|
||||
is_async: *is_async,
|
||||
unpack: None,
|
||||
// `false` is arbitrary here---we don't actually use it other than in the actual unpacks
|
||||
first: false,
|
||||
}),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::ContextManager { context_manager },
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
self.visit_expr(optional_vars);
|
||||
self.pop_assignment();
|
||||
if current_assignment.is_some() {
|
||||
self.pop_assignment();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.visit_body(body);
|
||||
@@ -1304,6 +1353,7 @@ where
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
// SAFETY: the node `target` belongs to the `self.module` tree
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
@@ -1631,12 +1681,19 @@ where
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(CurrentAssignment::WithItem { item, is_async }) => {
|
||||
Some(CurrentAssignment::WithItem {
|
||||
item,
|
||||
first,
|
||||
is_async,
|
||||
unpack,
|
||||
}) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
WithItemDefinitionNodeRef {
|
||||
node: item,
|
||||
target: name_node,
|
||||
unpack,
|
||||
context_expr: &item.context_expr,
|
||||
name: name_node,
|
||||
first,
|
||||
is_async,
|
||||
},
|
||||
);
|
||||
@@ -1646,7 +1703,9 @@ where
|
||||
}
|
||||
|
||||
if let Some(
|
||||
CurrentAssignment::Assign { first, .. } | CurrentAssignment::For { first, .. },
|
||||
CurrentAssignment::Assign { first, .. }
|
||||
| CurrentAssignment::For { first, .. }
|
||||
| CurrentAssignment::WithItem { first, .. },
|
||||
) = self.current_assignment_mut()
|
||||
{
|
||||
*first = false;
|
||||
@@ -1826,6 +1885,10 @@ where
|
||||
| CurrentAssignment::For {
|
||||
unpack: Some(unpack),
|
||||
..
|
||||
}
|
||||
| CurrentAssignment::WithItem {
|
||||
unpack: Some(unpack),
|
||||
..
|
||||
},
|
||||
) = self.current_assignment()
|
||||
{
|
||||
@@ -1919,7 +1982,9 @@ enum CurrentAssignment<'a> {
|
||||
},
|
||||
WithItem {
|
||||
item: &'a ast::WithItem,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -201,8 +201,10 @@ pub(crate) struct AssignmentDefinitionNodeRef<'a> {
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct WithItemDefinitionNodeRef<'a> {
|
||||
pub(crate) node: &'a ast::WithItem,
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) context_expr: &'a ast::Expr,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
@@ -323,12 +325,16 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
DefinitionKind::Parameter(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef {
|
||||
node,
|
||||
target,
|
||||
unpack,
|
||||
context_expr,
|
||||
name,
|
||||
first,
|
||||
is_async,
|
||||
}) => DefinitionKind::WithItem(WithItemDefinitionKind {
|
||||
node: AstNodeRef::new(parsed.clone(), node),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
target: TargetKind::from(unpack),
|
||||
context_expr: AstNodeRef::new(parsed.clone(), context_expr),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
first,
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
|
||||
@@ -394,10 +400,12 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
Self::VariadicKeywordParameter(node) => node.into(),
|
||||
Self::Parameter(node) => node.into(),
|
||||
Self::WithItem(WithItemDefinitionNodeRef {
|
||||
node: _,
|
||||
target,
|
||||
unpack: _,
|
||||
context_expr: _,
|
||||
first: _,
|
||||
is_async: _,
|
||||
}) => target.into(),
|
||||
name,
|
||||
}) => name.into(),
|
||||
Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => {
|
||||
identifier.into()
|
||||
}
|
||||
@@ -467,7 +475,7 @@ pub enum DefinitionKind<'db> {
|
||||
VariadicPositionalParameter(AstNodeRef<ast::Parameter>),
|
||||
VariadicKeywordParameter(AstNodeRef<ast::Parameter>),
|
||||
Parameter(AstNodeRef<ast::ParameterWithDefault>),
|
||||
WithItem(WithItemDefinitionKind),
|
||||
WithItem(WithItemDefinitionKind<'db>),
|
||||
MatchPattern(MatchPatternDefinitionKind),
|
||||
ExceptHandler(ExceptHandlerDefinitionKind),
|
||||
TypeVar(AstNodeRef<ast::TypeParamTypeVar>),
|
||||
@@ -506,7 +514,7 @@ impl DefinitionKind<'_> {
|
||||
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.name.range(),
|
||||
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.name.range(),
|
||||
DefinitionKind::Parameter(parameter) => parameter.parameter.name.range(),
|
||||
DefinitionKind::WithItem(with_item) => with_item.target().range(),
|
||||
DefinitionKind::WithItem(with_item) => with_item.name().range(),
|
||||
DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(),
|
||||
DefinitionKind::ExceptHandler(handler) => handler.node().range(),
|
||||
DefinitionKind::TypeVar(type_var) => type_var.name.range(),
|
||||
@@ -688,19 +696,29 @@ impl<'db> AssignmentDefinitionKind<'db> {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WithItemDefinitionKind {
|
||||
node: AstNodeRef<ast::WithItem>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
pub struct WithItemDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
context_expr: AstNodeRef<ast::Expr>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
impl WithItemDefinitionKind {
|
||||
pub(crate) fn node(&self) -> &ast::WithItem {
|
||||
self.node.node()
|
||||
impl<'db> WithItemDefinitionKind<'db> {
|
||||
pub(crate) fn context_expr(&self) -> &ast::Expr {
|
||||
self.context_expr.node()
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||
self.target.node()
|
||||
pub(crate) fn target(&self) -> TargetKind<'db> {
|
||||
self.target
|
||||
}
|
||||
|
||||
pub(crate) fn name(&self) -> &ast::ExprName {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
|
||||
pub(crate) const fn is_async(&self) -> bool {
|
||||
|
||||
@@ -6,8 +6,8 @@ use hashbrown::hash_map::RawEntryMut;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::{self as ast};
|
||||
use rustc_hash::FxHasher;
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
|
||||
@@ -21,6 +21,15 @@ pub(crate) enum Boundness {
|
||||
PossiblyUnbound,
|
||||
}
|
||||
|
||||
impl Boundness {
|
||||
pub(crate) const fn max(self, other: Self) -> Self {
|
||||
match (self, other) {
|
||||
(Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound,
|
||||
(Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a symbol lookup, which can either be a (possibly unbound) type
|
||||
/// or a completely unbound symbol.
|
||||
///
|
||||
@@ -79,51 +88,6 @@ impl<'db> Symbol<'db> {
|
||||
.expect("Expected a (possibly unbound) type, not an unbound symbol")
|
||||
}
|
||||
|
||||
/// Transform the symbol into a [`LookupResult`],
|
||||
/// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol
|
||||
/// and the `Err` variant represents a symbol that is either definitely or possibly unbound.
|
||||
pub(crate) fn into_lookup_result(self) -> LookupResult<'db> {
|
||||
match self {
|
||||
Symbol::Type(ty, Boundness::Bound) => Ok(ty),
|
||||
Symbol::Type(ty, Boundness::PossiblyUnbound) => Err(LookupError::PossiblyUnbound(ty)),
|
||||
Symbol::Unbound => Err(LookupError::Unbound),
|
||||
}
|
||||
}
|
||||
|
||||
/// Safely unwrap the symbol into a [`Type`].
|
||||
///
|
||||
/// If the symbol is definitely unbound or possibly unbound, it will be transformed into a
|
||||
/// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning
|
||||
/// the result of `diagnostic_fn` (which will be a [`Type`]). This allows the caller to ensure
|
||||
/// that a diagnostic is emitted if the symbol is possibly or definitely unbound.
|
||||
pub(crate) fn unwrap_with_diagnostic(
|
||||
self,
|
||||
diagnostic_fn: impl FnOnce(LookupError<'db>) -> Type<'db>,
|
||||
) -> Type<'db> {
|
||||
self.into_lookup_result().unwrap_or_else(diagnostic_fn)
|
||||
}
|
||||
|
||||
/// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound.
|
||||
///
|
||||
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
|
||||
/// 2. Else, evaluate `fallback_fn()`:
|
||||
/// a. If `self` is definitely unbound, return the result of `fallback_fn()`.
|
||||
/// b. Else, if `fallback` is definitely unbound, return `self`.
|
||||
/// c. Else, if `self` is possibly unbound and `fallback` is definitely bound,
|
||||
/// return `Symbol(<union of self-type and fallback-type>, Boundness::Bound)`
|
||||
/// d. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
|
||||
/// return `Symbol(<union of self-type and fallback-type>, Boundness::PossiblyUnbound)`
|
||||
#[must_use]
|
||||
pub(crate) fn or_fall_back_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
fallback_fn: impl FnOnce() -> Self,
|
||||
) -> Self {
|
||||
self.into_lookup_result()
|
||||
.or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn()))
|
||||
.into()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Symbol<'db> {
|
||||
match self {
|
||||
@@ -131,14 +95,28 @@ impl<'db> Symbol<'db> {
|
||||
Symbol::Unbound => Symbol::Unbound,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn with_qualifiers(self, qualifiers: TypeQualifiers) -> SymbolAndQualifiers<'db> {
|
||||
SymbolAndQualifiers {
|
||||
symbol: self,
|
||||
qualifiers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<LookupResult<'db>> for Symbol<'db> {
|
||||
impl<'db> From<LookupResult<'db>> for SymbolAndQualifiers<'db> {
|
||||
fn from(value: LookupResult<'db>) -> Self {
|
||||
match value {
|
||||
Ok(ty) => Symbol::Type(ty, Boundness::Bound),
|
||||
Err(LookupError::Unbound) => Symbol::Unbound,
|
||||
Err(LookupError::PossiblyUnbound(ty)) => Symbol::Type(ty, Boundness::PossiblyUnbound),
|
||||
Ok(type_and_qualifiers) => {
|
||||
Symbol::Type(type_and_qualifiers.inner_type(), Boundness::Bound)
|
||||
.with_qualifiers(type_and_qualifiers.qualifiers())
|
||||
}
|
||||
Err(LookupError::Unbound(qualifiers)) => Symbol::Unbound.with_qualifiers(qualifiers),
|
||||
Err(LookupError::PossiblyUnbound(type_and_qualifiers)) => {
|
||||
Symbol::Type(type_and_qualifiers.inner_type(), Boundness::PossiblyUnbound)
|
||||
.with_qualifiers(type_and_qualifiers.qualifiers())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,8 +124,8 @@ impl<'db> From<LookupResult<'db>> for Symbol<'db> {
|
||||
/// Possible ways in which a symbol lookup can (possibly or definitely) fail.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub(crate) enum LookupError<'db> {
|
||||
Unbound,
|
||||
PossiblyUnbound(Type<'db>),
|
||||
Unbound(TypeQualifiers),
|
||||
PossiblyUnbound(TypeAndQualifiers<'db>),
|
||||
}
|
||||
|
||||
impl<'db> LookupError<'db> {
|
||||
@@ -155,18 +133,22 @@ impl<'db> LookupError<'db> {
|
||||
pub(crate) fn or_fall_back_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
fallback: Symbol<'db>,
|
||||
fallback: SymbolAndQualifiers<'db>,
|
||||
) -> LookupResult<'db> {
|
||||
let fallback = fallback.into_lookup_result();
|
||||
match (&self, &fallback) {
|
||||
(LookupError::Unbound, _) => fallback,
|
||||
(LookupError::PossiblyUnbound { .. }, Err(LookupError::Unbound)) => Err(self),
|
||||
(LookupError::PossiblyUnbound(ty), Ok(ty2)) => {
|
||||
Ok(UnionType::from_elements(db, [ty, ty2]))
|
||||
(LookupError::Unbound(_), _) => fallback,
|
||||
(LookupError::PossiblyUnbound { .. }, Err(LookupError::Unbound(_))) => Err(self),
|
||||
(LookupError::PossiblyUnbound(ty), Ok(ty2)) => Ok(TypeAndQualifiers::new(
|
||||
UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]),
|
||||
ty.qualifiers().union(ty2.qualifiers()),
|
||||
)),
|
||||
(LookupError::PossiblyUnbound(ty), Err(LookupError::PossiblyUnbound(ty2))) => {
|
||||
Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new(
|
||||
UnionType::from_elements(db, [ty.inner_type(), ty2.inner_type()]),
|
||||
ty.qualifiers().union(ty2.qualifiers()),
|
||||
)))
|
||||
}
|
||||
(LookupError::PossiblyUnbound(ty), Err(LookupError::PossiblyUnbound(ty2))) => Err(
|
||||
LookupError::PossiblyUnbound(UnionType::from_elements(db, [ty, ty2])),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,17 +158,25 @@ impl<'db> LookupError<'db> {
|
||||
///
|
||||
/// Note that this type is exactly isomorphic to [`Symbol`].
|
||||
/// In the future, we could possibly consider removing `Symbol` and using this type everywhere instead.
|
||||
pub(crate) type LookupResult<'db> = Result<Type<'db>, LookupError<'db>>;
|
||||
pub(crate) type LookupResult<'db> = Result<TypeAndQualifiers<'db>, LookupError<'db>>;
|
||||
|
||||
/// Infer the public type of a symbol (its type as seen from outside its scope) in the given
|
||||
/// `scope`.
|
||||
pub(crate) fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
|
||||
pub(crate) fn symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
name: &str,
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
symbol_impl(db, scope, name, RequiresExplicitReExport::No)
|
||||
}
|
||||
|
||||
/// Infer the public type of a class symbol (its type as seen from outside its scope) in the given
|
||||
/// `scope`.
|
||||
pub(crate) fn class_symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
|
||||
pub(crate) fn class_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
name: &str,
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
symbol_table(db, scope)
|
||||
.symbol_id_by_name(name)
|
||||
.map(|symbol| {
|
||||
@@ -195,10 +185,14 @@ pub(crate) fn class_symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str
|
||||
if symbol_and_quals.is_class_var() {
|
||||
// For declared class vars we do not need to check if they have bindings,
|
||||
// we just trust the declaration.
|
||||
return symbol_and_quals.0;
|
||||
return symbol_and_quals;
|
||||
}
|
||||
|
||||
if let SymbolAndQualifiers(Symbol::Type(ty, _), _) = symbol_and_quals {
|
||||
if let SymbolAndQualifiers {
|
||||
symbol: Symbol::Type(ty, _),
|
||||
qualifiers,
|
||||
} = symbol_and_quals
|
||||
{
|
||||
// Otherwise, we need to check if the symbol has bindings
|
||||
let use_def = use_def_map(db, scope);
|
||||
let bindings = use_def.public_bindings(symbol);
|
||||
@@ -208,14 +202,16 @@ pub(crate) fn class_symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str
|
||||
// TODO: we should not need to calculate inferred type second time. This is a temporary
|
||||
// solution until the notion of Boundness and Declaredness is split. See #16036, #16264
|
||||
match inferred {
|
||||
Symbol::Unbound => Symbol::Unbound,
|
||||
Symbol::Type(_, boundness) => Symbol::Type(ty, boundness),
|
||||
Symbol::Unbound => Symbol::Unbound.with_qualifiers(qualifiers),
|
||||
Symbol::Type(_, boundness) => {
|
||||
Symbol::Type(ty, boundness).with_qualifiers(qualifiers)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
Symbol::Unbound.into()
|
||||
}
|
||||
})
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Infers the public type of an explicit module-global symbol as seen from within the same file.
|
||||
@@ -226,7 +222,11 @@ pub(crate) fn class_symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str
|
||||
/// those additional symbols.
|
||||
///
|
||||
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
|
||||
pub(crate) fn explicit_global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
pub(crate) fn explicit_global_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
name: &str,
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, file),
|
||||
@@ -243,13 +243,21 @@ pub(crate) fn explicit_global_symbol<'db>(db: &'db dyn Db, file: File, name: &st
|
||||
///
|
||||
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
pub(crate) fn global_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
name: &str,
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
explicit_global_symbol(db, file, name)
|
||||
.or_fall_back_to(db, || module_type_implicit_global_symbol(db, name))
|
||||
}
|
||||
|
||||
/// Infers the public type of an imported symbol.
|
||||
pub(crate) fn imported_symbol<'db>(db: &'db dyn Db, module: &Module, name: &str) -> Symbol<'db> {
|
||||
pub(crate) fn imported_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
module: &Module,
|
||||
name: &str,
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
// If it's not found in the global scope, check if it's present as an instance on
|
||||
// `types.ModuleType` or `builtins.object`.
|
||||
//
|
||||
@@ -267,7 +275,7 @@ pub(crate) fn imported_symbol<'db>(db: &'db dyn Db, module: &Module, name: &str)
|
||||
// module we're dealing with.
|
||||
external_symbol_impl(db, module.file(), name).or_fall_back_to(db, || {
|
||||
if name == "__getattr__" {
|
||||
Symbol::Unbound
|
||||
Symbol::Unbound.into()
|
||||
} else {
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
}
|
||||
@@ -281,7 +289,7 @@ pub(crate) fn imported_symbol<'db>(db: &'db dyn Db, module: &Module, name: &str)
|
||||
/// Note that this function is only intended for use in the context of the builtins *namespace*
|
||||
/// and should not be used when a symbol is being explicitly imported from the `builtins` module
|
||||
/// (e.g. `from builtins import int`).
|
||||
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> SymbolAndQualifiers<'db> {
|
||||
resolve_module(db, &KnownModule::Builtins.name())
|
||||
.map(|module| {
|
||||
external_symbol_impl(db, module.file(), symbol).or_fall_back_to(db, || {
|
||||
@@ -291,7 +299,7 @@ pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db>
|
||||
module_type_implicit_global_symbol(db, symbol)
|
||||
})
|
||||
})
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in a given known module.
|
||||
@@ -301,10 +309,10 @@ pub(crate) fn known_module_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
known_module: KnownModule,
|
||||
symbol: &str,
|
||||
) -> Symbol<'db> {
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
resolve_module(db, &known_module.name())
|
||||
.map(|module| imported_symbol(db, &module, symbol))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing` module namespace.
|
||||
@@ -312,7 +320,7 @@ pub(crate) fn known_module_symbol<'db>(
|
||||
/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason.
|
||||
#[inline]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> SymbolAndQualifiers<'db> {
|
||||
known_module_symbol(db, KnownModule::Typing, symbol)
|
||||
}
|
||||
|
||||
@@ -320,7 +328,10 @@ pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the `typing_extensions` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn typing_extensions_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
pub(crate) fn typing_extensions_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
symbol: &str,
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
known_module_symbol(db, KnownModule::TypingExtensions, symbol)
|
||||
}
|
||||
|
||||
@@ -383,26 +394,97 @@ pub(crate) type SymbolFromDeclarationsResult<'db> =
|
||||
///
|
||||
/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) struct SymbolAndQualifiers<'db>(pub(crate) Symbol<'db>, pub(crate) TypeQualifiers);
|
||||
pub(crate) struct SymbolAndQualifiers<'db> {
|
||||
pub(crate) symbol: Symbol<'db>,
|
||||
pub(crate) qualifiers: TypeQualifiers,
|
||||
}
|
||||
|
||||
impl SymbolAndQualifiers<'_> {
|
||||
impl Default for SymbolAndQualifiers<'_> {
|
||||
fn default() -> Self {
|
||||
SymbolAndQualifiers {
|
||||
symbol: Symbol::Unbound,
|
||||
qualifiers: TypeQualifiers::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> SymbolAndQualifiers<'db> {
|
||||
/// Constructor that creates a [`SymbolAndQualifiers`] instance with a [`TodoType`] type
|
||||
/// and no qualifiers.
|
||||
///
|
||||
/// [`TodoType`]: crate::types::TodoType
|
||||
pub(crate) fn todo(message: &'static str) -> Self {
|
||||
Self(Symbol::todo(message), TypeQualifiers::empty())
|
||||
Self {
|
||||
symbol: Symbol::todo(message),
|
||||
qualifiers: TypeQualifiers::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the symbol has a `ClassVar` type qualifier.
|
||||
pub(crate) fn is_class_var(&self) -> bool {
|
||||
self.1.contains(TypeQualifiers::CLASS_VAR)
|
||||
self.qualifiers.contains(TypeQualifiers::CLASS_VAR)
|
||||
}
|
||||
|
||||
/// Transform symbol and qualifiers into a [`LookupResult`],
|
||||
/// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol
|
||||
/// and the `Err` variant represents a symbol that is either definitely or possibly unbound.
|
||||
pub(crate) fn into_lookup_result(self) -> LookupResult<'db> {
|
||||
match self {
|
||||
SymbolAndQualifiers {
|
||||
symbol: Symbol::Type(ty, Boundness::Bound),
|
||||
qualifiers,
|
||||
} => Ok(TypeAndQualifiers::new(ty, qualifiers)),
|
||||
SymbolAndQualifiers {
|
||||
symbol: Symbol::Type(ty, Boundness::PossiblyUnbound),
|
||||
qualifiers,
|
||||
} => Err(LookupError::PossiblyUnbound(TypeAndQualifiers::new(
|
||||
ty, qualifiers,
|
||||
))),
|
||||
SymbolAndQualifiers {
|
||||
symbol: Symbol::Unbound,
|
||||
qualifiers,
|
||||
} => Err(LookupError::Unbound(qualifiers)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Safely unwrap the symbol and the qualifiers into a [`TypeQualifiers`].
|
||||
///
|
||||
/// If the symbol is definitely unbound or possibly unbound, it will be transformed into a
|
||||
/// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning
|
||||
/// the result of `diagnostic_fn` (which will be a [`TypeQualifiers`]). This allows the caller
|
||||
/// to ensure that a diagnostic is emitted if the symbol is possibly or definitely unbound.
|
||||
pub(crate) fn unwrap_with_diagnostic(
|
||||
self,
|
||||
diagnostic_fn: impl FnOnce(LookupError<'db>) -> TypeAndQualifiers<'db>,
|
||||
) -> TypeAndQualifiers<'db> {
|
||||
self.into_lookup_result().unwrap_or_else(diagnostic_fn)
|
||||
}
|
||||
|
||||
/// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound.
|
||||
///
|
||||
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
|
||||
/// 2. Else, evaluate `fallback_fn()`:
|
||||
/// a. If `self` is definitely unbound, return the result of `fallback_fn()`.
|
||||
/// b. Else, if `fallback` is definitely unbound, return `self`.
|
||||
/// c. Else, if `self` is possibly unbound and `fallback` is definitely bound,
|
||||
/// return `Symbol(<union of self-type and fallback-type>, Boundness::Bound)`
|
||||
/// d. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
|
||||
/// return `Symbol(<union of self-type and fallback-type>, Boundness::PossiblyUnbound)`
|
||||
#[must_use]
|
||||
pub(crate) fn or_fall_back_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
fallback_fn: impl FnOnce() -> SymbolAndQualifiers<'db>,
|
||||
) -> Self {
|
||||
self.into_lookup_result()
|
||||
.or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn()))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Symbol<'db>> for SymbolAndQualifiers<'db> {
|
||||
fn from(symbol: Symbol<'db>) -> Self {
|
||||
SymbolAndQualifiers(symbol, TypeQualifiers::empty())
|
||||
symbol.with_qualifiers(TypeQualifiers::empty())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,11 +505,17 @@ fn symbol_by_id<'db>(
|
||||
|
||||
match declared {
|
||||
// Symbol is declared, trust the declared type
|
||||
Ok(symbol_and_quals @ SymbolAndQualifiers(Symbol::Type(_, Boundness::Bound), _)) => {
|
||||
symbol_and_quals
|
||||
}
|
||||
Ok(
|
||||
symbol_and_quals @ SymbolAndQualifiers {
|
||||
symbol: Symbol::Type(_, Boundness::Bound),
|
||||
qualifiers: _,
|
||||
},
|
||||
) => symbol_and_quals,
|
||||
// Symbol is possibly declared
|
||||
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, Boundness::PossiblyUnbound), quals)) => {
|
||||
Ok(SymbolAndQualifiers {
|
||||
symbol: Symbol::Type(declared_ty, Boundness::PossiblyUnbound),
|
||||
qualifiers,
|
||||
}) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport);
|
||||
|
||||
@@ -446,10 +534,13 @@ fn symbol_by_id<'db>(
|
||||
),
|
||||
};
|
||||
|
||||
SymbolAndQualifiers(symbol, quals)
|
||||
SymbolAndQualifiers { symbol, qualifiers }
|
||||
}
|
||||
// Symbol is undeclared, return the union of `Unknown` with the inferred type
|
||||
Ok(SymbolAndQualifiers(Symbol::Unbound, _)) => {
|
||||
Ok(SymbolAndQualifiers {
|
||||
symbol: Symbol::Unbound,
|
||||
qualifiers: _,
|
||||
}) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings_impl(db, bindings, requires_explicit_reexport);
|
||||
|
||||
@@ -471,13 +562,10 @@ fn symbol_by_id<'db>(
|
||||
.into()
|
||||
}
|
||||
// Symbol has conflicting declared types
|
||||
Err((declared_ty, _)) => {
|
||||
Err((declared, _)) => {
|
||||
// Intentionally ignore conflicting declared types; that's not our problem,
|
||||
// it's the problem of the module we are importing from.
|
||||
SymbolAndQualifiers(
|
||||
Symbol::bound(declared_ty.inner_type()),
|
||||
declared_ty.qualifiers(),
|
||||
)
|
||||
Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,7 +591,7 @@ fn symbol_impl<'db>(
|
||||
scope: ScopeId<'db>,
|
||||
name: &str,
|
||||
requires_explicit_reexport: RequiresExplicitReExport,
|
||||
) -> Symbol<'db> {
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
let _span = tracing::trace_span!("symbol", ?name).entered();
|
||||
|
||||
if name == "platform"
|
||||
@@ -512,7 +600,7 @@ fn symbol_impl<'db>(
|
||||
{
|
||||
match Program::get(db).python_platform(db) {
|
||||
crate::PythonPlatform::Identifier(platform) => {
|
||||
return Symbol::bound(Type::string_literal(db, platform.as_str()));
|
||||
return Symbol::bound(Type::string_literal(db, platform.as_str())).into();
|
||||
}
|
||||
crate::PythonPlatform::All => {
|
||||
// Fall through to the looked up type
|
||||
@@ -522,8 +610,8 @@ fn symbol_impl<'db>(
|
||||
|
||||
symbol_table(db, scope)
|
||||
.symbol_id_by_name(name)
|
||||
.map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport).0)
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
.map(|symbol| symbol_by_id(db, scope, symbol, requires_explicit_reexport))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Implementation of [`symbol_from_bindings`].
|
||||
@@ -669,7 +757,7 @@ fn symbol_from_declarations_impl<'db>(
|
||||
|
||||
if let Some(first) = types.next() {
|
||||
let mut conflicting: Vec<Type<'db>> = vec![];
|
||||
let declared_ty = if let Some(second) = types.next() {
|
||||
let declared = if let Some(second) = types.next() {
|
||||
let ty_first = first.inner_type();
|
||||
let mut qualifiers = first.qualifiers();
|
||||
|
||||
@@ -695,13 +783,11 @@ fn symbol_from_declarations_impl<'db>(
|
||||
Truthiness::Ambiguous => Boundness::PossiblyUnbound,
|
||||
};
|
||||
|
||||
Ok(SymbolAndQualifiers(
|
||||
Symbol::Type(declared_ty.inner_type(), boundness),
|
||||
declared_ty.qualifiers(),
|
||||
))
|
||||
Ok(Symbol::Type(declared.inner_type(), boundness)
|
||||
.with_qualifiers(declared.qualifiers()))
|
||||
} else {
|
||||
Err((
|
||||
declared_ty,
|
||||
declared,
|
||||
std::iter::once(first.inner_type())
|
||||
.chain(conflicting)
|
||||
.collect(),
|
||||
@@ -717,6 +803,7 @@ mod implicit_globals {
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::semantic_index::{self, symbol_table};
|
||||
use crate::symbol::SymbolAndQualifiers;
|
||||
use crate::types::KnownClass;
|
||||
|
||||
use super::Symbol;
|
||||
@@ -738,7 +825,7 @@ mod implicit_globals {
|
||||
pub(crate) fn module_type_implicit_global_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
) -> Symbol<'db> {
|
||||
) -> SymbolAndQualifiers<'db> {
|
||||
// In general we wouldn't check to see whether a symbol exists on a class before doing the
|
||||
// `.member()` call on the instance type -- we'd just do the `.member`() call on the instance
|
||||
// type, since it has the same end result. The reason to only call `.member()` on `ModuleType`
|
||||
@@ -750,7 +837,7 @@ mod implicit_globals {
|
||||
{
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
Symbol::Unbound.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,7 +907,7 @@ mod implicit_globals {
|
||||
///
|
||||
/// This will take into account whether the definition of the symbol is being explicitly
|
||||
/// re-exported from a stub file or not.
|
||||
fn external_symbol_impl<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
fn external_symbol_impl<'db>(db: &'db dyn Db, file: File, name: &str) -> SymbolAndQualifiers<'db> {
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, file),
|
||||
@@ -881,48 +968,45 @@ mod tests {
|
||||
let ty1 = Type::IntLiteral(1);
|
||||
let ty2 = Type::IntLiteral(2);
|
||||
|
||||
let unbound = || Symbol::Unbound.with_qualifiers(TypeQualifiers::empty());
|
||||
|
||||
let possibly_unbound_ty1 =
|
||||
|| Symbol::Type(ty1, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty());
|
||||
let possibly_unbound_ty2 =
|
||||
|| Symbol::Type(ty2, PossiblyUnbound).with_qualifiers(TypeQualifiers::empty());
|
||||
|
||||
let bound_ty1 = || Symbol::Type(ty1, Bound).with_qualifiers(TypeQualifiers::empty());
|
||||
let bound_ty2 = || Symbol::Type(ty2, Bound).with_qualifiers(TypeQualifiers::empty());
|
||||
|
||||
// Start from an unbound symbol
|
||||
assert_eq!(unbound().or_fall_back_to(&db, unbound), unbound());
|
||||
assert_eq!(
|
||||
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Unbound),
|
||||
Symbol::Unbound
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Type(ty1, PossiblyUnbound)),
|
||||
Symbol::Type(ty1, PossiblyUnbound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Type(ty1, Bound)),
|
||||
Symbol::Type(ty1, Bound)
|
||||
unbound().or_fall_back_to(&db, possibly_unbound_ty1),
|
||||
possibly_unbound_ty1()
|
||||
);
|
||||
assert_eq!(unbound().or_fall_back_to(&db, bound_ty1), bound_ty1());
|
||||
|
||||
// Start from a possibly unbound symbol
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, || Symbol::Unbound),
|
||||
Symbol::Type(ty1, PossiblyUnbound)
|
||||
possibly_unbound_ty1().or_fall_back_to(&db, unbound),
|
||||
possibly_unbound_ty1()
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, PossiblyUnbound)
|
||||
.or_fall_back_to(&db, || Symbol::Type(ty2, PossiblyUnbound)),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound)
|
||||
possibly_unbound_ty1().or_fall_back_to(&db, possibly_unbound_ty2),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound).into()
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, || Symbol::Type(ty2, Bound)),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound)
|
||||
possibly_unbound_ty1().or_fall_back_to(&db, bound_ty2),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound).into()
|
||||
);
|
||||
|
||||
// Start from a definitely bound symbol
|
||||
assert_eq!(bound_ty1().or_fall_back_to(&db, unbound), bound_ty1());
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Unbound),
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Type(ty2, PossiblyUnbound)),
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Type(ty2, Bound)),
|
||||
Symbol::Type(ty1, Bound)
|
||||
bound_ty1().or_fall_back_to(&db, possibly_unbound_ty2),
|
||||
bound_ty1()
|
||||
);
|
||||
assert_eq!(bound_ty1().or_fall_back_to(&db, bound_ty2), bound_ty1());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -937,24 +1021,27 @@ mod tests {
|
||||
#[test]
|
||||
fn implicit_builtin_globals() {
|
||||
let db = setup_db();
|
||||
assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__"));
|
||||
assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__").symbol);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_typing_globals() {
|
||||
let db = setup_db();
|
||||
assert_bound_string_symbol(&db, typing_symbol(&db, "__name__"));
|
||||
assert_bound_string_symbol(&db, typing_symbol(&db, "__name__").symbol);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_typing_extensions_globals() {
|
||||
let db = setup_db();
|
||||
assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__"));
|
||||
assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__").symbol);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_sys_globals() {
|
||||
let db = setup_db();
|
||||
assert_bound_string_symbol(&db, known_module_symbol(&db, KnownModule::Sys, "__name__"));
|
||||
assert_bound_string_symbol(
|
||||
&db,
|
||||
known_module_symbol(&db, KnownModule::Sys, "__name__").symbol,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@ use crate::{
|
||||
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
|
||||
},
|
||||
types::{
|
||||
definition_expression_type, CallArguments, CallError, MetaclassCandidate, TupleType,
|
||||
UnionBuilder, UnionCallError,
|
||||
definition_expression_type, CallArguments, CallError, DynamicType, MetaclassCandidate,
|
||||
TupleType, UnionBuilder, UnionCallError, UnionType,
|
||||
},
|
||||
Db, KnownModule, Program,
|
||||
};
|
||||
@@ -318,10 +318,10 @@ impl<'db> Class<'db> {
|
||||
/// The member resolves to a member on the class itself or any of its proper superclasses.
|
||||
///
|
||||
/// TODO: Should this be made private...?
|
||||
pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
||||
if name == "__mro__" {
|
||||
let tuple_elements = self.iter_mro(db).map(Type::from);
|
||||
return Symbol::bound(TupleType::from_elements(db, tuple_elements));
|
||||
return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into();
|
||||
}
|
||||
|
||||
// If we encounter a dynamic type in this class's MRO, we'll save that dynamic type
|
||||
@@ -332,10 +332,16 @@ impl<'db> Class<'db> {
|
||||
// from the non-dynamic members of the class's MRO.
|
||||
let mut dynamic_type_to_intersect_with: Option<Type<'db>> = None;
|
||||
|
||||
let mut lookup_result: LookupResult<'db> = Err(LookupError::Unbound);
|
||||
let mut lookup_result: LookupResult<'db> =
|
||||
Err(LookupError::Unbound(TypeQualifiers::empty()));
|
||||
|
||||
for superclass in self.iter_mro(db) {
|
||||
match superclass {
|
||||
ClassBase::Dynamic(DynamicType::TodoProtocol) => {
|
||||
// TODO: We currently skip `Protocol` when looking up class members, in order to
|
||||
// avoid creating many dynamic types in our test suite that would otherwise
|
||||
// result from looking up attributes on builtin types like `str`, `list`, `tuple`
|
||||
}
|
||||
ClassBase::Dynamic(_) => {
|
||||
// Note: calling `Type::from(superclass).member()` would be incorrect here.
|
||||
// What we'd really want is a `Type::Any.own_class_member()` method,
|
||||
@@ -353,15 +359,33 @@ impl<'db> Class<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
match (Symbol::from(lookup_result), dynamic_type_to_intersect_with) {
|
||||
(symbol, None) => symbol,
|
||||
(Symbol::Type(ty, _), Some(dynamic_type)) => Symbol::bound(
|
||||
match (
|
||||
SymbolAndQualifiers::from(lookup_result),
|
||||
dynamic_type_to_intersect_with,
|
||||
) {
|
||||
(symbol_and_qualifiers, None) => symbol_and_qualifiers,
|
||||
|
||||
(
|
||||
SymbolAndQualifiers {
|
||||
symbol: Symbol::Type(ty, _),
|
||||
qualifiers,
|
||||
},
|
||||
Some(dynamic_type),
|
||||
) => Symbol::bound(
|
||||
IntersectionBuilder::new(db)
|
||||
.add_positive(ty)
|
||||
.add_positive(dynamic_type)
|
||||
.build(),
|
||||
),
|
||||
(Symbol::Unbound, Some(dynamic_type)) => Symbol::bound(dynamic_type),
|
||||
)
|
||||
.with_qualifiers(qualifiers),
|
||||
|
||||
(
|
||||
SymbolAndQualifiers {
|
||||
symbol: Symbol::Unbound,
|
||||
qualifiers,
|
||||
},
|
||||
Some(dynamic_type),
|
||||
) => Symbol::bound(dynamic_type).with_qualifiers(qualifiers),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +395,7 @@ impl<'db> Class<'db> {
|
||||
/// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope
|
||||
/// directly. Use [`Class::class_member`] if you require a method that will
|
||||
/// traverse through the MRO until it finds the member.
|
||||
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
||||
let body_scope = self.body_scope(db);
|
||||
class_symbol(db, body_scope, name)
|
||||
}
|
||||
@@ -388,17 +412,24 @@ impl<'db> Class<'db> {
|
||||
|
||||
for superclass in self.iter_mro(db) {
|
||||
match superclass {
|
||||
ClassBase::Dynamic(DynamicType::TodoProtocol) => {
|
||||
// TODO: We currently skip `Protocol` when looking up instance members, in order to
|
||||
// avoid creating many dynamic types in our test suite that would otherwise
|
||||
// result from looking up attributes on builtin types like `str`, `list`, `tuple`
|
||||
}
|
||||
ClassBase::Dynamic(_) => {
|
||||
return SymbolAndQualifiers::todo(
|
||||
"instance attribute on class with dynamic base",
|
||||
);
|
||||
}
|
||||
ClassBase::Class(class) => {
|
||||
if let member @ SymbolAndQualifiers(Symbol::Type(ty, boundness), qualifiers) =
|
||||
class.own_instance_member(db, name)
|
||||
if let member @ SymbolAndQualifiers {
|
||||
symbol: Symbol::Type(ty, boundness),
|
||||
qualifiers,
|
||||
} = class.own_instance_member(db, name)
|
||||
{
|
||||
// TODO: We could raise a diagnostic here if there are conflicting type qualifiers
|
||||
union_qualifiers = union_qualifiers.union(qualifiers);
|
||||
union_qualifiers |= qualifiers;
|
||||
|
||||
if boundness == Boundness::Bound {
|
||||
if union.is_empty() {
|
||||
@@ -406,10 +437,8 @@ impl<'db> Class<'db> {
|
||||
return member;
|
||||
}
|
||||
|
||||
return SymbolAndQualifiers(
|
||||
Symbol::bound(union.add(ty).build()),
|
||||
union_qualifiers,
|
||||
);
|
||||
return Symbol::bound(union.add(ty).build())
|
||||
.with_qualifiers(union_qualifiers);
|
||||
}
|
||||
|
||||
// If we see a possibly-unbound symbol, we need to keep looking
|
||||
@@ -421,15 +450,13 @@ impl<'db> Class<'db> {
|
||||
}
|
||||
|
||||
if union.is_empty() {
|
||||
SymbolAndQualifiers(Symbol::Unbound, TypeQualifiers::empty())
|
||||
Symbol::Unbound.with_qualifiers(TypeQualifiers::empty())
|
||||
} else {
|
||||
// If we have reached this point, we know that we have only seen possibly-unbound symbols.
|
||||
// This means that the final result is still possibly-unbound.
|
||||
|
||||
SymbolAndQualifiers(
|
||||
Symbol::Type(union.build(), Boundness::PossiblyUnbound),
|
||||
union_qualifiers,
|
||||
)
|
||||
Symbol::Type(union.build(), Boundness::PossiblyUnbound)
|
||||
.with_qualifiers(union_qualifiers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,31 +466,18 @@ impl<'db> Class<'db> {
|
||||
db: &'db dyn Db,
|
||||
class_body_scope: ScopeId<'db>,
|
||||
name: &str,
|
||||
inferred_from_class_body: &Symbol<'db>,
|
||||
) -> Symbol<'db> {
|
||||
) -> Option<Type<'db>> {
|
||||
// If we do not see any declarations of an attribute, neither in the class body nor in
|
||||
// any method, we build a union of `Unknown` with the inferred types of all bindings of
|
||||
// that attribute. We include `Unknown` in that union to account for the fact that the
|
||||
// attribute might be externally modified.
|
||||
let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown());
|
||||
let mut union_boundness = Boundness::Bound;
|
||||
|
||||
if let Symbol::Type(ty, boundness) = inferred_from_class_body {
|
||||
union_of_inferred_types = union_of_inferred_types.add(*ty);
|
||||
union_boundness = *boundness;
|
||||
}
|
||||
|
||||
let attribute_assignments = attribute_assignments(db, class_body_scope);
|
||||
|
||||
let Some(attribute_assignments) = attribute_assignments
|
||||
let attribute_assignments = attribute_assignments
|
||||
.as_deref()
|
||||
.and_then(|assignments| assignments.get(name))
|
||||
else {
|
||||
if inferred_from_class_body.is_unbound() {
|
||||
return Symbol::Unbound;
|
||||
}
|
||||
return Symbol::Type(union_of_inferred_types.build(), union_boundness);
|
||||
};
|
||||
.and_then(|assignments| assignments.get(name))?;
|
||||
|
||||
for attribute_assignment in attribute_assignments {
|
||||
match attribute_assignment {
|
||||
@@ -477,7 +491,7 @@ impl<'db> Class<'db> {
|
||||
let annotation_ty = infer_expression_type(db, *annotation);
|
||||
|
||||
// TODO: check if there are conflicting declarations
|
||||
return Symbol::bound(annotation_ty);
|
||||
return Some(annotation_ty);
|
||||
}
|
||||
AttributeAssignment::Unannotated { value } => {
|
||||
// We found an un-annotated attribute assignment of the form:
|
||||
@@ -499,6 +513,16 @@ impl<'db> Class<'db> {
|
||||
|
||||
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
|
||||
}
|
||||
AttributeAssignment::ContextManager { context_manager } => {
|
||||
// We found an attribute assignment like:
|
||||
//
|
||||
// with <context_manager> as self.name:
|
||||
|
||||
let context_ty = infer_expression_type(db, *context_manager);
|
||||
let inferred_ty = context_ty.enter(db);
|
||||
|
||||
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
|
||||
}
|
||||
AttributeAssignment::Unpack {
|
||||
attribute_expression_id,
|
||||
unpack,
|
||||
@@ -516,7 +540,7 @@ impl<'db> Class<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
Symbol::Type(union_of_inferred_types.build(), union_boundness)
|
||||
Some(union_of_inferred_types.build())
|
||||
}
|
||||
|
||||
/// A helper function for `instance_member` that looks up the `name` attribute only on
|
||||
@@ -533,55 +557,93 @@ impl<'db> Class<'db> {
|
||||
let use_def = use_def_map(db, body_scope);
|
||||
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
|
||||
match symbol_from_declarations(db, declarations) {
|
||||
Ok(SymbolAndQualifiers(declared @ Symbol::Type(declared_ty, _), qualifiers)) => {
|
||||
let declared_and_qualifiers = symbol_from_declarations(db, declarations);
|
||||
match declared_and_qualifiers {
|
||||
Ok(SymbolAndQualifiers {
|
||||
symbol: declared @ Symbol::Type(declared_ty, declaredness),
|
||||
qualifiers,
|
||||
}) => {
|
||||
// The attribute is declared in the class body.
|
||||
|
||||
if let Some(function) = declared_ty.into_function_literal() {
|
||||
// TODO: Eventually, we are going to process all decorators correctly. This is
|
||||
// just a temporary heuristic to provide a broad categorization
|
||||
|
||||
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||
&& function.decorators(db).len() == 1
|
||||
{
|
||||
SymbolAndQualifiers(declared, qualifiers)
|
||||
} else if function.has_known_class_decorator(db, KnownClass::Property) {
|
||||
SymbolAndQualifiers::todo("@property")
|
||||
} else if function.has_known_function_decorator(db, KnownFunction::Overload)
|
||||
{
|
||||
SymbolAndQualifiers::todo("overloaded method")
|
||||
} else if !function.decorators(db).is_empty() {
|
||||
SymbolAndQualifiers::todo("decorated method")
|
||||
} else {
|
||||
SymbolAndQualifiers(declared, qualifiers)
|
||||
}
|
||||
} else {
|
||||
SymbolAndQualifiers(declared, qualifiers)
|
||||
}
|
||||
}
|
||||
Ok(SymbolAndQualifiers(Symbol::Unbound, _)) => {
|
||||
// The attribute is not *declared* in the class body. It could still be declared
|
||||
// in a method, and it could also be *bound* in the class body (and/or in a method).
|
||||
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
let has_binding = !inferred.is_unbound();
|
||||
|
||||
Self::implicit_instance_attribute(db, body_scope, name, &inferred).into()
|
||||
if has_binding {
|
||||
// The attribute is declared and bound in the class body.
|
||||
|
||||
if let Some(implicit_ty) =
|
||||
Self::implicit_instance_attribute(db, body_scope, name)
|
||||
{
|
||||
if declaredness == Boundness::Bound {
|
||||
// If a symbol is definitely declared, and we see
|
||||
// attribute assignments in methods of the class,
|
||||
// we trust the declared type.
|
||||
declared.with_qualifiers(qualifiers)
|
||||
} else {
|
||||
Symbol::Type(
|
||||
UnionType::from_elements(db, [declared_ty, implicit_ty]),
|
||||
declaredness,
|
||||
)
|
||||
.with_qualifiers(qualifiers)
|
||||
}
|
||||
} else {
|
||||
// The symbol is declared and bound in the class body,
|
||||
// but we did not find any attribute assignments in
|
||||
// methods of the class. This means that the attribute
|
||||
// has a class-level default value, but it would not be
|
||||
// found in a `__dict__` lookup.
|
||||
|
||||
Symbol::Unbound.into()
|
||||
}
|
||||
} else {
|
||||
// The attribute is declared but not bound in the class body.
|
||||
// We take this as a sign that this is intended to be a pure
|
||||
// instance attribute, and we trust the declared type, unless
|
||||
// it is possibly-undeclared. In the latter case, we also
|
||||
// union with the inferred type from attribute assignments.
|
||||
|
||||
if declaredness == Boundness::Bound {
|
||||
declared.with_qualifiers(qualifiers)
|
||||
} else {
|
||||
if let Some(implicit_ty) =
|
||||
Self::implicit_instance_attribute(db, body_scope, name)
|
||||
{
|
||||
Symbol::Type(
|
||||
UnionType::from_elements(db, [declared_ty, implicit_ty]),
|
||||
declaredness,
|
||||
)
|
||||
.with_qualifiers(qualifiers)
|
||||
} else {
|
||||
declared.with_qualifiers(qualifiers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err((declared_ty, _conflicting_declarations)) => {
|
||||
|
||||
Ok(SymbolAndQualifiers {
|
||||
symbol: Symbol::Unbound,
|
||||
qualifiers: _,
|
||||
}) => {
|
||||
// The attribute is not *declared* in the class body. It could still be declared/bound
|
||||
// in a method.
|
||||
|
||||
Self::implicit_instance_attribute(db, body_scope, name)
|
||||
.map_or(Symbol::Unbound, Symbol::bound)
|
||||
.into()
|
||||
}
|
||||
Err((declared, _conflicting_declarations)) => {
|
||||
// There are conflicting declarations for this attribute in the class body.
|
||||
SymbolAndQualifiers(
|
||||
Symbol::bound(declared_ty.inner_type()),
|
||||
declared_ty.qualifiers(),
|
||||
)
|
||||
Symbol::bound(declared.inner_type()).with_qualifiers(declared.qualifiers())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This attribute is neither declared nor bound in the class body.
|
||||
// It could still be implicitly defined in a method.
|
||||
|
||||
Self::implicit_instance_attribute(db, body_scope, name, &Symbol::Unbound).into()
|
||||
Self::implicit_instance_attribute(db, body_scope, name)
|
||||
.map_or(Symbol::Unbound, Symbol::bound)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,7 +725,7 @@ impl<'db> ClassLiteralType<'db> {
|
||||
self.class.body_scope(db)
|
||||
}
|
||||
|
||||
pub(super) fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
||||
self.class.class_member(db, name)
|
||||
}
|
||||
}
|
||||
@@ -881,6 +943,7 @@ impl<'db> KnownClass {
|
||||
|
||||
pub(crate) fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> {
|
||||
known_module_symbol(db, self.canonical_module(db), self.as_str(db))
|
||||
.symbol
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::unknown())
|
||||
}
|
||||
@@ -896,6 +959,7 @@ impl<'db> KnownClass {
|
||||
/// *and* `class` is a subclass of `other`.
|
||||
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: Class<'db>) -> bool {
|
||||
known_module_symbol(db, self.canonical_module(db), self.as_str(db))
|
||||
.symbol
|
||||
.ignore_possibly_unbound()
|
||||
.and_then(Type::into_class_literal)
|
||||
.is_some_and(|ClassLiteralType { class }| class.is_subclass_of(db, other))
|
||||
@@ -1203,6 +1267,8 @@ pub enum KnownInstanceType<'db> {
|
||||
Deque,
|
||||
/// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`)
|
||||
OrderedDict,
|
||||
/// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`)
|
||||
Protocol,
|
||||
/// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`)
|
||||
Type,
|
||||
/// A single instance of `typing.TypeVar`
|
||||
@@ -1274,6 +1340,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
| Self::Deque
|
||||
| Self::ChainMap
|
||||
| Self::OrderedDict
|
||||
| Self::Protocol
|
||||
| Self::ReadOnly
|
||||
| Self::TypeAliasType(_)
|
||||
| Self::Unknown
|
||||
@@ -1318,6 +1385,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::Deque => "typing.Deque",
|
||||
Self::ChainMap => "typing.ChainMap",
|
||||
Self::OrderedDict => "typing.OrderedDict",
|
||||
Self::Protocol => "typing.Protocol",
|
||||
Self::ReadOnly => "typing.ReadOnly",
|
||||
Self::TypeVar(typevar) => typevar.name(db),
|
||||
Self::TypeAliasType(_) => "typing.TypeAliasType",
|
||||
@@ -1364,6 +1432,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::Deque => KnownClass::StdlibAlias,
|
||||
Self::ChainMap => KnownClass::StdlibAlias,
|
||||
Self::OrderedDict => KnownClass::StdlibAlias,
|
||||
Self::Protocol => KnownClass::SpecialForm,
|
||||
Self::TypeVar(_) => KnownClass::TypeVar,
|
||||
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
|
||||
Self::TypeOf => KnownClass::SpecialForm,
|
||||
@@ -1406,6 +1475,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
"Counter" => Self::Counter,
|
||||
"ChainMap" => Self::ChainMap,
|
||||
"OrderedDict" => Self::OrderedDict,
|
||||
"Protocol" => Self::Protocol,
|
||||
"Optional" => Self::Optional,
|
||||
"Union" => Self::Union,
|
||||
"NoReturn" => Self::NoReturn,
|
||||
@@ -1457,6 +1527,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
| Self::Counter
|
||||
| Self::ChainMap
|
||||
| Self::OrderedDict
|
||||
| Self::Protocol
|
||||
| Self::Optional
|
||||
| Self::Union
|
||||
| Self::NoReturn
|
||||
@@ -1489,15 +1560,6 @@ impl<'db> KnownInstanceType<'db> {
|
||||
| Self::TypeOf => module.is_knot_extensions(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
let ty = match (self, name) {
|
||||
(Self::TypeVar(typevar), "__name__") => Type::string_literal(db, typevar.name(db)),
|
||||
(Self::TypeAliasType(alias), "__name__") => Type::string_literal(db, alias.name(db)),
|
||||
_ => return self.instance_fallback(db).static_member(db, name),
|
||||
};
|
||||
Symbol::bound(ty)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
|
||||
@@ -144,6 +144,7 @@ impl<'db> ClassBase<'db> {
|
||||
KnownInstanceType::Callable => {
|
||||
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
|
||||
}
|
||||
KnownInstanceType::Protocol => Some(ClassBase::Dynamic(DynamicType::TodoProtocol)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1151,3 +1151,18 @@ pub(crate) fn report_invalid_arguments_to_annotated<'db>(
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn report_invalid_arguments_to_callable<'db>(
|
||||
db: &'db dyn Db,
|
||||
context: &InferContext<'db>,
|
||||
subscript: &ast::ExprSubscript,
|
||||
) {
|
||||
context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
subscript,
|
||||
format_args!(
|
||||
"Special form `{}` expected exactly two arguments (parameter types and return type)",
|
||||
KnownInstanceType::Callable.repr(db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use ruff_python_ast::str::{Quote, TripleQuotes};
|
||||
use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::signatures::{Parameter, Parameters, Signature};
|
||||
use crate::types::{
|
||||
CallableType, ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
|
||||
Type, UnionType,
|
||||
@@ -88,6 +89,9 @@ impl Display for DisplayRepresentation<'_> {
|
||||
},
|
||||
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
|
||||
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
|
||||
Type::Callable(CallableType::General(callable)) => {
|
||||
callable.signature(self.db).display(self.db).fmt(f)
|
||||
}
|
||||
Type::Callable(CallableType::BoundMethod(bound_method)) => {
|
||||
write!(
|
||||
f,
|
||||
@@ -156,6 +160,99 @@ impl Display for DisplayRepresentation<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Signature<'db> {
|
||||
fn display(&'db self, db: &'db dyn Db) -> DisplaySignature<'db> {
|
||||
DisplaySignature {
|
||||
parameters: self.parameters(),
|
||||
return_ty: self.return_ty.as_ref(),
|
||||
db,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplaySignature<'db> {
|
||||
parameters: &'db Parameters<'db>,
|
||||
return_ty: Option<&'db Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplaySignature<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_char('(')?;
|
||||
|
||||
if self.parameters.is_gradual() {
|
||||
// We represent gradual form as `...` in the signature, internally the parameters still
|
||||
// contain `(*args, **kwargs)` parameters.
|
||||
f.write_str("...")?;
|
||||
} else {
|
||||
let mut star_added = false;
|
||||
let mut needs_slash = false;
|
||||
let mut join = f.join(", ");
|
||||
|
||||
for parameter in self.parameters.as_slice() {
|
||||
if !star_added && parameter.is_keyword_only() {
|
||||
join.entry(&'*');
|
||||
star_added = true;
|
||||
}
|
||||
if parameter.is_positional_only() {
|
||||
needs_slash = true;
|
||||
} else if needs_slash {
|
||||
join.entry(&'/');
|
||||
needs_slash = false;
|
||||
}
|
||||
join.entry(¶meter.display(self.db));
|
||||
}
|
||||
if needs_slash {
|
||||
join.entry(&'/');
|
||||
}
|
||||
join.finish()?;
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
") -> {}",
|
||||
self.return_ty.unwrap_or(&Type::unknown()).display(self.db)
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Parameter<'db> {
|
||||
fn display(&'db self, db: &'db dyn Db) -> DisplayParameter<'db> {
|
||||
DisplayParameter { param: self, db }
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayParameter<'db> {
|
||||
param: &'db Parameter<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayParameter<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Some(name) = self.param.display_name() {
|
||||
write!(f, "{name}")?;
|
||||
if let Some(annotated_type) = self.param.annotated_type() {
|
||||
write!(f, ": {}", annotated_type.display(self.db))?;
|
||||
}
|
||||
// Default value can only be specified if `name` is given.
|
||||
if let Some(default_ty) = self.param.default_type() {
|
||||
if self.param.annotated_type().is_some() {
|
||||
write!(f, " = {}", default_ty.display(self.db))?;
|
||||
} else {
|
||||
write!(f, "={}", default_ty.display(self.db))?;
|
||||
}
|
||||
}
|
||||
} else if let Some(ty) = self.param.annotated_type() {
|
||||
// This case is specifically for the `Callable` signature where name and default value
|
||||
// cannot be provided.
|
||||
ty.display(self.db).fmt(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> UnionType<'db> {
|
||||
fn display(&'db self, db: &'db dyn Db) -> DisplayUnionType<'db> {
|
||||
DisplayUnionType { db, ty: self }
|
||||
@@ -375,8 +472,14 @@ impl Display for DisplayStringLiteralType<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::{SliceLiteralType, StringLiteralType, Type};
|
||||
use crate::types::{
|
||||
KnownClass, Parameter, ParameterKind, Parameters, Signature, SliceLiteralType,
|
||||
StringLiteralType, Type,
|
||||
};
|
||||
use crate::Db;
|
||||
|
||||
#[test]
|
||||
fn test_slice_literal_display() {
|
||||
@@ -443,4 +546,226 @@ mod tests {
|
||||
r#"Literal["\""]"#
|
||||
);
|
||||
}
|
||||
|
||||
fn display_signature<'db>(
|
||||
db: &dyn Db,
|
||||
parameters: impl IntoIterator<Item = Parameter<'db>>,
|
||||
return_ty: Option<Type<'db>>,
|
||||
) -> String {
|
||||
Signature::new(Parameters::new(parameters), return_ty)
|
||||
.display(db)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_display() {
|
||||
let db = setup_db();
|
||||
|
||||
// Empty parameters with no return type.
|
||||
assert_eq!(display_signature(&db, [], None), "() -> Unknown");
|
||||
|
||||
// Empty parameters with a return type.
|
||||
assert_eq!(
|
||||
display_signature(&db, [], Some(Type::none(&db))),
|
||||
"() -> None"
|
||||
);
|
||||
|
||||
// Single parameter type (no name) with a return type.
|
||||
assert_eq!(
|
||||
display_signature(
|
||||
&db,
|
||||
[Parameter::new(
|
||||
None,
|
||||
Some(Type::none(&db)),
|
||||
ParameterKind::PositionalOrKeyword { default_ty: None }
|
||||
)],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
"(None) -> None"
|
||||
);
|
||||
|
||||
// Two parameters where one has annotation and the other doesn't.
|
||||
assert_eq!(
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
Some(Name::new_static("x")),
|
||||
None,
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
default_ty: Some(KnownClass::Int.to_instance(&db))
|
||||
}
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("y")),
|
||||
Some(KnownClass::Str.to_instance(&db)),
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
default_ty: Some(KnownClass::Str.to_instance(&db))
|
||||
}
|
||||
)
|
||||
],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
"(x=int, y: str = str) -> None"
|
||||
);
|
||||
|
||||
// All positional only parameters.
|
||||
assert_eq!(
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
Some(Name::new_static("x")),
|
||||
None,
|
||||
ParameterKind::PositionalOnly { default_ty: None }
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("y")),
|
||||
None,
|
||||
ParameterKind::PositionalOnly { default_ty: None }
|
||||
)
|
||||
],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
"(x, y, /) -> None"
|
||||
);
|
||||
|
||||
// Positional-only parameters mixed with non-positional-only parameters.
|
||||
assert_eq!(
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
Some(Name::new_static("x")),
|
||||
None,
|
||||
ParameterKind::PositionalOnly { default_ty: None }
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("y")),
|
||||
None,
|
||||
ParameterKind::PositionalOrKeyword { default_ty: None }
|
||||
)
|
||||
],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
"(x, /, y) -> None"
|
||||
);
|
||||
|
||||
// All keyword-only parameters.
|
||||
assert_eq!(
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
Some(Name::new_static("x")),
|
||||
None,
|
||||
ParameterKind::KeywordOnly { default_ty: None }
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("y")),
|
||||
None,
|
||||
ParameterKind::KeywordOnly { default_ty: None }
|
||||
)
|
||||
],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
"(*, x, y) -> None"
|
||||
);
|
||||
|
||||
// Keyword-only parameters mixed with non-keyword-only parameters.
|
||||
assert_eq!(
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
Some(Name::new_static("x")),
|
||||
None,
|
||||
ParameterKind::PositionalOrKeyword { default_ty: None }
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("y")),
|
||||
None,
|
||||
ParameterKind::KeywordOnly { default_ty: None }
|
||||
)
|
||||
],
|
||||
Some(Type::none(&db))
|
||||
),
|
||||
"(x, *, y) -> None"
|
||||
);
|
||||
|
||||
// A mix of all parameter kinds.
|
||||
assert_eq!(
|
||||
display_signature(
|
||||
&db,
|
||||
[
|
||||
Parameter::new(
|
||||
Some(Name::new_static("a")),
|
||||
None,
|
||||
ParameterKind::PositionalOnly { default_ty: None },
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("b")),
|
||||
Some(KnownClass::Int.to_instance(&db)),
|
||||
ParameterKind::PositionalOnly { default_ty: None },
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("c")),
|
||||
None,
|
||||
ParameterKind::PositionalOnly {
|
||||
default_ty: Some(Type::IntLiteral(1)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("d")),
|
||||
Some(KnownClass::Int.to_instance(&db)),
|
||||
ParameterKind::PositionalOnly {
|
||||
default_ty: Some(Type::IntLiteral(2)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("e")),
|
||||
None,
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
default_ty: Some(Type::IntLiteral(3)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("f")),
|
||||
Some(KnownClass::Int.to_instance(&db)),
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
default_ty: Some(Type::IntLiteral(4)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("args")),
|
||||
Some(Type::object(&db)),
|
||||
ParameterKind::Variadic,
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("g")),
|
||||
None,
|
||||
ParameterKind::KeywordOnly {
|
||||
default_ty: Some(Type::IntLiteral(5)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("h")),
|
||||
Some(KnownClass::Int.to_instance(&db)),
|
||||
ParameterKind::KeywordOnly {
|
||||
default_ty: Some(Type::IntLiteral(6)),
|
||||
},
|
||||
),
|
||||
Parameter::new(
|
||||
Some(Name::new_static("kwargs")),
|
||||
Some(KnownClass::Str.to_instance(&db)),
|
||||
ParameterKind::KeywordVariadic,
|
||||
),
|
||||
],
|
||||
Some(KnownClass::Bytes.to_instance(&db))
|
||||
),
|
||||
"(a, b: int, c=Literal[1], d: int = Literal[2], \
|
||||
/, e=Literal[3], f: int = Literal[4], *args: object, \
|
||||
*, g=Literal[5], h: int = Literal[6], **kwargs: str) -> bytes"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ use crate::module_resolver::{file_to_module, resolve_module};
|
||||
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId, ScopedExpressionId};
|
||||
use crate::semantic_index::definition::{
|
||||
AssignmentDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey,
|
||||
ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind,
|
||||
ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind, WithItemDefinitionKind,
|
||||
};
|
||||
use crate::semantic_index::expression::{Expression, ExpressionKind};
|
||||
use crate::semantic_index::semantic_index;
|
||||
@@ -56,24 +56,26 @@ use crate::symbol::{
|
||||
};
|
||||
use crate::types::call::{Argument, CallArguments, UnionCallError};
|
||||
use crate::types::diagnostic::{
|
||||
report_invalid_arguments_to_annotated, report_invalid_assignment,
|
||||
report_invalid_attribute_assignment, report_unresolved_module, TypeCheckDiagnostics,
|
||||
CALL_NON_CALLABLE, CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS,
|
||||
CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE,
|
||||
INCONSISTENT_MRO, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_CONTEXT_MANAGER,
|
||||
INVALID_DECLARATION, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT,
|
||||
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
|
||||
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
|
||||
report_invalid_assignment, report_invalid_attribute_assignment, report_unresolved_module,
|
||||
TypeCheckDiagnostics, CALL_NON_CALLABLE, CALL_POSSIBLY_UNBOUND_METHOD,
|
||||
CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO,
|
||||
DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION,
|
||||
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS,
|
||||
POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
|
||||
UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
|
||||
};
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, InstanceType,
|
||||
IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType,
|
||||
MetaclassCandidate, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness,
|
||||
TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers,
|
||||
TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
|
||||
MetaclassCandidate, Parameter, Parameters, SliceLiteralType, SubclassOfType, Symbol,
|
||||
SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
|
||||
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
|
||||
UnionType,
|
||||
};
|
||||
use crate::types::{CallableType, GeneralCallableType, ParameterKind, Signature};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
use crate::Db;
|
||||
@@ -117,7 +119,14 @@ fn infer_definition_types_cycle_recovery<'db>(
|
||||
_cycle: &salsa::Cycle,
|
||||
input: Definition<'db>,
|
||||
) -> TypeInference<'db> {
|
||||
tracing::trace!("infer_definition_types_cycle_recovery");
|
||||
let file = input.file(db);
|
||||
let _span = tracing::trace_span!(
|
||||
"infer_definition_types_cycle_recovery",
|
||||
range = ?input.kind(db).target_range(),
|
||||
file = %file.path(db)
|
||||
)
|
||||
.entered();
|
||||
|
||||
TypeInference::cycle_fallback(input.scope(db), todo_type!("cycle recovery"))
|
||||
}
|
||||
|
||||
@@ -315,7 +324,7 @@ impl<'db> TypeInference<'db> {
|
||||
#[track_caller]
|
||||
pub(crate) fn expression_type(&self, expression: ScopedExpressionId) -> Type<'db> {
|
||||
self.try_expression_type(expression).expect(
|
||||
"expression should belong to this TypeInference region and
|
||||
"expression should belong to this TypeInference region and \
|
||||
TypeInferenceBuilder should have inferred a type for it",
|
||||
)
|
||||
}
|
||||
@@ -839,13 +848,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
DefinitionKind::Parameter(parameter_with_default) => {
|
||||
self.infer_parameter_definition(parameter_with_default, definition);
|
||||
}
|
||||
DefinitionKind::WithItem(with_item) => {
|
||||
self.infer_with_item_definition(
|
||||
with_item.target(),
|
||||
with_item.node(),
|
||||
with_item.is_async(),
|
||||
definition,
|
||||
);
|
||||
DefinitionKind::WithItem(with_item_definition) => {
|
||||
self.infer_with_item_definition(with_item_definition, definition);
|
||||
}
|
||||
DefinitionKind::MatchPattern(match_pattern) => {
|
||||
self.infer_match_pattern_definition(
|
||||
@@ -937,7 +941,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let declarations = use_def.declarations_at_binding(binding);
|
||||
let mut bound_ty = ty;
|
||||
let declared_ty = symbol_from_declarations(self.db(), declarations)
|
||||
.map(|SymbolAndQualifiers(s, _)| s.ignore_possibly_unbound().unwrap_or(Type::unknown()))
|
||||
.map(|SymbolAndQualifiers { symbol, .. }| {
|
||||
symbol.ignore_possibly_unbound().unwrap_or(Type::unknown())
|
||||
})
|
||||
.unwrap_or_else(|(ty, conflicting)| {
|
||||
// TODO point out the conflicting declarations in the diagnostic?
|
||||
let symbol_table = self.index.symbol_table(binding.file_scope(self.db()));
|
||||
@@ -1362,7 +1368,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
///
|
||||
/// The annotated type is implicitly wrapped in a homogeneous tuple.
|
||||
///
|
||||
/// See `infer_parameter_definition` doc comment for some relevant observations about scopes.
|
||||
/// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes.
|
||||
///
|
||||
/// [`infer_parameter_definition`]: Self::infer_parameter_definition
|
||||
fn infer_variadic_positional_parameter_definition(
|
||||
&mut self,
|
||||
parameter: &ast::Parameter,
|
||||
@@ -1391,7 +1399,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
///
|
||||
/// The annotated type is implicitly wrapped in a string-keyed dictionary.
|
||||
///
|
||||
/// See `infer_parameter_definition` doc comment for some relevant observations about scopes.
|
||||
/// See [`infer_parameter_definition`] doc comment for some relevant observations about scopes.
|
||||
///
|
||||
/// [`infer_parameter_definition`]: Self::infer_parameter_definition
|
||||
fn infer_variadic_keyword_parameter_definition(
|
||||
&mut self,
|
||||
parameter: &ast::Parameter,
|
||||
@@ -1595,18 +1605,17 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
} = with_statement;
|
||||
for item in items {
|
||||
let target = item.optional_vars.as_deref();
|
||||
if let Some(ast::Expr::Name(name)) = target {
|
||||
self.infer_definition(name);
|
||||
if let Some(target) = target {
|
||||
self.infer_target(target, &item.context_expr, |db, ctx_manager_ty| {
|
||||
// TODO: `infer_with_statement_definition` reports a diagnostic if `ctx_manager_ty` isn't a context manager
|
||||
// but only if the target is a name. We should report a diagnostic here if the target isn't a name:
|
||||
// `with not_context_manager as a.x: ...
|
||||
ctx_manager_ty.enter(db)
|
||||
});
|
||||
} else {
|
||||
// TODO infer definitions in unpacking assignment
|
||||
|
||||
// Call into the context expression inference to validate that it evaluates
|
||||
// to a valid context manager.
|
||||
let context_expression_ty = if target.is_some() {
|
||||
self.infer_standalone_expression(&item.context_expr)
|
||||
} else {
|
||||
self.infer_expression(&item.context_expr)
|
||||
};
|
||||
let context_expression_ty = self.infer_expression(&item.context_expr);
|
||||
self.infer_context_expression(&item.context_expr, context_expression_ty, *is_async);
|
||||
self.infer_optional_expression(target);
|
||||
}
|
||||
@@ -1617,24 +1626,36 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
fn infer_with_item_definition(
|
||||
&mut self,
|
||||
target: &ast::ExprName,
|
||||
with_item: &ast::WithItem,
|
||||
is_async: bool,
|
||||
with_item: &WithItemDefinitionKind<'db>,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
self.infer_standalone_expression(&with_item.context_expr);
|
||||
let context_expr = with_item.context_expr();
|
||||
let name = with_item.name();
|
||||
|
||||
let target_ty = self.infer_context_expression(
|
||||
&with_item.context_expr,
|
||||
self.expression_type(&with_item.context_expr),
|
||||
is_async,
|
||||
);
|
||||
let context_expr_ty = self.infer_standalone_expression(context_expr);
|
||||
|
||||
self.types.expressions.insert(
|
||||
target.scoped_expression_id(self.db(), self.scope()),
|
||||
target_ty,
|
||||
);
|
||||
self.add_binding(target.into(), definition, target_ty);
|
||||
let target_ty = if with_item.is_async() {
|
||||
todo_type!("async `with` statement")
|
||||
} else {
|
||||
match with_item.target() {
|
||||
TargetKind::Sequence(unpack) => {
|
||||
let unpacked = infer_unpack_types(self.db(), unpack);
|
||||
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
|
||||
if with_item.is_first() {
|
||||
self.context.extend(unpacked);
|
||||
}
|
||||
unpacked.expression_type(name_ast_id)
|
||||
}
|
||||
TargetKind::Name => self.infer_context_expression(
|
||||
context_expr,
|
||||
context_expr_ty,
|
||||
with_item.is_async(),
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
self.store_expression_type(name, target_ty);
|
||||
self.add_binding(name.into(), definition, target_ty);
|
||||
}
|
||||
|
||||
/// Infers the type of a context expression (`with expr`) and returns the target's type
|
||||
@@ -1654,120 +1675,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
return todo_type!("async `with` statement");
|
||||
}
|
||||
|
||||
let context_manager_ty = context_expression_ty.to_meta_type(self.db());
|
||||
|
||||
let enter = context_manager_ty.member(self.db(), "__enter__");
|
||||
let exit = context_manager_ty.member(self.db(), "__exit__");
|
||||
|
||||
// TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`).
|
||||
match (enter, exit) {
|
||||
(Symbol::Unbound, Symbol::Unbound) => {
|
||||
self.context.report_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression,
|
||||
format_args!(
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`",
|
||||
context_expression_ty.display(self.db())
|
||||
),
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
(Symbol::Unbound, _) => {
|
||||
self.context.report_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression,
|
||||
format_args!(
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__`",
|
||||
context_expression_ty.display(self.db())
|
||||
),
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
(Symbol::Type(enter_ty, enter_boundness), exit) => {
|
||||
if enter_boundness == Boundness::PossiblyUnbound {
|
||||
self.context.report_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression,
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` is possibly unbound",
|
||||
context_expression = context_expression_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let target_ty = enter_ty
|
||||
.try_call(self.db(), &CallArguments::positional([context_expression_ty]))
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.unwrap_or_else(|err| {
|
||||
// TODO: Use more specific error messages for the different error cases.
|
||||
// E.g. hint toward the union variant that doesn't correctly implement enter,
|
||||
// distinguish between a not callable `__enter__` attribute and a wrong signature.
|
||||
self.context.report_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression,
|
||||
format_args!("
|
||||
Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__enter__`",
|
||||
context_expression = context_expression_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
err.fallback_return_type(self.db())
|
||||
});
|
||||
|
||||
match exit {
|
||||
Symbol::Unbound => {
|
||||
self.context.report_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression,
|
||||
format_args!(
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__exit__`",
|
||||
context_expression_ty.display(self.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
Symbol::Type(exit_ty, exit_boundness) => {
|
||||
// TODO: Use the `exit_ty` to determine if any raised exception is suppressed.
|
||||
|
||||
if exit_boundness == Boundness::PossiblyUnbound {
|
||||
self.context.report_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression,
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` is possibly unbound",
|
||||
context_expression = context_expression_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if exit_ty
|
||||
.try_call(
|
||||
self.db(),
|
||||
&CallArguments::positional([
|
||||
context_manager_ty,
|
||||
Type::none(self.db()),
|
||||
Type::none(self.db()),
|
||||
Type::none(self.db()),
|
||||
]),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
// TODO: Use more specific error messages for the different error cases.
|
||||
// E.g. hint toward the union variant that doesn't correctly implement enter,
|
||||
// distinguish between a not callable `__exit__` attribute and a wrong signature.
|
||||
self.context.report_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression,
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__exit__`",
|
||||
context_expression = context_expression_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target_ty
|
||||
}
|
||||
}
|
||||
context_expression_ty
|
||||
.try_enter(self.db())
|
||||
.unwrap_or_else(|err| {
|
||||
err.report_diagnostic(&self.context, context_expression.into());
|
||||
err.fallback_enter_type(self.db())
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_except_handler_definition(
|
||||
@@ -2353,6 +2266,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
if let Symbol::Type(class_member, boundness) = instance
|
||||
.class()
|
||||
.class_member(self.db(), op.in_place_dunder())
|
||||
.symbol
|
||||
{
|
||||
let call = class_member.try_call(
|
||||
self.db(),
|
||||
@@ -2768,7 +2682,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
} = alias;
|
||||
|
||||
// First try loading the requested attribute from the module.
|
||||
if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), name) {
|
||||
if let Symbol::Type(ty, boundness) = module_ty.member(self.db(), &name.id).symbol {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
// TODO: Consider loading _both_ the attribute and any submodule and unioning them
|
||||
// together if the attribute exists but is possibly-unbound.
|
||||
@@ -3380,18 +3294,83 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
body: _,
|
||||
} = lambda_expression;
|
||||
|
||||
if let Some(parameters) = parameters {
|
||||
for default in parameters
|
||||
.iter_non_variadic_params()
|
||||
.filter_map(|param| param.default.as_deref())
|
||||
{
|
||||
self.infer_expression(default);
|
||||
}
|
||||
let parameters = if let Some(parameters) = parameters {
|
||||
let positional_only = parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.map(|parameter| {
|
||||
Parameter::new(
|
||||
Some(parameter.name().id.clone()),
|
||||
None,
|
||||
ParameterKind::PositionalOnly {
|
||||
default_ty: parameter
|
||||
.default()
|
||||
.map(|default| self.infer_expression(default)),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let positional_or_keyword = parameters
|
||||
.args
|
||||
.iter()
|
||||
.map(|parameter| {
|
||||
Parameter::new(
|
||||
Some(parameter.name().id.clone()),
|
||||
None,
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
default_ty: parameter
|
||||
.default()
|
||||
.map(|default| self.infer_expression(default)),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let variadic = parameters.vararg.as_ref().map(|parameter| {
|
||||
Parameter::new(
|
||||
Some(parameter.name.id.clone()),
|
||||
None,
|
||||
ParameterKind::Variadic,
|
||||
)
|
||||
});
|
||||
let keyword_only = parameters
|
||||
.kwonlyargs
|
||||
.iter()
|
||||
.map(|parameter| {
|
||||
Parameter::new(
|
||||
Some(parameter.name().id.clone()),
|
||||
None,
|
||||
ParameterKind::KeywordOnly {
|
||||
default_ty: parameter
|
||||
.default()
|
||||
.map(|default| self.infer_expression(default)),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let keyword_variadic = parameters.kwarg.as_ref().map(|parameter| {
|
||||
Parameter::new(
|
||||
Some(parameter.name.id.clone()),
|
||||
None,
|
||||
ParameterKind::KeywordVariadic,
|
||||
)
|
||||
});
|
||||
|
||||
self.infer_parameters(parameters);
|
||||
}
|
||||
Parameters::new(
|
||||
positional_only
|
||||
.into_iter()
|
||||
.chain(positional_or_keyword)
|
||||
.chain(variadic)
|
||||
.chain(keyword_only)
|
||||
.chain(keyword_variadic),
|
||||
)
|
||||
} else {
|
||||
Parameters::empty()
|
||||
};
|
||||
|
||||
todo_type!("typing.Callable type")
|
||||
Type::Callable(CallableType::General(GeneralCallableType::new(
|
||||
self.db(),
|
||||
Signature::new(parameters, Some(todo_type!("lambda return type"))),
|
||||
)))
|
||||
}
|
||||
|
||||
fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> {
|
||||
@@ -3647,7 +3626,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
symbol_from_bindings(db, use_def.bindings_at_use(use_id))
|
||||
};
|
||||
|
||||
let symbol = local_scope_symbol.or_fall_back_to(db, || {
|
||||
let symbol = SymbolAndQualifiers::from(local_scope_symbol).or_fall_back_to(db, || {
|
||||
let has_bindings_in_this_scope = match symbol_table.symbol_by_name(symbol_name) {
|
||||
Some(symbol) => symbol.is_bound(),
|
||||
None => {
|
||||
@@ -3669,7 +3648,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// function-like scope, it is considered a local variable; it never references another
|
||||
// scope. (At runtime, it would use the `LOAD_FAST` opcode.)
|
||||
if has_bindings_in_this_scope && scope.is_function_like(db) {
|
||||
return Symbol::Unbound;
|
||||
return Symbol::Unbound.into();
|
||||
}
|
||||
|
||||
let current_file = self.file();
|
||||
@@ -3699,7 +3678,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
symbol_name,
|
||||
file_scope_id,
|
||||
) {
|
||||
return symbol_from_bindings(db, bindings);
|
||||
return symbol_from_bindings(db, bindings).into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3718,12 +3697,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
Symbol::Unbound
|
||||
SymbolAndQualifiers::from(Symbol::Unbound)
|
||||
// No nonlocal binding? Check the module's explicit globals.
|
||||
// Avoid infinite recursion if `self.scope` already is the module's global scope.
|
||||
.or_fall_back_to(db, || {
|
||||
if file_scope_id.is_global() {
|
||||
return Symbol::Unbound;
|
||||
return Symbol::Unbound.into();
|
||||
}
|
||||
|
||||
if !self.is_deferred() {
|
||||
@@ -3732,7 +3711,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
symbol_name,
|
||||
file_scope_id,
|
||||
) {
|
||||
return symbol_from_bindings(db, bindings);
|
||||
return symbol_from_bindings(db, bindings).into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3746,7 +3725,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// (without infinite recursion if we're already in builtins.)
|
||||
.or_fall_back_to(db, || {
|
||||
if Some(self.scope()) == builtins_module_scope(db) {
|
||||
Symbol::Unbound
|
||||
Symbol::Unbound.into()
|
||||
} else {
|
||||
builtins_symbol(db, symbol_name)
|
||||
}
|
||||
@@ -3764,21 +3743,23 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
typing_extensions_symbol(db, symbol_name)
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
Symbol::Unbound.into()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
symbol.unwrap_with_diagnostic(|lookup_error| match lookup_error {
|
||||
LookupError::Unbound => {
|
||||
report_unresolved_reference(&self.context, name_node);
|
||||
Type::unknown()
|
||||
}
|
||||
LookupError::PossiblyUnbound(type_when_bound) => {
|
||||
report_possibly_unresolved_reference(&self.context, name_node);
|
||||
type_when_bound
|
||||
}
|
||||
})
|
||||
symbol
|
||||
.unwrap_with_diagnostic(|lookup_error| match lookup_error {
|
||||
LookupError::Unbound(qualifiers) => {
|
||||
report_unresolved_reference(&self.context, name_node);
|
||||
TypeAndQualifiers::new(Type::unknown(), qualifiers)
|
||||
}
|
||||
LookupError::PossiblyUnbound(type_when_bound) => {
|
||||
report_possibly_unresolved_reference(&self.context, name_node);
|
||||
type_when_bound
|
||||
}
|
||||
})
|
||||
.inner_type()
|
||||
}
|
||||
|
||||
fn infer_name_expression(&mut self, name: &ast::ExprName) -> Type<'db> {
|
||||
@@ -3804,15 +3785,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
value_type
|
||||
.member(db, &attr.id)
|
||||
.unwrap_with_diagnostic(|lookup_error| match lookup_error {
|
||||
LookupError::Unbound => {
|
||||
LookupError::Unbound(_) => {
|
||||
let bound_on_instance = match value_type {
|
||||
Type::ClassLiteral(class) => {
|
||||
!class.class().instance_member(db, attr).0.is_unbound()
|
||||
!class.class().instance_member(db, attr).symbol.is_unbound()
|
||||
}
|
||||
Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => {
|
||||
match subclass_of.subclass_of() {
|
||||
ClassBase::Class(class) => {
|
||||
!class.instance_member(db, attr).0.is_unbound()
|
||||
!class.instance_member(db, attr).symbol.is_unbound()
|
||||
}
|
||||
ClassBase::Dynamic(_) => unreachable!(
|
||||
"Attribute lookup on a dynamic `SubclassOf` type should always return a bound symbol"
|
||||
@@ -3844,7 +3825,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
}
|
||||
|
||||
Type::unknown()
|
||||
Type::unknown().into()
|
||||
}
|
||||
LookupError::PossiblyUnbound(type_when_bound) => {
|
||||
self.context.report_lint(
|
||||
@@ -3858,7 +3839,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
type_when_bound
|
||||
}
|
||||
})
|
||||
}).inner_type()
|
||||
}
|
||||
|
||||
fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
|
||||
@@ -3875,8 +3856,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let value_ty = self.infer_expression(value);
|
||||
|
||||
let symbol = match value_ty {
|
||||
Type::Instance(instance) => {
|
||||
let instance_member = instance.class().instance_member(self.db(), attr);
|
||||
Type::Instance(_) => {
|
||||
let instance_member = value_ty.member(self.db(), &attr.id);
|
||||
if instance_member.is_class_var() {
|
||||
self.context.report_lint(
|
||||
&INVALID_ATTRIBUTE_ACCESS,
|
||||
@@ -3888,10 +3869,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
}
|
||||
|
||||
instance_member.0
|
||||
instance_member.symbol
|
||||
}
|
||||
Type::ClassLiteral(_) | Type::SubclassOf(_) => {
|
||||
let class_member = value_ty.member(self.db(), attr);
|
||||
let class_member = value_ty.member(self.db(), &attr.id).symbol;
|
||||
|
||||
if class_member.is_unbound() {
|
||||
let class = match value_ty {
|
||||
@@ -3905,10 +3886,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
_ => None,
|
||||
};
|
||||
if let Some(class) = class {
|
||||
let instance_member = class.instance_member(self.db(), attr);
|
||||
let instance_member = class.instance_member(self.db(), attr).symbol;
|
||||
|
||||
// Attribute is declared or bound on instance. Forbid access from the class object
|
||||
if !instance_member.0.is_unbound() {
|
||||
if !instance_member.is_unbound() {
|
||||
self.context.report_lint(
|
||||
&INVALID_ATTRIBUTE_ACCESS,
|
||||
attribute,
|
||||
@@ -3922,7 +3903,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
class_member
|
||||
}
|
||||
_ => value_ty.member(self.db(), attr),
|
||||
_ => value_ty.member(self.db(), &attr.id).symbol,
|
||||
};
|
||||
|
||||
// TODO: The unbound-case might also yield a diagnostic, but we can not activate
|
||||
@@ -4075,6 +4056,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
| (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown),
|
||||
(todo @ Type::Dynamic(DynamicType::Todo(_)), _, _)
|
||||
| (_, todo @ Type::Dynamic(DynamicType::Todo(_)), _) => Some(todo),
|
||||
(todo @ Type::Dynamic(DynamicType::TodoProtocol), _, _)
|
||||
| (_, todo @ Type::Dynamic(DynamicType::TodoProtocol), _) => Some(todo),
|
||||
(Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never),
|
||||
|
||||
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some(
|
||||
@@ -4244,11 +4227,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let right_class = right_ty.to_meta_type(self.db());
|
||||
if left_ty != right_ty && right_ty.is_subtype_of(self.db(), left_ty) {
|
||||
let reflected_dunder = op.reflected_dunder();
|
||||
let rhs_reflected = right_class.member(self.db(), reflected_dunder);
|
||||
let rhs_reflected = right_class.member(self.db(), reflected_dunder).symbol;
|
||||
// TODO: if `rhs_reflected` is possibly unbound, we should union the two possible
|
||||
// CallOutcomes together
|
||||
if !rhs_reflected.is_unbound()
|
||||
&& rhs_reflected != left_class.member(self.db(), reflected_dunder)
|
||||
&& rhs_reflected != left_class.member(self.db(), reflected_dunder).symbol
|
||||
{
|
||||
return right_ty
|
||||
.try_call_dunder(
|
||||
@@ -4980,7 +4963,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||
let db = self.db();
|
||||
|
||||
let contains_dunder = right.class().class_member(db, "__contains__");
|
||||
let contains_dunder = right.class().class_member(db, "__contains__").symbol;
|
||||
let compare_result_opt = match contains_dunder {
|
||||
Symbol::Type(contains_dunder, Boundness::Bound) => {
|
||||
// If `__contains__` is available, it is used directly for the membership test.
|
||||
@@ -5248,6 +5231,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
value_ty,
|
||||
Type::IntLiteral(i64::from(bool)),
|
||||
),
|
||||
(Type::KnownInstance(KnownInstanceType::Protocol), _) => {
|
||||
Type::Dynamic(DynamicType::TodoProtocol)
|
||||
}
|
||||
(value_ty, slice_ty) => {
|
||||
// If the class defines `__getitem__`, return its return type.
|
||||
//
|
||||
@@ -5299,7 +5285,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// method in these `sys.version_info` branches.
|
||||
if value_ty.is_subtype_of(self.db(), KnownClass::Type.to_instance(self.db())) {
|
||||
let dunder_class_getitem_method =
|
||||
value_ty.member(self.db(), "__class_getitem__");
|
||||
value_ty.member(self.db(), "__class_getitem__").symbol;
|
||||
|
||||
match dunder_class_getitem_method {
|
||||
Symbol::Unbound => {}
|
||||
@@ -5353,7 +5339,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
}
|
||||
|
||||
Type::unknown()
|
||||
match value_ty {
|
||||
Type::ClassLiteral(_) => {
|
||||
// TODO: proper support for generic classes
|
||||
// For now, just infer `Sequence`, if we see something like `Sequence[str]`. This allows us
|
||||
// to look up attributes on generic base classes, even if we don't understand generics yet.
|
||||
value_ty
|
||||
}
|
||||
_ => Type::unknown(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5677,14 +5671,16 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
// TODO: an Ellipsis literal *on its own* does not have any meaning in annotation
|
||||
// expressions, but is meaningful in the context of a number of special forms.
|
||||
ast::Expr::EllipsisLiteral(_literal) => todo_type!(),
|
||||
ast::Expr::EllipsisLiteral(_literal) => {
|
||||
todo_type!("ellipsis literal in type expression")
|
||||
}
|
||||
|
||||
// Other literals do not have meaningful values in the annotation expression context.
|
||||
// However, we will we want to handle these differently when working with special forms,
|
||||
// since (e.g.) `123` is not valid in an annotation expression but `Literal[123]` is.
|
||||
ast::Expr::BytesLiteral(_literal) => todo_type!(),
|
||||
ast::Expr::NumberLiteral(_literal) => todo_type!(),
|
||||
ast::Expr::BooleanLiteral(_literal) => todo_type!(),
|
||||
ast::Expr::BytesLiteral(_literal) => todo_type!("bytes literal in type expression"),
|
||||
ast::Expr::NumberLiteral(_literal) => todo_type!("number literal in type expression"),
|
||||
ast::Expr::BooleanLiteral(_literal) => todo_type!("boolean literal in type expression"),
|
||||
|
||||
ast::Expr::Subscript(subscript) => {
|
||||
let ast::ExprSubscript {
|
||||
@@ -6084,8 +6080,61 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
todo_type!("Generic PEP-695 type alias")
|
||||
}
|
||||
KnownInstanceType::Callable => {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
todo_type!("Callable types")
|
||||
let ast::Expr::Tuple(ast::ExprTuple {
|
||||
elts: arguments, ..
|
||||
}) = arguments_slice
|
||||
else {
|
||||
report_invalid_arguments_to_callable(self.db(), &self.context, subscript);
|
||||
|
||||
// If it's not a tuple, defer it to inferring the parameter types which could
|
||||
// return an `Err` if the expression is invalid in that position. In which
|
||||
// case, we'll fallback to using an unknown list of parameters.
|
||||
let parameters = self
|
||||
.infer_callable_parameter_types(arguments_slice)
|
||||
.unwrap_or_else(|()| Parameters::unknown());
|
||||
|
||||
let callable_type =
|
||||
Type::Callable(CallableType::General(GeneralCallableType::new(
|
||||
self.db(),
|
||||
Signature::new(parameters, Some(Type::unknown())),
|
||||
)));
|
||||
|
||||
// `Parameters` is not a `Type` variant, so we're storing the outer callable
|
||||
// type on the arguments slice instead.
|
||||
self.store_expression_type(arguments_slice, callable_type);
|
||||
|
||||
return callable_type;
|
||||
};
|
||||
|
||||
let [first_argument, second_argument] = arguments.as_slice() else {
|
||||
report_invalid_arguments_to_callable(self.db(), &self.context, subscript);
|
||||
self.infer_type_expression(arguments_slice);
|
||||
return Type::Callable(CallableType::General(GeneralCallableType::unknown(
|
||||
self.db(),
|
||||
)));
|
||||
};
|
||||
|
||||
let Ok(parameters) = self.infer_callable_parameter_types(first_argument) else {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
return Type::Callable(CallableType::General(GeneralCallableType::unknown(
|
||||
self.db(),
|
||||
)));
|
||||
};
|
||||
|
||||
let return_type = self.infer_type_expression(second_argument);
|
||||
|
||||
let callable_type =
|
||||
Type::Callable(CallableType::General(GeneralCallableType::new(
|
||||
self.db(),
|
||||
Signature::new(parameters, Some(return_type)),
|
||||
)));
|
||||
|
||||
// `Signature` / `Parameters` are not a `Type` variant, so we're storing the outer
|
||||
// callable type on the these expressions instead.
|
||||
self.store_expression_type(arguments_slice, callable_type);
|
||||
self.store_expression_type(first_argument, callable_type);
|
||||
|
||||
callable_type
|
||||
}
|
||||
|
||||
// Type API special forms
|
||||
@@ -6214,6 +6263,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
todo_type!("`Unpack[]` special form")
|
||||
}
|
||||
KnownInstanceType::Protocol => {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
Type::Dynamic(DynamicType::TodoProtocol)
|
||||
}
|
||||
KnownInstanceType::NoReturn
|
||||
| KnownInstanceType::Never
|
||||
| KnownInstanceType::Any
|
||||
@@ -6320,6 +6373,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// TODO: Check that value type is enum otherwise return None
|
||||
value_ty
|
||||
.member(self.db(), &attr.id)
|
||||
.symbol
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::unknown())
|
||||
}
|
||||
@@ -6336,6 +6390,73 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Infer the first argument to a `typing.Callable` type expression and returns the
|
||||
/// corresponding [`Parameters`].
|
||||
///
|
||||
/// It returns an [`Err`] if the argument is invalid i.e., not a list of types, parameter
|
||||
/// specification, `typing.Concatenate`, or `...`.
|
||||
fn infer_callable_parameter_types(
|
||||
&mut self,
|
||||
parameters: &ast::Expr,
|
||||
) -> Result<Parameters<'db>, ()> {
|
||||
Ok(match parameters {
|
||||
ast::Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { .. }) => {
|
||||
Parameters::gradual_form()
|
||||
}
|
||||
ast::Expr::List(ast::ExprList { elts: params, .. }) => {
|
||||
let mut parameter_types = Vec::with_capacity(params.len());
|
||||
|
||||
// Whether to infer `Todo` for the parameters
|
||||
let mut return_todo = false;
|
||||
|
||||
for param in params {
|
||||
let param_type = self.infer_type_expression(param);
|
||||
// This is similar to what we currently do for inferring tuple type expression.
|
||||
// We currently infer `Todo` for the parameters to avoid invalid diagnostics
|
||||
// when trying to check for assignability or any other relation. For example,
|
||||
// `*tuple[int, str]`, `Unpack[]`, etc. are not yet supported.
|
||||
return_todo |= param_type.is_todo()
|
||||
&& matches!(param, ast::Expr::Starred(_) | ast::Expr::Subscript(_));
|
||||
parameter_types.push(param_type);
|
||||
}
|
||||
|
||||
if return_todo {
|
||||
// TODO: `Unpack`
|
||||
Parameters::todo()
|
||||
} else {
|
||||
Parameters::new(parameter_types.iter().map(|param_type| {
|
||||
Parameter::new(
|
||||
None,
|
||||
Some(*param_type),
|
||||
ParameterKind::PositionalOnly { default_ty: None },
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
ast::Expr::Subscript(_) => {
|
||||
// TODO: Support `Concatenate[...]`
|
||||
Parameters::todo()
|
||||
}
|
||||
ast::Expr::Name(name) if name.is_invalid() => {
|
||||
// This is a special case to avoid raising the error suggesting what the first
|
||||
// argument should be. This only happens when there's already a syntax error like
|
||||
// `Callable[]`.
|
||||
return Err(());
|
||||
}
|
||||
_ => {
|
||||
// TODO: Check whether `Expr::Name` is a ParamSpec
|
||||
self.context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
parameters,
|
||||
format_args!(
|
||||
"The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`",
|
||||
),
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The deferred state of a specific expression in an inference region.
|
||||
@@ -6577,7 +6698,7 @@ mod tests {
|
||||
assert_eq!(scope.name(db), *expected_scope_name);
|
||||
}
|
||||
|
||||
symbol(db, scope, symbol_name)
|
||||
symbol(db, scope, symbol_name).symbol
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -6732,7 +6853,7 @@ mod tests {
|
||||
assert_eq!(var_ty.display(&db).to_string(), var);
|
||||
|
||||
let expected_name_ty = format!(r#"Literal["{var}"]"#);
|
||||
let name_ty = var_ty.member(&db, "__name__").expect_type();
|
||||
let name_ty = var_ty.member(&db, "__name__").symbol.expect_type();
|
||||
assert_eq!(name_ty.display(&db).to_string(), expected_name_ty);
|
||||
|
||||
let KnownInstanceType::TypeVar(typevar) = var_ty.expect_known_instance() else {
|
||||
@@ -6793,7 +6914,7 @@ mod tests {
|
||||
])?;
|
||||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let x_ty = global_symbol(&db, a, "x").expect_type();
|
||||
let x_ty = global_symbol(&db, a, "x").symbol.expect_type();
|
||||
|
||||
assert_eq!(x_ty.display(&db).to_string(), "int");
|
||||
|
||||
@@ -6802,7 +6923,7 @@ mod tests {
|
||||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
|
||||
let x_ty_2 = global_symbol(&db, a, "x").expect_type();
|
||||
let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type();
|
||||
|
||||
assert_eq!(x_ty_2.display(&db).to_string(), "bool");
|
||||
|
||||
@@ -6819,7 +6940,7 @@ mod tests {
|
||||
])?;
|
||||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let x_ty = global_symbol(&db, a, "x").expect_type();
|
||||
let x_ty = global_symbol(&db, a, "x").symbol.expect_type();
|
||||
|
||||
assert_eq!(x_ty.display(&db).to_string(), "int");
|
||||
|
||||
@@ -6829,7 +6950,7 @@ mod tests {
|
||||
|
||||
db.clear_salsa_events();
|
||||
|
||||
let x_ty_2 = global_symbol(&db, a, "x").expect_type();
|
||||
let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type();
|
||||
|
||||
assert_eq!(x_ty_2.display(&db).to_string(), "int");
|
||||
|
||||
@@ -6855,7 +6976,7 @@ mod tests {
|
||||
])?;
|
||||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let x_ty = global_symbol(&db, a, "x").expect_type();
|
||||
let x_ty = global_symbol(&db, a, "x").symbol.expect_type();
|
||||
|
||||
assert_eq!(x_ty.display(&db).to_string(), "int");
|
||||
|
||||
@@ -6865,7 +6986,7 @@ mod tests {
|
||||
|
||||
db.clear_salsa_events();
|
||||
|
||||
let x_ty_2 = global_symbol(&db, a, "x").expect_type();
|
||||
let x_ty_2 = global_symbol(&db, a, "x").symbol.expect_type();
|
||||
|
||||
assert_eq!(x_ty_2.display(&db).to_string(), "int");
|
||||
|
||||
@@ -6912,7 +7033,7 @@ mod tests {
|
||||
)?;
|
||||
|
||||
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
|
||||
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
|
||||
|
||||
// Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
|
||||
@@ -6927,7 +7048,7 @@ mod tests {
|
||||
|
||||
let events = {
|
||||
db.clear_salsa_events();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
|
||||
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
|
||||
db.take_salsa_events()
|
||||
};
|
||||
@@ -6946,7 +7067,7 @@ mod tests {
|
||||
|
||||
let events = {
|
||||
db.clear_salsa_events();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
|
||||
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
|
||||
db.take_salsa_events()
|
||||
};
|
||||
@@ -6997,7 +7118,7 @@ mod tests {
|
||||
)?;
|
||||
|
||||
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
|
||||
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
|
||||
|
||||
// Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
|
||||
@@ -7014,7 +7135,7 @@ mod tests {
|
||||
|
||||
let events = {
|
||||
db.clear_salsa_events();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
|
||||
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
|
||||
db.take_salsa_events()
|
||||
};
|
||||
@@ -7035,7 +7156,7 @@ mod tests {
|
||||
|
||||
let events = {
|
||||
db.clear_salsa_events();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").expect_type();
|
||||
let attr_ty = global_symbol(&db, file_main, "x").symbol.expect_type();
|
||||
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
|
||||
db.take_salsa_events()
|
||||
};
|
||||
|
||||
@@ -100,13 +100,16 @@ impl Ty {
|
||||
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
|
||||
Ty::LiteralString => Type::LiteralString,
|
||||
Ty::BytesLiteral(s) => Type::bytes_literal(db, s.as_bytes()),
|
||||
Ty::BuiltinInstance(s) => builtins_symbol(db, s).expect_type().to_instance(db),
|
||||
Ty::BuiltinInstance(s) => builtins_symbol(db, s).symbol.expect_type().to_instance(db),
|
||||
Ty::AbcInstance(s) => known_module_symbol(db, KnownModule::Abc, s)
|
||||
.symbol
|
||||
.expect_type()
|
||||
.to_instance(db),
|
||||
Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s).expect_type(),
|
||||
Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s)
|
||||
.symbol
|
||||
.expect_type(),
|
||||
Ty::TypingLiteral => Type::KnownInstance(KnownInstanceType::Literal),
|
||||
Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).expect_type(),
|
||||
Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).symbol.expect_type(),
|
||||
Ty::KnownClassInstance(known_class) => known_class.to_instance(db),
|
||||
Ty::Union(tys) => {
|
||||
UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db)))
|
||||
@@ -129,6 +132,7 @@ impl Ty {
|
||||
Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from(
|
||||
db,
|
||||
builtins_symbol(db, s)
|
||||
.symbol
|
||||
.expect_type()
|
||||
.expect_class_literal()
|
||||
.class,
|
||||
@@ -136,16 +140,17 @@ impl Ty {
|
||||
Ty::SubclassOfAbcClass(s) => SubclassOfType::from(
|
||||
db,
|
||||
known_module_symbol(db, KnownModule::Abc, s)
|
||||
.symbol
|
||||
.expect_type()
|
||||
.expect_class_literal()
|
||||
.class,
|
||||
),
|
||||
Ty::AlwaysTruthy => Type::AlwaysTruthy,
|
||||
Ty::AlwaysFalsy => Type::AlwaysFalsy,
|
||||
Ty::BuiltinsFunction(name) => builtins_symbol(db, name).expect_type(),
|
||||
Ty::BuiltinsFunction(name) => builtins_symbol(db, name).symbol.expect_type(),
|
||||
Ty::BuiltinsBoundMethod { class, method } => {
|
||||
let builtins_class = builtins_symbol(db, class).expect_type();
|
||||
let function = builtins_class.static_member(db, method).expect_type();
|
||||
let builtins_class = builtins_symbol(db, class).symbol.expect_type();
|
||||
let function = builtins_class.member(db, method).symbol.expect_type();
|
||||
|
||||
create_bound_method(db, function, builtins_class)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::{definition_expression_type, Type};
|
||||
use super::{definition_expression_type, DynamicType, Type};
|
||||
use crate::Db;
|
||||
use crate::{semantic_index::definition::Definition, types::todo_type};
|
||||
use ruff_python_ast::{self as ast, name::Name};
|
||||
|
||||
/// A typed callable signature.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) struct Signature<'db> {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub struct Signature<'db> {
|
||||
/// Parameters, in source order.
|
||||
///
|
||||
/// The ordering of parameters in a valid signature must be: first positional-only parameters,
|
||||
@@ -67,29 +67,113 @@ impl<'db> Signature<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use SmallVec here once invariance bug is fixed
|
||||
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) struct Parameters<'db>(Vec<Parameter<'db>>);
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub(crate) struct Parameters<'db> {
|
||||
// TODO: use SmallVec here once invariance bug is fixed
|
||||
value: Vec<Parameter<'db>>,
|
||||
|
||||
/// Whether this parameter list represents a gradual form using `...` as the only parameter.
|
||||
///
|
||||
/// If this is `true`, the `value` will still contain the variadic and keyword-variadic
|
||||
/// parameters. This flag is used to distinguish between an explicit `...` in the callable type
|
||||
/// as in `Callable[..., int]` and the variadic arguments in `lambda` expression as in
|
||||
/// `lambda *args, **kwargs: None`.
|
||||
///
|
||||
/// The display implementation utilizes this flag to use `...` instead of displaying the
|
||||
/// individual variadic and keyword-variadic parameters.
|
||||
///
|
||||
/// Note: This flag is also used to indicate invalid forms of `Callable` annotations.
|
||||
is_gradual: bool,
|
||||
}
|
||||
|
||||
impl<'db> Parameters<'db> {
|
||||
pub(crate) fn new(parameters: impl IntoIterator<Item = Parameter<'db>>) -> Self {
|
||||
Self(parameters.into_iter().collect())
|
||||
Self {
|
||||
value: parameters.into_iter().collect(),
|
||||
is_gradual: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty parameter list.
|
||||
pub(crate) fn empty() -> Self {
|
||||
Self {
|
||||
value: Vec::new(),
|
||||
is_gradual: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_slice(&self) -> &[Parameter<'db>] {
|
||||
self.value.as_slice()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_gradual(&self) -> bool {
|
||||
self.is_gradual
|
||||
}
|
||||
|
||||
/// Return todo parameters: (*args: Todo, **kwargs: Todo)
|
||||
fn todo() -> Self {
|
||||
Self(vec![
|
||||
Parameter {
|
||||
name: Some(Name::new_static("args")),
|
||||
annotated_ty: Some(todo_type!("todo signature *args")),
|
||||
kind: ParameterKind::Variadic,
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("kwargs")),
|
||||
annotated_ty: Some(todo_type!("todo signature **kwargs")),
|
||||
kind: ParameterKind::KeywordVariadic,
|
||||
},
|
||||
])
|
||||
pub(crate) fn todo() -> Self {
|
||||
Self {
|
||||
value: vec![
|
||||
Parameter {
|
||||
name: Some(Name::new_static("args")),
|
||||
annotated_ty: Some(todo_type!("todo signature *args")),
|
||||
kind: ParameterKind::Variadic,
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("kwargs")),
|
||||
annotated_ty: Some(todo_type!("todo signature **kwargs")),
|
||||
kind: ParameterKind::KeywordVariadic,
|
||||
},
|
||||
],
|
||||
is_gradual: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return parameters that represents a gradual form using `...` as the only parameter.
|
||||
///
|
||||
/// Internally, this is represented as `(*Any, **Any)` that accepts parameters of type [`Any`].
|
||||
///
|
||||
/// [`Any`]: crate::types::DynamicType::Any
|
||||
pub(crate) fn gradual_form() -> Self {
|
||||
Self {
|
||||
value: vec![
|
||||
Parameter {
|
||||
name: None,
|
||||
annotated_ty: Some(Type::Dynamic(DynamicType::Any)),
|
||||
kind: ParameterKind::Variadic,
|
||||
},
|
||||
Parameter {
|
||||
name: None,
|
||||
annotated_ty: Some(Type::Dynamic(DynamicType::Any)),
|
||||
kind: ParameterKind::KeywordVariadic,
|
||||
},
|
||||
],
|
||||
is_gradual: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return parameters that represents an unknown list of parameters.
|
||||
///
|
||||
/// Internally, this is represented as `(*Unknown, **Unknown)` that accepts parameters of type
|
||||
/// [`Unknown`].
|
||||
///
|
||||
/// [`Unknown`]: crate::types::DynamicType::Unknown
|
||||
pub(crate) fn unknown() -> Self {
|
||||
Self {
|
||||
value: vec![
|
||||
Parameter {
|
||||
name: None,
|
||||
annotated_ty: Some(Type::Dynamic(DynamicType::Unknown)),
|
||||
kind: ParameterKind::Variadic,
|
||||
},
|
||||
Parameter {
|
||||
name: None,
|
||||
annotated_ty: Some(Type::Dynamic(DynamicType::Unknown)),
|
||||
kind: ParameterKind::KeywordVariadic,
|
||||
},
|
||||
],
|
||||
is_gradual: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_parameters(
|
||||
@@ -146,22 +230,21 @@ impl<'db> Parameters<'db> {
|
||||
let keywords = kwarg.as_ref().map(|arg| {
|
||||
Parameter::from_node_and_kind(db, definition, arg, ParameterKind::KeywordVariadic)
|
||||
});
|
||||
Self(
|
||||
Self::new(
|
||||
positional_only
|
||||
.chain(positional_or_keyword)
|
||||
.chain(variadic)
|
||||
.chain(keyword_only)
|
||||
.chain(keywords)
|
||||
.collect(),
|
||||
.chain(keywords),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
self.value.len()
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> std::slice::Iter<Parameter<'db>> {
|
||||
self.0.iter()
|
||||
self.value.iter()
|
||||
}
|
||||
|
||||
/// Iterate initial positional parameters, not including variadic parameter, if any.
|
||||
@@ -175,7 +258,7 @@ impl<'db> Parameters<'db> {
|
||||
|
||||
/// Return parameter at given index, or `None` if index is out-of-range.
|
||||
pub(crate) fn get(&self, index: usize) -> Option<&Parameter<'db>> {
|
||||
self.0.get(index)
|
||||
self.value.get(index)
|
||||
}
|
||||
|
||||
/// Return positional parameter at given index, or `None` if `index` is out of range.
|
||||
@@ -218,7 +301,7 @@ impl<'db, 'a> IntoIterator for &'a Parameters<'db> {
|
||||
type IntoIter = std::slice::Iter<'a, Parameter<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
self.value.iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,11 +309,11 @@ impl<'db> std::ops::Index<usize> for Parameters<'db> {
|
||||
type Output = Parameter<'db>;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
&self.value[index]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub(crate) struct Parameter<'db> {
|
||||
/// Parameter name.
|
||||
///
|
||||
@@ -272,6 +355,14 @@ impl<'db> Parameter<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_keyword_only(&self) -> bool {
|
||||
matches!(self.kind, ParameterKind::KeywordOnly { .. })
|
||||
}
|
||||
|
||||
pub(crate) fn is_positional_only(&self) -> bool {
|
||||
matches!(self.kind, ParameterKind::PositionalOnly { .. })
|
||||
}
|
||||
|
||||
pub(crate) fn is_variadic(&self) -> bool {
|
||||
matches!(self.kind, ParameterKind::Variadic)
|
||||
}
|
||||
@@ -328,7 +419,7 @@ impl<'db> Parameter<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub(crate) enum ParameterKind<'db> {
|
||||
/// Positional-only parameter, e.g. `def f(x, /): ...`
|
||||
PositionalOnly { default_ty: Option<Type<'db>> },
|
||||
@@ -354,13 +445,14 @@ mod tests {
|
||||
fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> {
|
||||
let module = ruff_db::files::system_path_to_file(db, file).unwrap();
|
||||
global_symbol(db, module, "f")
|
||||
.symbol
|
||||
.expect_type()
|
||||
.expect_function_literal()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_params<'db>(signature: &Signature<'db>, expected: &[Parameter<'db>]) {
|
||||
assert_eq!(signature.parameters.0.as_slice(), expected);
|
||||
assert_eq!(signature.parameters.value.as_slice(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -489,7 +581,7 @@ mod tests {
|
||||
name: Some(name),
|
||||
annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { .. },
|
||||
}] = &sig.parameters.0[..]
|
||||
}] = &sig.parameters.value[..]
|
||||
else {
|
||||
panic!("expected one positional-or-keyword parameter");
|
||||
};
|
||||
@@ -523,7 +615,7 @@ mod tests {
|
||||
name: Some(name),
|
||||
annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { .. },
|
||||
}] = &sig.parameters.0[..]
|
||||
}] = &sig.parameters.value[..]
|
||||
else {
|
||||
panic!("expected one positional-or-keyword parameter");
|
||||
};
|
||||
@@ -561,7 +653,7 @@ mod tests {
|
||||
name: Some(b_name),
|
||||
annotated_ty: b_annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { .. },
|
||||
}] = &sig.parameters.0[..]
|
||||
}] = &sig.parameters.value[..]
|
||||
else {
|
||||
panic!("expected two positional-or-keyword parameters");
|
||||
};
|
||||
@@ -604,7 +696,7 @@ mod tests {
|
||||
name: Some(b_name),
|
||||
annotated_ty: b_annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { .. },
|
||||
}] = &sig.parameters.0[..]
|
||||
}] = &sig.parameters.value[..]
|
||||
else {
|
||||
panic!("expected two positional-or-keyword parameters");
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ enum SlotsKind {
|
||||
|
||||
impl SlotsKind {
|
||||
fn from(db: &dyn Db, base: Class) -> Self {
|
||||
let Symbol::Type(slots_ty, bound) = base.own_class_member(db, "__slots__") else {
|
||||
let Symbol::Type(slots_ty, bound) = base.own_class_member(db, "__slots__").symbol else {
|
||||
return Self::NotSpecified;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::{ClassBase, ClassLiteralType, Db, KnownClass, Symbol, Type};
|
||||
use crate::symbol::SymbolAndQualifiers;
|
||||
|
||||
use super::{ClassBase, ClassLiteralType, Db, KnownClass, Type};
|
||||
|
||||
/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
|
||||
@@ -64,8 +66,12 @@ impl<'db> SubclassOfType<'db> {
|
||||
!self.is_dynamic()
|
||||
}
|
||||
|
||||
pub(crate) fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
Type::from(self.subclass_of).static_member(db, name)
|
||||
pub(crate) fn find_name_in_mro(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
) -> Option<SymbolAndQualifiers<'db>> {
|
||||
Type::from(self.subclass_of).find_name_in_mro(db, name)
|
||||
}
|
||||
|
||||
/// Return `true` if `self` is a subtype of `other`.
|
||||
|
||||
@@ -77,6 +77,12 @@ pub(super) fn union_elements_ordering<'db>(left: &Type<'db>, right: &Type<'db>)
|
||||
(Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater,
|
||||
|
||||
(Type::Callable(CallableType::General(_)), Type::Callable(CallableType::General(_))) => {
|
||||
Ordering::Equal
|
||||
}
|
||||
(Type::Callable(CallableType::General(_)), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::General(_))) => Ordering::Greater,
|
||||
|
||||
(Type::Tuple(left), Type::Tuple(right)) => left.cmp(right),
|
||||
(Type::Tuple(_), _) => Ordering::Less,
|
||||
(_, Type::Tuple(_)) => Ordering::Greater,
|
||||
@@ -184,6 +190,9 @@ pub(super) fn union_elements_ordering<'db>(left: &Type<'db>, right: &Type<'db>)
|
||||
(KnownInstanceType::OrderedDict, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::OrderedDict) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Protocol, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Protocol) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::NoReturn, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::NoReturn) => Ordering::Greater,
|
||||
|
||||
@@ -285,5 +294,8 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
(DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal,
|
||||
|
||||
(DynamicType::TodoProtocol, _) => Ordering::Less,
|
||||
(_, DynamicType::TodoProtocol) => Ordering::Greater,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,26 +42,28 @@ impl<'db> Unpacker<'db> {
|
||||
"Unpacking target must be a list or tuple expression"
|
||||
);
|
||||
|
||||
let mut value_ty = infer_expression_types(self.db(), value.expression())
|
||||
let value_ty = infer_expression_types(self.db(), value.expression())
|
||||
.expression_type(value.scoped_expression_id(self.db(), self.scope));
|
||||
|
||||
if value.is_assign()
|
||||
&& self.context.in_stub()
|
||||
&& value
|
||||
.expression()
|
||||
.node_ref(self.db())
|
||||
.is_ellipsis_literal_expr()
|
||||
{
|
||||
value_ty = Type::unknown();
|
||||
}
|
||||
if value.is_iterable() {
|
||||
// If the value is an iterable, then the type that needs to be unpacked is the iterator
|
||||
// type.
|
||||
value_ty = value_ty.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||
let value_ty = match value {
|
||||
UnpackValue::Assign(expression) => {
|
||||
if self.context.in_stub()
|
||||
&& expression.node_ref(self.db()).is_ellipsis_literal_expr()
|
||||
{
|
||||
Type::unknown()
|
||||
} else {
|
||||
value_ty
|
||||
}
|
||||
}
|
||||
UnpackValue::Iterable(_) => value_ty.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||
err.report_diagnostic(&self.context, value.as_any_node_ref(self.db()));
|
||||
err.fallback_element_type(self.db())
|
||||
});
|
||||
}
|
||||
}),
|
||||
UnpackValue::ContextManager(_) => value_ty.try_enter(self.db()).unwrap_or_else(|err| {
|
||||
err.report_diagnostic(&self.context, value.as_any_node_ref(self.db()));
|
||||
err.fallback_enter_type(self.db())
|
||||
}),
|
||||
};
|
||||
|
||||
self.unpack_inner(target, value.as_any_node_ref(self.db()), value_ty);
|
||||
}
|
||||
@@ -121,7 +123,7 @@ impl<'db> Unpacker<'db> {
|
||||
if let Some(tuple_ty) = ty.into_tuple() {
|
||||
let tuple_ty_elements = self.tuple_ty_elements(target, elts, tuple_ty);
|
||||
|
||||
match elts.len().cmp(&tuple_ty_elements.len()) {
|
||||
let length_mismatch = match elts.len().cmp(&tuple_ty_elements.len()) {
|
||||
Ordering::Less => {
|
||||
self.context.report_lint(
|
||||
&INVALID_ASSIGNMENT,
|
||||
@@ -132,6 +134,7 @@ impl<'db> Unpacker<'db> {
|
||||
tuple_ty_elements.len()
|
||||
),
|
||||
);
|
||||
true
|
||||
}
|
||||
Ordering::Greater => {
|
||||
self.context.report_lint(
|
||||
@@ -143,13 +146,18 @@ impl<'db> Unpacker<'db> {
|
||||
tuple_ty_elements.len()
|
||||
),
|
||||
);
|
||||
true
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
}
|
||||
Ordering::Equal => false,
|
||||
};
|
||||
|
||||
for (index, ty) in tuple_ty_elements.iter().enumerate() {
|
||||
if let Some(element_types) = target_types.get_mut(index) {
|
||||
element_types.push(*ty);
|
||||
if length_mismatch {
|
||||
element_types.push(Type::unknown());
|
||||
} else {
|
||||
element_types.push(*ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -243,15 +251,7 @@ impl<'db> Unpacker<'db> {
|
||||
),
|
||||
);
|
||||
|
||||
let mut element_types = tuple_ty.elements(self.db()).to_vec();
|
||||
|
||||
// Subtract 1 to insert the starred expression type at the correct
|
||||
// index.
|
||||
element_types.resize(targets.len() - 1, Type::unknown());
|
||||
// TODO: This should be `list[Unknown]`
|
||||
element_types.insert(starred_index, todo_type!("starred unpacking"));
|
||||
|
||||
Cow::Owned(element_types)
|
||||
Cow::Owned(vec![Type::unknown(); targets.len()])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,25 +63,19 @@ impl<'db> Unpack<'db> {
|
||||
pub(crate) enum UnpackValue<'db> {
|
||||
/// An iterable expression like the one in a `for` loop or a comprehension.
|
||||
Iterable(Expression<'db>),
|
||||
/// An context manager expression like the one in a `with` statement.
|
||||
ContextManager(Expression<'db>),
|
||||
/// An expression that is being assigned to a target.
|
||||
Assign(Expression<'db>),
|
||||
}
|
||||
|
||||
impl<'db> UnpackValue<'db> {
|
||||
/// Returns `true` if the value is an iterable expression.
|
||||
pub(crate) const fn is_iterable(self) -> bool {
|
||||
matches!(self, UnpackValue::Iterable(_))
|
||||
}
|
||||
|
||||
/// Returns `true` if the value is being assigned to a target.
|
||||
pub(crate) const fn is_assign(self) -> bool {
|
||||
matches!(self, UnpackValue::Assign(_))
|
||||
}
|
||||
|
||||
/// Returns the underlying [`Expression`] that is being unpacked.
|
||||
pub(crate) const fn expression(self) -> Expression<'db> {
|
||||
match self {
|
||||
UnpackValue::Assign(expr) | UnpackValue::Iterable(expr) => expr,
|
||||
UnpackValue::Assign(expr)
|
||||
| UnpackValue::Iterable(expr)
|
||||
| UnpackValue::ContextManager(expr) => expr,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
crates/red_knot_vendored/.gitignore
vendored
Normal file
4
crates/red_knot_vendored/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Do not ignore any of the vendored files. If this pattern is not present,
|
||||
# we will gitignore the `venv/` stubs in typeshed, as there is a general
|
||||
# rule to ignore `venv/` directories in the root `.gitignore`.
|
||||
!/vendor/typeshed/**/*
|
||||
107
crates/red_knot_vendored/vendor/typeshed/stdlib/venv/__init__.pyi
vendored
Normal file
107
crates/red_knot_vendored/vendor/typeshed/stdlib/venv/__init__.pyi
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
import logging
|
||||
import sys
|
||||
from _typeshed import StrOrBytesPath
|
||||
from collections.abc import Iterable, Sequence
|
||||
from types import SimpleNamespace
|
||||
|
||||
logger: logging.Logger
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
CORE_VENV_DEPS: tuple[str, ...]
|
||||
|
||||
class EnvBuilder:
|
||||
system_site_packages: bool
|
||||
clear: bool
|
||||
symlinks: bool
|
||||
upgrade: bool
|
||||
with_pip: bool
|
||||
prompt: str | None
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
def __init__(
|
||||
self,
|
||||
system_site_packages: bool = False,
|
||||
clear: bool = False,
|
||||
symlinks: bool = False,
|
||||
upgrade: bool = False,
|
||||
with_pip: bool = False,
|
||||
prompt: str | None = None,
|
||||
upgrade_deps: bool = False,
|
||||
*,
|
||||
scm_ignore_files: Iterable[str] = ...,
|
||||
) -> None: ...
|
||||
elif sys.version_info >= (3, 9):
|
||||
def __init__(
|
||||
self,
|
||||
system_site_packages: bool = False,
|
||||
clear: bool = False,
|
||||
symlinks: bool = False,
|
||||
upgrade: bool = False,
|
||||
with_pip: bool = False,
|
||||
prompt: str | None = None,
|
||||
upgrade_deps: bool = False,
|
||||
) -> None: ...
|
||||
else:
|
||||
def __init__(
|
||||
self,
|
||||
system_site_packages: bool = False,
|
||||
clear: bool = False,
|
||||
symlinks: bool = False,
|
||||
upgrade: bool = False,
|
||||
with_pip: bool = False,
|
||||
prompt: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
def create(self, env_dir: StrOrBytesPath) -> None: ...
|
||||
def clear_directory(self, path: StrOrBytesPath) -> None: ... # undocumented
|
||||
def ensure_directories(self, env_dir: StrOrBytesPath) -> SimpleNamespace: ...
|
||||
def create_configuration(self, context: SimpleNamespace) -> None: ...
|
||||
def symlink_or_copy(
|
||||
self, src: StrOrBytesPath, dst: StrOrBytesPath, relative_symlinks_ok: bool = False
|
||||
) -> None: ... # undocumented
|
||||
def setup_python(self, context: SimpleNamespace) -> None: ...
|
||||
def _setup_pip(self, context: SimpleNamespace) -> None: ... # undocumented
|
||||
def setup_scripts(self, context: SimpleNamespace) -> None: ...
|
||||
def post_setup(self, context: SimpleNamespace) -> None: ...
|
||||
def replace_variables(self, text: str, context: SimpleNamespace) -> str: ... # undocumented
|
||||
def install_scripts(self, context: SimpleNamespace, path: str) -> None: ...
|
||||
if sys.version_info >= (3, 9):
|
||||
def upgrade_dependencies(self, context: SimpleNamespace) -> None: ...
|
||||
if sys.version_info >= (3, 13):
|
||||
def create_git_ignore_file(self, context: SimpleNamespace) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
def create(
|
||||
env_dir: StrOrBytesPath,
|
||||
system_site_packages: bool = False,
|
||||
clear: bool = False,
|
||||
symlinks: bool = False,
|
||||
with_pip: bool = False,
|
||||
prompt: str | None = None,
|
||||
upgrade_deps: bool = False,
|
||||
*,
|
||||
scm_ignore_files: Iterable[str] = ...,
|
||||
) -> None: ...
|
||||
|
||||
elif sys.version_info >= (3, 9):
|
||||
def create(
|
||||
env_dir: StrOrBytesPath,
|
||||
system_site_packages: bool = False,
|
||||
clear: bool = False,
|
||||
symlinks: bool = False,
|
||||
with_pip: bool = False,
|
||||
prompt: str | None = None,
|
||||
upgrade_deps: bool = False,
|
||||
) -> None: ...
|
||||
|
||||
else:
|
||||
def create(
|
||||
env_dir: StrOrBytesPath,
|
||||
system_site_packages: bool = False,
|
||||
clear: bool = False,
|
||||
symlinks: bool = False,
|
||||
with_pip: bool = False,
|
||||
prompt: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
def main(args: Sequence[str] | None = None) -> None: ...
|
||||
@@ -45,3 +45,10 @@ crypt.crypt("test", salt=crypt.METHOD_SHA512)
|
||||
crypt.mksalt()
|
||||
crypt.mksalt(crypt.METHOD_SHA256)
|
||||
crypt.mksalt(crypt.METHOD_SHA512)
|
||||
|
||||
# From issue: https://github.com/astral-sh/ruff/issues/16525#issuecomment-2706188584
|
||||
# Errors
|
||||
hashlib.new("Md5")
|
||||
|
||||
# OK
|
||||
hashlib.new('Sha256')
|
||||
|
||||
@@ -135,11 +135,11 @@ fn detect_insecure_hashlib_calls(
|
||||
return;
|
||||
};
|
||||
|
||||
// `hashlib.new` accepts both lowercase and uppercase names for hash
|
||||
// `hashlib.new` accepts mixed lowercase and uppercase names for hash
|
||||
// functions.
|
||||
if matches!(
|
||||
hash_func_name,
|
||||
"md4" | "md5" | "sha" | "sha1" | "MD4" | "MD5" | "SHA" | "SHA1"
|
||||
hash_func_name.to_ascii_lowercase().as_str(),
|
||||
"md4" | "md5" | "sha" | "sha1"
|
||||
) {
|
||||
checker.report_diagnostic(Diagnostic::new(
|
||||
HashlibInsecureHashFunction {
|
||||
|
||||
@@ -195,3 +195,13 @@ S324.py:29:14: S324 Probable use of insecure hash functions in `crypt`: `crypt.M
|
||||
30 |
|
||||
31 | # OK
|
||||
|
|
||||
|
||||
S324.py:51:13: S324 Probable use of insecure hash functions in `hashlib`: `Md5`
|
||||
|
|
||||
49 | # From issue: https://github.com/astral-sh/ruff/issues/16525#issuecomment-2706188584
|
||||
50 | # Errors
|
||||
51 | hashlib.new("Md5")
|
||||
| ^^^^^ S324
|
||||
52 |
|
||||
53 | # OK
|
||||
|
|
||||
|
||||
@@ -37,6 +37,8 @@ use crate::rules::pep8_naming::helpers;
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-import-conventions.aliases`
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/
|
||||
#[derive(ViolationMetadata)]
|
||||
|
||||
@@ -44,6 +44,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// A common example of a single uppercase character being used for a class
|
||||
/// name can be found in Django's `django.db.models.Q` class.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct CamelcaseImportedAsConstant {
|
||||
|
||||
@@ -29,6 +29,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// from example import MyClassName
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct CamelcaseImportedAsLowercase {
|
||||
|
||||
@@ -42,6 +42,10 @@ use crate::rules::pep8_naming::{helpers, settings::IgnoreNames};
|
||||
/// A common example of a single uppercase character being used for a class
|
||||
/// name can be found in Django's `django.db.models.Q` class.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct ConstantImportedAsNonConstant {
|
||||
|
||||
@@ -31,6 +31,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct DunderFunctionName;
|
||||
|
||||
@@ -28,6 +28,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// class ValidationError(Exception): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#exception-names
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct ErrorSuffixOnExceptionName {
|
||||
|
||||
@@ -37,6 +37,10 @@ use crate::checkers::ast::Checker;
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
|
||||
@@ -34,6 +34,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#class-names
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct InvalidClassName {
|
||||
|
||||
@@ -28,6 +28,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// from example import myclassname
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct LowercaseImportedAsNonLowercase {
|
||||
|
||||
@@ -35,6 +35,10 @@ use crate::rules::pep8_naming::helpers;
|
||||
/// another_variable = "world"
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct MixedCaseVariableInClassScope {
|
||||
|
||||
@@ -46,6 +46,10 @@ use crate::rules::pep8_naming::helpers;
|
||||
/// yet_another_variable = "foo"
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
/// - `lint.pep8-naming.extend-ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#global-variable-names
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct MixedCaseVariableInGlobalScope {
|
||||
|
||||
@@ -2179,6 +2179,13 @@ impl ExprName {
|
||||
pub fn id(&self) -> &Name {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// Returns `true` if this node represents an invalid name i.e., the `ctx` is [`Invalid`].
|
||||
///
|
||||
/// [`Invalid`]: ExprContext::Invalid
|
||||
pub const fn is_invalid(&self) -> bool {
|
||||
matches!(self.ctx, ExprContext::Invalid)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprList {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
# Migrating from `ruff-lsp`
|
||||
|
||||
To provide some context, [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) is the LSP implementation for Ruff to power the editor
|
||||
integrations which is written in Python and is a separate package from Ruff itself. The **native
|
||||
server** is the LSP implementation which is written in Rust and is available under the `ruff server`
|
||||
command. This guide is intended to help users migrate from
|
||||
[`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) to the native server.
|
||||
[`ruff-lsp`][ruff-lsp] is the [Language Server Protocol] implementation for Ruff to power the editor
|
||||
integrations. It is written in Python and is a separate package from Ruff itself. The **native
|
||||
server**, however, is the [Language Server Protocol] implementation which is **written in Rust** and
|
||||
is available under the `ruff server` command. This guide is intended to help users migrate from
|
||||
[`ruff-lsp`][ruff-lsp] to the native server.
|
||||
|
||||
!!! note
|
||||
|
||||
The native server was first introduced in Ruff version `0.3.5`. It was marked as beta in version
|
||||
`0.4.5` and officially stabilized in version `0.5.3`. It is recommended to use the latest
|
||||
version of Ruff to ensure the best experience.
|
||||
The native server was first introduced in Ruff version `0.3.5`. It was marked as [beta in
|
||||
version `0.4.5`](https://astral.sh/blog/ruff-v0.4.5) and officially [stabilized in version
|
||||
`0.5.3`](https://github.com/astral-sh/ruff/releases/tag/0.5.3). It is recommended to use the
|
||||
latest version of Ruff to ensure the best experience.
|
||||
|
||||
The migration process involves any or all of the following:
|
||||
|
||||
@@ -18,32 +19,36 @@ The migration process involves any or all of the following:
|
||||
1. [Remove settings](#removed-settings) that are no longer supported
|
||||
1. Update the `ruff` version
|
||||
|
||||
Read on to learn more about the unsupported or new settings, or jump to the [examples](#examples)
|
||||
that enumerate some of the common settings and how to migrate them.
|
||||
|
||||
## Unsupported Settings
|
||||
|
||||
The following [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) settings are not supported by `ruff server`:
|
||||
The following [`ruff-lsp`][ruff-lsp] settings are not supported by the native server:
|
||||
|
||||
- `lint.run`: This setting is no longer relevant for the native language server, which runs on every
|
||||
keystroke by default.
|
||||
- `lint.args`, `format.args`: These settings have been replaced by more granular settings in `ruff server` like [`lint.select`](settings.md#select), [`format.preview`](settings.md#format_preview),
|
||||
etc. along with the ability to provide a default configuration file using [`configuration`](settings.md#configuration).
|
||||
- [`lint.run`](settings.md#lintrun): This setting is no longer relevant for the native language
|
||||
server, which runs on every keystroke by default.
|
||||
- [`lint.args`](settings.md#lintargs), [`format.args`](settings.md#formatargs): These settings have
|
||||
been replaced by more granular settings in the native server like [`lint.select`](settings.md#select),
|
||||
[`format.preview`](settings.md#format_preview), etc. along with the ability to override any
|
||||
configuration using the [`configuration`](settings.md#configuration) setting.
|
||||
|
||||
The following settings are not accepted by the language server but are still used by the VS Code
|
||||
extension. Refer to their respective documentation for more information on how it's being used by
|
||||
the extension:
|
||||
The following settings are not accepted by the language server but are still used by the [VS Code extension].
|
||||
Refer to their respective documentation for more information on how each is used by the extension:
|
||||
|
||||
- [`path`](settings.md#path)
|
||||
- [`interpreter`](settings.md#interpreter)
|
||||
|
||||
## Removed Settings
|
||||
|
||||
Additionally, the following settings are not supported by the native server, they should be removed:
|
||||
Additionally, the following settings are not supported by the native server and should be removed:
|
||||
|
||||
- `ignoreStandardLibrary`
|
||||
- `showNotifications`
|
||||
- [`ignoreStandardLibrary`](settings.md#ignorestandardlibrary)
|
||||
- [`showNotifications`](settings.md#shownotifications)
|
||||
|
||||
## New Settings
|
||||
|
||||
`ruff server` introduces several new settings that [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) does not have. These are, as follows:
|
||||
The native server introduces several new settings that [`ruff-lsp`][ruff-lsp] does not have:
|
||||
|
||||
- [`configuration`](settings.md#configuration)
|
||||
- [`configurationPreference`](settings.md#configurationpreference)
|
||||
@@ -55,44 +60,96 @@ Additionally, the following settings are not supported by the native server, the
|
||||
- [`lint.ignore`](settings.md#ignore)
|
||||
- [`lint.preview`](settings.md#lint_preview)
|
||||
|
||||
Several of these new settings are replacements for the now-unsupported `format.args` and `lint.args`. For example, if
|
||||
you've been passing `--select=<RULES>` to `lint.args`, you can migrate to the new server by using `lint.select` with a
|
||||
value of `["<RULES>"]`.
|
||||
|
||||
## Examples
|
||||
|
||||
Let's say you have these settings in VS Code:
|
||||
All of the examples mentioned below are only valid for the [VS Code extension]. For other editors,
|
||||
please refer to their respective documentation sections in the [settings](settings.md) page.
|
||||
|
||||
### Configuration file
|
||||
|
||||
If you've been providing a configuration file as shown below:
|
||||
|
||||
```json
|
||||
{
|
||||
"ruff.lint.args": "--select=E,F --line-length 80 --config ~/.config/custom_ruff_config.toml"
|
||||
"ruff.lint.args": "--config ~/.config/custom_ruff_config.toml",
|
||||
"ruff.format.args": "--config ~/.config/custom_ruff_config.toml"
|
||||
}
|
||||
```
|
||||
|
||||
After enabling the native server, you can migrate your settings like so:
|
||||
You can migrate to the new server by using the [`configuration`](settings.md#configuration) setting
|
||||
like below which will apply the configuration to both the linter and the formatter:
|
||||
|
||||
```json
|
||||
{
|
||||
"ruff.configuration": "~/.config/custom_ruff_config.toml"
|
||||
}
|
||||
```
|
||||
|
||||
### `lint.args`
|
||||
|
||||
If you're providing the linter flags by using `ruff.lint.args` like so:
|
||||
|
||||
```json
|
||||
{
|
||||
"ruff.lint.args": "--select=E,F --unfixable=F401 --unsafe-fixes"
|
||||
}
|
||||
```
|
||||
|
||||
You can migrate to the new server by using the [`lint.select`](settings.md#select) and
|
||||
[`configuration`](settings.md#configuration) setting like so:
|
||||
|
||||
```json
|
||||
{
|
||||
"ruff.lint.select": ["E", "F"],
|
||||
"ruff.configuration": {
|
||||
"unsafe-fixes": true,
|
||||
"lint": {
|
||||
"unfixable": ["F401"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The following options can be set directly in the editor settings:
|
||||
|
||||
- [`lint.select`](settings.md#select)
|
||||
- [`lint.extendSelect`](settings.md#extendselect)
|
||||
- [`lint.ignore`](settings.md#ignore)
|
||||
- [`lint.preview`](settings.md#lint_preview)
|
||||
|
||||
The remaining options can be set using the [`configuration`](settings.md#configuration) setting.
|
||||
|
||||
### `format.args`
|
||||
|
||||
If you're also providing formatter flags by using `ruff.format.args` like so:
|
||||
|
||||
```json
|
||||
{
|
||||
"ruff.format.args": "--line-length 80 --config='format.quote-style=double'"
|
||||
}
|
||||
```
|
||||
|
||||
You can migrate to the new server by using the [`lineLength`](settings.md#linelength) and
|
||||
[`configuration`](settings.md#configuration) setting like so:
|
||||
|
||||
```json
|
||||
{
|
||||
"ruff.configuration": "~/.config/custom_ruff_config.toml",
|
||||
"ruff.lineLength": 80,
|
||||
"ruff.lint.select": ["E", "F"]
|
||||
"ruff.configuration": {
|
||||
"format": {
|
||||
"quote-style": "double"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, let's say you have these settings in Helix:
|
||||
The following options can be set directly in the editor settings:
|
||||
|
||||
```toml
|
||||
[language-server.ruff.config.lint]
|
||||
args = "--select=E,F --line-length 80 --config ~/.config/custom_ruff_config.toml"
|
||||
```
|
||||
- [`lineLength`](settings.md#linelength)
|
||||
- [`format.preview`](settings.md#format_preview)
|
||||
|
||||
These can be migrated like so:
|
||||
The remaining options can be set using the [`configuration`](settings.md#configuration) setting.
|
||||
|
||||
```toml
|
||||
[language-server.ruff.config]
|
||||
configuration = "~/.config/custom_ruff_config.toml"
|
||||
lineLength = 80
|
||||
|
||||
[language-server.ruff.config.lint]
|
||||
select = ["E", "F"]
|
||||
```
|
||||
[language server protocol]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
|
||||
[ruff-lsp]: https://github.com/astral-sh/ruff-lsp
|
||||
[vs code extension]: https://github.com/astral-sh/ruff-vscode
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PyYAML==6.0.2
|
||||
ruff==0.9.9
|
||||
ruff==0.9.10
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@39da7a5e761410349e9a1b8abf593b0cdd5453ff
|
||||
mkdocs-redirects==1.2.2
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PyYAML==6.0.2
|
||||
ruff==0.9.9
|
||||
ruff==0.9.10
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.5.38
|
||||
mkdocs-redirects==1.2.2
|
||||
|
||||
Reference in New Issue
Block a user