Compare commits

..

3 Commits

Author SHA1 Message Date
Carl Meyer
a3daed0178 temporarily change output to see if the comment works 2025-04-19 06:38:26 -07:00
Carl Meyer
f218b228e3 attempt to shard mypy-primer 2025-04-19 06:38:23 -07:00
Carl Meyer
b78112aae4 [red-knot] update mypy-primer projects list 2025-04-19 06:36:24 -07:00
36 changed files with 729 additions and 1059 deletions

View File

@@ -377,7 +377,7 @@ jobs:
args: --release --locked --out dist
- name: "Test wheel"
if: matrix.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3
uses: addnab/docker-run-action@v3
with:
image: alpine:latest
options: -v ${{ github.workspace }}:/io -w /io

View File

@@ -237,13 +237,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2
with:
tool: cargo-insta
- name: Red-knot mdtests (GitHub annotations)
@@ -291,13 +291,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -320,7 +320,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -376,7 +376,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- name: "Build"
run: cargo build --release --locked
@@ -401,13 +401,13 @@ jobs:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -433,7 +433,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
uses: cargo-bins/cargo-binstall@main
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -455,7 +455,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
name: Download Ruff binary to test
id: download-cached-binary
@@ -641,7 +641,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
- uses: cargo-bins/cargo-binstall@main
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -681,7 +681,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- name: "Cache pre-commit"
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
@@ -720,7 +720,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -857,7 +857,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@be7c31b6745feec79dec5eb79178466c0670bb2d # v2
with:
tool: cargo-codspeed

View File

@@ -34,11 +34,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: Build ruff
# A debug build means the script runs slower once it gets started,

View File

@@ -36,7 +36,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: Build Red Knot
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release

View File

@@ -26,6 +26,10 @@ jobs:
mypy_primer:
name: Run mypy_primer
runs-on: depot-ubuntu-22.04-16
strategy:
matrix:
shard-index: [0, 1, 2]
fail-fast: false
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -35,7 +39,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
@@ -64,6 +68,8 @@ jobs:
cd ..
DIFF_FILE="mypy_primer_${{ matrix.shard-index }}.diff"
echo "Project selector: $PRIMER_SELECTOR"
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx mypy_primer \
@@ -71,26 +77,31 @@ jobs:
--type-checker knot \
--old base_commit \
--new "$GITHUB_SHA" \
--num-shards 3 --shard-index ${{ matrix.shard-index }} \
--project-selector "/($PRIMER_SELECTOR)\$" \
--output concise \
--debug > mypy_primer.diff || [ $? -eq 1 ]
--debug > $DIFF_FILE || [ $? -eq 1 ]
# Output diff with ANSI color codes
cat mypy_primer.diff
cat $DIFF_FILE
# Remove ANSI color codes before uploading
sed -ie 's/\x1b\[[0-9;]*m//g' mypy_primer.diff
sed -ie 's/\x1b\[[0-9;]*m//g' $DIFF_FILE
echo ${{ github.event.number }} > pr-number
- if: ${{ matrix.shard-index == 0 }}
name: Save PR number
run: |
echo ${{ github.event.pull_request.number }} | tee pr-number
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: mypy_primer_diff
path: mypy_primer.diff
name: mypy_primer_diffs-${{ matrix.shard-index }}
path: mypy_primer_${{ matrix.shard-index }}.diff
- name: Upload pr-number
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ matrix.shard-index == 0 }}
with:
name: pr-number
path: pr-number

View File

@@ -1,97 +1,96 @@
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
workflows:
- Run mypy_primer
types:
- completed
permissions: {}
jobs:
comment:
runs-on: ubuntu-24.04
name: Comment PR from mypy_primer
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download PR number
- name: Download diffs
uses: actions/github-script@v7
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
script: |
const fs = require('fs');
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }},
});
const [matchArtifact] = artifacts.data.artifacts.filter((artifact) =>
artifact.name == "mypy_primer_diffs");
- name: Parse pull request number
id: pr-number
run: |
if [[ -f pr-number ]]
then
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
const download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: "zip",
});
fs.writeFileSync("diff.zip", Buffer.from(download.data));
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: "Download mypy_primer results"
id: download-mypy_primer_diff
if: steps.pr-number.outputs.pr-number
- run: unzip diff.zip
- run: |
cat mypy_primer_*.diff | tee fulldiff.txt
- name: Post comment
id: post-comment
uses: actions/github-script@v7
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
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const MAX_CHARACTERS = 50000
const MAX_CHARACTERS_PER_PROJECT = MAX_CHARACTERS / 3
- 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
const fs = require('fs')
let data = fs.readFileSync('fulldiff.txt', { encoding: 'utf8' })
# Note this identifier is used to find the comment to update on
# subsequent runs
echo '<!-- generated-comment mypy_primer -->' >> comment.txt
function truncateIfNeeded(original, maxLength) {
if (original.length <= maxLength) {
return original
}
let truncated = original.substring(0, maxLength)
// further, remove last line that might be truncated
truncated = truncated.substring(0, truncated.lastIndexOf('\n'))
let lines_truncated = original.split('\n').length - truncated.split('\n').length
return `${truncated}\n\n... (truncated ${lines_truncated} lines) ...`
}
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
const projects = data.split('\n\n')
// don't let one project dominate
data = projects.map(project => truncateIfNeeded(project, MAX_CHARACTERS_PER_PROJECT)).join('\n\n')
// posting comment fails if too long, so truncate
data = truncateIfNeeded(data, MAX_CHARACTERS)
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.txt >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
console.log("Diff from mypy_primer:")
console.log(data)
- name: Find existing comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
if: steps.generate-comment.outcome == 'success'
id: find-comment
let body
if (data.trim()) {
body = 'Diff from [mypy_primer](https://github.com/hauntsaninja/mypy_primer), showing the effect of this PR on open source code:\n```diff\n' + data + '```'
} else {
body = "According to [mypy_primer](https://github.com/hauntsaninja/mypy_primer), this change doesn't affect type check results on a corpus of open source code. ✅"
}
const prNumber = parseInt(fs.readFileSync("pr-number", { encoding: "utf8" }))
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body
})
return prNumber
- name: Hide old comments
# v0.4.0
uses: kanga333/comment-hider@c12bb20b48aeb8fc098e35967de8d4f8018fffdf
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@71345be0265236311c031f5c7866368bd1eff043 # 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
github_token: ${{ secrets.GITHUB_TOKEN }}
leave_visible: 1
issue_number: ${{ steps.post-comment.outputs.result }}

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
pattern: wheels-*

View File

@@ -79,7 +79,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.6
rev: v0.11.5
hooks:
- id: ruff-format
- id: ruff
@@ -97,7 +97,7 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.6.0
rev: v1.5.2
hooks:
- id: zizmor

55
Cargo.lock generated
View File

@@ -334,9 +334,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.37"
version = "4.5.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
dependencies = [
"clap_builder",
"clap_derive",
@@ -344,9 +344,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.37"
version = "4.5.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
dependencies = [
"anstream",
"anstyle",
@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -487,7 +487,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -918,7 +918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1499,7 +1499,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.0",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1553,9 +1553,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.9"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ec30f7142be6fe14e1b021f50b85db8df2d4324ea6e91ec3e5dcde092021d0"
checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
@@ -1563,14 +1563,14 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
name = "jiff-static"
version = "0.2.9"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "526b834d727fd59d37b076b0c3236d9adde1b1729a4361e20b2026f738cc1dbe"
checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9"
dependencies = [
"proc-macro2",
"quote",
@@ -1645,9 +1645,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.172"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libcst"
@@ -2327,9 +2327,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.95"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
@@ -2420,12 +2420,13 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.1"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy",
]
[[package]]
@@ -3083,7 +3084,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
"rand 0.9.1",
"rand 0.9.0",
"ruff_diagnostics",
"ruff_source_file",
"ruff_text_size",
@@ -3427,7 +3428,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -3440,7 +3441,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.3",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -3674,9 +3675,9 @@ dependencies = [
[[package]]
name = "shellexpand"
version = "3.1.1"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
dependencies = [
"dirs",
]
@@ -3826,7 +3827,7 @@ dependencies = [
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.2",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -4327,7 +4328,7 @@ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.2",
"js-sys",
"rand 0.9.1",
"rand 0.9.0",
"uuid-macro-internal",
"wasm-bindgen",
]
@@ -4598,7 +4599,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.48.0",
]
[[package]]

View File

@@ -295,7 +295,7 @@ impl MainLoop {
writeln!(
stdout,
"Found {} diagnostic{}",
"Founded {} diagnostic{}",
diagnostics_count,
if diagnostics_count > 1 { "s" } else { "" }
)?;

View File

@@ -28,7 +28,7 @@ fn config_override_python_version() -> anyhow::Result<()> {
),
])?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -40,10 +40,10 @@ fn config_override_python_version() -> anyhow::Result<()> {
| ^^^^^^^^^^^^ Type `<module 'sys'>` has no attribute `last_exc`
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
");
"###);
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r"
success: true
@@ -79,7 +79,7 @@ fn config_override_python_platform() -> anyhow::Result<()> {
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -92,12 +92,12 @@ fn config_override_python_platform() -> anyhow::Result<()> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `Literal["linux"]`
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
"#);
"###);
assert_cmd_snapshot!(case.command().arg("--python-platform").arg("all"), @r"
assert_cmd_snapshot!(case.command().arg("--python-platform").arg("all"), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -110,10 +110,10 @@ fn config_override_python_platform() -> anyhow::Result<()> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `LiteralString`
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -161,7 +161,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
])?;
// Make sure that the CLI fails when the `libs` directory is not in the search path.
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -174,10 +174,10 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
4 | stat = add(10, 15)
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
");
"###);
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r"
success: true
@@ -261,7 +261,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -283,10 +283,10 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
| ^ Name `x` used when possibly not defined
|
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
");
"###);
case.write_file(
"pyproject.toml",
@@ -297,7 +297,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -310,10 +310,10 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
4 | for a in range(0, int(y)):
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -337,7 +337,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -370,10 +370,10 @@ fn cli_rule_severity() -> anyhow::Result<()> {
| ^ Name `x` used when possibly not defined
|
Found 3 diagnostics
Founded 3 diagnostics
----- stderr -----
");
"###);
assert_cmd_snapshot!(
case
@@ -384,7 +384,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
.arg("division-by-zero")
.arg("--warn")
.arg("unresolved-import"),
@r"
@r###"
success: true
exit_code: 0
----- stdout -----
@@ -408,10 +408,10 @@ fn cli_rule_severity() -> anyhow::Result<()> {
6 | for a in range(0, int(y)):
|
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
"
"###
);
Ok(())
@@ -435,7 +435,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -457,10 +457,10 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
| ^ Name `x` used when possibly not defined
|
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
");
"###);
assert_cmd_snapshot!(
case
@@ -472,7 +472,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
// Override the error severity with warning
.arg("--ignore")
.arg("possibly-unresolved-reference"),
@r"
@r###"
success: true
exit_code: 0
----- stdout -----
@@ -485,10 +485,10 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
4 | for a in range(0, int(y)):
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
"
"###
);
Ok(())
@@ -508,7 +508,7 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
("test.py", "print(10)"),
])?;
assert_cmd_snapshot!(case.command(), @r#"
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -520,10 +520,10 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
| ^^^^^^^^^^^^^^^ Unknown lint rule `division-by-zer`
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
"#);
"###);
Ok(())
}
@@ -533,16 +533,16 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
fn cli_unknown_rules() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", "print(10)")?;
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r"
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r###"
success: true
exit_code: 0
----- stdout -----
warning: unknown-rule: Unknown lint rule `division-by-zer`
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -551,7 +551,7 @@ fn cli_unknown_rules() -> anyhow::Result<()> {
fn exit_code_only_warnings() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -562,10 +562,10 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
| ^ Name `x` used when not defined
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -580,7 +580,7 @@ fn exit_code_only_info() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -592,10 +592,10 @@ fn exit_code_only_info() -> anyhow::Result<()> {
| ^^^^^^^^^^^^^^ `Literal[1]`
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -610,7 +610,7 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -622,10 +622,10 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
| ^^^^^^^^^^^^^^ `Literal[1]`
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -634,7 +634,7 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -645,10 +645,10 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
| ^ Name `x` used when not defined
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -666,7 +666,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
),
])?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -677,10 +677,10 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
| ^ Name `x` used when not defined
|
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -695,7 +695,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -715,10 +715,10 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
");
"###);
Ok(())
}
@@ -733,7 +733,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
"###,
)?;
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -753,10 +753,10 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
");
"###);
Ok(())
}
@@ -771,7 +771,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--exit-zero"), @r"
assert_cmd_snapshot!(case.command().arg("--exit-zero"), @r###"
success: true
exit_code: 0
----- stdout -----
@@ -791,10 +791,10 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
");
"###);
Ok(())
}
@@ -831,7 +831,7 @@ fn user_configuration() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r"
@r###"
success: true
exit_code: 0
----- stdout -----
@@ -853,10 +853,10 @@ fn user_configuration() -> anyhow::Result<()> {
| ^ Name `x` used when possibly not defined
|
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
"
"###
);
// The user-level configuration promotes `possibly-unresolved-reference` to an error.
@@ -873,7 +873,7 @@ fn user_configuration() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r"
@r###"
success: false
exit_code: 1
----- stdout -----
@@ -895,10 +895,10 @@ fn user_configuration() -> anyhow::Result<()> {
| ^ Name `x` used when possibly not defined
|
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
"
"###
);
Ok(())
@@ -931,7 +931,7 @@ fn check_specific_paths() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case.command(),
@r"
@r###"
success: false
exit_code: 1
----- stdout -----
@@ -958,17 +958,17 @@ fn check_specific_paths() -> anyhow::Result<()> {
4 | print(z)
|
Found 3 diagnostics
Founded 3 diagnostics
----- stderr -----
"
"###
);
// Now check only the `tests` and `other.py` files.
// We should no longer see any diagnostics related to `main.py`.
assert_cmd_snapshot!(
case.command().arg("project/tests").arg("project/other.py"),
@r"
@r###"
success: false
exit_code: 1
----- stdout -----
@@ -988,10 +988,10 @@ fn check_specific_paths() -> anyhow::Result<()> {
4 | print(z)
|
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
"
"###
);
Ok(())
@@ -1010,7 +1010,7 @@ fn check_non_existing_path() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case.command().arg("project/main.py").arg("project/tests"),
@r"
@r###"
success: false
exit_code: 1
----- stdout -----
@@ -1018,11 +1018,11 @@ fn check_non_existing_path() -> anyhow::Result<()> {
error: io: `<temp_dir>/project/tests`: No such file or directory (os error 2)
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
WARN No python files found under the given path(s)
"
"###
);
Ok(())
@@ -1038,16 +1038,16 @@ fn concise_diagnostics() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r"
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r###"
success: false
exit_code: 1
----- stdout -----
warning[lint:unresolved-reference] <temp_dir>/test.py:2:7: Name `x` used when not defined
error[lint:non-subscriptable] <temp_dir>/test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
Found 2 diagnostics
Founded 2 diagnostics
----- stderr -----
");
"###);
Ok(())
}
@@ -1072,15 +1072,15 @@ fn concise_revealed_type() -> anyhow::Result<()> {
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r#"
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r###"
success: true
exit_code: 0
----- stdout -----
info[revealed-type] <temp_dir>/test.py:5:1: Revealed type: `Literal["hello"]`
Found 1 diagnostic
Founded 1 diagnostic
----- stderr -----
"#);
"###);
Ok(())
}

View File

@@ -6,9 +6,7 @@ use ruff_db::parsed::parsed_module;
use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem};
use ruff_python_ast::visitor::source_order;
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
use ruff_python_ast::{
self as ast, Alias, Comprehension, Expr, Parameter, ParameterWithDefault, Stmt,
};
use ruff_python_ast::{self as ast, Alias, Expr, Parameter, ParameterWithDefault, Stmt};
fn setup_db(project_root: &SystemPath, system: TestSystem) -> anyhow::Result<ProjectDatabase> {
let project = ProjectMetadata::discover(project_root, &system)?;
@@ -260,14 +258,6 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
source_order::walk_expr(self, expr);
}
fn visit_comprehension(&mut self, comprehension: &Comprehension) {
self.visit_expr(&comprehension.iter);
self.visit_target(&comprehension.target);
for if_expr in &comprehension.ifs {
self.visit_expr(if_expr);
}
}
fn visit_parameter(&mut self, parameter: &Parameter) {
let _ty = parameter.inferred_type(&self.model);

View File

@@ -397,27 +397,15 @@ class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
class TupleIterator:
def __next__(self) -> tuple[int, str]:
return (1, "a")
class TupleIterable:
def __iter__(self) -> TupleIterator:
return TupleIterator()
class C:
def __init__(self) -> None:
[... for self.a in IntIterable()]
[... for (self.b, self.c) in TupleIterable()]
[... for self.d in IntIterable() for self.e in IntIterable()]
c_instance = C()
reveal_type(c_instance.a) # revealed: Unknown | int
reveal_type(c_instance.b) # revealed: Unknown | int
reveal_type(c_instance.c) # revealed: Unknown | str
reveal_type(c_instance.d) # revealed: Unknown | int
reveal_type(c_instance.e) # revealed: Unknown | int
# TODO: Should be `Unknown | int`
# error: [unresolved-attribute]
reveal_type(c_instance.a) # revealed: Unknown
```
#### Conditionally declared / bound attributes

View File

@@ -74,6 +74,8 @@ class Baz(Bar):
T = TypeVar("T")
class Qux(Protocol[T]):
# TODO: no error
# error: [invalid-return-type]
def f(self) -> int: ...
class Foo(Protocol):

View File

@@ -40,63 +40,27 @@ class Foo(Protocol, Protocol): ... # error: [inconsistent-mro]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
```
Protocols can also be generic, either by including `Generic[]` in the bases list, subscripting
`Protocol` directly in the bases list, using PEP-695 type parameters, or some combination of the
above:
```py
from typing import TypeVar, Generic
T = TypeVar("T")
class Bar0(Protocol[T]):
x: T
class Bar1(Protocol[T], Generic[T]):
x: T
class Bar2[T](Protocol):
x: T
class Bar3[T](Protocol[T]):
x: T
```
It's an error to include both bare `Protocol` and subscripted `Protocol[]` in the bases list
simultaneously:
```py
# TODO: should emit a `[duplicate-bases]` error here:
class DuplicateBases(Protocol, Protocol[T]):
x: T
# TODO: should not have `Generic` multiple times and `Protocol` multiple times
# revealed: tuple[Literal[DuplicateBases], typing.Protocol, typing.Generic, @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(DuplicateBases.__mro__)
```
The introspection helper `typing(_extensions).is_protocol` can be used to verify whether a class is
a protocol class or not:
```py
from typing_extensions import is_protocol
reveal_type(is_protocol(MyProtocol)) # revealed: Literal[True]
reveal_type(is_protocol(Bar0)) # revealed: Literal[True]
reveal_type(is_protocol(Bar1)) # revealed: Literal[True]
reveal_type(is_protocol(Bar2)) # revealed: Literal[True]
reveal_type(is_protocol(Bar3)) # revealed: Literal[True]
# TODO: should be `Literal[True]`
reveal_type(is_protocol(MyProtocol)) # revealed: bool
class NotAProtocol: ...
reveal_type(is_protocol(NotAProtocol)) # revealed: Literal[False]
# TODO: should be `Literal[False]`
reveal_type(is_protocol(NotAProtocol)) # revealed: bool
```
A type checker should follow the typeshed stubs if a non-class is passed in, and typeshed's stubs
indicate that the argument passed in must be an instance of `type`.
indicate that the argument passed in must be an instance of `type`. `Literal[False]` should be
inferred as the return type, however.
```py
# We could also reasonably infer `Literal[False]` here, but it probably doesn't matter that much:
# TODO: the diagnostic is correct, but should infer `Literal[False]`
# error: [invalid-argument-type]
reveal_type(is_protocol("not a class")) # revealed: bool
```
@@ -110,7 +74,8 @@ class SubclassOfMyProtocol(MyProtocol): ...
# revealed: tuple[Literal[SubclassOfMyProtocol], Literal[MyProtocol], typing.Protocol, typing.Generic, Literal[object]]
reveal_type(SubclassOfMyProtocol.__mro__)
reveal_type(is_protocol(SubclassOfMyProtocol)) # revealed: Literal[False]
# TODO: should be `Literal[False]`
reveal_type(is_protocol(SubclassOfMyProtocol)) # revealed: bool
```
A protocol class may inherit from other protocols, however, as long as it re-inherits from
@@ -119,7 +84,8 @@ A protocol class may inherit from other protocols, however, as long as it re-inh
```py
class SubProtocol(MyProtocol, Protocol): ...
reveal_type(is_protocol(SubProtocol)) # revealed: Literal[True]
# TODO: should be `Literal[True]`
reveal_type(is_protocol(SubProtocol)) # revealed: bool
class OtherProtocol(Protocol):
some_attribute: str
@@ -129,20 +95,21 @@ class ComplexInheritance(SubProtocol, OtherProtocol, Protocol): ...
# revealed: tuple[Literal[ComplexInheritance], Literal[SubProtocol], Literal[MyProtocol], Literal[OtherProtocol], typing.Protocol, typing.Generic, Literal[object]]
reveal_type(ComplexInheritance.__mro__)
reveal_type(is_protocol(ComplexInheritance)) # revealed: Literal[True]
# TODO: should be `Literal[True]`
reveal_type(is_protocol(ComplexInheritance)) # revealed: bool
```
If `Protocol` is present in the bases tuple, all other bases in the tuple must be protocol classes,
or `TypeError` is raised at runtime when the class is created.
```py
# error: [invalid-protocol] "Protocol class `Invalid` cannot inherit from non-protocol class `NotAProtocol`"
# TODO: should emit `[invalid-protocol]`
class Invalid(NotAProtocol, Protocol): ...
# revealed: tuple[Literal[Invalid], Literal[NotAProtocol], typing.Protocol, typing.Generic, Literal[object]]
reveal_type(Invalid.__mro__)
# error: [invalid-protocol] "Protocol class `AlsoInvalid` cannot inherit from non-protocol class `NotAProtocol`"
# TODO: should emit an `[invalid-protocol`] error
class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ...
# revealed: tuple[Literal[AlsoInvalid], Literal[MyProtocol], Literal[OtherProtocol], Literal[NotAProtocol], typing.Protocol, typing.Generic, Literal[object]]
@@ -167,8 +134,6 @@ reveal_type(Fine.__mro__) # revealed: tuple[Literal[Fine], typing.Protocol, typ
class StillFine(Protocol, Generic[T], object): ...
class EvenThis[T](Protocol, object): ...
class OrThis(Protocol[T], Generic[T]): ...
class AndThis(Protocol[T], Generic[T], object): ...
```
And multiple inheritance from a mix of protocol and non-protocol classes is fine as long as
@@ -185,7 +150,8 @@ But if `Protocol` is not present in the bases list, the resulting class doesn't
class anymore:
```py
reveal_type(is_protocol(FineAndDandy)) # revealed: Literal[False]
# TODO: should reveal `Literal[False]`
reveal_type(is_protocol(FineAndDandy)) # revealed: bool
```
A class does not *have* to inherit from a protocol class in order for it to be considered a subtype
@@ -264,10 +230,9 @@ class Foo(typing.Protocol):
class Bar(typing_extensions.Protocol):
x: int
static_assert(typing_extensions.is_protocol(Foo))
static_assert(typing_extensions.is_protocol(Bar))
# TODO: should pass
# TODO: these should pass
static_assert(typing_extensions.is_protocol(Foo)) # error: [static-assert-error]
static_assert(typing_extensions.is_protocol(Bar)) # error: [static-assert-error]
static_assert(is_equivalent_to(Foo, Bar)) # error: [static-assert-error]
```
@@ -282,10 +247,9 @@ class RuntimeCheckableFoo(typing.Protocol):
class RuntimeCheckableBar(typing_extensions.Protocol):
x: int
static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo))
static_assert(typing_extensions.is_protocol(RuntimeCheckableBar))
# TODO: should pass
# TODO: these should pass
static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo)) # error: [static-assert-error]
static_assert(typing_extensions.is_protocol(RuntimeCheckableBar)) # error: [static-assert-error]
static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar)) # error: [static-assert-error]
# These should not error because the protocols are decorated with `@runtime_checkable`

View File

@@ -708,95 +708,3 @@ with ContextManager() as (a, b, c):
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
```
## Comprehension
Unpacking in a comprehension.
### Same types
```py
def _(arg: tuple[tuple[int, int], tuple[int, int]]):
# revealed: tuple[int, int]
[reveal_type((a, b)) for a, b in arg]
```
### Mixed types (1)
```py
def _(arg: tuple[tuple[int, int], tuple[int, str]]):
# revealed: tuple[int, int | str]
[reveal_type((a, b)) for a, b in arg]
```
### Mixed types (2)
```py
def _(arg: tuple[tuple[int, str], tuple[str, int]]):
# revealed: tuple[int | str, str | int]
[reveal_type((a, b)) for a, b in arg]
```
### Mixed types (3)
```py
def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, str]]):
# revealed: tuple[int, int | str, int | bytes | str]
[reveal_type((a, b, c)) for a, b, c in arg]
```
### Same literal values
```py
# revealed: tuple[Literal[1, 3], Literal[2, 4]]
[reveal_type((a, b)) for a, b in ((1, 2), (3, 4))]
```
### Mixed literal values (1)
```py
# revealed: tuple[Literal[1, "a"], Literal[2, "b"]]
[reveal_type((a, b)) for a, b in ((1, 2), ("a", "b"))]
```
### Mixed literals values (2)
```py
# error: "Object of type `Literal[1]` is not iterable"
# error: "Object of type `Literal[2]` is not iterable"
# error: "Object of type `Literal[4]` is not iterable"
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# revealed: tuple[Unknown | Literal[3, 5], Unknown | Literal["a", "b"]]
[reveal_type((a, b)) for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c")]
```
### Custom iterator (1)
```py
class Iterator:
def __next__(self) -> tuple[int, int]:
return (1, 2)
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
# revealed: tuple[int, int]
[reveal_type((a, b)) for a, b in Iterable()]
```
### Custom iterator (2)
```py
class Iterator:
def __next__(self) -> bytes:
return b""
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
def _(arg: tuple[tuple[int, str], Iterable]):
# revealed: tuple[int | bytes, str | bytes]
[reveal_type((a, b)) for a, b in arg]
```

View File

@@ -0,0 +1,32 @@
Tanjun # hangs?
aiohttp # missing expression ID
alerta # missing expression ID
altair # cycle panics (try_metaclass_)
antidote # hangs / slow
artigraph # cycle panics (value_type_)
cpython # missing expression ID, access to field whilst being initialized, too many cycle iterations
colour # cycle panics (try_metaclass_)
core # cycle panics (value_type_)
dragonchain # too many cycle iterations (member_lookup_with_policy)
manticore # stack overflow
materialize # stack overflow
meson # missing expression ID
mypy # cycle panic (signature_)
pandas # too many cycle iterations (member_lookup_with_policy)
pandas-stubs # cycle panics (try_metaclass)
pip # too many cycle iterations (infer_expression_types)
poetry # too many cycle iterations (member_lookup_with_policy, infer_expression_type)
prefect # slow
pytest # cycle panics (signature_), missing expression ID
pywin32 # bad use-def map (binding with definitely-visible unbound)
schemathesis # cycle panics (signature_)
scikit-learn # success, but mypy-primer hangs processing the output
scipy # missing expression ID
setuptools # too many cycle iterations (infer_definition_types)
spack # success, but mypy-primer hangs processing the output
spark # missing expression ID
sphinx # missing expression ID
steam.py # missing expression ID
streamlit # cycle panic (signature)
sympy # stack overflow
trio # missing expression ID

View File

@@ -1,23 +1,111 @@
AutoSplit
Expression
PyGithub
PyWinCtl
SinbadCogs
aiohttp-devtools
aioredis
aiortc
alectryon
anyio
apprise
arrow
arviz
async-utils
asynq
attrs
bandersnatch
beartype
bidict
black
bokeh
boostedblob
check-jsonschema
cki-lib
cloud-init
com2ann
comtypes
cwltool
dacite
dd-trace-py
dedupe
django-stubs
downforeveryone
dulwich
flake8
flake8-pyi
freqtrade
git-revise
graphql-core
httpx-caching
hydpy
hydra-zen
ibis
ignite
imagehash
isort
itsdangerous
janus
jax
jinja
koda-validate
kopf
kornia
mitmproxy
mkdocs
mkosi
mongo-python-driver
more-itertools
mypy-protobuf
mypy_primer
nionutils
nox
openlibrary
operator
optuna
paasta
packaging
pandera
paroxython
parso
pegen
porcupine
ppb-vector
psycopg
pwndbg
pybind11
pycryptodome
pydantic
pyinstrument
pyjwt
pylint
pylox
pyodide
pyp
pyppeteer
pytest-robotframework
python-chess
python-htmlgen
python-sop
rclip
rich
rotki
schema_salad
scrapy
sockeye
speedrun.com_global_scoreboard_webapp
starlette
static-frame
stone
tornado
twine
typeshed-stats
urllib3
vision
websockets
werkzeug
xarray
xarray-dataclasses
yarl
zipp
zulip

View File

@@ -940,7 +940,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
panic!("expected generator definition")
};
let target = comprehension.target();
let name = target.as_name_expr().unwrap().id().as_str();
let name = target.id().as_str();
assert_eq!(name, "x");
assert_eq!(target.range(), TextRange::new(23.into(), 24.into()));

View File

@@ -18,12 +18,11 @@ use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::definition::{
AnnotatedAssignmentDefinitionKind, AnnotatedAssignmentDefinitionNodeRef,
AssignmentDefinitionKind, AssignmentDefinitionNodeRef, ComprehensionDefinitionKind,
ComprehensionDefinitionNodeRef, Definition, DefinitionCategory, DefinitionKind,
DefinitionNodeKey, DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef,
ForStmtDefinitionKind, ForStmtDefinitionNodeRef, ImportDefinitionNodeRef,
ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef, StarImportDefinitionNodeRef,
TargetKind, WithItemDefinitionKind, WithItemDefinitionNodeRef,
AssignmentDefinitionKind, AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef,
Definition, DefinitionCategory, DefinitionKind, DefinitionNodeKey, DefinitionNodeRef,
Definitions, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionKind, ForStmtDefinitionNodeRef,
ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef,
StarImportDefinitionNodeRef, TargetKind, WithItemDefinitionKind, WithItemDefinitionNodeRef,
};
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::predicate::{
@@ -355,14 +354,15 @@ impl<'db> SemanticIndexBuilder<'db> {
self.current_use_def_map_mut().merge(state);
}
/// Add a symbol to the symbol table and the use-def map.
/// Return the [`ScopedSymbolId`] that uniquely identifies the symbol in both.
fn add_symbol(&mut self, name: Name) -> ScopedSymbolId {
/// Return a 2-element tuple, where the first element is the [`ScopedSymbolId`] of the
/// symbol added, and the second element is a boolean indicating whether the symbol was *newly*
/// added or not
fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) {
let (symbol_id, added) = self.current_symbol_table().add_symbol(name);
if added {
self.current_use_def_map_mut().add_symbol(symbol_id);
}
symbol_id
(symbol_id, added)
}
fn add_attribute(&mut self, name: Name) -> ScopedSymbolId {
@@ -796,7 +796,7 @@ impl<'db> SemanticIndexBuilder<'db> {
..
}) => (name, &None, default),
};
let symbol = self.add_symbol(name.id.clone());
let (symbol, _) = self.add_symbol(name.id.clone());
// TODO create Definition for PEP 695 typevars
// note that the "bound" on the typevar is a totally different thing than whether
// or not a name is "bound" by a typevar declaration; the latter is always true.
@@ -850,35 +850,31 @@ impl<'db> SemanticIndexBuilder<'db> {
// The `iter` of the first generator is evaluated in the outer scope, while all subsequent
// nodes are evaluated in the inner scope.
let value = self.add_standalone_expression(&generator.iter);
self.add_standalone_expression(&generator.iter);
self.visit_expr(&generator.iter);
self.push_scope(scope);
self.add_unpackable_assignment(
&Unpackable::Comprehension {
node: generator,
first: true,
},
&generator.target,
value,
);
self.push_assignment(CurrentAssignment::Comprehension {
node: generator,
first: true,
});
self.visit_expr(&generator.target);
self.pop_assignment();
for expr in &generator.ifs {
self.visit_expr(expr);
}
for generator in generators_iter {
let value = self.add_standalone_expression(&generator.iter);
self.add_standalone_expression(&generator.iter);
self.visit_expr(&generator.iter);
self.add_unpackable_assignment(
&Unpackable::Comprehension {
node: generator,
first: false,
},
&generator.target,
value,
);
self.push_assignment(CurrentAssignment::Comprehension {
node: generator,
first: false,
});
self.visit_expr(&generator.target);
self.pop_assignment();
for expr in &generator.ifs {
self.visit_expr(expr);
@@ -894,20 +890,20 @@ impl<'db> SemanticIndexBuilder<'db> {
self.declare_parameter(parameter);
}
if let Some(vararg) = parameters.vararg.as_ref() {
let symbol = self.add_symbol(vararg.name.id().clone());
let (symbol, _) = self.add_symbol(vararg.name.id().clone());
self.add_definition(
symbol,
DefinitionNodeRef::VariadicPositionalParameter(vararg),
);
}
if let Some(kwarg) = parameters.kwarg.as_ref() {
let symbol = self.add_symbol(kwarg.name.id().clone());
let (symbol, _) = self.add_symbol(kwarg.name.id().clone());
self.add_definition(symbol, DefinitionNodeRef::VariadicKeywordParameter(kwarg));
}
}
fn declare_parameter(&mut self, parameter: &'db ast::ParameterWithDefault) {
let symbol = self.add_symbol(parameter.name().id().clone());
let (symbol, _) = self.add_symbol(parameter.name().id().clone());
let definition = self.add_definition(symbol, parameter);
@@ -937,30 +933,9 @@ impl<'db> SemanticIndexBuilder<'db> {
let current_assignment = match target {
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
if matches!(unpackable, Unpackable::Comprehension { .. }) {
debug_assert_eq!(
self.scopes[self.current_scope()].node().scope_kind(),
ScopeKind::Comprehension
);
}
// The first iterator of the comprehension is evaluated in the outer scope, while all subsequent
// nodes are evaluated in the inner scope.
// SAFETY: The current scope is the comprehension, and the comprehension scope must have a parent scope.
let value_file_scope =
if let Unpackable::Comprehension { first: true, .. } = unpackable {
self.scope_stack
.iter()
.rev()
.nth(1)
.expect("The comprehension scope must have a parent scope")
.file_scope_id
} else {
self.current_scope()
};
let unpack = Some(Unpack::new(
self.db,
self.file,
value_file_scope,
self.current_scope(),
// SAFETY: `target` belongs to the `self.module` tree
#[allow(unsafe_code)]
@@ -1138,7 +1113,7 @@ where
// The symbol for the function name itself has to be evaluated
// at the end to match the runtime evaluation of parameter defaults
// and return-type annotations.
let symbol = self.add_symbol(name.id.clone());
let (symbol, _) = self.add_symbol(name.id.clone());
// Record a use of the function name in the scope that it is defined in, so that it
// can be used to find previously defined functions with the same name. This is
@@ -1173,11 +1148,11 @@ where
);
// In Python runtime semantics, a class is registered after its scope is evaluated.
let symbol = self.add_symbol(class.name.id.clone());
let (symbol, _) = self.add_symbol(class.name.id.clone());
self.add_definition(symbol, class);
}
ast::Stmt::TypeAlias(type_alias) => {
let symbol = self.add_symbol(
let (symbol, _) = self.add_symbol(
type_alias
.name
.as_name_expr()
@@ -1214,7 +1189,7 @@ where
(Name::new(alias.name.id.split('.').next().unwrap()), false)
};
let symbol = self.add_symbol(symbol_name);
let (symbol, _) = self.add_symbol(symbol_name);
self.add_definition(
symbol,
ImportDefinitionNodeRef {
@@ -1285,7 +1260,7 @@ where
//
// For more details, see the doc-comment on `StarImportPlaceholderPredicate`.
for export in exported_names(self.db, referenced_module) {
let symbol_id = self.add_symbol(export.clone());
let (symbol_id, newly_added) = self.add_symbol(export.clone());
let node_ref = StarImportDefinitionNodeRef { node, symbol_id };
let star_import = StarImportPlaceholderPredicate::new(
self.db,
@@ -1294,15 +1269,28 @@ where
referenced_module,
);
let pre_definition =
self.current_use_def_map().single_symbol_snapshot(symbol_id);
self.push_additional_definition(symbol_id, node_ref);
self.current_use_def_map_mut()
.record_and_negate_star_import_visibility_constraint(
star_import,
symbol_id,
pre_definition,
);
// Fast path for if there were no previous definitions
// of the symbol defined through the `*` import:
// we can apply the visibility constraint to *only* the added definition,
// rather than all definitions
if newly_added {
self.push_additional_definition(symbol_id, node_ref);
self.current_use_def_map_mut()
.record_and_negate_star_import_visibility_constraint(
star_import,
symbol_id,
);
} else {
let pre_definition = self.flow_snapshot();
self.push_additional_definition(symbol_id, node_ref);
let constraint_id =
self.record_visibility_constraint(star_import.into());
let post_definition = self.flow_snapshot();
self.flow_restore(pre_definition.clone());
self.record_negated_visibility_constraint(constraint_id);
self.flow_merge(post_definition);
self.simplify_visibility_constraints(pre_definition);
}
}
continue;
@@ -1322,7 +1310,7 @@ where
self.has_future_annotations |= alias.name.id == "annotations"
&& node.module.as_deref() == Some("__future__");
let symbol = self.add_symbol(symbol_name.clone());
let (symbol, _) = self.add_symbol(symbol_name.clone());
self.add_definition(
symbol,
@@ -1740,7 +1728,7 @@ where
// which is invalid syntax. However, it's still pretty obvious here that the user
// *wanted* `e` to be bound, so we should still create a definition here nonetheless.
if let Some(symbol_name) = symbol_name {
let symbol = self.add_symbol(symbol_name.id.clone());
let (symbol, _) = self.add_symbol(symbol_name.id.clone());
self.add_definition(
symbol,
@@ -1816,7 +1804,7 @@ where
let node_key = NodeKey::from_node(expr);
match expr {
ast::Expr::Name(ast::ExprName { id, ctx, .. }) => {
ast::Expr::Name(name_node @ ast::ExprName { id, ctx, .. }) => {
let (is_use, is_definition) = match (ctx, self.current_assignment()) {
(ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => {
// For augmented assignment, the target expression is also used.
@@ -1827,7 +1815,7 @@ where
(ast::ExprContext::Del, _) => (false, true),
(ast::ExprContext::Invalid, _) => (false, false),
};
let symbol = self.add_symbol(id.clone());
let (symbol, _) = self.add_symbol(id.clone());
if is_use {
self.mark_symbol_used(symbol);
@@ -1879,17 +1867,12 @@ where
// implemented.
self.add_definition(symbol, named);
}
Some(CurrentAssignment::Comprehension {
unpack,
node,
first,
}) => {
Some(CurrentAssignment::Comprehension { node, first }) => {
self.add_definition(
symbol,
ComprehensionDefinitionNodeRef {
unpack,
iterable: &node.iter,
target: expr,
target: name_node,
first,
is_async: node.is_async,
},
@@ -2160,37 +2143,14 @@ where
DefinitionKind::WithItem(assignment),
);
}
Some(CurrentAssignment::Comprehension {
unpack,
node,
first,
}) => {
// SAFETY: `iter` and `expr` belong to the `self.module` tree
#[allow(unsafe_code)]
let assignment = ComprehensionDefinitionKind {
target_kind: TargetKind::from(unpack),
iterable: unsafe {
AstNodeRef::new(self.module.clone(), &node.iter)
},
target: unsafe { AstNodeRef::new(self.module.clone(), expr) },
first,
is_async: node.is_async,
};
// Temporarily move to the scope of the method to which the instance attribute is defined.
// SAFETY: `self.scope_stack` is not empty because the targets in comprehensions should always introduce a new scope.
let scope = self.scope_stack.pop().expect("The popped scope must be a comprehension, which must have a parent scope");
self.register_attribute_assignment(
object,
attr,
DefinitionKind::Comprehension(assignment),
);
self.scope_stack.push(scope);
Some(CurrentAssignment::Comprehension { .. }) => {
// TODO:
}
Some(CurrentAssignment::AugAssign(_)) => {
// TODO:
}
Some(CurrentAssignment::Named(_)) => {
// A named expression whose target is an attribute is syntactically prohibited
// TODO:
}
None => {}
}
@@ -2231,7 +2191,7 @@ where
range: _,
}) = pattern
{
let symbol = self.add_symbol(name.id().clone());
let (symbol, _) = self.add_symbol(name.id().clone());
let state = self.current_match_case.as_ref().unwrap();
self.add_definition(
symbol,
@@ -2252,7 +2212,7 @@ where
rest: Some(name), ..
}) = pattern
{
let symbol = self.add_symbol(name.id().clone());
let (symbol, _) = self.add_symbol(name.id().clone());
let state = self.current_match_case.as_ref().unwrap();
self.add_definition(
symbol,
@@ -2284,7 +2244,6 @@ enum CurrentAssignment<'a> {
Comprehension {
node: &'a ast::Comprehension,
first: bool,
unpack: Option<(UnpackPosition, Unpack<'a>)>,
},
WithItem {
item: &'a ast::WithItem,
@@ -2298,9 +2257,11 @@ impl CurrentAssignment<'_> {
match self {
Self::Assign { unpack, .. }
| Self::For { unpack, .. }
| Self::WithItem { unpack, .. }
| Self::Comprehension { unpack, .. } => unpack.as_mut().map(|(position, _)| position),
Self::AnnAssign(_) | Self::AugAssign(_) | Self::Named(_) => None,
| Self::WithItem { unpack, .. } => unpack.as_mut().map(|(position, _)| position),
Self::AnnAssign(_)
| Self::AugAssign(_)
| Self::Named(_)
| Self::Comprehension { .. } => None,
}
}
}
@@ -2355,17 +2316,13 @@ enum Unpackable<'a> {
item: &'a ast::WithItem,
is_async: bool,
},
Comprehension {
first: bool,
node: &'a ast::Comprehension,
},
}
impl<'a> Unpackable<'a> {
const fn kind(&self) -> UnpackKind {
match self {
Unpackable::Assign(_) => UnpackKind::Assign,
Unpackable::For(_) | Unpackable::Comprehension { .. } => UnpackKind::Iterable,
Unpackable::For(_) => UnpackKind::Iterable,
Unpackable::WithItem { .. } => UnpackKind::ContextManager,
}
}
@@ -2380,11 +2337,6 @@ impl<'a> Unpackable<'a> {
is_async: *is_async,
unpack,
},
Unpackable::Comprehension { node, first } => CurrentAssignment::Comprehension {
node,
first: *first,
unpack,
},
}
}
}

View File

@@ -281,9 +281,8 @@ pub(crate) struct ExceptHandlerDefinitionNodeRef<'a> {
#[derive(Copy, Clone, Debug)]
pub(crate) struct ComprehensionDefinitionNodeRef<'a> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
pub(crate) iterable: &'a ast::Expr,
pub(crate) target: &'a ast::Expr,
pub(crate) target: &'a ast::ExprName,
pub(crate) first: bool,
pub(crate) is_async: bool,
}
@@ -375,13 +374,11 @@ impl<'db> DefinitionNodeRef<'db> {
is_async,
}),
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
unpack,
iterable,
target,
first,
is_async,
}) => DefinitionKind::Comprehension(ComprehensionDefinitionKind {
target_kind: TargetKind::from(unpack),
iterable: AstNodeRef::new(parsed.clone(), iterable),
target: AstNodeRef::new(parsed, target),
first,
@@ -477,9 +474,7 @@ impl<'db> DefinitionNodeRef<'db> {
unpack: _,
is_async: _,
}) => DefinitionNodeKey(NodeKey::from_node(target)),
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => {
DefinitionNodeKey(NodeKey::from_node(target))
}
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
Self::VariadicPositionalParameter(node) => node.into(),
Self::VariadicKeywordParameter(node) => node.into(),
Self::Parameter(node) => node.into(),
@@ -555,7 +550,7 @@ pub enum DefinitionKind<'db> {
AnnotatedAssignment(AnnotatedAssignmentDefinitionKind),
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
For(ForStmtDefinitionKind<'db>),
Comprehension(ComprehensionDefinitionKind<'db>),
Comprehension(ComprehensionDefinitionKind),
VariadicPositionalParameter(AstNodeRef<ast::Parameter>),
VariadicKeywordParameter(AstNodeRef<ast::Parameter>),
Parameter(AstNodeRef<ast::ParameterWithDefault>),
@@ -754,24 +749,19 @@ impl MatchPatternDefinitionKind {
}
#[derive(Clone, Debug)]
pub struct ComprehensionDefinitionKind<'db> {
pub(super) target_kind: TargetKind<'db>,
pub(super) iterable: AstNodeRef<ast::Expr>,
pub(super) target: AstNodeRef<ast::Expr>,
pub(super) first: bool,
pub(super) is_async: bool,
pub struct ComprehensionDefinitionKind {
iterable: AstNodeRef<ast::Expr>,
target: AstNodeRef<ast::ExprName>,
first: bool,
is_async: bool,
}
impl<'db> ComprehensionDefinitionKind<'db> {
impl ComprehensionDefinitionKind {
pub(crate) fn iterable(&self) -> &ast::Expr {
self.iterable.node()
}
pub(crate) fn target_kind(&self) -> TargetKind<'db> {
self.target_kind
}
pub(crate) fn target(&self) -> &ast::Expr {
pub(crate) fn target(&self) -> &ast::ExprName {
self.target.node()
}

View File

@@ -775,16 +775,7 @@ impl<'db> UseDefMapBuilder<'db> {
.add_and_constraint(self.scope_start_visibility, constraint);
}
/// Snapshot the state of a single symbol at the current point in control flow.
///
/// This is only used for `*`-import visibility constraints, which are handled differently
/// to most other visibility constraints. See the doc-comment for
/// [`Self::record_and_negate_star_import_visibility_constraint`] for more details.
pub(super) fn single_symbol_snapshot(&self, symbol: ScopedSymbolId) -> SymbolState {
self.symbol_states[symbol].clone()
}
/// This method exists solely for handling `*`-import visibility constraints.
/// This method exists solely as a fast path for handling `*`-import visibility constraints.
///
/// The reason why we add visibility constraints for [`Definition`]s created by `*` imports
/// is laid out in the doc-comment for [`StarImportPlaceholderPredicate`]. But treating these
@@ -793,11 +784,12 @@ impl<'db> UseDefMapBuilder<'db> {
/// dominates. (Although `*` imports are not common generally, they are used in several
/// important places by typeshed.)
///
/// To solve these regressions, it was observed that we could do significantly less work for
/// `*`-import definitions. We do a number of things differently here to our normal handling of
/// visibility constraints:
/// To solve these regressions, it was observed that we could add a fast path for `*`-import
/// definitions which added a new symbol to the global scope (as opposed to `*`-import definitions
/// that provided redefinitions for *pre-existing* global-scope symbols). The fast path does a
/// number of things differently to our normal handling of visibility constraints:
///
/// - We only apply and negate the visibility constraints to a single symbol, rather than to
/// - It only applies and negates the visibility constraints to a single symbol, rather than to
/// all symbols. This is possible here because, unlike most definitions, we know in advance that
/// exactly one definition occurs inside the "if-true" predicate branch, and we know exactly
/// which definition it is.
@@ -808,9 +800,9 @@ impl<'db> UseDefMapBuilder<'db> {
/// the visibility constraints is only important for symbols that did not have any new
/// definitions inside either the "if-predicate-true" branch or the "if-predicate-false" branch.
///
/// - We only snapshot the state for a single symbol prior to the definition, rather than doing
/// expensive calls to [`Self::snapshot`]. Again, this is possible because we know
/// that only a single definition occurs inside the "if-predicate-true" predicate branch.
/// - It avoids multiple expensive calls to [`Self::snapshot`]. This is possible because we know
/// the symbol is newly added, so we know the prior state of the symbol was
/// [`SymbolState::undefined`].
///
/// - Normally we take care to check whether an "if-predicate-true" branch or an
/// "if-predicate-false" branch contains a terminal statement: these can affect the visibility
@@ -823,7 +815,6 @@ impl<'db> UseDefMapBuilder<'db> {
&mut self,
star_import: StarImportPlaceholderPredicate<'db>,
symbol: ScopedSymbolId,
pre_definition_state: SymbolState,
) {
let predicate_id = self.add_predicate(star_import.into());
let visibility_id = self.visibility_constraints.add_atom(predicate_id);
@@ -831,9 +822,10 @@ impl<'db> UseDefMapBuilder<'db> {
.visibility_constraints
.add_not_constraint(visibility_id);
let mut post_definition_state =
std::mem::replace(&mut self.symbol_states[symbol], pre_definition_state);
let mut post_definition_state = std::mem::replace(
&mut self.symbol_states[symbol],
SymbolState::undefined(self.scope_start_visibility),
);
post_definition_state
.record_visibility_constraint(&mut self.visibility_constraints, visibility_id);

View File

@@ -314,7 +314,7 @@ impl SymbolBindings {
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(in crate::semantic_index) struct SymbolState {
pub(super) struct SymbolState {
declarations: SymbolDeclarations,
bindings: SymbolBindings,
}

View File

@@ -4605,8 +4605,8 @@ impl<'db> Type<'db> {
match self {
Type::TypeVar(typevar) => specialization.get(db, typevar).unwrap_or(self),
Type::FunctionLiteral(function) =>{
Type::FunctionLiteral(FunctionType::Specialized(SpecializedFunction::new(db, function, specialization)))
Type::FunctionLiteral(function) => {
Type::FunctionLiteral(function.apply_specialization(db, specialization))
}
// Note that we don't need to apply the specialization to `self_instance`, since it
@@ -4619,19 +4619,19 @@ impl<'db> Type<'db> {
// specialized.)
Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new(
db,
FunctionType::Specialized(SpecializedFunction::new(db, method.function(db), specialization)),
method.function(db).apply_specialization(db, specialization),
method.self_instance(db),
)),
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(
FunctionType::Specialized(SpecializedFunction::new(db, function, specialization))
function.apply_specialization(db, specialization),
))
}
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(function)) => {
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderCall(
FunctionType::Specialized(SpecializedFunction::new(db, function, specialization))
function.apply_specialization(db, specialization),
))
}
@@ -5834,34 +5834,6 @@ impl<'db> FunctionSignature<'db> {
pub(crate) fn iter(&self) -> Iter<Signature<'db>> {
self.as_slice().iter()
}
fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) {
match self {
Self::Single(signature) => signature.apply_specialization(db, specialization),
Self::Overloaded(signatures, implementation) => {
signatures
.iter_mut()
.for_each(|signature| signature.apply_specialization(db, specialization));
implementation
.as_mut()
.map(|signature| signature.apply_specialization(db, specialization));
}
}
}
fn set_generic_context(&mut self, generic_context: GenericContext<'db>) {
match self {
Self::Single(signature) => signature.set_generic_context(generic_context),
Self::Overloaded(signatures, implementation) => {
signatures
.iter_mut()
.for_each(|signature| signature.set_generic_context(generic_context));
implementation
.as_mut()
.map(|signature| signature.set_generic_context(generic_context));
}
}
}
}
impl<'db> IntoIterator for &'db FunctionSignature<'db> {
@@ -5873,40 +5845,34 @@ impl<'db> IntoIterator for &'db FunctionSignature<'db> {
}
}
/// A callable type that represents a single Python function.
#[derive(
Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Supertype, salsa::Update,
)]
pub enum FunctionType<'db> {
/// A function literal in the Python AST
FunctionLiteral(FunctionLiteral<'db>),
#[salsa::interned(debug)]
pub struct FunctionType<'db> {
/// Name of the function at definition.
#[return_ref]
pub name: ast::name::Name,
/// A function that has a specialization applied to its signature.
///
/// (This does not necessarily mean that the function itself is generic — the methods of a
/// generic class, for instance, will have the class's specialization applied so that we
/// correctly substitute any class typevars that appear in the signature.)
Specialized(SpecializedFunction<'db>),
/// Is this a function that we special-case somehow? If so, which one?
known: Option<KnownFunction>,
/// A function that we treat as generic because it inherits a containing generic context.
///
/// This is currently only used for the `__new__` and `__init__` methods of a generic class.
/// That lets us pretend those methods are generic, so that we can infer a class specialization
/// from the arguments to its constructor.
InheritedGenericContext(FunctionWithInheritedGenericContext<'db>),
/// The scope that's created by the function, in which the function body is evaluated.
body_scope: ScopeId<'db>,
/// A set of special decorators that were applied to this function
decorators: FunctionDecorators,
/// The generic context of a generic function.
generic_context: Option<GenericContext<'db>>,
/// A specialization that should be applied to the function's parameter and return types,
/// either because the function is itself generic, or because it appears in the body of a
/// generic class.
specialization: Option<Specialization<'db>>,
}
#[salsa::tracked]
impl<'db> FunctionType<'db> {
fn function_literal(self, db: &'db dyn Db) -> FunctionLiteral<'db> {
match self {
FunctionType::FunctionLiteral(literal) => literal,
FunctionType::Specialized(specialized) => specialized.function(db).function_literal(db),
FunctionType::InheritedGenericContext(inherited) => inherited.function(db),
}
}
pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool {
self.function_literal(db).decorators(db).contains(decorator)
self.decorators(db).contains(decorator)
}
/// Convert the `FunctionType` into a [`Type::Callable`].
@@ -5919,80 +5885,20 @@ impl<'db> FunctionType<'db> {
/// Returns the [`FileRange`] of the function's name.
pub fn focus_range(self, db: &dyn Db) -> FileRange {
let body_scope = self.function_literal(db).body_scope(db);
FileRange::new(
body_scope.file(db),
body_scope.node(db).expect_function().name.range,
self.body_scope(db).file(db),
self.body_scope(db).node(db).expect_function().name.range,
)
}
pub fn full_range(self, db: &dyn Db) -> FileRange {
let body_scope = self.function_literal(db).body_scope(db);
FileRange::new(
body_scope.file(db),
body_scope.node(db).expect_function().range,
self.body_scope(db).file(db),
self.body_scope(db).node(db).expect_function().range,
)
}
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
self.function_literal(db).definition(db)
}
/// Typed externally-visible signature for this function.
///
/// This is the signature as seen by external callers, possibly modified by decorators and/or
/// overloaded.
///
/// ## Why is this a salsa query?
///
/// This is a salsa query to short-circuit the invalidation
/// when the function's AST node changes.
///
/// Were this not a salsa query, then the calling query
/// would depend on the function's AST and rerun for every change in that file.
pub(crate) fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> {
match self {
FunctionType::FunctionLiteral(literal) => literal.signature(db),
FunctionType::Specialized(specialized) => specialized.signature(db),
FunctionType::InheritedGenericContext(inherited) => inherited.signature(db),
}
}
pub(crate) fn known(self, db: &'db dyn Db) -> Option<KnownFunction> {
self.function_literal(db).known(db)
}
pub(crate) fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool {
self.known(db) == Some(known_function)
}
}
#[salsa::interned(debug)]
pub struct FunctionLiteral<'db> {
/// Name of the function at definition.
#[return_ref]
pub name: ast::name::Name,
/// Is this a function that we special-case somehow? If so, which one?
known: Option<KnownFunction>,
/// The scope that's created by the function, in which the function body is evaluated.
body_scope: ScopeId<'db>,
/// The scope containing the PEP 695 type parameters in the function definition, if any.
type_params_scope: Option<ScopeId<'db>>,
/// A set of special decorators that were applied to this function
decorators: FunctionDecorators,
}
#[salsa::tracked]
impl<'db> FunctionLiteral<'db> {
fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool {
self.decorators(db).contains(decorator)
}
fn definition(self, db: &'db dyn Db) -> Definition<'db> {
let body_scope = self.body_scope(db);
let index = semantic_index(db, body_scope.file(db));
index.expect_single_definition(body_scope.node(db).expect_function())
@@ -6010,9 +5916,13 @@ impl<'db> FunctionLiteral<'db> {
///
/// Were this not a salsa query, then the calling query
/// would depend on the function's AST and rerun for every change in that file.
#[salsa::tracked]
fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> {
let internal_signature = self.internal_signature(db);
#[salsa::tracked(return_ref)]
pub(crate) fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> {
let mut internal_signature = self.internal_signature(db);
if let Some(specialization) = self.specialization(db) {
internal_signature = internal_signature.apply_specialization(db, specialization);
}
// The semantic model records a use for each function on the name node. This is used here
// to get the previous function definition with the same name.
@@ -6072,61 +5982,39 @@ impl<'db> FunctionLiteral<'db> {
let scope = self.body_scope(db);
let function_stmt_node = scope.node(db).expect_function();
let definition = self.definition(db);
let generic_context = function_stmt_node.type_params.as_ref().map(|type_params| {
let index = semantic_index(db, scope.file(db));
GenericContext::from_type_params(db, index, type_params)
});
Signature::from_function(db, generic_context, definition, function_stmt_node)
Signature::from_function(db, self.generic_context(db), definition, function_stmt_node)
}
fn with_generic_context(
self,
db: &'db dyn Db,
generic_context: GenericContext<'db>,
) -> FunctionType<'db> {
FunctionType::InheritedGenericContext(FunctionWithInheritedGenericContext::new(
pub(crate) fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool {
self.known(db) == Some(known_function)
}
fn with_generic_context(self, db: &'db dyn Db, generic_context: GenericContext<'db>) -> Self {
Self::new(
db,
self,
generic_context,
))
self.name(db).clone(),
self.known(db),
self.body_scope(db),
self.decorators(db),
Some(generic_context),
self.specialization(db),
)
}
}
impl<'db> From<FunctionLiteral<'db>> for Type<'db> {
fn from(literal: FunctionLiteral<'db>) -> Type<'db> {
Type::FunctionLiteral(FunctionType::FunctionLiteral(literal))
}
}
#[salsa::interned(debug)]
pub struct SpecializedFunction<'db> {
function: FunctionType<'db>,
specialization: Specialization<'db>,
}
#[salsa::tracked]
impl<'db> SpecializedFunction<'db> {
#[salsa::tracked]
fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> {
let mut signature = self.function(db).signature(db);
signature.apply_specialization(db, self.specialization(db));
signature
}
}
#[salsa::interned(debug)]
pub struct FunctionWithInheritedGenericContext<'db> {
function: FunctionLiteral<'db>,
generic_context: GenericContext<'db>,
}
#[salsa::tracked]
impl<'db> FunctionWithInheritedGenericContext<'db> {
#[salsa::tracked]
fn signature(self, db: &'db dyn Db) -> FunctionSignature<'db> {
let mut signature = self.function(db).signature(db);
signature.set_generic_context(self.generic_context(db));
signature
fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
let specialization = match self.specialization(db) {
Some(existing) => existing.apply_specialization(db, specialization),
None => specialization,
};
Self::new(
db,
self.name(db).clone(),
self.known(db),
self.body_scope(db),
self.decorators(db),
self.generic_context(db),
Some(specialization),
)
}
}
@@ -6342,11 +6230,9 @@ impl<'db> CallableType<'db> {
fn apply_specialization(self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
CallableType::from_overloads(
db,
self.signatures(db).iter().map(|signature| {
let mut signature = signature.clone();
signature.apply_specialization(db, specialization);
signature
}),
self.signatures(db)
.iter()
.map(|signature| signature.apply_specialization(db, specialization)),
)
}

View File

@@ -219,8 +219,7 @@ impl<'db> Bindings<'db> {
match binding_type {
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
let function_literal = function.function_literal(db);
if function_literal.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) {
if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) {
match overload.parameter_types() {
[_, Some(owner)] => {
overload.set_return_type(Type::BoundMethod(BoundMethodType::new(
@@ -251,9 +250,7 @@ impl<'db> Bindings<'db> {
if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] =
overload.parameter_types()
{
let function_literal = function.function_literal(db);
if function_literal.has_known_decorator(db, FunctionDecorators::CLASSMETHOD)
{
if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) {
match overload.parameter_types() {
[_, _, Some(owner)] => {
overload.set_return_type(Type::BoundMethod(
@@ -301,7 +298,7 @@ impl<'db> Bindings<'db> {
if property.getter(db).is_some_and(|getter| {
getter
.into_function_literal()
.is_some_and(|f| f.function_literal(db).name(db) == "__name__")
.is_some_and(|f| f.name(db) == "__name__")
}) =>
{
overload.set_return_type(Type::string_literal(db, type_alias.name(db)));
@@ -310,7 +307,7 @@ impl<'db> Bindings<'db> {
if property.getter(db).is_some_and(|getter| {
getter
.into_function_literal()
.is_some_and(|f| f.function_literal(db).name(db) == "__name__")
.is_some_and(|f| f.name(db) == "__name__")
}) =>
{
overload.set_return_type(Type::string_literal(db, type_var.name(db)));
@@ -419,12 +416,7 @@ impl<'db> Bindings<'db> {
Type::BoundMethod(bound_method)
if bound_method.self_instance(db).is_property_instance() =>
{
match bound_method
.function(db)
.function_literal(db)
.name(db)
.as_str()
{
match bound_method.function(db).name(db).as_str() {
"setter" => {
if let [Some(_), Some(setter)] = overload.parameter_types() {
let mut ty_property = bound_method.self_instance(db);
@@ -464,10 +456,7 @@ impl<'db> Bindings<'db> {
}
}
Type::FunctionLiteral(function_type) => match function_type
.function_literal(db)
.known(db)
{
Type::FunctionLiteral(function_type) => match function_type.known(db) {
Some(KnownFunction::IsEquivalentTo) => {
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
@@ -546,15 +535,6 @@ impl<'db> Bindings<'db> {
}
}
Some(KnownFunction::IsProtocol) => {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty.into_class_literal()
.is_some_and(|class| class.is_protocol(db)),
));
}
}
Some(KnownFunction::Overload) => {
// TODO: This can be removed once we understand legacy generics because the
// typeshed definition for `typing.overload` is an identity function.
@@ -1186,7 +1166,7 @@ impl<'db> CallableDescription<'db> {
match callable_type {
Type::FunctionLiteral(function) => Some(CallableDescription {
kind: "function",
name: function.function_literal(db).name(db),
name: function.name(db),
}),
Type::ClassLiteral(class_type) => Some(CallableDescription {
kind: "class",
@@ -1194,12 +1174,12 @@ impl<'db> CallableDescription<'db> {
}),
Type::BoundMethod(bound_method) => Some(CallableDescription {
kind: "bound method",
name: bound_method.function(db).function_literal(db).name(db),
name: bound_method.function(db).name(db),
}),
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
Some(CallableDescription {
kind: "method wrapper `__get__` of function",
name: function.function_literal(db).name(db),
name: function.name(db),
})
}
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => {
@@ -1324,7 +1304,7 @@ impl<'db> BindingError<'db> {
) -> Option<(Span, Span)> {
match callable_ty {
Type::FunctionLiteral(function) => {
let function_scope = function.function_literal(db).body_scope(db);
let function_scope = function.body_scope(db);
let span = Span::from(function_scope.file(db));
let node = function_scope.node(db);
if let Some(func_def) = node.as_function() {

View File

@@ -582,17 +582,6 @@ impl<'db> ClassLiteralType<'db> {
.collect()
}
/// Determine if this class is a protocol.
pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool {
self.explicit_bases(db).iter().any(|base| {
matches!(
base,
Type::KnownInstance(KnownInstanceType::Protocol)
| Type::Dynamic(DynamicType::SubscriptedProtocol)
)
})
}
/// Return the types of the decorators on this class
#[salsa::tracked(return_ref)]
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
@@ -962,9 +951,7 @@ impl<'db> ClassLiteralType<'db> {
Some(_),
"__new__" | "__init__",
) => Type::FunctionLiteral(
function
.function_literal(db)
.with_generic_context(db, origin.generic_context(db)),
function.with_generic_context(db, origin.generic_context(db)),
),
_ => ty,
}
@@ -1429,42 +1416,14 @@ impl<'db> ClassLiteralType<'db> {
}
}
}
DefinitionKind::Comprehension(comprehension) => {
match comprehension.target_kind() {
TargetKind::Sequence(_, unpack) => {
// We found an unpacking assignment like:
//
// [... for .., self.name, .. in <iterable>]
let unpacked = infer_unpack_types(db, unpack);
let target_ast_id = comprehension
.target()
.scoped_expression_id(db, unpack.target_scope(db));
let inferred_ty = unpacked.expression_type(target_ast_id);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
TargetKind::NameOrAttribute => {
// We found an attribute assignment like:
//
// [... for self.name in <iterable>]
let iterable_ty = infer_expression_type(
db,
index.expression(comprehension.iterable()),
);
// TODO: Potential diagnostics resulting from the iterable are currently not reported.
let inferred_ty = iterable_ty.iterate(db);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
}
}
DefinitionKind::Comprehension(_) => {
// TODO:
}
DefinitionKind::AugmentedAssignment(_) => {
// TODO:
}
DefinitionKind::NamedExpression(_) => {
// A named expression whose target is an attribute is syntactically prohibited
// TODO:
}
_ => {}
}

View File

@@ -169,9 +169,7 @@ impl<'db> InferContext<'db> {
// Iterate over all functions and test if any is decorated with `@no_type_check`.
function_scope_tys.any(|function_ty| {
function_ty
.function_literal(self.db)
.has_known_decorator(self.db, FunctionDecorators::NO_TYPE_CHECK)
function_ty.has_known_decorator(self.db, FunctionDecorators::NO_TYPE_CHECK)
})
}
InNoTypeCheck::Yes => true,

View File

@@ -36,7 +36,6 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
registry.register_lint(&INVALID_PROTOCOL);
registry.register_lint(&INVALID_RAISE);
registry.register_lint(&INVALID_SUPER_ARGUMENT);
registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT);
@@ -231,34 +230,6 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for invalidly defined protocol classes.
///
/// ## Why is this bad?
/// An invalidly defined protocol class may lead to the type checker inferring
/// unexpected things. It may also lead to `TypeError`s at runtime.
///
/// ## Examples
/// A `Protocol` class cannot inherit from a non-`Protocol` class;
/// this raises a `TypeError` at runtime:
///
/// ```pycon
/// >>> from typing import Protocol
/// >>> class Foo(int, Protocol): ...
/// ...
/// Traceback (most recent call last):
/// File "<python-input-1>", line 1, in <module>
/// class Foo(int, Protocol): ...
/// TypeError: Protocols can only inherit from other protocols, got <class 'int'>
/// ```
pub(crate) static INVALID_PROTOCOL = {
summary: "detects invalid protocol class definitions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
pub(crate) static INCONSISTENT_MRO = {
@@ -1125,7 +1096,7 @@ fn report_invalid_assignment_with_message(
Type::FunctionLiteral(function) => {
context.report_lint_old(&INVALID_ASSIGNMENT, node, format_args!(
"Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional",
function.function_literal(context.db()).name(context.db())));
function.name(context.db())));
}
_ => {
context.report_lint_old(&INVALID_ASSIGNMENT, node, message);

View File

@@ -10,7 +10,7 @@ use crate::types::class::{ClassType, GenericAlias, GenericClass};
use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::{
FunctionSignature, FunctionType, InstanceType, IntersectionType, KnownClass, MethodWrapperKind,
FunctionSignature, InstanceType, IntersectionType, KnownClass, MethodWrapperKind,
StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance,
UnionType, WrapperDescriptorKind,
};
@@ -108,7 +108,7 @@ impl Display for DisplayRepresentation<'_> {
f,
// "def {name}{specialization}{signature}",
"def {name}{signature}",
name = function.function_literal(self.db).name(self.db),
name = function.name(self.db),
signature = signature.display(self.db)
)
}
@@ -135,7 +135,7 @@ impl Display for DisplayRepresentation<'_> {
write!(
f,
"bound method {instance}.{method}{signature}",
method = function.function_literal(self.db).name(self.db),
method = function.name(self.db),
instance = bound_method.self_instance(self.db).display(self.db),
signature = signature.bind_self().display(self.db)
)
@@ -155,12 +155,10 @@ impl Display for DisplayRepresentation<'_> {
write!(
f,
"<method-wrapper `__get__` of `{function}{specialization}`>",
function = function.function_literal(self.db).name(self.db),
specialization = if let FunctionType::Specialized(specialized) = function {
specialized
.specialization(self.db)
.display_short(self.db)
.to_string()
function = function.name(self.db),
specialization = if let Some(specialization) = function.specialization(self.db)
{
specialization.display_short(self.db).to_string()
} else {
String::new()
},
@@ -170,12 +168,10 @@ impl Display for DisplayRepresentation<'_> {
write!(
f,
"<method-wrapper `__call__` of `{function}{specialization}`>",
function = function.function_literal(self.db).name(self.db),
specialization = if let FunctionType::Specialized(specialized) = function {
specialized
.specialization(self.db)
.display_short(self.db)
.to_string()
function = function.name(self.db),
specialization = if let Some(specialization) = function.specialization(self.db)
{
specialization.display_short(self.db).to_string()
} else {
String::new()
},

View File

@@ -49,9 +49,9 @@ use crate::module_resolver::resolve_module;
use crate::node_key::NodeKey;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId, ScopedExpressionId};
use crate::semantic_index::definition::{
AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, ComprehensionDefinitionKind,
Definition, DefinitionKind, DefinitionNodeKey, ExceptHandlerDefinitionKind,
ForStmtDefinitionKind, TargetKind, WithItemDefinitionKind,
AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, Definition, DefinitionKind,
DefinitionNodeKey, ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind,
WithItemDefinitionKind,
};
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::symbol::{
@@ -81,13 +81,13 @@ use crate::types::generics::GenericContext;
use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
binding_type, todo_type, CallDunderError, CallableSignature, CallableType, Class,
ClassLiteralType, ClassType, DataclassMetadata, DynamicType, FunctionDecorators,
FunctionLiteral, GenericAlias, GenericClass, IntersectionBuilder, IntersectionType, KnownClass,
KnownFunction, KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, NonGenericClass,
Parameter, ParameterForm, Parameters, Signature, Signatures, SliceLiteralType,
StringLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type,
TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints,
todo_type, CallDunderError, CallableSignature, CallableType, Class, ClassLiteralType,
ClassType, DataclassMetadata, DynamicType, FunctionDecorators, FunctionType, GenericAlias,
GenericClass, IntersectionBuilder, IntersectionType, KnownClass, KnownFunction,
KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, NonGenericClass, Parameter,
ParameterForm, Parameters, Signature, Signatures, SliceLiteralType, StringLiteralType,
SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType,
TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints,
TypeVarInstance, UnionBuilder, UnionType,
};
use crate::unpack::{Unpack, UnpackPosition};
@@ -99,8 +99,8 @@ use super::diagnostic::{
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_invalid_type_checking_constant,
report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero,
report_unresolved_reference, INVALID_METACLASS, INVALID_PROTOCOL, REDUNDANT_CAST,
STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
report_unresolved_reference, INVALID_METACLASS, REDUNDANT_CAST, STATIC_ASSERT_ERROR,
SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
};
use super::slots::check_class_slots;
use super::string_annotation::{
@@ -306,7 +306,7 @@ pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> U
let _span =
tracing::trace_span!("infer_unpack_types", range=?unpack.range(db), ?file).entered();
let mut unpacker = Unpacker::new(db, unpack.target_scope(db), unpack.value_scope(db));
let mut unpacker = Unpacker::new(db, unpack.scope(db));
unpacker.unpack(unpack.target(db), unpack.value(db));
unpacker.finish()
}
@@ -763,21 +763,17 @@ impl<'db> TypeInferenceBuilder<'db> {
continue;
}
let is_protocol = class.is_protocol(self.db());
// (2) Iterate through the class's explicit bases to check for various possible errors:
// - Check for inheritance from plain `Generic`,
// - Check for inheritance from a `@final` classes
// - If the class is a protocol class: check for inheritance from a non-protocol class
// (2) Check for inheritance from plain `Generic`,
// and from classes that inherit from `@final` classes
for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() {
let base_class = match base_class {
Type::KnownInstance(KnownInstanceType::Generic) => {
// Unsubscripted `Generic` can appear in the MRO of many classes,
// `Generic` can appear in the MRO of many classes,
// but it is never valid as an explicit base class in user code.
self.context.report_lint_old(
&INVALID_BASE,
&class_node.bases()[i],
format_args!("Cannot inherit from plain `Generic`"),
format_args!("Cannot inherit from plain `Generic`",),
);
continue;
}
@@ -786,32 +782,18 @@ impl<'db> TypeInferenceBuilder<'db> {
_ => continue,
};
if is_protocol
&& !(base_class.is_protocol(self.db())
|| base_class.is_known(self.db(), KnownClass::Object))
{
self.context.report_lint_old(
&INVALID_PROTOCOL,
&class_node.bases()[i],
format_args!(
"Protocol class `{}` cannot inherit from non-protocol class `{}`",
class.name(self.db()),
base_class.name(self.db()),
),
);
}
if base_class.is_final(self.db()) {
self.context.report_lint_old(
&SUBCLASS_OF_FINAL_CLASS,
&class_node.bases()[i],
format_args!(
"Class `{}` cannot inherit from final class `{}`",
class.name(self.db()),
base_class.name(self.db()),
),
);
if !base_class.is_final(self.db()) {
continue;
}
self.context.report_lint_old(
&SUBCLASS_OF_FINAL_CLASS,
&class_node.bases()[i],
format_args!(
"Class `{}` cannot inherit from final class `{}`",
class.name(self.db()),
base_class.name(self.db()),
),
);
}
// (3) Check that the class's MRO is resolvable
@@ -964,7 +946,13 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_named_expression_definition(named_expression.node(), definition);
}
DefinitionKind::Comprehension(comprehension) => {
self.infer_comprehension_definition(comprehension, definition);
self.infer_comprehension_definition(
comprehension.iterable(),
comprehension.target(),
comprehension.is_first(),
comprehension.is_async(),
definition,
);
}
DefinitionKind::VariadicPositionalParameter(parameter) => {
self.infer_variadic_positional_parameter_definition(parameter, definition);
@@ -1242,7 +1230,7 @@ impl<'db> TypeInferenceBuilder<'db> {
/// Returns `true` if the current scope is the function body scope of a method of a protocol
/// (that is, a class which directly inherits `typing.Protocol`.)
fn in_protocol_class(&self) -> bool {
fn in_class_that_inherits_protocol_directly(&self) -> bool {
let current_scope_id = self.scope().file_scope_id(self.db());
let current_scope = self.index.scope(current_scope_id);
let Some(parent_scope_id) = current_scope.parent() else {
@@ -1270,13 +1258,13 @@ impl<'db> TypeInferenceBuilder<'db> {
return false;
};
let class_definition = self.index.expect_single_definition(node_ref.node());
let Type::ClassLiteral(class) = binding_type(self.db(), class_definition) else {
return false;
};
class.is_protocol(self.db())
// TODO move this to `Class` once we add proper `Protocol` support
node_ref.bases().iter().any(|base| {
matches!(
self.file_expression_type(base),
Type::KnownInstance(KnownInstanceType::Protocol)
)
})
}
/// Returns `true` if the current scope is the function body scope of a function overload (that
@@ -1340,7 +1328,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if (self.in_stub()
|| self.in_function_overload_or_abstractmethod()
|| self.in_protocol_class())
|| self.in_class_that_inherits_protocol_directly())
&& self.return_types_and_ranges.is_empty()
&& is_stub_suite(&function.body)
{
@@ -1503,6 +1491,10 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
let generic_context = type_params.as_ref().map(|type_params| {
GenericContext::from_type_params(self.db(), self.index, type_params)
});
let function_kind =
KnownFunction::try_from_definition_and_name(self.db(), definition, name);
@@ -1511,19 +1503,16 @@ impl<'db> TypeInferenceBuilder<'db> {
.node_scope(NodeWithScopeRef::Function(function))
.to_scope_id(self.db(), self.file());
let type_params_scope = type_params.as_ref().map(|_| {
self.index
.node_scope(NodeWithScopeRef::FunctionTypeParameters(function))
.to_scope_id(self.db(), self.file())
});
let specialization = None;
let mut inferred_ty = Type::from(FunctionLiteral::new(
let mut inferred_ty = Type::FunctionLiteral(FunctionType::new(
self.db(),
&name.id,
function_kind,
body_scope,
type_params_scope,
function_decorators,
generic_context,
specialization,
));
for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() {
@@ -1642,7 +1631,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
} else if (self.in_stub()
|| self.in_function_overload_or_abstractmethod()
|| self.in_protocol_class())
|| self.in_class_that_inherits_protocol_directly())
&& default
.as_ref()
.is_some_and(|d| d.is_ellipsis_literal_expr())
@@ -1948,13 +1937,11 @@ impl<'db> TypeInferenceBuilder<'db> {
for item in items {
let target = item.optional_vars.as_deref();
if let Some(target) = target {
self.infer_target(target, &item.context_expr, |builder, context_expr| {
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: ...
builder
.infer_standalone_expression(context_expr)
.enter(builder.db())
ctx_manager_ty.enter(db)
});
} else {
// Call into the context expression inference to validate that it evaluates
@@ -2360,9 +2347,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} = assignment;
for target in targets {
self.infer_target(target, value, |builder, value_expr| {
builder.infer_standalone_expression(value_expr)
});
self.infer_target(target, value, |_, ty| ty);
}
}
@@ -2372,16 +2357,23 @@ impl<'db> TypeInferenceBuilder<'db> {
/// targets (unpacking). If `target` is an attribute expression, we check that the assignment
/// is valid. For 'target's that are definitions, this check happens elsewhere.
///
/// The `infer_value_expr` function is used to infer the type of the `value` expression which
/// are not `Name` expressions. The returned type is the one that is eventually assigned to the
/// `target`.
fn infer_target<F>(&mut self, target: &ast::Expr, value: &ast::Expr, infer_value_expr: F)
/// The `to_assigned_ty` function is used to convert the inferred type of the `value` expression
/// to the type that is eventually assigned to the `target`.
///
/// # Panics
///
/// If the `value` is not a standalone expression.
fn infer_target<F>(&mut self, target: &ast::Expr, value: &ast::Expr, to_assigned_ty: F)
where
F: Fn(&mut TypeInferenceBuilder<'db>, &ast::Expr) -> Type<'db>,
F: Fn(&'db dyn Db, Type<'db>) -> Type<'db>,
{
let assigned_ty = match target {
ast::Expr::Name(_) => None,
_ => Some(infer_value_expr(self, value)),
_ => {
let value_ty = self.infer_standalone_expression(value);
Some(to_assigned_ty(self.db(), value_ty))
}
};
self.infer_target_impl(target, assigned_ty);
}
@@ -3134,13 +3126,11 @@ impl<'db> TypeInferenceBuilder<'db> {
is_async: _,
} = for_statement;
self.infer_target(target, iter, |builder, iter_expr| {
self.infer_target(target, iter, |db, iter_ty| {
// TODO: `infer_for_statement_definition` reports a diagnostic if `iter_ty` isn't iterable
// but only if the target is a name. We should report a diagnostic here if the target isn't a name:
// `for a.x in not_iterable: ...
builder
.infer_standalone_expression(iter_expr)
.iterate(builder.db())
iter_ty.iterate(db)
});
self.infer_body(body);
@@ -3969,17 +3959,15 @@ impl<'db> TypeInferenceBuilder<'db> {
is_async: _,
} = comprehension;
self.infer_target(target, iter, |builder, iter_expr| {
// TODO: `infer_comprehension_definition` reports a diagnostic if `iter_ty` isn't iterable
// but only if the target is a name. We should report a diagnostic here if the target isn't a name:
// `[... for a.x in not_iterable]
if is_first {
infer_same_file_expression_type(builder.db(), builder.index.expression(iter_expr))
} else {
builder.infer_standalone_expression(iter_expr)
}
.iterate(builder.db())
});
if !is_first {
self.infer_standalone_expression(iter);
}
// TODO more complex assignment targets
if let ast::Expr::Name(name) = target {
self.infer_definition(name);
} else {
self.infer_expression(target);
}
for expr in ifs {
self.infer_expression(expr);
}
@@ -3987,12 +3975,12 @@ impl<'db> TypeInferenceBuilder<'db> {
fn infer_comprehension_definition(
&mut self,
comprehension: &ComprehensionDefinitionKind<'db>,
iterable: &ast::Expr,
target: &ast::ExprName,
is_first: bool,
is_async: bool,
definition: Definition<'db>,
) {
let iterable = comprehension.iterable();
let target = comprehension.target();
let expression = self.index.expression(iterable);
let result = infer_expression_types(self.db(), expression);
@@ -4002,7 +3990,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// (2) We must *not* call `self.extend()` on the result of the type inference,
// because `ScopedExpressionId`s are only meaningful within their own scope, so
// we'd add types for random wrong expressions in the current scope
let iterable_type = if comprehension.is_first() {
let iterable_type = if is_first {
let lookup_scope = self
.index
.parent_scope_id(self.scope().file_scope_id(self.db()))
@@ -4014,26 +4002,14 @@ impl<'db> TypeInferenceBuilder<'db> {
result.expression_type(iterable.scoped_expression_id(self.db(), self.scope()))
};
let target_type = if comprehension.is_async() {
let target_type = if is_async {
// TODO: async iterables/iterators! -- Alex
todo_type!("async iterables/iterators")
} else {
match comprehension.target_kind() {
TargetKind::Sequence(unpack_position, unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
if unpack_position == UnpackPosition::First {
self.context.extend(unpacked.diagnostics());
}
let target_ast_id = target.scoped_expression_id(self.db(), self.scope());
unpacked.expression_type(target_ast_id)
}
TargetKind::NameOrAttribute => {
iterable_type.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, iterable_type, iterable.into());
err.fallback_element_type(self.db())
})
}
}
iterable_type.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, iterable_type, iterable.into());
err.fallback_element_type(self.db())
})
};
self.types.expressions.insert(

View File

@@ -289,18 +289,17 @@ impl<'db> Signature<'db> {
}
pub(crate) fn apply_specialization(
&mut self,
&self,
db: &'db dyn Db,
specialization: Specialization<'db>,
) {
self.parameters.apply_specialization(db, specialization);
self.return_ty
.as_mut()
.map(|ty| *ty = ty.apply_specialization(db, specialization));
}
pub(crate) fn set_generic_context(&mut self, generic_context: GenericContext<'db>) {
self.generic_context = Some(generic_context);
) -> Self {
Self {
generic_context: self.generic_context,
parameters: self.parameters.apply_specialization(db, specialization),
return_ty: self
.return_ty
.map(|ty| ty.apply_specialization(db, specialization)),
}
}
/// Return the parameters in this signature.
@@ -1001,10 +1000,15 @@ impl<'db> Parameters<'db> {
)
}
fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) {
self.value
.iter_mut()
.for_each(|param| param.apply_specialization(db, specialization));
fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
Self {
value: self
.value
.iter()
.map(|param| param.apply_specialization(db, specialization))
.collect(),
is_gradual: self.is_gradual,
}
}
pub(crate) fn len(&self) -> usize {
@@ -1168,11 +1172,14 @@ impl<'db> Parameter<'db> {
self
}
fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) {
self.annotated_type
.as_mut()
.map(|ty| *ty = ty.apply_specialization(db, specialization));
self.kind.apply_specialization(db, specialization);
fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
Self {
annotated_type: self
.annotated_type
.map(|ty| ty.apply_specialization(db, specialization)),
kind: self.kind.apply_specialization(db, specialization),
form: self.form,
}
}
/// Strip information from the parameter so that two equivalent parameters compare equal.
@@ -1362,16 +1369,27 @@ pub(crate) enum ParameterKind<'db> {
}
impl<'db> ParameterKind<'db> {
fn apply_specialization(&mut self, db: &'db dyn Db, specialization: Specialization<'db>) {
fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self {
match self {
Self::PositionalOnly { default_type, .. }
| Self::PositionalOrKeyword { default_type, .. }
| Self::KeywordOnly { default_type, .. } => {
default_type
.as_mut()
.map(|ty| *ty = ty.apply_specialization(db, specialization));
}
Self::Variadic { .. } | Self::KeywordVariadic { .. } => {}
Self::PositionalOnly { default_type, name } => Self::PositionalOnly {
default_type: default_type
.as_ref()
.map(|ty| ty.apply_specialization(db, specialization)),
name: name.clone(),
},
Self::PositionalOrKeyword { default_type, name } => Self::PositionalOrKeyword {
default_type: default_type
.as_ref()
.map(|ty| ty.apply_specialization(db, specialization)),
name: name.clone(),
},
Self::KeywordOnly { default_type, name } => Self::KeywordOnly {
default_type: default_type
.as_ref()
.map(|ty| ty.apply_specialization(db, specialization)),
name: name.clone(),
},
Self::Variadic { .. } | Self::KeywordVariadic { .. } => self.clone(),
}
}
}
@@ -1388,20 +1406,16 @@ mod tests {
use super::*;
use crate::db::tests::{setup_db, TestDb};
use crate::symbol::global_symbol;
use crate::types::{FunctionLiteral, FunctionSignature, FunctionType, KnownClass};
use crate::types::{FunctionSignature, FunctionType, KnownClass};
use ruff_db::system::DbWithWritableSystem as _;
#[track_caller]
fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionLiteral<'db> {
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();
let function = global_symbol(db, module, "f")
global_symbol(db, module, "f")
.symbol
.expect_type()
.expect_function_literal();
let FunctionType::FunctionLiteral(literal) = function else {
panic!("function should be a function literal");
};
literal
.expect_function_literal()
}
#[track_caller]
@@ -1639,6 +1653,9 @@ mod tests {
let expected_sig = func.internal_signature(&db);
// With no decorators, internal and external signature are the same
assert_eq!(func.signature(&db), FunctionSignature::Single(expected_sig));
assert_eq!(
func.signature(&db),
&FunctionSignature::Single(expected_sig)
);
}
}

View File

@@ -18,22 +18,16 @@ use super::{TupleType, UnionType};
/// Unpacks the value expression type to their respective targets.
pub(crate) struct Unpacker<'db> {
context: InferContext<'db>,
target_scope: ScopeId<'db>,
value_scope: ScopeId<'db>,
scope: ScopeId<'db>,
targets: FxHashMap<ScopedExpressionId, Type<'db>>,
}
impl<'db> Unpacker<'db> {
pub(crate) fn new(
db: &'db dyn Db,
target_scope: ScopeId<'db>,
value_scope: ScopeId<'db>,
) -> Self {
pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self {
Self {
context: InferContext::new(db, target_scope),
context: InferContext::new(db, scope),
targets: FxHashMap::default(),
target_scope,
value_scope,
scope,
}
}
@@ -49,7 +43,7 @@ impl<'db> Unpacker<'db> {
);
let value_type = infer_expression_types(self.db(), value.expression())
.expression_type(value.scoped_expression_id(self.db(), self.value_scope));
.expression_type(value.scoped_expression_id(self.db(), self.scope));
let value_type = match value.kind() {
UnpackKind::Assign => {
@@ -85,10 +79,8 @@ impl<'db> Unpacker<'db> {
) {
match target {
ast::Expr::Name(_) | ast::Expr::Attribute(_) => {
self.targets.insert(
target.scoped_expression_id(self.db(), self.target_scope),
value_ty,
);
self.targets
.insert(target.scoped_expression_id(self.db(), self.scope), value_ty);
}
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
self.unpack_inner(value, value_expr, value_ty);

View File

@@ -30,9 +30,7 @@ use crate::Db;
pub(crate) struct Unpack<'db> {
pub(crate) file: File,
pub(crate) value_file_scope: FileScopeId,
pub(crate) target_file_scope: FileScopeId,
pub(crate) file_scope: FileScopeId,
/// The target expression that is being unpacked. For example, in `(a, b) = (1, 2)`, the target
/// expression is `(a, b)`.
@@ -49,19 +47,9 @@ pub(crate) struct Unpack<'db> {
}
impl<'db> Unpack<'db> {
/// Returns the scope in which the unpack value expression belongs.
///
/// The scope in which the target and value expression belongs to are usually the same
/// except in generator expressions and comprehensions (list/dict/set), where the value
/// expression of the first generator is evaluated in the outer scope, while the ones in the subsequent
/// generators are evaluated in the comprehension scope.
pub(crate) fn value_scope(self, db: &'db dyn Db) -> ScopeId<'db> {
self.value_file_scope(db).to_scope_id(db, self.file(db))
}
/// Returns the scope where the unpack target expression belongs to.
pub(crate) fn target_scope(self, db: &'db dyn Db) -> ScopeId<'db> {
self.target_file_scope(db).to_scope_id(db, self.file(db))
/// Returns the scope where the unpacking is happening.
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
self.file_scope(db).to_scope_id(db, self.file(db))
}
/// Returns the range of the unpack target expression.

View File

@@ -1,5 +1,5 @@
PyYAML==6.0.2
ruff==0.11.6
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

View File

@@ -1,5 +1,5 @@
PyYAML==6.0.2
ruff==0.11.6
ruff==0.9.10
mkdocs==1.6.1
mkdocs-material==9.5.38
mkdocs-redirects==1.2.2

View File

@@ -460,16 +460,6 @@
}
]
},
"invalid-protocol": {
"title": "detects invalid protocol class definitions",
"description": "## What it does\nChecks for invalidly defined protocol classes.\n\n## Why is this bad?\nAn invalidly defined protocol class may lead to the type checker inferring\nunexpected things. It may also lead to `TypeError`s at runtime.\n\n## Examples\nA `Protocol` class cannot inherit from a non-`Protocol` class;\nthis raises a `TypeError` at runtime:\n\n```pycon\n>>> from typing import Protocol\n>>> class Foo(int, Protocol): ...\n...\nTraceback (most recent call last):\n File \"<python-input-1>\", line 1, in <module>\n class Foo(int, Protocol): ...\nTypeError: Protocols can only inherit from other protocols, got <class 'int'>\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"invalid-raise": {
"title": "detects `raise` statements that raise invalid exceptions or use invalid causes",
"description": "Checks for `raise` statements that raise non-exceptions or use invalid\ncauses for their raised exceptions.\n\n## Why is this bad?\nOnly subclasses or instances of `BaseException` can be raised.\nFor an exception's cause, the same rules apply, except that `None` is also\npermitted. Violating these rules results in a `TypeError` at runtime.\n\n## Examples\n```python\ndef f():\n try:\n something()\n except NameError:\n raise \"oops!\" from f\n\ndef g():\n raise NotImplemented from 42\n```\n\nUse instead:\n```python\ndef f():\n try:\n something()\n except NameError as e:\n raise RuntimeError(\"oops!\") from e\n\ndef g():\n raise NotImplementedError from None\n```\n\n## References\n- [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#raise)\n- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)",