Compare commits
130 Commits
micha/perf
...
refactor-A
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b3d63b964 | ||
|
|
9a615d618a | ||
|
|
11e873eb45 | ||
|
|
89ea0371a4 | ||
|
|
f8c9665742 | ||
|
|
af95f6b577 | ||
|
|
79682a28b8 | ||
|
|
1ef0f615f1 | ||
|
|
7ca3f9515c | ||
|
|
32de5801f7 | ||
|
|
cfd6093579 | ||
|
|
3c9021ffcb | ||
|
|
68d2466832 | ||
|
|
ecf00cdf6a | ||
|
|
86bdc2e7b1 | ||
|
|
253c274afa | ||
|
|
2a1aa29366 | ||
|
|
42cc67a87c | ||
|
|
280ba75100 | ||
|
|
04d538113a | ||
|
|
0b15f17939 | ||
|
|
0caab81d3d | ||
|
|
d4ee6abf4a | ||
|
|
8a98d88847 | ||
|
|
901b7dd8f8 | ||
|
|
d3492178e1 | ||
|
|
383f6d0967 | ||
|
|
e953ecf42f | ||
|
|
e8cf2d3027 | ||
|
|
0701437104 | ||
|
|
db0468bae5 | ||
|
|
7e491c4d94 | ||
|
|
957df82400 | ||
|
|
9ba2fb0a48 | ||
|
|
8ff9cb75cd | ||
|
|
f170932585 | ||
|
|
bc3a735d93 | ||
|
|
7ea3a549b2 | ||
|
|
2288cc7478 | ||
|
|
79816f965c | ||
|
|
8d2d1a73c5 | ||
|
|
2419fdb2ef | ||
|
|
6ed27c3786 | ||
|
|
5d6aae839e | ||
|
|
8b9c843c72 | ||
|
|
5bc9d6d3aa | ||
|
|
97965ff114 | ||
|
|
8d327087ef | ||
|
|
2835d94ec5 | ||
|
|
68ada05b00 | ||
|
|
2a99c0be02 | ||
|
|
dcb85b7088 | ||
|
|
8440f3ea9f | ||
|
|
1c3d11e8a8 | ||
|
|
2f85749fa0 | ||
|
|
9eb73cb7e0 | ||
|
|
03bb9425df | ||
|
|
113c804a62 | ||
|
|
b6c8f5d79e | ||
|
|
da8acabc55 | ||
|
|
fd2b8deddd | ||
|
|
efbadbd966 | ||
|
|
130cc24e2c | ||
|
|
d555eca729 | ||
|
|
489f4fb27a | ||
|
|
429d3d78b8 | ||
|
|
4dae9ee1cf | ||
|
|
d8b4779a87 | ||
|
|
88cdfcfb11 | ||
|
|
f764f59971 | ||
|
|
3b27d5dbad | ||
|
|
60e433c3b5 | ||
|
|
2fb6b320d8 | ||
|
|
fd4bea52e5 | ||
|
|
bd023c4500 | ||
|
|
000948ad3b | ||
|
|
d3f51cf3a6 | ||
|
|
d47fba1e4a | ||
|
|
089a98e904 | ||
|
|
913bce3cd5 | ||
|
|
6195c026ff | ||
|
|
c0b0491703 | ||
|
|
51863b460b | ||
|
|
14a5a2629e | ||
|
|
3aed14935d | ||
|
|
bcec5e615b | ||
|
|
a06099dffe | ||
|
|
bb43085939 | ||
|
|
c1eaf6ff72 | ||
|
|
3bb0dac235 | ||
|
|
40cba5dc8a | ||
|
|
596d80cc8e | ||
|
|
d8b9a366c8 | ||
|
|
85e71ba91a | ||
|
|
2802cbde29 | ||
|
|
ed2bce6ebb | ||
|
|
f0012df686 | ||
|
|
0fc4e8f795 | ||
|
|
ac81c72bf3 | ||
|
|
c0b7c36d43 | ||
|
|
e8e461da6a | ||
|
|
91c9168dd7 | ||
|
|
80577a49f8 | ||
|
|
f463fa7b7c | ||
|
|
c3b6139f39 | ||
|
|
c9fdb1f5e3 | ||
|
|
463046ae07 | ||
|
|
dcb99cc817 | ||
|
|
7c2e7cf25e | ||
|
|
867a8f9497 | ||
|
|
e22718f25f | ||
|
|
dcdc6e7c64 | ||
|
|
4ddf9228f6 | ||
|
|
6d72be2683 | ||
|
|
712c886749 | ||
|
|
50739f91dc | ||
|
|
6a5eff6017 | ||
|
|
a623d8f7c4 | ||
|
|
aa429b413f | ||
|
|
425c248232 | ||
|
|
bcd944347d | ||
|
|
86eff81c6a | ||
|
|
24ace68560 | ||
|
|
b664505d7b | ||
|
|
aa575da1e7 | ||
|
|
921eb2acb3 | ||
|
|
8665d2dc95 | ||
|
|
1cc27c995c | ||
|
|
a93bc2af6b | ||
|
|
d848182340 |
9
.github/actionlint.yaml
vendored
Normal file
9
.github/actionlint.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Configuration for the actionlint tool, which we run via pre-commit
|
||||
# to verify the correctness of the syntax in our GitHub Actions workflows.
|
||||
|
||||
self-hosted-runner:
|
||||
# Various runners we use that aren't recognized out-of-the-box by actionlint:
|
||||
labels:
|
||||
- depot-ubuntu-latest-8
|
||||
- depot-ubuntu-22.04-16
|
||||
- windows-latest-xlarge
|
||||
8
.github/workflows/build-binaries.yml
vendored
8
.github/workflows/build-binaries.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
args: --out dist
|
||||
- name: "Test sdist"
|
||||
run: |
|
||||
pip install dist/${PACKAGE_NAME}-*.tar.gz --force-reinstall
|
||||
pip install dist/"${PACKAGE_NAME}"-*.tar.gz --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload sdist"
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
args: --release --locked --out dist
|
||||
- name: "Test wheel - aarch64"
|
||||
run: |
|
||||
pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
|
||||
pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Upload wheels"
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
|
||||
shell: bash
|
||||
run: |
|
||||
python -m pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
|
||||
python -m pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload wheels"
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
- name: "Test wheel"
|
||||
if: ${{ startsWith(matrix.target, 'x86_64') }}
|
||||
run: |
|
||||
pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
|
||||
pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload wheels"
|
||||
|
||||
3
.github/workflows/build-docker.yml
vendored
3
.github/workflows/build-docker.yml
vendored
@@ -142,6 +142,7 @@ jobs:
|
||||
# The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
|
||||
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
|
||||
run: |
|
||||
# shellcheck disable=SC2046
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${RUFF_BASE_IMG}@sha256:%s " *)
|
||||
@@ -286,6 +287,8 @@ jobs:
|
||||
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
|
||||
run: |
|
||||
readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done
|
||||
|
||||
# shellcheck disable=SC2046
|
||||
docker buildx imagetools create \
|
||||
"${annotations[@]}" \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
|
||||
74
.github/workflows/ci.yaml
vendored
74
.github/workflows/ci.yaml
vendored
@@ -290,7 +290,9 @@ jobs:
|
||||
file: "Cargo.toml"
|
||||
field: "workspace.package.rust-version"
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup default ${{ steps.msrv.outputs.value }}
|
||||
env:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
run: rustup default "${MSRV}"
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
@@ -306,7 +308,8 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
run: cargo +${{ steps.msrv.outputs.value }} insta test --all-features --unreferenced reject --test-runner nextest
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
run: cargo "+${MSRV}" insta test --all-features --unreferenced reject --test-runner nextest
|
||||
|
||||
cargo-fuzz-build:
|
||||
name: "cargo fuzz build"
|
||||
@@ -346,7 +349,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
@@ -354,16 +357,18 @@ jobs:
|
||||
name: ruff
|
||||
path: ruff-to-test
|
||||
- name: Fuzz
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.download-cached-binary.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
chmod +x "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
(
|
||||
uvx \
|
||||
--python=${{ env.PYTHON_VERSION }} \
|
||||
--python="${PYTHON_VERSION}" \
|
||||
--from=./python/py-fuzzer \
|
||||
fuzz \
|
||||
--test-executable=${{ steps.download-cached-binary.outputs.download-path }}/ruff \
|
||||
--test-executable="${DOWNLOAD_PATH}/ruff" \
|
||||
--bin=ruff \
|
||||
0-500
|
||||
)
|
||||
@@ -429,64 +434,72 @@ jobs:
|
||||
|
||||
- name: Run `ruff check` stable ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
|
||||
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
|
||||
|
||||
cat ecosystem-result-check-stable > $GITHUB_STEP_SUMMARY
|
||||
cat ecosystem-result-check-stable > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Linter (stable)" > ecosystem-result
|
||||
cat ecosystem-result-check-stable >> ecosystem-result
|
||||
echo "" >> ecosystem-result
|
||||
|
||||
- name: Run `ruff check` preview ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
|
||||
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
|
||||
|
||||
cat ecosystem-result-check-preview > $GITHUB_STEP_SUMMARY
|
||||
cat ecosystem-result-check-preview > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Linter (preview)" >> ecosystem-result
|
||||
cat ecosystem-result-check-preview >> ecosystem-result
|
||||
echo "" >> ecosystem-result
|
||||
|
||||
- name: Run `ruff format` stable ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
|
||||
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
|
||||
|
||||
cat ecosystem-result-format-stable > $GITHUB_STEP_SUMMARY
|
||||
cat ecosystem-result-format-stable > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Formatter (stable)" >> ecosystem-result
|
||||
cat ecosystem-result-format-stable >> ecosystem-result
|
||||
echo "" >> ecosystem-result
|
||||
|
||||
- name: Run `ruff format` preview ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
|
||||
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
|
||||
|
||||
cat ecosystem-result-format-preview > $GITHUB_STEP_SUMMARY
|
||||
cat ecosystem-result-format-preview > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Formatter (preview)" >> ecosystem-result
|
||||
cat ecosystem-result-format-preview >> ecosystem-result
|
||||
echo "" >> ecosystem-result
|
||||
@@ -541,7 +554,7 @@ jobs:
|
||||
args: --out dist
|
||||
- name: "Test wheel"
|
||||
run: |
|
||||
pip install --force-reinstall --find-links dist ${{ env.PACKAGE_NAME }}
|
||||
pip install --force-reinstall --find-links dist "${PACKAGE_NAME}"
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Remove wheels from cache"
|
||||
@@ -570,13 +583,14 @@ jobs:
|
||||
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: "Run pre-commit"
|
||||
run: |
|
||||
echo '```console' > $GITHUB_STEP_SUMMARY
|
||||
echo '```console' > "$GITHUB_STEP_SUMMARY"
|
||||
# Enable color output for pre-commit and remove it for the summary
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always | \
|
||||
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> $GITHUB_STEP_SUMMARY) >&1
|
||||
exit_code=${PIPESTATUS[0]}
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit $exit_code
|
||||
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
|
||||
exit_code="${PIPESTATUS[0]}"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
exit "$exit_code"
|
||||
|
||||
docs:
|
||||
name: "mkdocs"
|
||||
@@ -599,7 +613,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
@@ -637,7 +651,7 @@ jobs:
|
||||
- name: "Run checks"
|
||||
run: scripts/formatter_ecosystem_checks.sh
|
||||
- name: "Github step summary"
|
||||
run: cat target/formatter-ecosystem/stats.txt > $GITHUB_STEP_SUMMARY
|
||||
run: cat target/formatter-ecosystem/stats.txt > "$GITHUB_STEP_SUMMARY"
|
||||
- name: "Remove checkouts from cache"
|
||||
run: rm -r target/formatter-ecosystem
|
||||
|
||||
@@ -676,11 +690,13 @@ jobs:
|
||||
just install
|
||||
|
||||
- name: Run ruff-lsp tests
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Setup development binary
|
||||
pip uninstall --yes ruff
|
||||
chmod +x ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
export PATH=${{ steps.ruff-target.outputs.download-path }}:$PATH
|
||||
chmod +x "${DOWNLOAD_PATH}/ruff"
|
||||
export PATH="${DOWNLOAD_PATH}:${PATH}"
|
||||
ruff version
|
||||
|
||||
just test
|
||||
|
||||
3
.github/workflows/daily_fuzz.yaml
vendored
3
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -46,6 +46,7 @@ jobs:
|
||||
run: cargo build --locked
|
||||
- name: Fuzz
|
||||
run: |
|
||||
# shellcheck disable=SC2046
|
||||
(
|
||||
uvx \
|
||||
--python=3.12 \
|
||||
|
||||
13
.github/workflows/pr-comment.yaml
vendored
13
.github/workflows/pr-comment.yaml
vendored
@@ -10,12 +10,11 @@ on:
|
||||
description: The ecosystem workflow that triggers the workflow run
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
name: Download pull request number
|
||||
@@ -30,7 +29,7 @@ jobs:
|
||||
run: |
|
||||
if [[ -f pr-number ]]
|
||||
then
|
||||
echo "pr-number=$(<pr-number)" >> $GITHUB_OUTPUT
|
||||
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
@@ -66,9 +65,9 @@ jobs:
|
||||
cat pr/ecosystem/ecosystem-result >> comment.txt
|
||||
echo "" >> comment.txt
|
||||
|
||||
echo 'comment<<EOF' >> $GITHUB_OUTPUT
|
||||
cat comment.txt >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
|
||||
cat comment.txt >> "$GITHUB_OUTPUT"
|
||||
echo 'EOF' >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find existing comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
|
||||
20
.github/workflows/publish-docs.yml
vendored
20
.github/workflows/publish-docs.yml
vendored
@@ -44,8 +44,8 @@ jobs:
|
||||
# Use version as display name for now
|
||||
display_name="$version"
|
||||
|
||||
echo "version=$version" >> $GITHUB_ENV
|
||||
echo "display_name=$display_name" >> $GITHUB_ENV
|
||||
echo "version=$version" >> "$GITHUB_ENV"
|
||||
echo "display_name=$display_name" >> "$GITHUB_ENV"
|
||||
|
||||
- name: "Set branch name"
|
||||
run: |
|
||||
@@ -55,8 +55,8 @@ jobs:
|
||||
# characters disallowed in git branch names with hyphens
|
||||
branch_display_name="$(echo "${display_name}" | tr -c '[:alnum:]._' '-' | tr -s '-')"
|
||||
|
||||
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> $GITHUB_ENV
|
||||
echo "timestamp=$timestamp" >> $GITHUB_ENV
|
||||
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> "$GITHUB_ENV"
|
||||
echo "timestamp=$timestamp" >> "$GITHUB_ENV"
|
||||
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
|
||||
run: |
|
||||
# set the PR title
|
||||
pull_request_title="Update ruff documentation for "${display_name}""
|
||||
pull_request_title="Update ruff documentation for ${display_name}"
|
||||
|
||||
# Delete any existing pull requests that are open for this version
|
||||
# by checking against pull_request_title because the new PR will
|
||||
@@ -124,10 +124,12 @@ jobs:
|
||||
git push origin "${branch_name}"
|
||||
|
||||
# create the PR
|
||||
gh pr create --base main --head "${branch_name}" \
|
||||
--title "$pull_request_title" \
|
||||
--body "Automated documentation update for "${display_name}"" \
|
||||
--label "documentation"
|
||||
gh pr create \
|
||||
--base=main \
|
||||
--head="${branch_name}" \
|
||||
--title="${pull_request_title}" \
|
||||
--body="Automated documentation update for ${display_name}" \
|
||||
--label="documentation"
|
||||
|
||||
- name: "Merge Pull Request"
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
|
||||
2
.github/workflows/publish-pypi.yml
vendored
2
.github/workflows/publish-pypi.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@v4
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
2
.github/workflows/sync_typeshed.yaml
vendored
2
.github/workflows/sync_typeshed.yaml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
run: |
|
||||
cd ruff
|
||||
git push --force origin typeshedbot/sync-typeshed
|
||||
gh pr list --repo $GITHUB_REPOSITORY --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
|
||||
gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
|
||||
gh pr create --title "Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "internal"
|
||||
|
||||
create-issue-on-failure:
|
||||
|
||||
6
.github/zizmor.yml
vendored
Normal file
6
.github/zizmor.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
|
||||
# https://woodruffw.github.io/zizmor/configuration/
|
||||
rules:
|
||||
dangerous-triggers:
|
||||
ignore:
|
||||
- pr-comment.yaml
|
||||
@@ -21,3 +21,11 @@ MD014: false
|
||||
MD024:
|
||||
# Allow when nested under different parents e.g. CHANGELOG.md
|
||||
siblings_only: true
|
||||
|
||||
# MD046/code-block-style
|
||||
#
|
||||
# Ignore this because it conflicts with the code block style used in content
|
||||
# tabs of mkdocs-material which is to add a blank line after the content title.
|
||||
#
|
||||
# Ref: https://github.com/astral-sh/ruff/pull/15011#issuecomment-2544790854
|
||||
MD046: false
|
||||
|
||||
@@ -2,6 +2,7 @@ fail_fast: false
|
||||
|
||||
exclude: |
|
||||
(?x)^(
|
||||
.github/workflows/release.yml|
|
||||
crates/red_knot_vendored/vendor/.*|
|
||||
crates/red_knot_workspace/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
@@ -22,13 +23,12 @@ repos:
|
||||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/executablebooks/mdformat
|
||||
rev: 0.7.19
|
||||
rev: 0.7.21
|
||||
hooks:
|
||||
- id: mdformat
|
||||
additional_dependencies:
|
||||
- mdformat-mkdocs
|
||||
- mdformat-admon
|
||||
- mdformat-footnote
|
||||
- mdformat-mkdocs==4.0.0
|
||||
- mdformat-footnote==0.1.1
|
||||
exclude: |
|
||||
(?x)^(
|
||||
docs/formatter/black\.md
|
||||
@@ -59,7 +59,7 @@ repos:
|
||||
- black==24.10.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.28.2
|
||||
rev: v1.28.4
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -73,7 +73,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.2
|
||||
rev: v0.8.4
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -88,18 +88,37 @@ repos:
|
||||
- id: prettier
|
||||
types: [yaml]
|
||||
|
||||
# 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: v0.8.0
|
||||
rev: v0.10.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
# `release.yml` is autogenerated by `dist`; security issues need to be fixed there
|
||||
# (https://opensource.axo.dev/cargo-dist/)
|
||||
exclude: .github/workflows/release.yml
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.30.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
|
||||
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
rev: v1.7.5
|
||||
hooks:
|
||||
- id: actionlint
|
||||
stages:
|
||||
# This hook is disabled by default, since it's quite slow.
|
||||
# To run all hooks *including* this hook, use `uvx pre-commit run -a --hook-stage=manual`.
|
||||
# To run *just* this hook, use `uvx pre-commit run -a actionlint --hook-stage=manual`.
|
||||
- manual
|
||||
args:
|
||||
- "-ignore=SC2129" # ignorable stylistic lint from shellcheck
|
||||
- "-ignore=SC2016" # another shellcheck lint: seems to have false positives?
|
||||
additional_dependencies:
|
||||
# actionlint has a shellcheck integration which extracts shell scripts in `run:` steps from GitHub Actions
|
||||
# and checks these with shellcheck. This is arguably its most useful feature,
|
||||
# but the integration only works if shellcheck is installed
|
||||
- "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.10.0"
|
||||
|
||||
ci:
|
||||
skip: [cargo-fmt, dev-generate-all]
|
||||
|
||||
69
CHANGELOG.md
69
CHANGELOG.md
@@ -1,5 +1,74 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.5
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Extend names moved from core to provider (`AIR303`) ([#15145](https://github.com/astral-sh/ruff/pull/15145), [#15159](https://github.com/astral-sh/ruff/pull/15159), [#15196](https://github.com/astral-sh/ruff/pull/15196), [#15216](https://github.com/astral-sh/ruff/pull/15216))
|
||||
- \[`airflow`\] Extend rule to check class attributes, methods, arguments (`AIR302`) ([#15054](https://github.com/astral-sh/ruff/pull/15054), [#15083](https://github.com/astral-sh/ruff/pull/15083))
|
||||
- \[`fastapi`\] Update `FAST002` to check keyword-only arguments ([#15119](https://github.com/astral-sh/ruff/pull/15119))
|
||||
- \[`flake8-type-checking`\] Disable `TC006` and `TC007` in stub files ([#15179](https://github.com/astral-sh/ruff/pull/15179))
|
||||
- \[`pylint`\] Detect nested methods correctly (`PLW1641`) ([#15032](https://github.com/astral-sh/ruff/pull/15032))
|
||||
- \[`ruff`\] Detect more strict-integer expressions (`RUF046`) ([#14833](https://github.com/astral-sh/ruff/pull/14833))
|
||||
- \[`ruff`\] Implement `falsy-dict-get-fallback` (`RUF056`) ([#15160](https://github.com/astral-sh/ruff/pull/15160))
|
||||
- \[`ruff`\] Implement `unnecessary-round` (`RUF057`) ([#14828](https://github.com/astral-sh/ruff/pull/14828))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Visit PEP 764 inline `TypedDict` keys as non-type-expressions ([#15073](https://github.com/astral-sh/ruff/pull/15073))
|
||||
- \[`flake8-comprehensions`\] Skip `C416` if comprehension contains unpacking ([#14909](https://github.com/astral-sh/ruff/pull/14909))
|
||||
- \[`flake8-pie`\] Allow `cast(SomeType, ...)` (`PIE796`) ([#15141](https://github.com/astral-sh/ruff/pull/15141))
|
||||
- \[`flake8-simplify`\] More precise inference for dictionaries (`SIM300`) ([#15164](https://github.com/astral-sh/ruff/pull/15164))
|
||||
- \[`flake8-use-pathlib`\] Catch redundant joins in `PTH201` and avoid syntax errors ([#15177](https://github.com/astral-sh/ruff/pull/15177))
|
||||
- \[`pycodestyle`\] Preserve original value format (`E731`) ([#15097](https://github.com/astral-sh/ruff/pull/15097))
|
||||
- \[`pydocstyle`\] Split on first whitespace character (`D403`) ([#15082](https://github.com/astral-sh/ruff/pull/15082))
|
||||
- \[`pyupgrade`\] Add all PEP-585 names to `UP006` rule ([#5454](https://github.com/astral-sh/ruff/pull/5454))
|
||||
|
||||
### Configuration
|
||||
|
||||
- \[`flake8-type-checking`\] Improve flexibility of `runtime-evaluated-decorators` ([#15204](https://github.com/astral-sh/ruff/pull/15204))
|
||||
- \[`pydocstyle`\] Add setting to ignore missing documentation for `*args` and `**kwargs` parameters (`D417`) ([#15210](https://github.com/astral-sh/ruff/pull/15210))
|
||||
- \[`ruff`\] Add an allowlist for `unsafe-markup-use` (`RUF035`) ([#15076](https://github.com/astral-sh/ruff/pull/15076))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix type subscript on older python versions ([#15090](https://github.com/astral-sh/ruff/pull/15090))
|
||||
- Use `TypeChecker` for detecting `fastapi` routes ([#15093](https://github.com/astral-sh/ruff/pull/15093))
|
||||
- \[`pycodestyle`\] Avoid false positives and negatives related to type parameter default syntax (`E225`, `E251`) ([#15214](https://github.com/astral-sh/ruff/pull/15214))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix incorrect doc in `shebang-not-executable` (`EXE001`) and add git+windows solution to executable bit ([#15208](https://github.com/astral-sh/ruff/pull/15208))
|
||||
- Rename rules currently not conforming to naming convention ([#15102](https://github.com/astral-sh/ruff/pull/15102))
|
||||
|
||||
## 0.8.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Extend `AIR302` with additional functions and classes ([#15015](https://github.com/astral-sh/ruff/pull/15015))
|
||||
- \[`airflow`\] Implement `moved-to-provider-in-3` for modules that has been moved to Airflow providers (`AIR303`) ([#14764](https://github.com/astral-sh/ruff/pull/14764))
|
||||
- \[`flake8-use-pathlib`\] Extend check for invalid path suffix to include the case `"."` (`PTH210`) ([#14902](https://github.com/astral-sh/ruff/pull/14902))
|
||||
- \[`perflint`\] Fix panic in `PERF401` when list variable is after the `for` loop ([#14971](https://github.com/astral-sh/ruff/pull/14971))
|
||||
- \[`perflint`\] Simplify finding the loop target in `PERF401` ([#15025](https://github.com/astral-sh/ruff/pull/15025))
|
||||
- \[`pylint`\] Preserve original value format (`PLR6104`) ([#14978](https://github.com/astral-sh/ruff/pull/14978))
|
||||
- \[`ruff`\] Avoid false positives for `RUF027` for typing context bindings ([#15037](https://github.com/astral-sh/ruff/pull/15037))
|
||||
- \[`ruff`\] Check for ambiguous pattern passed to `pytest.raises()` (`RUF043`) ([#14966](https://github.com/astral-sh/ruff/pull/14966))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-bandit`\] Check `S105` for annotated assignment ([#15059](https://github.com/astral-sh/ruff/pull/15059))
|
||||
- \[`flake8-pyi`\] More autofixes for `redundant-none-literal` (`PYI061`) ([#14872](https://github.com/astral-sh/ruff/pull/14872))
|
||||
- \[`pydocstyle`\] Skip leading whitespace for `D403` ([#14963](https://github.com/astral-sh/ruff/pull/14963))
|
||||
- \[`ruff`\] Skip `SQLModel` base classes for `mutable-class-default` (`RUF012`) ([#14949](https://github.com/astral-sh/ruff/pull/14949))
|
||||
|
||||
### Bug
|
||||
|
||||
- \[`perflint`\] Parenthesize walrus expressions in autofix for `manual-list-comprehension` (`PERF401`) ([#15050](https://github.com/astral-sh/ruff/pull/15050))
|
||||
|
||||
### Server
|
||||
|
||||
- Check diagnostic refresh support from client capability which enables dynamic configuration for various editors ([#15014](https://github.com/astral-sh/ruff/pull/15014))
|
||||
|
||||
## 0.8.3
|
||||
|
||||
### Preview features
|
||||
|
||||
@@ -467,7 +467,7 @@ cargo build --release && hyperfine --warmup 10 \
|
||||
"./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e --select W505,E501"
|
||||
```
|
||||
|
||||
You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the
|
||||
You can run `uv venv --project ./scripts/benchmarks`, activate the venv and then run `uv sync --project ./scripts/benchmarks` to create a working environment for the
|
||||
above. All reported benchmarks were computed using the versions specified by
|
||||
`./scripts/benchmarks/pyproject.toml` on Python 3.11.
|
||||
|
||||
|
||||
175
Cargo.lock
generated
175
Cargo.lock
generated
@@ -117,9 +117,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.94"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
|
||||
[[package]]
|
||||
name = "append-only-vec"
|
||||
@@ -220,9 +220,9 @@ checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.11.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22"
|
||||
checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
@@ -314,9 +314,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.38"
|
||||
version = "0.4.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
@@ -413,7 +413,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -465,9 +465,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.48.0",
|
||||
@@ -475,9 +475,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
@@ -693,7 +693,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -704,7 +704,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -774,7 +774,7 @@ dependencies = [
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -826,7 +826,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -877,9 +877,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.5"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
|
||||
checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -923,9 +923,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
|
||||
[[package]]
|
||||
name = "fern"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69ff9c9d5fb3e6da8ac2f77ab76fe7e8087d512ce095200f8f29ac5b656cf6dc"
|
||||
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
@@ -1019,9 +1019,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
@@ -1246,7 +1246,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1420,7 +1420,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1521,9 +1521,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.167"
|
||||
version = "0.2.169"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
|
||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -1547,7 +1547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1607,13 +1607,14 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "lsp-server"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "550446e84739dcaf6d48a4a093973850669e13e8a34d8f8d64851041be267cd9"
|
||||
checksum = "9462c4dc73e17f971ec1f171d44bfffb72e65a130117233388a0ebc7ec5656f9"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -2014,7 +2015,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2161,7 +2162,7 @@ dependencies = [
|
||||
"newtype-uuid",
|
||||
"quick-xml",
|
||||
"strip-ansi-escapes",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.9",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2196,9 +2197,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -2288,6 +2289,7 @@ dependencies = [
|
||||
"compact_str",
|
||||
"countme",
|
||||
"dir-test",
|
||||
"drop_bomb",
|
||||
"hashbrown 0.15.2",
|
||||
"indexmap",
|
||||
"insta",
|
||||
@@ -2305,6 +2307,7 @@ dependencies = [
|
||||
"ruff_python_literal",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
@@ -2314,7 +2317,7 @@ dependencies = [
|
||||
"static_assertions",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.9",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -2411,7 +2414,7 @@ dependencies = [
|
||||
"rustc-hash 2.1.0",
|
||||
"salsa",
|
||||
"serde",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.9",
|
||||
"toml",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2517,7 +2520,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2564,7 +2567,7 @@ dependencies = [
|
||||
"strum",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.9",
|
||||
"tikv-jemallocator",
|
||||
"toml",
|
||||
"tracing",
|
||||
@@ -2634,7 +2637,7 @@ dependencies = [
|
||||
"salsa",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.9",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
@@ -2736,7 +2739,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2786,7 +2789,7 @@ dependencies = [
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"test-case",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.9",
|
||||
"toml",
|
||||
"typed-arena",
|
||||
"unicode-normalization",
|
||||
@@ -2803,7 +2806,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2820,7 +2823,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"test-case",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.9",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2892,7 +2895,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.9",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -2966,6 +2969,7 @@ dependencies = [
|
||||
"rustc-hash 2.1.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3025,7 +3029,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.9",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
@@ -3051,7 +3055,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3193,7 +3197,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
@@ -3203,6 +3207,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"rustc-hash 2.1.0",
|
||||
"salsa-macro-rules",
|
||||
"salsa-macros",
|
||||
@@ -3213,17 +3218,17 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -3257,7 +3262,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3280,9 +3285,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.215"
|
||||
version = "1.0.217"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -3300,13 +3305,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.215"
|
||||
version = "1.0.217"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3317,14 +3322,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.133"
|
||||
version = "1.0.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
||||
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3340,7 +3345,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3363,9 +3368,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.11.0"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
|
||||
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -3374,14 +3379,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.11.0"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
|
||||
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3495,7 +3500,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3517,9 +3522,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.90"
|
||||
version = "2.0.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
||||
checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3534,7 +3539,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3597,7 +3602,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3608,7 +3613,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
@@ -3623,11 +3628,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.6"
|
||||
version = "2.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
|
||||
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.6",
|
||||
"thiserror-impl 2.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3638,18 +3643,18 @@ checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.6"
|
||||
version = "2.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
|
||||
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3771,7 +3776,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4041,7 +4046,7 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4136,7 +4141,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -4171,7 +4176,7 @@ checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -4205,7 +4210,7 @@ checksum = "222ebde6ea87fbfa6bdd2e9f1fd8a91d60aee5db68792632176c4e16a74fc7d8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4508,7 +4513,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4529,7 +4534,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4549,7 +4554,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4578,7 +4583,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -118,7 +118,8 @@ rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "254c749b02cde2fd29852a7463a33e800b771758" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3c7f1694c9efba751dbeeacfbc93b227586e316a" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
@@ -140,8 +140,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.8.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.3/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.8.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.5/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -174,7 +174,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.3
|
||||
rev: v0.8.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/ruff-action@v1
|
||||
- uses: astral-sh/ruff-action@v3
|
||||
```
|
||||
|
||||
### Configuration<a id="configuration"></a>
|
||||
|
||||
@@ -279,7 +279,7 @@ impl MainLoop {
|
||||
while let Ok(message) = self.receiver.recv() {
|
||||
match message {
|
||||
MainLoopMessage::CheckWorkspace => {
|
||||
let db = db.snapshot();
|
||||
let db = db.clone();
|
||||
let sender = self.sender.clone();
|
||||
|
||||
// Spawn a new task that checks the workspace. This needs to be done in a separate thread
|
||||
|
||||
@@ -20,12 +20,14 @@ ruff_python_stdlib = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ruff_python_literal = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
drop_bomb = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
ordermap = { workspace = true }
|
||||
@@ -58,4 +60,3 @@ serde = ["ruff_db/serde", "dep:serde"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -73,12 +73,12 @@ qux = (foo, bar)
|
||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||
|
||||
# TODO: Infer "LiteralString"
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(call todo)
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(Attribute access on `StringLiteral` types)
|
||||
|
||||
template: LiteralString = "{}, {}"
|
||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||
# TODO: Infer `LiteralString`
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(call todo)
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(Attribute access on `StringLiteral` types)
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
@@ -47,7 +47,9 @@ def f():
|
||||
|
||||
## `typing.Never`
|
||||
|
||||
`typing.Never` is only available in Python 3.11 and later:
|
||||
`typing.Never` is only available in Python 3.11 and later.
|
||||
|
||||
### Python 3.11
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
@@ -57,8 +59,17 @@ python-version = "3.11"
|
||||
```py
|
||||
from typing import Never
|
||||
|
||||
x: Never
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Never
|
||||
reveal_type(Never) # revealed: typing.Never
|
||||
```
|
||||
|
||||
### Python 3.10
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [unresolved-import]
|
||||
from typing import Never
|
||||
```
|
||||
|
||||
@@ -3,43 +3,59 @@
|
||||
The `typing` module has various aliases to other stdlib classes. These are a legacy feature, but
|
||||
still need to be supported by a type checker.
|
||||
|
||||
## Currently unsupported
|
||||
## Correspondence
|
||||
|
||||
Support for most of these symbols is currently a TODO:
|
||||
All of the following symbols can be mapped one-to-one with the actual type:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
def f(
|
||||
a: typing.List,
|
||||
b: typing.List[int],
|
||||
c: typing.Dict,
|
||||
d: typing.Dict[int, str],
|
||||
e: typing.DefaultDict,
|
||||
f: typing.DefaultDict[str, int],
|
||||
g: typing.Set,
|
||||
h: typing.Set[int],
|
||||
i: typing.FrozenSet,
|
||||
j: typing.FrozenSet[str],
|
||||
k: typing.OrderedDict,
|
||||
l: typing.OrderedDict[int, str],
|
||||
m: typing.Counter,
|
||||
n: typing.Counter[int],
|
||||
list_bare: typing.List,
|
||||
list_parametrized: typing.List[int],
|
||||
dict_bare: typing.Dict,
|
||||
dict_parametrized: typing.Dict[int, str],
|
||||
set_bare: typing.Set,
|
||||
set_parametrized: typing.Set[int],
|
||||
frozen_set_bare: typing.FrozenSet,
|
||||
frozen_set_parametrized: typing.FrozenSet[str],
|
||||
chain_map_bare: typing.ChainMap,
|
||||
chain_map_parametrized: typing.ChainMap[int],
|
||||
counter_bare: typing.Counter,
|
||||
counter_parametrized: typing.Counter[int],
|
||||
default_dict_bare: typing.DefaultDict,
|
||||
default_dict_parametrized: typing.DefaultDict[str, int],
|
||||
deque_bare: typing.Deque,
|
||||
deque_parametrized: typing.Deque[str],
|
||||
ordered_dict_bare: typing.OrderedDict,
|
||||
ordered_dict_parametrized: typing.OrderedDict[int, str],
|
||||
):
|
||||
reveal_type(a) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(b) # revealed: @Todo(typing.List alias)
|
||||
reveal_type(c) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(d) # revealed: @Todo(typing.Dict alias)
|
||||
reveal_type(e) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(f) # revealed: @Todo(typing.DefaultDict[] alias)
|
||||
reveal_type(g) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(h) # revealed: @Todo(typing.Set alias)
|
||||
reveal_type(i) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(j) # revealed: @Todo(typing.FrozenSet alias)
|
||||
reveal_type(k) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(l) # revealed: @Todo(typing.OrderedDict alias)
|
||||
reveal_type(m) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(n) # revealed: @Todo(typing.Counter[] alias)
|
||||
reveal_type(list_bare) # revealed: list
|
||||
reveal_type(list_parametrized) # revealed: list
|
||||
|
||||
reveal_type(dict_bare) # revealed: dict
|
||||
reveal_type(dict_parametrized) # revealed: dict
|
||||
|
||||
reveal_type(set_bare) # revealed: set
|
||||
reveal_type(set_parametrized) # revealed: set
|
||||
|
||||
reveal_type(frozen_set_bare) # revealed: frozenset
|
||||
reveal_type(frozen_set_parametrized) # revealed: frozenset
|
||||
|
||||
reveal_type(chain_map_bare) # revealed: ChainMap
|
||||
reveal_type(chain_map_parametrized) # revealed: ChainMap
|
||||
|
||||
reveal_type(counter_bare) # revealed: Counter
|
||||
reveal_type(counter_parametrized) # revealed: Counter
|
||||
|
||||
reveal_type(default_dict_bare) # revealed: defaultdict
|
||||
reveal_type(default_dict_parametrized) # revealed: defaultdict
|
||||
|
||||
reveal_type(deque_bare) # revealed: deque
|
||||
reveal_type(deque_parametrized) # revealed: deque
|
||||
|
||||
reveal_type(ordered_dict_bare) # revealed: OrderedDict
|
||||
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
@@ -49,35 +65,63 @@ The aliases can be inherited from. Some of these are still partially or wholly T
|
||||
```py
|
||||
import typing
|
||||
|
||||
class A(typing.Dict): ...
|
||||
####################
|
||||
### Built-ins
|
||||
|
||||
class ListSubclass(typing.List): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[dict], Unknown, Literal[object]]
|
||||
# revealed: tuple[Literal[ListSubclass], Literal[list], Unknown, Literal[object]]
|
||||
reveal_type(ListSubclass.__mro__)
|
||||
|
||||
class B(typing.List): ...
|
||||
class DictSubclass(typing.Dict): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[list], Unknown, Literal[object]]
|
||||
# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]]
|
||||
reveal_type(DictSubclass.__mro__)
|
||||
|
||||
class C(typing.Set): ...
|
||||
class SetSubclass(typing.Set): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[set], Unknown, Literal[object]]
|
||||
# revealed: tuple[Literal[SetSubclass], Literal[set], Unknown, Literal[object]]
|
||||
reveal_type(SetSubclass.__mro__)
|
||||
|
||||
class D(typing.FrozenSet): ...
|
||||
class FrozenSetSubclass(typing.FrozenSet): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[frozenset], Unknown, Literal[object]]
|
||||
# revealed: tuple[Literal[FrozenSetSubclass], Literal[frozenset], Unknown, Literal[object]]
|
||||
reveal_type(FrozenSetSubclass.__mro__)
|
||||
|
||||
class E(typing.DefaultDict): ...
|
||||
####################
|
||||
### `collections`
|
||||
|
||||
reveal_type(E.__mro__) # revealed: tuple[Literal[E], @Todo(Support for more typing aliases as base classes), Literal[object]]
|
||||
class ChainMapSubclass(typing.ChainMap): ...
|
||||
|
||||
class F(typing.OrderedDict): ...
|
||||
# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]]
|
||||
reveal_type(ChainMapSubclass.__mro__)
|
||||
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for more typing aliases as base classes), Literal[object]]
|
||||
class CounterSubclass(typing.Counter): ...
|
||||
|
||||
class G(typing.Counter): ...
|
||||
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[CounterSubclass], Literal[Counter], Unknown, Literal[object]]
|
||||
reveal_type(CounterSubclass.__mro__)
|
||||
|
||||
reveal_type(G.__mro__) # revealed: tuple[Literal[G], @Todo(Support for more typing aliases as base classes), Literal[object]]
|
||||
class DefaultDictSubclass(typing.DefaultDict): ...
|
||||
|
||||
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], Unknown, Literal[object]]
|
||||
reveal_type(DefaultDictSubclass.__mro__)
|
||||
|
||||
class DequeSubclass(typing.Deque): ...
|
||||
|
||||
# TODO: Should be (DequeSubclass, deque, MutableSequence, Sequence, Reversible, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Unknown, Literal[object]]
|
||||
reveal_type(DequeSubclass.__mro__)
|
||||
|
||||
class OrderedDictSubclass(typing.OrderedDict): ...
|
||||
|
||||
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], Unknown, Literal[object]]
|
||||
reveal_type(OrderedDictSubclass.__mro__)
|
||||
```
|
||||
|
||||
@@ -51,7 +51,7 @@ class D(TypeIs): ... # error: [invalid-base]
|
||||
class E(Concatenate): ... # error: [invalid-base]
|
||||
class F(Callable): ...
|
||||
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for more typing aliases as base classes), Literal[object]]
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
|
||||
```
|
||||
|
||||
## Subscriptability
|
||||
|
||||
@@ -33,8 +33,6 @@ b: tuple[int] = (42,)
|
||||
c: tuple[str, int] = ("42", 42)
|
||||
d: tuple[tuple[str, str], tuple[int, int]] = (("foo", "foo"), (42, 42))
|
||||
e: tuple[str, ...] = ()
|
||||
# TODO: we should not emit this error
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[tuple]` is possibly unbound"
|
||||
f: tuple[str, *tuple[int, ...], bytes] = ("42", b"42")
|
||||
g: tuple[str, Unpack[tuple[int, ...]], bytes] = ("42", b"42")
|
||||
h: tuple[list[int], list[int]] = ([], [])
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Class attributes
|
||||
# Attributes
|
||||
|
||||
Tests for attribute access on various kinds of types.
|
||||
|
||||
## Union of attributes
|
||||
|
||||
@@ -155,3 +157,67 @@ class Foo: ...
|
||||
|
||||
reveal_type(Foo.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## Function-literal attributes
|
||||
|
||||
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
|
||||
functions are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
def f(): ...
|
||||
|
||||
reveal_type(f.__defaults__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(f.__kwdefaults__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
def f(): ...
|
||||
|
||||
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
|
||||
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
|
||||
```
|
||||
|
||||
## Int-literal attributes
|
||||
|
||||
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
|
||||
integers are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
reveal_type((2).bit_length) # revealed: @Todo(instance attributes)
|
||||
reveal_type((2).denominator) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
reveal_type((2).numerator) # revealed: Literal[2]
|
||||
reveal_type((2).real) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## Literal `bool` attributes
|
||||
|
||||
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
|
||||
bols are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
reveal_type(True.__and__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(False.__or__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
reveal_type(True.numerator) # revealed: Literal[1]
|
||||
reveal_type(False.real) # revealed: Literal[0]
|
||||
```
|
||||
|
||||
## Bytes-literal attributes
|
||||
|
||||
All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`:
|
||||
|
||||
```py
|
||||
reveal_type(b"foo".join) # revealed: @Todo(instance attributes)
|
||||
reveal_type(b"foo".endswith) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
@@ -32,13 +32,10 @@ def _(flag: bool):
|
||||
|
||||
```py
|
||||
if True or (x := 1):
|
||||
# TODO: infer that the second arm is never executed, and raise `unresolved-reference`.
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
if True and (x := 1):
|
||||
# TODO: infer that the second arm is always executed, do not raise a diagnostic
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@ class C:
|
||||
def __lt__(self, other) -> C: ...
|
||||
|
||||
x = A() < B() < C()
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(x) # revealed: A & ~AlwaysTruthy | B
|
||||
|
||||
y = 0 < 1 < A() < 3
|
||||
reveal_type(y) # revealed: bool | A
|
||||
reveal_type(y) # revealed: Literal[False] | A
|
||||
|
||||
z = 10 < 0 < A() < B() < C()
|
||||
reveal_type(z) # revealed: Literal[False]
|
||||
|
||||
@@ -19,14 +19,17 @@ def _(flag: bool):
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
```
|
||||
|
||||
## Partial declarations
|
||||
## Incompatible declarations for 2 (out of 3) types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
def _(flag1: bool, flag2: bool):
|
||||
if flag1:
|
||||
x: str
|
||||
elif flag2:
|
||||
x: int
|
||||
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
|
||||
# Here, the declared type for `x` is `int | str | Unknown`.
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
```
|
||||
|
||||
## Incompatible declarations with bad assignment
|
||||
@@ -42,3 +45,31 @@ def _(flag: bool):
|
||||
# error: [invalid-assignment]
|
||||
x = b"foo"
|
||||
```
|
||||
|
||||
## No errors
|
||||
|
||||
Currently, we avoid raising the conflicting-declarations for the following cases:
|
||||
|
||||
### Partial declarations
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: int
|
||||
|
||||
x = 1
|
||||
```
|
||||
|
||||
### Partial declarations in try-except
|
||||
|
||||
Refer to <https://github.com/astral-sh/ruff/issues/13966>
|
||||
|
||||
```py
|
||||
def _():
|
||||
try:
|
||||
x: int = 1
|
||||
except:
|
||||
x = 2
|
||||
|
||||
x = 3
|
||||
```
|
||||
|
||||
@@ -90,3 +90,83 @@ def foo(
|
||||
# TODO: should emit a diagnostic here:
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
|
||||
## Object raised is not an exception
|
||||
|
||||
```py
|
||||
try:
|
||||
raise AttributeError() # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise FloatingPointError # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise 1 # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise int # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
def _(e: Exception | type[Exception]):
|
||||
raise e # fine
|
||||
|
||||
def _(e: Exception | type[Exception] | None):
|
||||
raise e # error: [invalid-raise]
|
||||
```
|
||||
|
||||
## Exception cause is not an exception
|
||||
|
||||
```py
|
||||
try:
|
||||
raise EOFError() from GeneratorExit # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise StopIteration from MemoryError() # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise BufferError() from None # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise ZeroDivisionError from False # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise SystemExit from bool() # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise
|
||||
except KeyboardInterrupt as e: # fine
|
||||
reveal_type(e) # revealed: KeyboardInterrupt
|
||||
raise LookupError from e # fine
|
||||
|
||||
try:
|
||||
raise
|
||||
except int as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
raise KeyError from e
|
||||
|
||||
def _(e: Exception | type[Exception]):
|
||||
raise ModuleNotFoundError from e # fine
|
||||
|
||||
def _(e: Exception | type[Exception] | None):
|
||||
raise IndexError from e # fine
|
||||
|
||||
def _(e: int | None):
|
||||
raise IndexError from e # error: [invalid-raise]
|
||||
```
|
||||
|
||||
@@ -10,8 +10,8 @@ def _(foo: str):
|
||||
reveal_type(False or "z") # revealed: Literal["z"]
|
||||
reveal_type(False or True) # revealed: Literal[True]
|
||||
reveal_type(False or False) # revealed: Literal[False]
|
||||
reveal_type(foo or False) # revealed: str | Literal[False]
|
||||
reveal_type(foo or True) # revealed: str | Literal[True]
|
||||
reveal_type(foo or False) # revealed: str & ~AlwaysFalsy | Literal[False]
|
||||
reveal_type(foo or True) # revealed: str & ~AlwaysFalsy | Literal[True]
|
||||
```
|
||||
|
||||
## AND
|
||||
@@ -20,8 +20,8 @@ def _(foo: str):
|
||||
def _(foo: str):
|
||||
reveal_type(True and False) # revealed: Literal[False]
|
||||
reveal_type(False and True) # revealed: Literal[False]
|
||||
reveal_type(foo and False) # revealed: str | Literal[False]
|
||||
reveal_type(foo and True) # revealed: str | Literal[True]
|
||||
reveal_type(foo and False) # revealed: str & ~AlwaysTruthy | Literal[False]
|
||||
reveal_type(foo and True) # revealed: str & ~AlwaysTruthy | Literal[True]
|
||||
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
|
||||
reveal_type("x" and "y" and "") # revealed: Literal[""]
|
||||
reveal_type("" and "y") # revealed: Literal[""]
|
||||
|
||||
@@ -7,7 +7,7 @@ def _(flag: bool):
|
||||
reveal_type(1 if flag else 2) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## Statically known branches
|
||||
## Statically known conditions in if-expressions
|
||||
|
||||
```py
|
||||
reveal_type(1 if True else 2) # revealed: Literal[1]
|
||||
|
||||
31
crates/red_knot_python_semantic/resources/mdtest/final.md
Normal file
31
crates/red_knot_python_semantic/resources/mdtest/final.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Tests for the `@typing(_extensions).final` decorator
|
||||
|
||||
## Cannot subclass
|
||||
|
||||
Don't do this:
|
||||
|
||||
```py
|
||||
import typing_extensions
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class A: ...
|
||||
|
||||
class B(A): ... # error: 9 [subclass-of-final-class] "Class `B` cannot inherit from final class `A`"
|
||||
|
||||
@typing_extensions.final
|
||||
class C: ...
|
||||
|
||||
class D(C): ... # error: [subclass-of-final-class]
|
||||
class E: ...
|
||||
class F: ...
|
||||
class G: ...
|
||||
|
||||
# fmt: off
|
||||
class H(
|
||||
E,
|
||||
F,
|
||||
A, # error: [subclass-of-final-class]
|
||||
G,
|
||||
): ...
|
||||
```
|
||||
@@ -25,3 +25,82 @@ reveal_type(D) # revealed: Literal[C]
|
||||
```py path=b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
```py
|
||||
import a.b
|
||||
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Deeply nested
|
||||
|
||||
```py
|
||||
import a.b.c
|
||||
|
||||
reveal_type(a.b.c.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b/c.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Nested with rename
|
||||
|
||||
```py
|
||||
import a.b as b
|
||||
|
||||
reveal_type(b.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Deeply nested with rename
|
||||
|
||||
```py
|
||||
import a.b.c as c
|
||||
|
||||
reveal_type(c.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b/c.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Unresolvable submodule imports
|
||||
|
||||
```py
|
||||
# Topmost component resolvable, submodule not resolvable:
|
||||
import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
|
||||
|
||||
# Topmost component unresolvable:
|
||||
import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Conflicting attributes and submodules
|
||||
|
||||
## Via import
|
||||
|
||||
```py
|
||||
import a.b
|
||||
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
```
|
||||
|
||||
## Via from/import
|
||||
|
||||
```py
|
||||
from a import b
|
||||
|
||||
reveal_type(b) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
```
|
||||
|
||||
## Via both
|
||||
|
||||
```py
|
||||
import a.b
|
||||
from a import b
|
||||
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
```
|
||||
|
||||
## Via both (backwards)
|
||||
|
||||
In this test, we infer a different type for `b` than the runtime behavior of the Python interpreter.
|
||||
The interpreter will not load the submodule `a.b` during the `from a import b` statement, since `a`
|
||||
contains a non-module attribute named `b`. (See the [definition][from-import] of a `from...import`
|
||||
statement for details.) However, because our import tracking is flow-insensitive, we will see that
|
||||
`a.b` is imported somewhere in the file, and therefore assume that the `from...import` statement
|
||||
sees the submodule as the value of `b` instead of the integer.
|
||||
|
||||
```py
|
||||
from a import b
|
||||
import a.b
|
||||
|
||||
# Python would say `Literal[42]` for `b`
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
```
|
||||
|
||||
[from-import]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
|
||||
@@ -7,3 +7,25 @@ from import bar # error: [invalid-syntax]
|
||||
|
||||
reveal_type(bar) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Invalid nested module import
|
||||
|
||||
TODO: This is correctly flagged as an error, but we could clean up the diagnostics that we report.
|
||||
|
||||
```py
|
||||
# TODO: No second diagnostic
|
||||
# error: [invalid-syntax] "Expected ',', found '.'"
|
||||
# error: [unresolved-import] "Module `a` has no member `c`"
|
||||
from a import b.c
|
||||
|
||||
# TODO: Should these be inferred as Unknown?
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(b.c) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
c = 1
|
||||
```
|
||||
|
||||
@@ -121,23 +121,44 @@ X = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
# TODO: support submodule imports
|
||||
from . import foo # error: [unresolved-import]
|
||||
from . import foo
|
||||
|
||||
y = foo.X
|
||||
|
||||
# TODO: should be `Literal[42]`
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(foo.X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
## Non-existent + bare to module
|
||||
|
||||
This test verifies that we emit an error when we try to import a symbol that is neither a submodule
|
||||
nor an attribute of `package`.
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
# TODO: support submodule imports
|
||||
from . import foo # error: [unresolved-import]
|
||||
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Import submodule from self
|
||||
|
||||
We don't currently consider `from...import` statements when building up the `imported_modules` set
|
||||
in the semantic index. When accessing an attribute of a module, we only consider it a potential
|
||||
submodule when that submodule name appears in the `imported_modules` set. That means that submodules
|
||||
that are imported via `from...import` are not visible to our type inference if you also access that
|
||||
submodule via the attribute on its parent package.
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from . import foo
|
||||
import package
|
||||
|
||||
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
|
||||
reveal_type(package.foo.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# Tracking imported modules
|
||||
|
||||
These tests depend on how we track which modules have been imported. There are currently two
|
||||
characteristics of our module tracking that can lead to inaccuracies:
|
||||
|
||||
- Imports are tracked on a per-file basis. At runtime, importing a submodule in one file makes that
|
||||
submodule globally available via any reference to the containing package. We will flag an error
|
||||
if a file tries to access a submodule without there being an import of that submodule _in that
|
||||
same file_.
|
||||
|
||||
This is a purposeful decision, and not one we plan to change. If a module wants to re-export some
|
||||
other module that it imports, there are ways to do that (tested below) that are blessed by the
|
||||
typing spec and that are visible to our file-scoped import tracking.
|
||||
|
||||
- Imports are tracked flow-insensitively: submodule accesses are allowed and resolved if that
|
||||
submodule is imported _anywhere in the file_. This handles the common case where all imports are
|
||||
grouped at the top of the file, and is easiest to implement. We might revisit this decision and
|
||||
track submodule imports flow-sensitively, in which case we will have to update the assertions in
|
||||
some of these tests.
|
||||
|
||||
## Import submodule later in file
|
||||
|
||||
This test highlights our flow-insensitive analysis, since we access the `a.b` submodule before it
|
||||
has been imported.
|
||||
|
||||
```py
|
||||
import a
|
||||
|
||||
# Would be an error with flow-sensitive tracking
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
|
||||
import a.b
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Rename a re-export
|
||||
|
||||
This test highlights how import tracking is local to each file, but specifically to the file where a
|
||||
containing module is first referenced. This allows the main module to see that `q.a` contains a
|
||||
submodule `b`, even though `a.b` is never imported in the main module.
|
||||
|
||||
```py
|
||||
from q import a, b
|
||||
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(b.C) # revealed: Literal[C]
|
||||
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
```py path=q.py
|
||||
import a as a
|
||||
import a.b as b
|
||||
```
|
||||
|
||||
## Attribute overrides submodule
|
||||
|
||||
Technically, either a submodule or a non-module attribute could shadow the other, depending on the
|
||||
ordering of when the submodule is loaded relative to the parent module's `__init__.py` file being
|
||||
evaluated. We have chosen to always have the submodule take priority. (This matches pyright's
|
||||
current behavior, and opposite of mypy's current behavior.)
|
||||
|
||||
```py
|
||||
import sub.b
|
||||
import attr.b
|
||||
|
||||
# In the Python interpreter, `attr.b` is Literal[1]
|
||||
reveal_type(sub.b) # revealed: <module 'sub.b'>
|
||||
reveal_type(attr.b) # revealed: <module 'attr.b'>
|
||||
```
|
||||
|
||||
```py path=sub/__init__.py
|
||||
b = 1
|
||||
```
|
||||
|
||||
```py path=sub/b.py
|
||||
```
|
||||
|
||||
```py path=attr/__init__.py
|
||||
from . import b as _
|
||||
|
||||
b = 1
|
||||
```
|
||||
|
||||
```py path=attr/b.py
|
||||
```
|
||||
@@ -1,7 +1,23 @@
|
||||
# Ellipsis literals
|
||||
|
||||
## Simple
|
||||
## Python 3.9
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
reveal_type(...) # revealed: EllipsisType | ellipsis
|
||||
reveal_type(...) # revealed: ellipsis
|
||||
```
|
||||
|
||||
## Python 3.10
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
reveal_type(...) # revealed: EllipsisType
|
||||
```
|
||||
|
||||
@@ -95,10 +95,14 @@ def _(t: type[object]):
|
||||
|
||||
### Handling of `None`
|
||||
|
||||
`types.NoneType` is only available in Python 3.10 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO: this error should ideally go away once we (1) understand `sys.version_info` branches,
|
||||
# and (2) set the target Python version for this test to 3.10.
|
||||
# error: [possibly-unbound-import] "Member `NoneType` of module `types` is possibly unbound"
|
||||
from types import NoneType
|
||||
|
||||
def _(flag: bool):
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
# Narrowing For Truthiness Checks (`if x` or `if not x`)
|
||||
|
||||
## Value Literals
|
||||
|
||||
```py
|
||||
def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[()]:
|
||||
return 0
|
||||
|
||||
x = foo()
|
||||
|
||||
if x:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
|
||||
|
||||
if not x:
|
||||
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
|
||||
|
||||
if x and not x:
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
|
||||
|
||||
if not (x and not x):
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if x or not x:
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if not (x or not x):
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
|
||||
|
||||
if (isinstance(x, int) or isinstance(x, str)) and x:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[b"", b"bar"] | None | tuple[()] | Literal[0] | Literal[False] | Literal[""]
|
||||
```
|
||||
|
||||
## Function Literals
|
||||
|
||||
Basically functions are always truthy.
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
def foo(hello: int) -> bytes:
|
||||
return b""
|
||||
|
||||
def bar(world: str, *args, **kwargs) -> float:
|
||||
return 0.0
|
||||
|
||||
x = foo if flag() else bar
|
||||
|
||||
if x:
|
||||
reveal_type(x) # revealed: Literal[foo, bar]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
```
|
||||
|
||||
## Mutable Truthiness
|
||||
|
||||
### Truthiness of Instances
|
||||
|
||||
The boolean value of an instance is not always consistent. For example, `__bool__` can be customized
|
||||
to return random values, or in the case of a `list()`, the result depends on the number of elements
|
||||
in the list. Therefore, these types should not be narrowed by `if x` or `if not x`.
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def f(x: A | B):
|
||||
if x:
|
||||
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy
|
||||
else:
|
||||
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy
|
||||
|
||||
if x and not x:
|
||||
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
|
||||
else:
|
||||
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy | A & ~AlwaysFalsy | B & ~AlwaysFalsy
|
||||
|
||||
if x or not x:
|
||||
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy | A & ~AlwaysTruthy | B & ~AlwaysTruthy
|
||||
else:
|
||||
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
|
||||
```
|
||||
|
||||
### Truthiness of Types
|
||||
|
||||
Also, types may not be Truthy. This is because `__bool__` can be customized via a metaclass.
|
||||
Although this is a very rare case, we may consider metaclass checks in the future to handle this
|
||||
more accurately.
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
x = int if flag() else str
|
||||
reveal_type(x) # revealed: Literal[int, str]
|
||||
|
||||
if x:
|
||||
reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy
|
||||
```
|
||||
|
||||
## Determined Truthiness
|
||||
|
||||
Some custom classes can have a boolean value that is consistently determined as either `True` or
|
||||
`False`, regardless of the instance's state. This is achieved by defining a `__bool__` method that
|
||||
always returns a fixed value.
|
||||
|
||||
These types can always be fully narrowed in boolean contexts, as shown below:
|
||||
|
||||
```py
|
||||
class T:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class F:
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
t = T()
|
||||
|
||||
if t:
|
||||
reveal_type(t) # revealed: T
|
||||
else:
|
||||
reveal_type(t) # revealed: Never
|
||||
|
||||
f = F()
|
||||
|
||||
if f:
|
||||
reveal_type(f) # revealed: Never
|
||||
else:
|
||||
reveal_type(f) # revealed: F
|
||||
```
|
||||
|
||||
## Narrowing Complex Intersection and Union
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
def literals() -> Literal[0, 42, "", "hello"]:
|
||||
return 42
|
||||
|
||||
x = instance()
|
||||
y = literals()
|
||||
|
||||
if isinstance(x, str) and not isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & str & ~B
|
||||
reveal_type(y) # revealed: Literal[0, 42] | Literal["", "hello"]
|
||||
|
||||
z = x if flag() else y
|
||||
|
||||
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42] | Literal["", "hello"]
|
||||
|
||||
if z:
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42] | Literal["hello"]
|
||||
else:
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0] | Literal[""]
|
||||
```
|
||||
|
||||
## Narrowing Multiple Variables
|
||||
|
||||
```py
|
||||
def f(x: Literal[0, 1], y: Literal["", "hello"]):
|
||||
if x and y and not x and not y:
|
||||
reveal_type(x) # revealed: Never
|
||||
reveal_type(y) # revealed: Never
|
||||
else:
|
||||
# ~(x or not x) and ~(y or not y)
|
||||
reveal_type(x) # revealed: Literal[0, 1]
|
||||
reveal_type(y) # revealed: Literal["", "hello"]
|
||||
|
||||
if (x or not x) and (y and not y):
|
||||
reveal_type(x) # revealed: Literal[0, 1]
|
||||
reveal_type(y) # revealed: Never
|
||||
else:
|
||||
# ~(x or not x) or ~(y and not y)
|
||||
reveal_type(x) # revealed: Literal[0, 1]
|
||||
reveal_type(y) # revealed: Literal["", "hello"]
|
||||
```
|
||||
|
||||
## ControlFlow Merging
|
||||
|
||||
After merging control flows, when we take the union of all constraints applied in each branch, we
|
||||
should return to the original state.
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
x = A()
|
||||
|
||||
if x and not x:
|
||||
y = x
|
||||
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
|
||||
else:
|
||||
y = x
|
||||
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
|
||||
|
||||
# TODO: It should be A. We should improve UnionBuilder or IntersectionBuilder. (issue #15023)
|
||||
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
|
||||
```
|
||||
|
||||
## Truthiness of classes
|
||||
|
||||
```py
|
||||
class MetaAmbiguous(type):
|
||||
def __bool__(self) -> bool: ...
|
||||
|
||||
class MetaFalsy(type):
|
||||
def __bool__(self) -> Literal[False]: ...
|
||||
|
||||
class MetaTruthy(type):
|
||||
def __bool__(self) -> Literal[True]: ...
|
||||
|
||||
class MetaDeferred(type):
|
||||
def __bool__(self) -> MetaAmbiguous: ...
|
||||
|
||||
class AmbiguousClass(metaclass=MetaAmbiguous): ...
|
||||
class FalsyClass(metaclass=MetaFalsy): ...
|
||||
class TruthyClass(metaclass=MetaTruthy): ...
|
||||
class DeferredClass(metaclass=MetaDeferred): ...
|
||||
|
||||
def _(
|
||||
a: type[AmbiguousClass],
|
||||
t: type[TruthyClass],
|
||||
f: type[FalsyClass],
|
||||
d: type[DeferredClass],
|
||||
ta: type[TruthyClass | AmbiguousClass],
|
||||
af: type[AmbiguousClass] | type[FalsyClass],
|
||||
flag: bool,
|
||||
):
|
||||
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
|
||||
if ta:
|
||||
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] & ~AlwaysFalsy
|
||||
|
||||
reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass]
|
||||
if af:
|
||||
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
|
||||
|
||||
# TODO: Emit a diagnostic (`d` is not valid in boolean context)
|
||||
if d:
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
|
||||
|
||||
tf = TruthyClass if flag else FalsyClass
|
||||
reveal_type(tf) # revealed: Literal[TruthyClass, FalsyClass]
|
||||
|
||||
if tf:
|
||||
reveal_type(tf) # revealed: Literal[TruthyClass]
|
||||
else:
|
||||
reveal_type(tf) # revealed: Literal[FalsyClass]
|
||||
```
|
||||
|
||||
## Narrowing in chained boolean expressions
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class A: ...
|
||||
|
||||
def _(x: Literal[0, 1]):
|
||||
reveal_type(x or A()) # revealed: Literal[1] | A
|
||||
reveal_type(x and A()) # revealed: Literal[0] | A
|
||||
|
||||
def _(x: str):
|
||||
reveal_type(x or A()) # revealed: str & ~AlwaysFalsy | A
|
||||
reveal_type(x and A()) # revealed: str & ~AlwaysTruthy | A
|
||||
|
||||
def _(x: bool | str):
|
||||
reveal_type(x or A()) # revealed: Literal[True] | str & ~AlwaysFalsy | A
|
||||
reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A
|
||||
|
||||
class Falsy:
|
||||
def __bool__(self) -> Literal[False]: ...
|
||||
|
||||
class Truthy:
|
||||
def __bool__(self) -> Literal[True]: ...
|
||||
|
||||
def _(x: Falsy | Truthy):
|
||||
reveal_type(x or A()) # revealed: Truthy | A
|
||||
reveal_type(x and A()) # revealed: Falsy | A
|
||||
|
||||
class MetaFalsy(type):
|
||||
def __bool__(self) -> Literal[False]: ...
|
||||
|
||||
class MetaTruthy(type):
|
||||
def __bool__(self) -> Literal[True]: ...
|
||||
|
||||
class FalsyClass(metaclass=MetaFalsy): ...
|
||||
class TruthyClass(metaclass=MetaTruthy): ...
|
||||
|
||||
def _(x: type[FalsyClass] | type[TruthyClass]):
|
||||
reveal_type(x or A()) # revealed: type[TruthyClass] | A
|
||||
reveal_type(x and A()) # revealed: type[FalsyClass] | A
|
||||
```
|
||||
@@ -25,3 +25,29 @@ def f(): ...
|
||||
|
||||
f: int = 1
|
||||
```
|
||||
|
||||
## Explicit shadowing involving `def` statements
|
||||
|
||||
Since a `def` statement is a declaration, one `def` can shadow another `def`, or shadow a previous
|
||||
non-`def` declaration, without error.
|
||||
|
||||
```py
|
||||
f = 1
|
||||
reveal_type(f) # revealed: Literal[1]
|
||||
|
||||
def f(): ...
|
||||
|
||||
reveal_type(f) # revealed: Literal[f]
|
||||
|
||||
def f(x: int) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
reveal_type(f) # revealed: Literal[f]
|
||||
|
||||
f: int = 1
|
||||
reveal_type(f) # revealed: Literal[1]
|
||||
|
||||
def f(): ...
|
||||
|
||||
reveal_type(f) # revealed: Literal[f]
|
||||
```
|
||||
|
||||
184
crates/red_knot_python_semantic/resources/mdtest/slots.md
Normal file
184
crates/red_knot_python_semantic/resources/mdtest/slots.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# `__slots__`
|
||||
|
||||
## Not specified and empty
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
class B:
|
||||
__slots__ = ()
|
||||
|
||||
class C:
|
||||
__slots__ = ("lorem", "ipsum")
|
||||
|
||||
class AB(A, B): ... # fine
|
||||
class AC(A, C): ... # fine
|
||||
class BC(B, C): ... # fine
|
||||
class ABC(A, B, C): ... # fine
|
||||
```
|
||||
|
||||
## Incompatible tuples
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
): ...
|
||||
```
|
||||
|
||||
## Same value
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class B:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class C(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
): ...
|
||||
```
|
||||
|
||||
## Strings
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = "abc"
|
||||
|
||||
class B:
|
||||
__slots__ = ("abc",)
|
||||
|
||||
class AB(
|
||||
A, # error: [incompatible-slots]
|
||||
B, # error: [incompatible-slots]
|
||||
): ...
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
TODO: Emit diagnostics
|
||||
|
||||
```py
|
||||
class NonString1:
|
||||
__slots__ = 42
|
||||
|
||||
class NonString2:
|
||||
__slots__ = b"ar"
|
||||
|
||||
class NonIdentifier1:
|
||||
__slots__ = "42"
|
||||
|
||||
class NonIdentifier2:
|
||||
__slots__ = ("lorem", "42")
|
||||
|
||||
class NonIdentifier3:
|
||||
__slots__ = (e for e in ("lorem", "42"))
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class B(A): ...
|
||||
|
||||
class C:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
class D(C): ...
|
||||
class E(
|
||||
B, # error: [incompatible-slots]
|
||||
D, # error: [incompatible-slots]
|
||||
): ...
|
||||
```
|
||||
|
||||
## Single solid base
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class B(A): ...
|
||||
class C(A): ...
|
||||
class D(B, A): ... # fine
|
||||
class E(B, C, A): ... # fine
|
||||
```
|
||||
|
||||
## False negatives
|
||||
|
||||
### Possibly unbound
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A:
|
||||
if flag:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
# Might or might not be fine at runtime
|
||||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Bound but with different types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A:
|
||||
if flag:
|
||||
__slots__ = ("a", "b")
|
||||
else:
|
||||
__slots__ = ()
|
||||
|
||||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
# Might or might not be fine at runtime
|
||||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Non-tuples
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ["a", "b"] # This is treated as "dynamic"
|
||||
|
||||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
# False negative: [incompatible-slots]
|
||||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Post-hoc modifications
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ()
|
||||
__slots__ += ("a", "b")
|
||||
|
||||
reveal_type(A.__slots__) # revealed: @Todo(Support for more binary expressions)
|
||||
|
||||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
# False negative: [incompatible-slots]
|
||||
class C(A, B): ...
|
||||
```
|
||||
|
||||
### Built-ins with implicit layouts
|
||||
|
||||
```py
|
||||
# False negative: [incompatible-slots]
|
||||
class A(int, str): ...
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,10 +81,7 @@ python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO:
|
||||
# * `tuple.__class_getitem__` is always bound on 3.9 (`sys.version_info`)
|
||||
# * `tuple[int, str]` is a valid base (generics)
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[tuple]` is possibly unbound"
|
||||
# TODO: `tuple[int, str]` is a valid base (generics)
|
||||
# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class A(tuple[int, str]): ...
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# Suppressing errors with `knot: ignore`
|
||||
|
||||
Type check errors can be suppressed by a `knot: ignore` comment on the same line as the violation.
|
||||
|
||||
## Simple `knot: ignore`
|
||||
|
||||
```py
|
||||
a = 4 + test # knot: ignore
|
||||
```
|
||||
|
||||
## Suppressing a specific code
|
||||
|
||||
```py
|
||||
a = 4 + test # knot: ignore[unresolved-reference]
|
||||
```
|
||||
|
||||
## Unused suppression
|
||||
|
||||
```py
|
||||
test = 10
|
||||
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'"
|
||||
a = test + 3 # knot: ignore[possibly-unresolved-reference]
|
||||
```
|
||||
|
||||
## Unused suppression if the error codes don't match
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'"
|
||||
a = test + 3 # knot: ignore[possibly-unresolved-reference]
|
||||
```
|
||||
|
||||
## Suppressed unused comment
|
||||
|
||||
```py
|
||||
# error: [unused-ignore-comment]
|
||||
a = 10 / 2 # knot: ignore[division-by-zero]
|
||||
a = 10 / 2 # knot: ignore[division-by-zero, unused-ignore-comment]
|
||||
a = 10 / 2 # knot: ignore[unused-ignore-comment, division-by-zero]
|
||||
a = 10 / 2 # knot: ignore[unused-ignore-comment] # type: ignore
|
||||
a = 10 / 2 # type: ignore # knot: ignore[unused-ignore-comment]
|
||||
```
|
||||
|
||||
## Unused ignore comment
|
||||
|
||||
```py
|
||||
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unused-ignore-comment'"
|
||||
a = 10 / 0 # knot: ignore[division-by-zero, unused-ignore-comment]
|
||||
```
|
||||
|
||||
## Multiple unused comments
|
||||
|
||||
Today, Red Knot emits a diagnostic for every unused code. We might want to group the codes by
|
||||
comment at some point in the future.
|
||||
|
||||
```py
|
||||
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'division-by-zero'"
|
||||
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'"
|
||||
a = 10 / 2 # knot: ignore[division-by-zero, unresolved-reference]
|
||||
|
||||
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'invalid-assignment'"
|
||||
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'"
|
||||
a = 10 / 0 # knot: ignore[invalid-assignment, division-by-zero, unresolved-reference]
|
||||
```
|
||||
|
||||
## Multiple suppressions
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... # knot: ignore[fstring-type-annotation, byte-string-type-annotation]
|
||||
```
|
||||
|
||||
## Can't suppress syntax errors
|
||||
|
||||
<!-- blacken-docs:off -->
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [unused-ignore-comment]
|
||||
def test( # knot: ignore
|
||||
```
|
||||
|
||||
<!-- blacken-docs:on -->
|
||||
|
||||
## Can't suppress `revealed-type` diagnostics
|
||||
|
||||
```py
|
||||
a = 10
|
||||
# revealed: Literal[10]
|
||||
# error: [unknown-rule] "Unknown rule `revealed-type`"
|
||||
reveal_type(a) # knot: ignore[revealed-type]
|
||||
```
|
||||
|
||||
## Extra whitespace in type ignore comments is allowed
|
||||
|
||||
```py
|
||||
a = 10 / 0 # knot : ignore
|
||||
a = 10 / 0 # knot: ignore [ division-by-zero ]
|
||||
```
|
||||
|
||||
## Whitespace is optional
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
a = 10 / 0 #knot:ignore[division-by-zero]
|
||||
```
|
||||
|
||||
## Trailing codes comma
|
||||
|
||||
Trailing commas in the codes section are allowed:
|
||||
|
||||
```py
|
||||
a = 10 / 0 # knot: ignore[division-by-zero,]
|
||||
```
|
||||
|
||||
## Invalid characters in codes
|
||||
|
||||
```py
|
||||
# error: [division-by-zero]
|
||||
# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a alphanumeric character or `-` or `_` as code"
|
||||
a = 10 / 0 # knot: ignore[*-*]
|
||||
```
|
||||
|
||||
## Trailing whitespace
|
||||
|
||||
<!-- blacken-docs:off -->
|
||||
|
||||
```py
|
||||
a = 10 / 0 # knot: ignore[division-by-zero]
|
||||
# ^^^^^^ trailing whitespace
|
||||
```
|
||||
|
||||
<!-- blacken-docs:on -->
|
||||
|
||||
## Missing comma
|
||||
|
||||
A missing comma results in an invalid suppression comment. We may want to recover from this in the
|
||||
future.
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a comma separating the rule codes"
|
||||
a = x / 0 # knot: ignore[division-by-zero unresolved-reference]
|
||||
```
|
||||
|
||||
## Missing closing bracket
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference] "Name `x` used when not defined"
|
||||
# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a comma separating the rule codes"
|
||||
a = x / 2 # knot: ignore[unresolved-reference
|
||||
```
|
||||
|
||||
## Empty codes
|
||||
|
||||
An empty codes array suppresses no-diagnostics and is always useless
|
||||
|
||||
```py
|
||||
# error: [division-by-zero]
|
||||
# error: [unused-ignore-comment] "Unused `knot: ignore` without a code"
|
||||
a = 4 / 0 # knot: ignore[]
|
||||
```
|
||||
|
||||
## File-level suppression comments
|
||||
|
||||
File level suppression comments are currently intentionally unsupported because we've yet to decide
|
||||
if they should use a different syntax that also supports enabling rules or changing the rule's
|
||||
severity: `knot: possibly-undefined-reference=error`
|
||||
|
||||
```py
|
||||
# error: [unused-ignore-comment]
|
||||
# knot: ignore[division-by-zero]
|
||||
|
||||
a = 4 / 0 # error: [division-by-zero]
|
||||
```
|
||||
|
||||
## Unknown rule
|
||||
|
||||
```py
|
||||
# error: [unknown-rule] "Unknown rule `is-equal-14`"
|
||||
a = 10 + 4 # knot: ignore[is-equal-14]
|
||||
```
|
||||
@@ -0,0 +1,118 @@
|
||||
# `@no_type_check`
|
||||
|
||||
> If a type checker supports the `no_type_check` decorator for functions, it should suppress all
|
||||
> type errors for the def statement and its body including any nested functions or classes. It
|
||||
> should also ignore all parameter and return type annotations and treat the function as if it were
|
||||
> unannotated. [source](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check)
|
||||
|
||||
## Error in the function body
|
||||
|
||||
```py
|
||||
from typing import no_type_check
|
||||
|
||||
@no_type_check
|
||||
def test() -> int:
|
||||
return a + 5
|
||||
```
|
||||
|
||||
## Error in nested function
|
||||
|
||||
```py
|
||||
from typing import no_type_check
|
||||
|
||||
@no_type_check
|
||||
def test() -> int:
|
||||
def nested():
|
||||
return a + 5
|
||||
```
|
||||
|
||||
## Error in nested class
|
||||
|
||||
```py
|
||||
from typing import no_type_check
|
||||
|
||||
@no_type_check
|
||||
def test() -> int:
|
||||
class Nested:
|
||||
def inner(self):
|
||||
return a + 5
|
||||
```
|
||||
|
||||
## Error in preceding decorator
|
||||
|
||||
Don't suppress diagnostics for decorators appearing before the `no_type_check` decorator.
|
||||
|
||||
```py
|
||||
from typing import no_type_check
|
||||
|
||||
@unknown_decorator # error: [unresolved-reference]
|
||||
@no_type_check
|
||||
def test() -> int:
|
||||
return a + 5
|
||||
```
|
||||
|
||||
## Error in following decorator
|
||||
|
||||
Unlike Pyright and mypy, suppress diagnostics appearing after the `no_type_check` decorator. We do
|
||||
this because it more closely matches Python's runtime semantics of decorators. For more details, see
|
||||
the discussion on the
|
||||
[PR adding `@no_type_check` support](https://github.com/astral-sh/ruff/pull/15122#discussion_r1896869411).
|
||||
|
||||
```py
|
||||
from typing import no_type_check
|
||||
|
||||
@no_type_check
|
||||
@unknown_decorator
|
||||
def test() -> int:
|
||||
return a + 5
|
||||
```
|
||||
|
||||
## Error in default value
|
||||
|
||||
```py
|
||||
from typing import no_type_check
|
||||
|
||||
@no_type_check
|
||||
def test(a: int = "test"):
|
||||
return x + 5
|
||||
```
|
||||
|
||||
## Error in return value position
|
||||
|
||||
```py
|
||||
from typing import no_type_check
|
||||
|
||||
@no_type_check
|
||||
def test() -> Undefined:
|
||||
return x + 5
|
||||
```
|
||||
|
||||
## `no_type_check` on classes isn't supported
|
||||
|
||||
Red Knot does not support decorating classes with `no_type_check`. The behaviour of `no_type_check`
|
||||
when applied to classes is
|
||||
[not specified currently](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check),
|
||||
and is not supported by Pyright or mypy.
|
||||
|
||||
A future improvement might be to emit a diagnostic if a `no_type_check` annotation is applied to a
|
||||
class.
|
||||
|
||||
```py
|
||||
from typing import no_type_check
|
||||
|
||||
@no_type_check
|
||||
class Test:
|
||||
def test(self):
|
||||
return a + 5 # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
## `type: ignore` comments in `@no_type_check` blocks
|
||||
|
||||
```py
|
||||
from typing import no_type_check
|
||||
|
||||
@no_type_check
|
||||
def test():
|
||||
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'"
|
||||
return x + 5 # knot: ignore[unresolved-reference]
|
||||
```
|
||||
@@ -0,0 +1,162 @@
|
||||
# Suppressing errors with `type: ignore`
|
||||
|
||||
Type check errors can be suppressed by a `type: ignore` comment on the same line as the violation.
|
||||
|
||||
## Simple `type: ignore`
|
||||
|
||||
```py
|
||||
a = 4 + test # type: ignore
|
||||
```
|
||||
|
||||
## Multiline ranges
|
||||
|
||||
A diagnostic with a multiline range can be suppressed by a comment on the same line as the
|
||||
diagnostic's start or end. This is the same behavior as Mypy's.
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
y = (
|
||||
4 / 0 # type: ignore
|
||||
)
|
||||
|
||||
y = (
|
||||
4 / # type: ignore
|
||||
0
|
||||
)
|
||||
|
||||
y = (
|
||||
4 /
|
||||
0 # type: ignore
|
||||
)
|
||||
```
|
||||
|
||||
Pyright diverges from this behavior and instead applies a suppression if its range intersects with
|
||||
the diagnostic range. This can be problematic for nested expressions because a suppression in a
|
||||
child expression now suppresses errors in the outer expression.
|
||||
|
||||
For example, the `type: ignore` comment in this example suppresses the error of adding `2` to
|
||||
`"test"` and adding `"other"` to the result of the cast.
|
||||
|
||||
```py path=nested.py
|
||||
# fmt: off
|
||||
from typing import cast
|
||||
|
||||
y = (
|
||||
cast(int, "test" +
|
||||
# TODO: Remove the expected error after implementing `invalid-operator` for binary expressions
|
||||
# error: [unused-ignore-comment]
|
||||
2 # type: ignore
|
||||
)
|
||||
+ "other" # TODO: expected-error[invalid-operator]
|
||||
)
|
||||
```
|
||||
|
||||
Mypy flags the second usage.
|
||||
|
||||
## Before opening parenthesis
|
||||
|
||||
A suppression that applies to all errors before the opening parenthesis.
|
||||
|
||||
```py
|
||||
a: Test = ( # type: ignore
|
||||
Test() # error: [unresolved-reference]
|
||||
) # fmt: skip
|
||||
```
|
||||
|
||||
## Multiline string
|
||||
|
||||
```py
|
||||
a: int = 4
|
||||
a = """
|
||||
This is a multiline string and the suppression is at its end
|
||||
""" # type: ignore
|
||||
```
|
||||
|
||||
## Line continuations
|
||||
|
||||
Suppressions after a line continuation apply to all previous lines.
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
a = test \
|
||||
+ 2 # type: ignore
|
||||
|
||||
a = test \
|
||||
+ a \
|
||||
+ 2 # type: ignore
|
||||
```
|
||||
|
||||
## Codes
|
||||
|
||||
Mypy supports `type: ignore[code]`. Red Knot doesn't understand mypy's rule names. Therefore, ignore
|
||||
the codes and suppress all errors.
|
||||
|
||||
```py
|
||||
a = test # type: ignore[name-defined]
|
||||
```
|
||||
|
||||
## Nested comments
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
a = test \
|
||||
+ 2 # fmt: skip # type: ignore
|
||||
|
||||
a = test \
|
||||
+ 2 # type: ignore # fmt: skip
|
||||
```
|
||||
|
||||
## Misspelled `type: ignore`
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
# error: [invalid-ignore-comment]
|
||||
a = test + 2 # type: ignoree
|
||||
```
|
||||
|
||||
## Invalid - ignore on opening parentheses
|
||||
|
||||
`type: ignore` comments after an opening parentheses suppress any type errors inside the parentheses
|
||||
in Pyright. Neither Ruff, nor mypy support this and neither does Red Knot.
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
# error: [unused-ignore-comment]
|
||||
a = ( # type: ignore
|
||||
test + 4 # error: [unresolved-reference]
|
||||
)
|
||||
```
|
||||
|
||||
## File level suppression
|
||||
|
||||
```py
|
||||
# type: ignore
|
||||
|
||||
a = 10 / 0
|
||||
b = a / 0
|
||||
```
|
||||
|
||||
## File level suppression with leading shebang
|
||||
|
||||
```py
|
||||
#!/usr/bin/env/python
|
||||
# type: ignore
|
||||
|
||||
a = 10 / 0
|
||||
b = a / 0
|
||||
```
|
||||
|
||||
## Invalid own-line suppression
|
||||
|
||||
```py
|
||||
"""
|
||||
File level suppressions must come before any non-trivia token,
|
||||
including module docstrings.
|
||||
"""
|
||||
|
||||
# error: [unused-ignore-comment] "Unused blanket `type: ignore` directive"
|
||||
# type: ignore
|
||||
|
||||
a = 10 / 0 # error: [division-by-zero]
|
||||
b = a / 0 # error: [division-by-zero]
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
# `sys.platform`
|
||||
|
||||
## Default value
|
||||
|
||||
When no target platform is specified, we fall back to the type of `sys.platform` declared in
|
||||
typeshed:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
# No python-platform entry
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.platform) # revealed: str
|
||||
```
|
||||
|
||||
## Explicit selection of `all` platforms
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-platform = "all"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.platform) # revealed: str
|
||||
```
|
||||
|
||||
## Explicit selection of a specific platform
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-platform = "linux"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.platform) # revealed: Literal["linux"]
|
||||
```
|
||||
|
||||
## Testing for a specific platform
|
||||
|
||||
### Exact comparison
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-platform = "freebsd8"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.platform == "freebsd8") # revealed: Literal[True]
|
||||
reveal_type(sys.platform == "linux") # revealed: Literal[False]
|
||||
```
|
||||
|
||||
### Substring comparison
|
||||
|
||||
It is [recommended](https://docs.python.org/3/library/sys.html#sys.platform) to use
|
||||
`sys.platform.startswith(...)` for platform checks. This is not yet supported in type inference:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.platform.startswith("freebsd")) # revealed: @Todo(instance attributes)
|
||||
reveal_type(sys.platform.startswith("linux")) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
@@ -61,10 +61,8 @@ class B: ...
|
||||
```py path=a/test.py
|
||||
import a.b
|
||||
|
||||
# TODO: no diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
def f(c: type[a.b.C]):
|
||||
reveal_type(c) # revealed: @Todo(unsupported type[X] special form)
|
||||
reveal_type(c) # revealed: type[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
|
||||
165
crates/red_knot_python_semantic/resources/mdtest/unary/custom.md
Normal file
165
crates/red_knot_python_semantic/resources/mdtest/unary/custom.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# Custom unary operations
|
||||
|
||||
## Class instances
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __pos__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self) -> str:
|
||||
return "negative"
|
||||
|
||||
def __invert__(self) -> int:
|
||||
return 17
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
reveal_type(+Yes()) # revealed: bool
|
||||
reveal_type(-Yes()) # revealed: str
|
||||
reveal_type(~Yes()) # revealed: int
|
||||
|
||||
reveal_type(+Sub()) # revealed: bool
|
||||
reveal_type(-Sub()) # revealed: str
|
||||
reveal_type(~Sub()) # revealed: int
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `No`"
|
||||
reveal_type(+No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `No`"
|
||||
reveal_type(-No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `No`"
|
||||
reveal_type(~No()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Classes
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __pos__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self) -> str:
|
||||
return "negative"
|
||||
|
||||
def __invert__(self) -> int:
|
||||
return 17
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Yes]`"
|
||||
reveal_type(+Yes) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Yes]`"
|
||||
reveal_type(-Yes) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Yes]`"
|
||||
reveal_type(~Yes) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Sub]`"
|
||||
reveal_type(+Sub) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Sub]`"
|
||||
reveal_type(-Sub) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Sub]`"
|
||||
reveal_type(~Sub) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`"
|
||||
reveal_type(+No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`"
|
||||
reveal_type(-No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`"
|
||||
reveal_type(~No) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Function literals
|
||||
|
||||
```py
|
||||
def f():
|
||||
pass
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[f]`"
|
||||
reveal_type(+f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[f]`"
|
||||
reveal_type(-f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[f]`"
|
||||
reveal_type(~f) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Subclass
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __pos__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self) -> str:
|
||||
return "negative"
|
||||
|
||||
def __invert__(self) -> int:
|
||||
return 17
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
def yes() -> type[Yes]:
|
||||
return Yes
|
||||
|
||||
def sub() -> type[Sub]:
|
||||
return Sub
|
||||
|
||||
def no() -> type[No]:
|
||||
return No
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Yes]`"
|
||||
reveal_type(+yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Yes]`"
|
||||
reveal_type(-yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Yes]`"
|
||||
reveal_type(~yes()) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Sub]`"
|
||||
reveal_type(+sub()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Sub]`"
|
||||
reveal_type(-sub()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Sub]`"
|
||||
reveal_type(~sub()) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[No]`"
|
||||
reveal_type(+no()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[No]`"
|
||||
reveal_type(-no()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[No]`"
|
||||
reveal_type(~no()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Metaclass
|
||||
|
||||
```py
|
||||
class Meta(type):
|
||||
def __pos__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self) -> str:
|
||||
return "negative"
|
||||
|
||||
def __invert__(self) -> int:
|
||||
return 17
|
||||
|
||||
class Yes(metaclass=Meta): ...
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
reveal_type(+Yes) # revealed: bool
|
||||
reveal_type(-Yes) # revealed: str
|
||||
reveal_type(~Yes) # revealed: int
|
||||
|
||||
reveal_type(+Sub) # revealed: bool
|
||||
reveal_type(-Sub) # revealed: str
|
||||
reveal_type(~Sub) # revealed: int
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`"
|
||||
reveal_type(+No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`"
|
||||
reveal_type(-No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`"
|
||||
reveal_type(~No) # revealed: Unknown
|
||||
```
|
||||
@@ -61,7 +61,7 @@ reveal_type(c) # revealed: Literal[4]
|
||||
### Uneven unpacking (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (there aren't enough values to unpack)
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
|
||||
(a, b, c) = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
@@ -71,7 +71,7 @@ reveal_type(c) # revealed: Unknown
|
||||
### Uneven unpacking (2)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (too many values to unpack)
|
||||
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
|
||||
(a, b) = (1, 2, 3)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
@@ -80,7 +80,7 @@ reveal_type(b) # revealed: Literal[2]
|
||||
### Starred expression (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
|
||||
[a, *b, c, d] = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
@@ -133,7 +133,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
### Starred expression (6)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 5 or more, got 1)"
|
||||
(a, b, c, *d, e, f) = (1,)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
@@ -199,7 +199,7 @@ reveal_type(b) # revealed: LiteralString
|
||||
### Uneven unpacking (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (there aren't enough values to unpack)
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
|
||||
a, b, c = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
@@ -209,7 +209,7 @@ reveal_type(c) # revealed: Unknown
|
||||
### Uneven unpacking (2)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (too many values to unpack)
|
||||
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
|
||||
a, b = "abc"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
@@ -218,7 +218,7 @@ reveal_type(b) # revealed: LiteralString
|
||||
### Starred expression (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
|
||||
(a, *b, c, d) = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
@@ -271,7 +271,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
### Unicode
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, b) = "é"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
@@ -281,7 +281,7 @@ reveal_type(b) # revealed: Unknown
|
||||
### Unicode escape (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, b) = "\u9E6C"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
@@ -291,7 +291,7 @@ reveal_type(b) # revealed: Unknown
|
||||
### Unicode escape (2)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, b) = "\U0010FFFF"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
@@ -306,3 +306,273 @@ reveal_type(b) # revealed: Unknown
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
```
|
||||
|
||||
## Union
|
||||
|
||||
### Same types
|
||||
|
||||
Union of two tuples of equal length and each element is of the same type.
|
||||
|
||||
```py
|
||||
def _(arg: tuple[int, int] | tuple[int, int]):
|
||||
(a, b) = arg
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int
|
||||
```
|
||||
|
||||
### Mixed types (1)
|
||||
|
||||
Union of two tuples of equal length and one element differs in its type.
|
||||
|
||||
```py
|
||||
def _(arg: tuple[int, int] | tuple[int, str]):
|
||||
a, b = arg
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int | str
|
||||
```
|
||||
|
||||
### Mixed types (2)
|
||||
|
||||
Union of two tuples of equal length and both the element types are different.
|
||||
|
||||
```py
|
||||
def _(arg: tuple[int, str] | tuple[str, int]):
|
||||
a, b = arg
|
||||
reveal_type(a) # revealed: int | str
|
||||
reveal_type(b) # revealed: str | int
|
||||
```
|
||||
|
||||
### Mixed types (3)
|
||||
|
||||
Union of three tuples of equal length and various combination of element types:
|
||||
|
||||
1. All same types
|
||||
1. One different type
|
||||
1. All different types
|
||||
|
||||
```py
|
||||
def _(arg: tuple[int, int, int] | tuple[int, str, bytes] | tuple[int, int, str]):
|
||||
a, b, c = arg
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int | str
|
||||
reveal_type(c) # revealed: int | bytes | str
|
||||
```
|
||||
|
||||
### Nested
|
||||
|
||||
```py
|
||||
def _(arg: tuple[int, tuple[str, bytes]] | tuple[tuple[int, bytes], Literal["ab"]]):
|
||||
a, (b, c) = arg
|
||||
reveal_type(a) # revealed: int | tuple[int, bytes]
|
||||
reveal_type(b) # revealed: str
|
||||
reveal_type(c) # revealed: bytes | LiteralString
|
||||
```
|
||||
|
||||
### Starred expression
|
||||
|
||||
```py
|
||||
def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
|
||||
a, *b, c = arg
|
||||
reveal_type(a) # revealed: int
|
||||
# TODO: Should be `list[bytes | int | str]`
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: int | bytes
|
||||
```
|
||||
|
||||
### Size mismatch (1)
|
||||
|
||||
```py
|
||||
def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
|
||||
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
|
||||
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 5)"
|
||||
a, b = arg
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: bytes | int
|
||||
```
|
||||
|
||||
### Size mismatch (2)
|
||||
|
||||
```py
|
||||
def _(arg: tuple[int, bytes] | tuple[int, str]):
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
|
||||
a, b, c = arg
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: bytes | str
|
||||
reveal_type(c) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Same literal types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
value = (1, 2)
|
||||
else:
|
||||
value = (3, 4)
|
||||
|
||||
a, b = value
|
||||
reveal_type(a) # revealed: Literal[1, 3]
|
||||
reveal_type(b) # revealed: Literal[2, 4]
|
||||
```
|
||||
|
||||
### Mixed literal types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
value = (1, 2)
|
||||
else:
|
||||
value = ("a", "b")
|
||||
|
||||
a, b = value
|
||||
reveal_type(a) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(b) # revealed: Literal[2] | Literal["b"]
|
||||
```
|
||||
|
||||
### Typing literal
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(arg: tuple[int, int] | Literal["ab"]):
|
||||
a, b = arg
|
||||
reveal_type(a) # revealed: int | LiteralString
|
||||
reveal_type(b) # revealed: int | LiteralString
|
||||
```
|
||||
|
||||
### Custom iterator (1)
|
||||
|
||||
```py
|
||||
class Iterator:
|
||||
def __next__(self) -> tuple[int, int] | tuple[int, str]:
|
||||
return (1, 2)
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
((a, b), c) = Iterable()
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int | str
|
||||
reveal_type(c) # revealed: tuple[int, int] | tuple[int, str]
|
||||
```
|
||||
|
||||
### Custom iterator (2)
|
||||
|
||||
```py
|
||||
class Iterator:
|
||||
def __next__(self) -> bytes:
|
||||
return b""
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
def _(arg: tuple[int, str] | Iterable):
|
||||
a, b = arg
|
||||
reveal_type(a) # revealed: int | bytes
|
||||
reveal_type(b) # revealed: str | bytes
|
||||
```
|
||||
|
||||
## For statement
|
||||
|
||||
Unpacking in a `for` statement.
|
||||
|
||||
### Same types
|
||||
|
||||
```py
|
||||
def _(arg: tuple[tuple[int, int], tuple[int, int]]):
|
||||
for a, b in arg:
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int
|
||||
```
|
||||
|
||||
### Mixed types (1)
|
||||
|
||||
```py
|
||||
def _(arg: tuple[tuple[int, int], tuple[int, str]]):
|
||||
for a, b in arg:
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int | str
|
||||
```
|
||||
|
||||
### Mixed types (2)
|
||||
|
||||
```py
|
||||
def _(arg: tuple[tuple[int, str], tuple[str, int]]):
|
||||
for a, b in arg:
|
||||
reveal_type(a) # revealed: int | str
|
||||
reveal_type(b) # revealed: str | int
|
||||
```
|
||||
|
||||
### Mixed types (3)
|
||||
|
||||
```py
|
||||
def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, str]]):
|
||||
for a, b, c in arg:
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int | str
|
||||
reveal_type(c) # revealed: int | bytes | str
|
||||
```
|
||||
|
||||
### Same literal values
|
||||
|
||||
```py
|
||||
for a, b in ((1, 2), (3, 4)):
|
||||
reveal_type(a) # revealed: Literal[1, 3]
|
||||
reveal_type(b) # revealed: Literal[2, 4]
|
||||
```
|
||||
|
||||
### Mixed literal values (1)
|
||||
|
||||
```py
|
||||
for a, b in ((1, 2), ("a", "b")):
|
||||
reveal_type(a) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(b) # revealed: Literal[2] | Literal["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)"
|
||||
for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"):
|
||||
reveal_type(a) # revealed: Unknown | Literal[3, 5] | LiteralString
|
||||
reveal_type(b) # revealed: Unknown | Literal["a", "b"]
|
||||
```
|
||||
|
||||
### Custom iterator (1)
|
||||
|
||||
```py
|
||||
class Iterator:
|
||||
def __next__(self) -> tuple[int, int]:
|
||||
return (1, 2)
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
for a, b in Iterable():
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int
|
||||
```
|
||||
|
||||
### 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]):
|
||||
for a, b in arg:
|
||||
reveal_type(a) # revealed: int | bytes
|
||||
reveal_type(b) # revealed: str | bytes
|
||||
```
|
||||
|
||||
@@ -17,5 +17,5 @@ class Manager:
|
||||
|
||||
async def test():
|
||||
async with Manager() as f:
|
||||
reveal_type(f) # revealed: @Todo(async with statement)
|
||||
reveal_type(f) # revealed: @Todo(async `with` statement)
|
||||
```
|
||||
|
||||
@@ -43,7 +43,7 @@ impl<T> AstNodeRef<T> {
|
||||
}
|
||||
|
||||
/// Returns a reference to the wrapped node.
|
||||
pub fn node(&self) -> &T {
|
||||
pub const fn node(&self) -> &T {
|
||||
// SAFETY: Holding on to `parsed` ensures that the AST to which `node` belongs is still
|
||||
// alive and not moved.
|
||||
unsafe { self.node.as_ref() }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::lint::RuleSelection;
|
||||
use crate::lint::{LintRegistry, RuleSelection};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
@@ -8,6 +8,8 @@ pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||
fn is_file_open(&self, file: File) -> bool;
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection;
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -16,10 +18,10 @@ pub(crate) mod tests {
|
||||
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::{default_lint_registry, ProgramSettings};
|
||||
use crate::{default_lint_registry, ProgramSettings, PythonPlatform};
|
||||
|
||||
use super::Db;
|
||||
use crate::lint::RuleSelection;
|
||||
use crate::lint::{LintRegistry, RuleSelection};
|
||||
use anyhow::Context;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
|
||||
@@ -27,6 +29,7 @@ pub(crate) mod tests {
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
@@ -44,7 +47,7 @@ pub(crate) mod tests {
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
events: Arc::default(),
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(&default_lint_registry())),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +114,10 @@ pub(crate) mod tests {
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
default_lint_registry()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
@@ -126,6 +133,8 @@ pub(crate) mod tests {
|
||||
pub(crate) struct TestDbBuilder<'a> {
|
||||
/// Target Python version
|
||||
python_version: PythonVersion,
|
||||
/// Target Python platform
|
||||
python_platform: PythonPlatform,
|
||||
/// Path to a custom typeshed directory
|
||||
custom_typeshed: Option<SystemPathBuf>,
|
||||
/// Path and content pairs for files that should be present
|
||||
@@ -136,6 +145,7 @@ pub(crate) mod tests {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
python_version: PythonVersion::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
custom_typeshed: None,
|
||||
files: vec![],
|
||||
}
|
||||
@@ -172,6 +182,7 @@ pub(crate) mod tests {
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: self.python_version,
|
||||
python_platform: self.python_platform,
|
||||
search_paths,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3,10 +3,12 @@ use std::hash::BuildHasherDefault;
|
||||
use rustc_hash::FxHasher;
|
||||
|
||||
use crate::lint::{LintRegistry, LintRegistryBuilder};
|
||||
use crate::suppression::{INVALID_IGNORE_COMMENT, UNKNOWN_RULE, UNUSED_IGNORE_COMMENT};
|
||||
pub use db::Db;
|
||||
pub use module_name::ModuleName;
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, Module};
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, KnownModule, Module};
|
||||
pub use program::{Program, ProgramSettings, SearchPathSettings, SitePackages};
|
||||
pub use python_platform::PythonPlatform;
|
||||
pub use python_version::PythonVersion;
|
||||
pub use semantic_model::{HasTy, SemanticModel};
|
||||
|
||||
@@ -17,26 +19,36 @@ mod module_name;
|
||||
mod module_resolver;
|
||||
mod node_key;
|
||||
mod program;
|
||||
mod python_platform;
|
||||
mod python_version;
|
||||
pub mod semantic_index;
|
||||
mod semantic_model;
|
||||
pub(crate) mod site_packages;
|
||||
mod stdlib;
|
||||
mod suppression;
|
||||
pub(crate) mod symbol;
|
||||
pub mod types;
|
||||
mod unpack;
|
||||
mod util;
|
||||
mod visibility_constraints;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
/// Creates a new registry with all known semantic lints.
|
||||
pub fn default_lint_registry() -> LintRegistry {
|
||||
let mut registry = LintRegistryBuilder::default();
|
||||
register_lints(&mut registry);
|
||||
registry.build()
|
||||
/// Returns the default registry with all known semantic lints.
|
||||
pub fn default_lint_registry() -> &'static LintRegistry {
|
||||
static REGISTRY: std::sync::LazyLock<LintRegistry> = std::sync::LazyLock::new(|| {
|
||||
let mut registry = LintRegistryBuilder::default();
|
||||
register_lints(&mut registry);
|
||||
registry.build()
|
||||
});
|
||||
|
||||
®ISTRY
|
||||
}
|
||||
|
||||
/// Register all known semantic lints.
|
||||
pub fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
types::register_lints(registry);
|
||||
registry.register_lint(&UNUSED_IGNORE_COMMENT);
|
||||
registry.register_lint(&UNKNOWN_RULE);
|
||||
registry.register_lint(&INVALID_IGNORE_COMMENT);
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ impl LintRegistryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct LintRegistry {
|
||||
lints: Vec<LintId>,
|
||||
by_name: FxHashMap<&'static str, LintEntry>,
|
||||
@@ -374,7 +374,7 @@ impl LintRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
#[derive(Error, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GetLintError {
|
||||
/// The name maps to this removed lint.
|
||||
#[error("lint {0} has been removed")]
|
||||
@@ -385,7 +385,7 @@ pub enum GetLintError {
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum LintEntry {
|
||||
/// An existing lint rule. Can be in preview, stable or deprecated.
|
||||
Lint(LintId),
|
||||
@@ -444,6 +444,11 @@ impl RuleSelection {
|
||||
self.lints.get(&lint).copied()
|
||||
}
|
||||
|
||||
/// Returns `true` if the `lint` is enabled.
|
||||
pub fn is_enabled(&self, lint: LintId) -> bool {
|
||||
self.severity(lint).is_some()
|
||||
}
|
||||
|
||||
/// Enables `lint` and configures with the given `severity`.
|
||||
///
|
||||
/// Overrides any previous configuration for the lint.
|
||||
|
||||
@@ -186,6 +186,26 @@ impl ModuleName {
|
||||
self.0.push('.');
|
||||
self.0.push_str(other);
|
||||
}
|
||||
|
||||
/// Returns an iterator of this module name and all of its parent modules.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use red_knot_python_semantic::ModuleName;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ModuleName::new_static("foo.bar.baz").unwrap().ancestors().collect::<Vec<_>>(),
|
||||
/// vec![
|
||||
/// ModuleName::new_static("foo.bar.baz").unwrap(),
|
||||
/// ModuleName::new_static("foo.bar").unwrap(),
|
||||
/// ModuleName::new_static("foo").unwrap(),
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
pub fn ancestors(&self) -> impl Iterator<Item = Self> {
|
||||
std::iter::successors(Some(self.clone()), Self::parent)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ModuleName {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
pub use module::Module;
|
||||
pub use module::{KnownModule, Module};
|
||||
pub use resolver::resolve_module;
|
||||
pub(crate) use resolver::{file_to_module, SearchPaths};
|
||||
use ruff_db::system::SystemPath;
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::path::SearchPath;
|
||||
use crate::module_name::ModuleName;
|
||||
|
||||
/// Representation of a Python module.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Module {
|
||||
inner: Arc<ModuleInner>,
|
||||
}
|
||||
@@ -19,12 +19,14 @@ impl Module {
|
||||
search_path: SearchPath,
|
||||
file: File,
|
||||
) -> Self {
|
||||
let known = KnownModule::try_from_search_path_and_name(&search_path, &name);
|
||||
Self {
|
||||
inner: Arc::new(ModuleInner {
|
||||
name,
|
||||
kind,
|
||||
search_path,
|
||||
file,
|
||||
known,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -39,6 +41,16 @@ impl Module {
|
||||
self.inner.file
|
||||
}
|
||||
|
||||
/// Is this a module that we special-case somehow? If so, which one?
|
||||
pub fn known(&self) -> Option<KnownModule> {
|
||||
self.inner.known
|
||||
}
|
||||
|
||||
/// Does this module represent the given known module?
|
||||
pub fn is_known(&self, known_module: KnownModule) -> bool {
|
||||
self.known() == Some(known_module)
|
||||
}
|
||||
|
||||
/// The search path from which the module was resolved.
|
||||
pub(crate) fn search_path(&self) -> &SearchPath {
|
||||
&self.inner.search_path
|
||||
@@ -61,12 +73,13 @@ impl std::fmt::Debug for Module {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
struct ModuleInner {
|
||||
name: ModuleName,
|
||||
kind: ModuleKind,
|
||||
search_path: SearchPath,
|
||||
file: File,
|
||||
known: Option<KnownModule>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
@@ -83,3 +96,62 @@ impl ModuleKind {
|
||||
matches!(self, ModuleKind::Package)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration of various core stdlib modules in which important types are located
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum KnownModule {
|
||||
Builtins,
|
||||
Types,
|
||||
Typeshed,
|
||||
TypingExtensions,
|
||||
Typing,
|
||||
Sys,
|
||||
#[allow(dead_code)]
|
||||
Abc, // currently only used in tests
|
||||
Collections,
|
||||
}
|
||||
|
||||
impl KnownModule {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Builtins => "builtins",
|
||||
Self::Types => "types",
|
||||
Self::Typing => "typing",
|
||||
Self::Typeshed => "_typeshed",
|
||||
Self::TypingExtensions => "typing_extensions",
|
||||
Self::Sys => "sys",
|
||||
Self::Abc => "abc",
|
||||
Self::Collections => "collections",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(self) -> ModuleName {
|
||||
let self_as_str = self.as_str();
|
||||
ModuleName::new_static(self_as_str)
|
||||
.unwrap_or_else(|| panic!("{self_as_str} should be a valid module name!"))
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_search_path_and_name(
|
||||
search_path: &SearchPath,
|
||||
name: &ModuleName,
|
||||
) -> Option<Self> {
|
||||
if !search_path.is_standard_library() {
|
||||
return None;
|
||||
}
|
||||
match name.as_str() {
|
||||
"builtins" => Some(Self::Builtins),
|
||||
"types" => Some(Self::Types),
|
||||
"typing" => Some(Self::Typing),
|
||||
"_typeshed" => Some(Self::Typeshed),
|
||||
"typing_extensions" => Some(Self::TypingExtensions),
|
||||
"sys" => Some(Self::Sys),
|
||||
"abc" => Some(Self::Abc),
|
||||
"collections" => Some(Self::Collections),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_typing(self) -> bool {
|
||||
matches!(self, Self::Typing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,15 @@ enum SystemOrVendoredPathRef<'a> {
|
||||
Vendored(&'a VendoredPath),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SystemOrVendoredPathRef<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SystemOrVendoredPathRef::System(system) => system.fmt(f),
|
||||
SystemOrVendoredPathRef::Vendored(vendored) => vendored.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the module for the file with the given id.
|
||||
///
|
||||
/// Returns `None` if the file is not a module locatable via any of the known search paths.
|
||||
@@ -712,8 +721,8 @@ mod tests {
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::module::ModuleKind;
|
||||
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
use crate::ProgramSettings;
|
||||
use crate::PythonVersion;
|
||||
use crate::{ProgramSettings, PythonPlatform};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -1253,7 +1262,7 @@ mod tests {
|
||||
fn symlink() -> anyhow::Result<()> {
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::program::Program;
|
||||
use crate::{program::Program, PythonPlatform};
|
||||
use ruff_db::system::{OsSystem, SystemPath};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
@@ -1287,6 +1296,7 @@ mod tests {
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::PY38,
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
@@ -1792,6 +1802,7 @@ not_a_directory
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_db::vendored::VendoredPathBuf;
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::{ProgramSettings, SitePackages};
|
||||
use crate::{ProgramSettings, PythonPlatform, SitePackages};
|
||||
|
||||
/// A test case for the module resolver.
|
||||
///
|
||||
@@ -101,6 +101,7 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
pub(crate) struct TestCaseBuilder<T> {
|
||||
typeshed_option: T,
|
||||
python_version: PythonVersion,
|
||||
python_platform: PythonPlatform,
|
||||
first_party_files: Vec<FileSpec>,
|
||||
site_packages_files: Vec<FileSpec>,
|
||||
}
|
||||
@@ -147,6 +148,7 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
Self {
|
||||
typeshed_option: UnspecifiedTypeshed,
|
||||
python_version: PythonVersion::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
first_party_files: vec![],
|
||||
site_packages_files: vec![],
|
||||
}
|
||||
@@ -157,12 +159,14 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: _,
|
||||
python_version,
|
||||
python_platform,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
TestCaseBuilder {
|
||||
typeshed_option: VendoredTypeshed,
|
||||
python_version,
|
||||
python_platform,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
}
|
||||
@@ -176,6 +180,7 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: _,
|
||||
python_version,
|
||||
python_platform,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
@@ -183,6 +188,7 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
TestCaseBuilder {
|
||||
typeshed_option: typeshed,
|
||||
python_version,
|
||||
python_platform,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
}
|
||||
@@ -212,6 +218,7 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option,
|
||||
python_version,
|
||||
python_platform,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
@@ -227,6 +234,7 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version,
|
||||
python_platform,
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
@@ -269,6 +277,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: VendoredTypeshed,
|
||||
python_version,
|
||||
python_platform,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
@@ -283,6 +292,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version,
|
||||
python_platform,
|
||||
search_paths: SearchPathSettings {
|
||||
site_packages: SitePackages::Known(vec![site_packages.clone()]),
|
||||
..SearchPathSettings::new(src.clone())
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::python_platform::PythonPlatform;
|
||||
use crate::python_version::PythonVersion;
|
||||
use anyhow::Context;
|
||||
use salsa::Durability;
|
||||
@@ -12,6 +13,8 @@ use crate::Db;
|
||||
pub struct Program {
|
||||
pub python_version: PythonVersion,
|
||||
|
||||
pub python_platform: PythonPlatform,
|
||||
|
||||
#[return_ref]
|
||||
pub(crate) search_paths: SearchPaths,
|
||||
}
|
||||
@@ -20,6 +23,7 @@ impl Program {
|
||||
pub fn from_settings(db: &dyn Db, settings: &ProgramSettings) -> anyhow::Result<Self> {
|
||||
let ProgramSettings {
|
||||
python_version,
|
||||
python_platform,
|
||||
search_paths,
|
||||
} = settings;
|
||||
|
||||
@@ -28,9 +32,11 @@ impl Program {
|
||||
let search_paths = SearchPaths::from_settings(db, search_paths)
|
||||
.with_context(|| "Invalid search path settings")?;
|
||||
|
||||
Ok(Program::builder(settings.python_version, search_paths)
|
||||
.durability(Durability::HIGH)
|
||||
.new(db))
|
||||
Ok(
|
||||
Program::builder(*python_version, python_platform.clone(), search_paths)
|
||||
.durability(Durability::HIGH)
|
||||
.new(db),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_search_paths(
|
||||
@@ -57,6 +63,7 @@ impl Program {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct ProgramSettings {
|
||||
pub python_version: PythonVersion,
|
||||
pub python_platform: PythonPlatform,
|
||||
pub search_paths: SearchPathSettings,
|
||||
}
|
||||
|
||||
|
||||
19
crates/red_knot_python_semantic/src/python_platform.rs
Normal file
19
crates/red_knot_python_semantic/src/python_platform.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
/// The target platform to assume when resolving types.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(rename_all = "kebab-case")
|
||||
)]
|
||||
pub enum PythonPlatform {
|
||||
/// Do not make any assumptions about the target platform.
|
||||
#[default]
|
||||
All,
|
||||
/// Assume a specific target platform like `linux`, `darwin` or `win32`.
|
||||
///
|
||||
/// We use a string (instead of individual enum variants), as the set of possible platforms
|
||||
/// may change over time. See <https://docs.python.org/3/library/sys.html#sys.platform> for
|
||||
/// some known platform identifiers.
|
||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||
Identifier(String),
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::iter::FusedIterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
use salsa::plumbing::AsId;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_index::{IndexSlice, IndexVec};
|
||||
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIds;
|
||||
use crate::semantic_index::builder::SemanticIndexBuilder;
|
||||
@@ -28,7 +29,8 @@ pub mod symbol;
|
||||
mod use_def;
|
||||
|
||||
pub(crate) use self::use_def::{
|
||||
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
|
||||
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,
|
||||
DeclarationsIterator, ScopedVisibilityConstraintId,
|
||||
};
|
||||
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), FxBuildHasher>;
|
||||
@@ -60,6 +62,22 @@ pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<Sym
|
||||
index.symbol_table(scope.file_scope_id(db))
|
||||
}
|
||||
|
||||
/// Returns the set of modules that are imported anywhere in `file`.
|
||||
///
|
||||
/// This set only considers `import` statements, not `from...import` statements, because:
|
||||
///
|
||||
/// - In `from foo import bar`, we cannot determine whether `foo.bar` is a submodule (and is
|
||||
/// therefore imported) without looking outside the content of this file. (We could turn this
|
||||
/// into a _potentially_ imported modules set, but that would change how it's used in our type
|
||||
/// inference logic.)
|
||||
///
|
||||
/// - We cannot resolve relative imports (which aren't allowed in `import` statements) without
|
||||
/// knowing the name of the current module, and whether it's a package.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc<FxHashSet<ModuleName>> {
|
||||
semantic_index(db, file).imported_modules.clone()
|
||||
}
|
||||
|
||||
/// Returns the use-def map for a specific `scope`.
|
||||
///
|
||||
/// Using [`use_def_map`] over [`semantic_index`] has the advantage that
|
||||
@@ -116,6 +134,9 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
/// changing a file invalidates all dependents.
|
||||
ast_ids: IndexVec<FileScopeId, AstIds>,
|
||||
|
||||
/// The set of modules that are imported anywhere within this file.
|
||||
imported_modules: Arc<FxHashSet<ModuleName>>,
|
||||
|
||||
/// Flags about the global scope (code usage impacting inference)
|
||||
has_future_annotations: bool,
|
||||
}
|
||||
@@ -358,14 +379,12 @@ mod tests {
|
||||
impl UseDefMap<'_> {
|
||||
fn first_public_binding(&self, symbol: ScopedSymbolId) -> Option<Definition<'_>> {
|
||||
self.public_bindings(symbol)
|
||||
.next()
|
||||
.map(|constrained_binding| constrained_binding.binding)
|
||||
.find_map(|constrained_binding| constrained_binding.binding)
|
||||
}
|
||||
|
||||
fn first_binding_at_use(&self, use_id: ScopedUseId) -> Option<Definition<'_>> {
|
||||
self.bindings_at_use(use_id)
|
||||
.next()
|
||||
.map(|constrained_binding| constrained_binding.binding)
|
||||
.find_map(|constrained_binding| constrained_binding.binding)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use except_handlers::TryNodeContextStackManager;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
@@ -9,11 +9,12 @@ use ruff_index::IndexVec;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
|
||||
use ruff_python_ast::{BoolOp, Expr};
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||
use crate::semantic_index::constraint::PatternConstraintKind;
|
||||
use crate::semantic_index::definition::{
|
||||
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionNodeKey,
|
||||
DefinitionNodeRef, ForStmtDefinitionNodeRef, ImportFromDefinitionNodeRef,
|
||||
@@ -23,9 +24,12 @@ use crate::semantic_index::symbol::{
|
||||
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId,
|
||||
SymbolTableBuilder,
|
||||
};
|
||||
use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
|
||||
use crate::semantic_index::use_def::{
|
||||
FlowSnapshot, ScopedConstraintId, ScopedVisibilityConstraintId, UseDefMapBuilder,
|
||||
};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::unpack::Unpack;
|
||||
use crate::unpack::{Unpack, UnpackValue};
|
||||
use crate::visibility_constraints::VisibilityConstraint;
|
||||
use crate::Db;
|
||||
|
||||
use super::constraint::{Constraint, ConstraintNode, PatternConstraint};
|
||||
@@ -79,6 +83,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
|
||||
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
|
||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
|
||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||
imported_modules: FxHashSet<ModuleName>,
|
||||
}
|
||||
|
||||
impl<'db> SemanticIndexBuilder<'db> {
|
||||
@@ -105,6 +110,8 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
scopes_by_node: FxHashMap::default(),
|
||||
definitions_by_node: FxHashMap::default(),
|
||||
expressions_by_node: FxHashMap::default(),
|
||||
|
||||
imported_modules: FxHashSet::default(),
|
||||
};
|
||||
|
||||
builder.push_scope_with_parent(NodeWithScopeRef::Module, None);
|
||||
@@ -281,11 +288,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
constraint
|
||||
}
|
||||
|
||||
fn record_constraint(&mut self, constraint: Constraint<'db>) {
|
||||
self.current_use_def_map_mut().record_constraint(constraint);
|
||||
}
|
||||
|
||||
fn build_constraint(&mut self, constraint_node: &Expr) -> Constraint<'db> {
|
||||
fn build_constraint(&mut self, constraint_node: &ast::Expr) -> Constraint<'db> {
|
||||
let expression = self.add_standalone_expression(constraint_node);
|
||||
Constraint {
|
||||
node: ConstraintNode::Expression(expression),
|
||||
@@ -293,12 +296,89 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn record_negated_constraint(&mut self, constraint: Constraint<'db>) {
|
||||
/// Adds a new constraint to the list of all constraints, but does not record it. Returns the
|
||||
/// constraint ID for later recording using [`SemanticIndexBuilder::record_constraint_id`].
|
||||
fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
self.current_use_def_map_mut().add_constraint(constraint)
|
||||
}
|
||||
|
||||
/// Negates a constraint and adds it to the list of all constraints, does not record it.
|
||||
fn add_negated_constraint(
|
||||
&mut self,
|
||||
constraint: Constraint<'db>,
|
||||
) -> (Constraint<'db>, ScopedConstraintId) {
|
||||
let negated = Constraint {
|
||||
node: constraint.node,
|
||||
is_positive: false,
|
||||
};
|
||||
let id = self.current_use_def_map_mut().add_constraint(negated);
|
||||
(negated, id)
|
||||
}
|
||||
|
||||
/// Records a previously added constraint by adding it to all live bindings.
|
||||
fn record_constraint_id(&mut self, constraint: ScopedConstraintId) {
|
||||
self.current_use_def_map_mut()
|
||||
.record_constraint(Constraint {
|
||||
node: constraint.node,
|
||||
is_positive: false,
|
||||
});
|
||||
.record_constraint_id(constraint);
|
||||
}
|
||||
|
||||
/// Adds and records a constraint, i.e. adds it to all live bindings.
|
||||
fn record_constraint(&mut self, constraint: Constraint<'db>) {
|
||||
self.current_use_def_map_mut().record_constraint(constraint);
|
||||
}
|
||||
|
||||
/// Negates the given constraint and then adds it to all live bindings.
|
||||
fn record_negated_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
let (_, id) = self.add_negated_constraint(constraint);
|
||||
self.record_constraint_id(id);
|
||||
id
|
||||
}
|
||||
|
||||
/// Adds a new visibility constraint, but does not record it. Returns the constraint ID
|
||||
/// for later recording using [`SemanticIndexBuilder::record_visibility_constraint_id`].
|
||||
fn add_visibility_constraint(
|
||||
&mut self,
|
||||
constraint: VisibilityConstraint<'db>,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
self.current_use_def_map_mut()
|
||||
.add_visibility_constraint(constraint)
|
||||
}
|
||||
|
||||
/// Records a previously added visibility constraint by applying it to all live bindings
|
||||
/// and declarations.
|
||||
fn record_visibility_constraint_id(&mut self, constraint: ScopedVisibilityConstraintId) {
|
||||
self.current_use_def_map_mut()
|
||||
.record_visibility_constraint_id(constraint);
|
||||
}
|
||||
|
||||
/// Negates the given visibility constraint and then adds it to all live bindings and declarations.
|
||||
fn record_negated_visibility_constraint(
|
||||
&mut self,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
self.current_use_def_map_mut()
|
||||
.record_visibility_constraint(VisibilityConstraint::VisibleIfNot(constraint))
|
||||
}
|
||||
|
||||
/// Records a visibility constraint by applying it to all live bindings and declarations.
|
||||
fn record_visibility_constraint(
|
||||
&mut self,
|
||||
constraint: Constraint<'db>,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
self.current_use_def_map_mut()
|
||||
.record_visibility_constraint(VisibilityConstraint::VisibleIf(constraint))
|
||||
}
|
||||
|
||||
/// Records a [`VisibilityConstraint::Ambiguous`] constraint.
|
||||
fn record_ambiguous_visibility(&mut self) -> ScopedVisibilityConstraintId {
|
||||
self.current_use_def_map_mut()
|
||||
.record_visibility_constraint(VisibilityConstraint::Ambiguous)
|
||||
}
|
||||
|
||||
/// Simplifies (resets) visibility constraints on all live bindings and declarations that did
|
||||
/// not see any new definitions since the given snapshot.
|
||||
fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
|
||||
self.current_use_def_map_mut()
|
||||
.simplify_visibility_constraints(snapshot);
|
||||
}
|
||||
|
||||
fn push_assignment(&mut self, assignment: CurrentAssignment<'db>) {
|
||||
@@ -320,30 +400,37 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
|
||||
fn add_pattern_constraint(
|
||||
&mut self,
|
||||
subject: &ast::Expr,
|
||||
subject: Expression<'db>,
|
||||
pattern: &ast::Pattern,
|
||||
) -> PatternConstraint<'db> {
|
||||
#[allow(unsafe_code)]
|
||||
let (subject, pattern) = unsafe {
|
||||
(
|
||||
AstNodeRef::new(self.module.clone(), subject),
|
||||
AstNodeRef::new(self.module.clone(), pattern),
|
||||
)
|
||||
guard: Option<&ast::Expr>,
|
||||
) -> Constraint<'db> {
|
||||
let guard = guard.map(|guard| self.add_standalone_expression(guard));
|
||||
|
||||
let kind = match pattern {
|
||||
ast::Pattern::MatchValue(pattern) => {
|
||||
let value = self.add_standalone_expression(&pattern.value);
|
||||
PatternConstraintKind::Value(value, guard)
|
||||
}
|
||||
ast::Pattern::MatchSingleton(singleton) => {
|
||||
PatternConstraintKind::Singleton(singleton.value, guard)
|
||||
}
|
||||
_ => PatternConstraintKind::Unsupported,
|
||||
};
|
||||
|
||||
let pattern_constraint = PatternConstraint::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
subject,
|
||||
pattern,
|
||||
kind,
|
||||
countme::Count::default(),
|
||||
);
|
||||
self.current_use_def_map_mut()
|
||||
.record_constraint(Constraint {
|
||||
node: ConstraintNode::Pattern(pattern_constraint),
|
||||
is_positive: true,
|
||||
});
|
||||
pattern_constraint
|
||||
let constraint = Constraint {
|
||||
node: ConstraintNode::Pattern(pattern_constraint),
|
||||
is_positive: true,
|
||||
};
|
||||
self.current_use_def_map_mut().record_constraint(constraint);
|
||||
constraint
|
||||
}
|
||||
|
||||
/// Record an expression that needs to be a Salsa ingredient, because we need to infer its type
|
||||
@@ -558,6 +645,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
scopes_by_expression: self.scopes_by_expression,
|
||||
scopes_by_node: self.scopes_by_node,
|
||||
use_def_maps,
|
||||
imported_modules: Arc::new(self.imported_modules),
|
||||
has_future_annotations: self.has_future_annotations,
|
||||
}
|
||||
}
|
||||
@@ -661,6 +749,12 @@ where
|
||||
}
|
||||
ast::Stmt::Import(node) => {
|
||||
for alias in &node.names {
|
||||
// Mark the imported module, and all of its parents, as being imported in this
|
||||
// file.
|
||||
if let Some(module_name) = ModuleName::new(&alias.name) {
|
||||
self.imported_modules.extend(module_name.ancestors());
|
||||
}
|
||||
|
||||
let symbol_name = if let Some(asname) = &alias.asname {
|
||||
asname.id.clone()
|
||||
} else {
|
||||
@@ -715,7 +809,7 @@ where
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
},
|
||||
value,
|
||||
UnpackValue::Assign(value),
|
||||
countme::Count::default(),
|
||||
)),
|
||||
})
|
||||
@@ -788,6 +882,10 @@ where
|
||||
let constraint = self.record_expression_constraint(&node.test);
|
||||
let mut constraints = vec![constraint];
|
||||
self.visit_body(&node.body);
|
||||
|
||||
let visibility_constraint_id = self.record_visibility_constraint(constraint);
|
||||
let mut vis_constraints = vec![visibility_constraint_id];
|
||||
|
||||
let mut post_clauses: Vec<FlowSnapshot> = vec![];
|
||||
let elif_else_clauses = node
|
||||
.elif_else_clauses
|
||||
@@ -814,15 +912,31 @@ where
|
||||
for constraint in &constraints {
|
||||
self.record_negated_constraint(*constraint);
|
||||
}
|
||||
if let Some(elif_test) = clause_test {
|
||||
|
||||
let elif_constraint = if let Some(elif_test) = clause_test {
|
||||
self.visit_expr(elif_test);
|
||||
constraints.push(self.record_expression_constraint(elif_test));
|
||||
}
|
||||
let constraint = self.record_expression_constraint(elif_test);
|
||||
constraints.push(constraint);
|
||||
Some(constraint)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.visit_body(clause_body);
|
||||
|
||||
for id in &vis_constraints {
|
||||
self.record_negated_visibility_constraint(*id);
|
||||
}
|
||||
if let Some(elif_constraint) = elif_constraint {
|
||||
let id = self.record_visibility_constraint(elif_constraint);
|
||||
vis_constraints.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
for post_clause_state in post_clauses {
|
||||
self.flow_merge(post_clause_state);
|
||||
}
|
||||
|
||||
self.simplify_visibility_constraints(pre_if);
|
||||
}
|
||||
ast::Stmt::While(ast::StmtWhile {
|
||||
test,
|
||||
@@ -845,6 +959,8 @@ where
|
||||
self.visit_body(body);
|
||||
self.set_inside_loop(outer_loop_state);
|
||||
|
||||
let vis_constraint_id = self.record_visibility_constraint(constraint);
|
||||
|
||||
// Get the break states from the body of this loop, and restore the saved outer
|
||||
// ones.
|
||||
let break_states =
|
||||
@@ -852,15 +968,21 @@ where
|
||||
|
||||
// We may execute the `else` clause without ever executing the body, so merge in
|
||||
// the pre-loop state before visiting `else`.
|
||||
self.flow_merge(pre_loop);
|
||||
self.flow_merge(pre_loop.clone());
|
||||
self.record_negated_constraint(constraint);
|
||||
self.visit_body(orelse);
|
||||
self.record_negated_visibility_constraint(vis_constraint_id);
|
||||
|
||||
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
|
||||
// states after visiting `else`.
|
||||
for break_state in break_states {
|
||||
self.flow_merge(break_state);
|
||||
let snapshot = self.flow_snapshot();
|
||||
self.flow_restore(break_state);
|
||||
self.record_visibility_constraint(constraint);
|
||||
self.flow_merge(snapshot);
|
||||
}
|
||||
|
||||
self.simplify_visibility_constraints(pre_loop);
|
||||
}
|
||||
ast::Stmt::With(ast::StmtWith {
|
||||
items,
|
||||
@@ -898,16 +1020,47 @@ where
|
||||
orelse,
|
||||
},
|
||||
) => {
|
||||
self.add_standalone_expression(iter);
|
||||
debug_assert_eq!(&self.current_assignments, &[]);
|
||||
|
||||
let iter_expr = self.add_standalone_expression(iter);
|
||||
self.visit_expr(iter);
|
||||
|
||||
self.record_ambiguous_visibility();
|
||||
|
||||
let pre_loop = self.flow_snapshot();
|
||||
let saved_break_states = std::mem::take(&mut self.loop_break_states);
|
||||
|
||||
debug_assert_eq!(&self.current_assignments, &[]);
|
||||
self.push_assignment(for_stmt.into());
|
||||
let current_assignment = match &**target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => Some(CurrentAssignment::For {
|
||||
node: for_stmt,
|
||||
first: true,
|
||||
unpack: Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
},
|
||||
UnpackValue::Iterable(iter_expr),
|
||||
countme::Count::default(),
|
||||
)),
|
||||
}),
|
||||
ast::Expr::Name(_) => Some(CurrentAssignment::For {
|
||||
node: for_stmt,
|
||||
unpack: None,
|
||||
first: false,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
self.visit_expr(target);
|
||||
self.pop_assignment();
|
||||
if current_assignment.is_some() {
|
||||
self.pop_assignment();
|
||||
}
|
||||
|
||||
// TODO: Definitions created by loop variables
|
||||
// (and definitions created inside the body)
|
||||
@@ -936,32 +1089,63 @@ where
|
||||
cases,
|
||||
range: _,
|
||||
}) => {
|
||||
self.add_standalone_expression(subject);
|
||||
let subject_expr = self.add_standalone_expression(subject);
|
||||
self.visit_expr(subject);
|
||||
|
||||
let after_subject = self.flow_snapshot();
|
||||
let Some((first, remaining)) = cases.split_first() else {
|
||||
return;
|
||||
};
|
||||
self.add_pattern_constraint(subject, &first.pattern);
|
||||
|
||||
let first_constraint_id = self.add_pattern_constraint(
|
||||
subject_expr,
|
||||
&first.pattern,
|
||||
first.guard.as_deref(),
|
||||
);
|
||||
|
||||
self.visit_match_case(first);
|
||||
|
||||
let first_vis_constraint_id =
|
||||
self.record_visibility_constraint(first_constraint_id);
|
||||
let mut vis_constraints = vec![first_vis_constraint_id];
|
||||
|
||||
let mut post_case_snapshots = vec![];
|
||||
for case in remaining {
|
||||
post_case_snapshots.push(self.flow_snapshot());
|
||||
self.flow_restore(after_subject.clone());
|
||||
self.add_pattern_constraint(subject, &case.pattern);
|
||||
let constraint_id = self.add_pattern_constraint(
|
||||
subject_expr,
|
||||
&case.pattern,
|
||||
case.guard.as_deref(),
|
||||
);
|
||||
self.visit_match_case(case);
|
||||
|
||||
for id in &vis_constraints {
|
||||
self.record_negated_visibility_constraint(*id);
|
||||
}
|
||||
let vis_constraint_id = self.record_visibility_constraint(constraint_id);
|
||||
vis_constraints.push(vis_constraint_id);
|
||||
}
|
||||
for post_clause_state in post_case_snapshots {
|
||||
self.flow_merge(post_clause_state);
|
||||
}
|
||||
|
||||
// If there is no final wildcard match case, pretend there is one. This is similar to how
|
||||
// we add an implicit `else` block in if-elif chains, in case it's not present.
|
||||
if !cases
|
||||
.last()
|
||||
.is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard())
|
||||
{
|
||||
self.flow_merge(after_subject);
|
||||
post_case_snapshots.push(self.flow_snapshot());
|
||||
self.flow_restore(after_subject.clone());
|
||||
|
||||
for id in &vis_constraints {
|
||||
self.record_negated_visibility_constraint(*id);
|
||||
}
|
||||
}
|
||||
|
||||
for post_clause_state in post_case_snapshots {
|
||||
self.flow_merge(post_clause_state);
|
||||
}
|
||||
|
||||
self.simplify_visibility_constraints(after_subject);
|
||||
}
|
||||
ast::Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
@@ -971,6 +1155,8 @@ where
|
||||
is_star,
|
||||
range: _,
|
||||
}) => {
|
||||
self.record_ambiguous_visibility();
|
||||
|
||||
// Save the state prior to visiting any of the `try` block.
|
||||
//
|
||||
// Potentially none of the `try` block could have been executed prior to executing
|
||||
@@ -1125,12 +1311,18 @@ where
|
||||
Some(CurrentAssignment::AugAssign(aug_assign)) => {
|
||||
self.add_definition(symbol, aug_assign);
|
||||
}
|
||||
Some(CurrentAssignment::For(node)) => {
|
||||
Some(CurrentAssignment::For {
|
||||
node,
|
||||
first,
|
||||
unpack,
|
||||
}) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
ForStmtDefinitionNodeRef {
|
||||
unpack,
|
||||
first,
|
||||
iterable: &node.iter,
|
||||
target: name_node,
|
||||
name: name_node,
|
||||
is_async: node.is_async,
|
||||
},
|
||||
);
|
||||
@@ -1166,7 +1358,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(CurrentAssignment::Assign { first, .. }) = self.current_assignment_mut()
|
||||
if let Some(
|
||||
CurrentAssignment::Assign { first, .. } | CurrentAssignment::For { first, .. },
|
||||
) = self.current_assignment_mut()
|
||||
{
|
||||
*first = false;
|
||||
}
|
||||
@@ -1211,19 +1405,19 @@ where
|
||||
ast::Expr::If(ast::ExprIf {
|
||||
body, test, orelse, ..
|
||||
}) => {
|
||||
// TODO detect statically known truthy or falsy test (via type inference, not naive
|
||||
// AST inspection, so we can't simplify here, need to record test expression for
|
||||
// later checking)
|
||||
self.visit_expr(test);
|
||||
let pre_if = self.flow_snapshot();
|
||||
let constraint = self.record_expression_constraint(test);
|
||||
self.visit_expr(body);
|
||||
let visibility_constraint = self.record_visibility_constraint(constraint);
|
||||
let post_body = self.flow_snapshot();
|
||||
self.flow_restore(pre_if);
|
||||
self.flow_restore(pre_if.clone());
|
||||
|
||||
self.record_negated_constraint(constraint);
|
||||
self.visit_expr(orelse);
|
||||
self.record_negated_visibility_constraint(visibility_constraint);
|
||||
self.flow_merge(post_body);
|
||||
self.simplify_visibility_constraints(pre_if);
|
||||
}
|
||||
ast::Expr::ListComp(
|
||||
list_comprehension @ ast::ExprListComp {
|
||||
@@ -1280,27 +1474,55 @@ where
|
||||
range: _,
|
||||
op,
|
||||
}) => {
|
||||
// TODO detect statically known truthy or falsy values (via type inference, not naive
|
||||
// AST inspection, so we can't simplify here, need to record test expression for
|
||||
// later checking)
|
||||
let pre_op = self.flow_snapshot();
|
||||
|
||||
let mut snapshots = vec![];
|
||||
let mut visibility_constraints = vec![];
|
||||
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
self.visit_expr(value);
|
||||
// In the last value we don't need to take a snapshot nor add a constraint
|
||||
|
||||
for vid in &visibility_constraints {
|
||||
self.record_visibility_constraint_id(*vid);
|
||||
}
|
||||
|
||||
// For the last value, we don't need to model control flow. There is short-circuiting
|
||||
// anymore.
|
||||
if index < values.len() - 1 {
|
||||
// Snapshot is taken after visiting the expression but before adding the constraint.
|
||||
snapshots.push(self.flow_snapshot());
|
||||
let constraint = self.build_constraint(value);
|
||||
match op {
|
||||
BoolOp::And => self.record_constraint(constraint),
|
||||
BoolOp::Or => self.record_negated_constraint(constraint),
|
||||
let (constraint, constraint_id) = match op {
|
||||
ast::BoolOp::And => (constraint, self.add_constraint(constraint)),
|
||||
ast::BoolOp::Or => self.add_negated_constraint(constraint),
|
||||
};
|
||||
let visibility_constraint = self
|
||||
.add_visibility_constraint(VisibilityConstraint::VisibleIf(constraint));
|
||||
|
||||
let after_expr = self.flow_snapshot();
|
||||
|
||||
// We first model the short-circuiting behavior. We take the short-circuit
|
||||
// path here if all of the previous short-circuit paths were not taken, so
|
||||
// we record all previously existing visibility constraints, and negate the
|
||||
// one for the current expression.
|
||||
for vid in &visibility_constraints {
|
||||
self.record_visibility_constraint_id(*vid);
|
||||
}
|
||||
self.record_negated_visibility_constraint(visibility_constraint);
|
||||
snapshots.push(self.flow_snapshot());
|
||||
|
||||
// Then we model the non-short-circuiting behavior. Here, we need to delay
|
||||
// the application of the visibility constraint until after the expression
|
||||
// has been evaluated, so we only push it onto the stack here.
|
||||
self.flow_restore(after_expr);
|
||||
self.record_constraint_id(constraint_id);
|
||||
visibility_constraints.push(visibility_constraint);
|
||||
}
|
||||
}
|
||||
|
||||
for snapshot in snapshots {
|
||||
self.flow_merge(snapshot);
|
||||
}
|
||||
|
||||
self.simplify_visibility_constraints(pre_op);
|
||||
}
|
||||
_ => {
|
||||
walk_expr(self, expr);
|
||||
@@ -1380,7 +1602,11 @@ enum CurrentAssignment<'a> {
|
||||
},
|
||||
AnnAssign(&'a ast::StmtAnnAssign),
|
||||
AugAssign(&'a ast::StmtAugAssign),
|
||||
For(&'a ast::StmtFor),
|
||||
For {
|
||||
node: &'a ast::StmtFor,
|
||||
first: bool,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
Named(&'a ast::ExprNamed),
|
||||
Comprehension {
|
||||
node: &'a ast::Comprehension,
|
||||
@@ -1404,12 +1630,6 @@ impl<'a> From<&'a ast::StmtAugAssign> for CurrentAssignment<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtFor> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::StmtFor) -> Self {
|
||||
Self::For(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprNamed> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::ExprNamed) -> Self {
|
||||
Self::Named(value)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::Singleton;
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::db::Db;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
|
||||
@@ -18,6 +17,14 @@ pub(crate) enum ConstraintNode<'db> {
|
||||
Pattern(PatternConstraint<'db>),
|
||||
}
|
||||
|
||||
/// Pattern kinds for which we support type narrowing and/or static visibility analysis.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum PatternConstraintKind<'db> {
|
||||
Singleton(Singleton, Option<Expression<'db>>),
|
||||
Value(Expression<'db>, Option<Expression<'db>>),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
pub(crate) struct PatternConstraint<'db> {
|
||||
#[id]
|
||||
@@ -28,11 +35,11 @@ pub(crate) struct PatternConstraint<'db> {
|
||||
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) subject: AstNodeRef<ast::Expr>,
|
||||
pub(crate) subject: Expression<'db>,
|
||||
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) pattern: AstNodeRef<ast::Pattern>,
|
||||
pub(crate) kind: PatternConstraintKind<'db>,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<PatternConstraint<'static>>,
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::module_resolver::file_to_module;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::Db;
|
||||
use crate::{Db, KnownModule};
|
||||
|
||||
/// A definition of a symbol.
|
||||
///
|
||||
@@ -63,17 +63,16 @@ impl<'db> Definition<'db> {
|
||||
}
|
||||
|
||||
pub(crate) fn is_builtin_definition(self, db: &'db dyn Db) -> bool {
|
||||
file_to_module(db, self.file(db)).is_some_and(|module| {
|
||||
module.search_path().is_standard_library() && matches!(&**module.name(), "builtins")
|
||||
})
|
||||
file_to_module(db, self.file(db))
|
||||
.is_some_and(|module| module.is_known(KnownModule::Builtins))
|
||||
}
|
||||
|
||||
/// Return true if this symbol was defined in the `typing` or `typing_extensions` modules
|
||||
pub(crate) fn is_typing_definition(self, db: &'db dyn Db) -> bool {
|
||||
file_to_module(db, self.file(db)).is_some_and(|module| {
|
||||
module.search_path().is_standard_library()
|
||||
&& matches!(&**module.name(), "typing" | "typing_extensions")
|
||||
})
|
||||
matches!(
|
||||
file_to_module(db, self.file(db)).and_then(|module| module.known()),
|
||||
Some(KnownModule::Typing | KnownModule::TypingExtensions)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,8 +225,10 @@ pub(crate) struct WithItemDefinitionNodeRef<'a> {
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) iterable: &'a ast::Expr,
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
@@ -299,12 +300,16 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment))
|
||||
}
|
||||
DefinitionNodeRef::For(ForStmtDefinitionNodeRef {
|
||||
unpack,
|
||||
iterable,
|
||||
target,
|
||||
name,
|
||||
first,
|
||||
is_async,
|
||||
}) => DefinitionKind::For(ForStmtDefinitionKind {
|
||||
target: TargetKind::from(unpack),
|
||||
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
first,
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
|
||||
@@ -383,10 +388,12 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
Self::AnnotatedAssignment(node) => node.into(),
|
||||
Self::AugmentedAssignment(node) => node.into(),
|
||||
Self::For(ForStmtDefinitionNodeRef {
|
||||
unpack: _,
|
||||
iterable: _,
|
||||
target,
|
||||
name,
|
||||
first: _,
|
||||
is_async: _,
|
||||
}) => target.into(),
|
||||
}) => name.into(),
|
||||
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
|
||||
Self::VariadicPositionalParameter(node) => node.into(),
|
||||
Self::VariadicKeywordParameter(node) => node.into(),
|
||||
@@ -453,7 +460,7 @@ pub enum DefinitionKind<'db> {
|
||||
Assignment(AssignmentDefinitionKind<'db>),
|
||||
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
|
||||
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
|
||||
For(ForStmtDefinitionKind),
|
||||
For(ForStmtDefinitionKind<'db>),
|
||||
Comprehension(ComprehensionDefinitionKind),
|
||||
VariadicPositionalParameter(AstNodeRef<ast::Parameter>),
|
||||
VariadicKeywordParameter(AstNodeRef<ast::Parameter>),
|
||||
@@ -466,8 +473,14 @@ pub enum DefinitionKind<'db> {
|
||||
TypeVarTuple(AstNodeRef<ast::TypeParamTypeVarTuple>),
|
||||
}
|
||||
|
||||
impl Ranged for DefinitionKind<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
impl DefinitionKind<'_> {
|
||||
/// Returns the [`TextRange`] of the definition target.
|
||||
///
|
||||
/// A definition target would mainly be the node representing the symbol being defined i.e.,
|
||||
/// [`ast::ExprName`] or [`ast::Identifier`] but could also be other nodes.
|
||||
///
|
||||
/// This is mainly used for logging and debugging purposes.
|
||||
pub(crate) fn target_range(&self) -> TextRange {
|
||||
match self {
|
||||
DefinitionKind::Import(alias) => alias.range(),
|
||||
DefinitionKind::ImportFrom(import) => import.alias().range(),
|
||||
@@ -478,7 +491,7 @@ impl Ranged for DefinitionKind<'_> {
|
||||
DefinitionKind::Assignment(assignment) => assignment.name().range(),
|
||||
DefinitionKind::AnnotatedAssignment(assign) => assign.target.range(),
|
||||
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.target.range(),
|
||||
DefinitionKind::For(for_stmt) => for_stmt.target().range(),
|
||||
DefinitionKind::For(for_stmt) => for_stmt.name().range(),
|
||||
DefinitionKind::Comprehension(comp) => comp.target().range(),
|
||||
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.name.range(),
|
||||
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.name.range(),
|
||||
@@ -491,9 +504,7 @@ impl Ranged for DefinitionKind<'_> {
|
||||
DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.name.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DefinitionKind<'_> {
|
||||
pub(crate) fn category(&self) -> DefinitionCategory {
|
||||
match self {
|
||||
// functions, classes, and imports always bind, and we consider them declarations
|
||||
@@ -666,22 +677,32 @@ impl WithItemDefinitionKind {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ForStmtDefinitionKind {
|
||||
pub struct ForStmtDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
iterable: AstNodeRef<ast::Expr>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
impl ForStmtDefinitionKind {
|
||||
impl<'db> ForStmtDefinitionKind<'db> {
|
||||
pub(crate) fn iterable(&self) -> &ast::Expr {
|
||||
self.iterable.node()
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||
self.target.node()
|
||||
pub(crate) fn target(&self) -> TargetKind<'db> {
|
||||
self.target
|
||||
}
|
||||
|
||||
pub(crate) fn is_async(&self) -> bool {
|
||||
pub(crate) fn name(&self) -> &ast::ExprName {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
|
||||
pub(crate) const fn is_async(&self) -> bool {
|
||||
self.is_async
|
||||
}
|
||||
}
|
||||
@@ -757,12 +778,6 @@ impl From<&ast::StmtAugAssign> for DefinitionNodeKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::StmtFor> for DefinitionNodeKey {
|
||||
fn from(value: &ast::StmtFor) -> Self {
|
||||
Self(NodeKey::from_node(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::Parameter> for DefinitionNodeKey {
|
||||
fn from(node: &ast::Parameter) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
|
||||
@@ -463,10 +463,7 @@ impl NodeWithScopeKind {
|
||||
}
|
||||
|
||||
pub fn expect_function(&self) -> &ast::StmtFunctionDef {
|
||||
match self {
|
||||
Self::Function(function) => function.node(),
|
||||
_ => panic!("expected function"),
|
||||
}
|
||||
self.as_function().expect("expected function")
|
||||
}
|
||||
|
||||
pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias {
|
||||
@@ -475,6 +472,13 @@ impl NodeWithScopeKind {
|
||||
_ => panic!("expected type alias"),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_function(&self) -> Option<&ast::StmtFunctionDef> {
|
||||
match self {
|
||||
Self::Function(function) => Some(function.node()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
|
||||
@@ -169,17 +169,11 @@
|
||||
//! indexvecs in the [`UseDefMap`].
|
||||
//!
|
||||
//! There is another special kind of possible "definition" for a symbol: there might be a path from
|
||||
//! the scope entry to a given use in which the symbol is never bound.
|
||||
//!
|
||||
//! The simplest way to model "unbound" would be as a "binding" itself: the initial "binding" for
|
||||
//! each symbol in a scope. But actually modeling it this way would unnecessarily increase the
|
||||
//! number of [`Definition`]s that Salsa must track. Since "unbound" is special in that all symbols
|
||||
//! share it, and it doesn't have any additional per-symbol state, and constraints are irrelevant
|
||||
//! to it, we can represent it more efficiently: we use the `may_be_unbound` boolean on the
|
||||
//! [`SymbolBindings`] struct. If this flag is `true` for a use of a symbol, it means the symbol
|
||||
//! has a path to the use in which it is never bound. If this flag is `false`, it means we've
|
||||
//! eliminated the possibility of unbound: every control flow path to the use includes a binding
|
||||
//! for this symbol.
|
||||
//! the scope entry to a given use in which the symbol is never bound. We model this with a special
|
||||
//! "unbound" definition (a `None` entry at the start of the `all_definitions` vector). If that
|
||||
//! sentinel definition is present in the live bindings at a given use, it means that there is a
|
||||
//! possible path through control flow in which that symbol is unbound. Similarly, if that sentinel
|
||||
//! is present in the live declarations, it means that the symbol is (possibly) undeclared.
|
||||
//!
|
||||
//! To build a [`UseDefMap`], the [`UseDefMapBuilder`] is notified of each new use, definition, and
|
||||
//! constraint as they are encountered by the
|
||||
@@ -190,11 +184,13 @@
|
||||
//! end of the scope, it records the state for each symbol as the public definitions of that
|
||||
//! symbol.
|
||||
//!
|
||||
//! Let's walk through the above example. Initially we record for `x` that it has no bindings, and
|
||||
//! may be unbound. When we see `x = 1`, we record that as the sole live binding of `x`, and flip
|
||||
//! `may_be_unbound` to `false`. Then we see `x = 2`, and we replace `x = 1` as the sole live
|
||||
//! binding of `x`. When we get to `y = x`, we record that the live bindings for that use of `x`
|
||||
//! are just the `x = 2` definition.
|
||||
//! Let's walk through the above example. Initially we do not have any record of `x`. When we add
|
||||
//! the new symbol (before we process the first binding), we create a new undefined `SymbolState`
|
||||
//! which has a single live binding (the "unbound" definition) and a single live declaration (the
|
||||
//! "undeclared" definition). When we see `x = 1`, we record that as the sole live binding of `x`.
|
||||
//! The "unbound" binding is no longer visible. Then we see `x = 2`, and we replace `x = 1` as the
|
||||
//! sole live binding of `x`. When we get to `y = x`, we record that the live bindings for that use
|
||||
//! of `x` are just the `x = 2` definition.
|
||||
//!
|
||||
//! Then we hit the `if` branch. We visit the `test` node (`flag` in this case), since that will
|
||||
//! happen regardless. Then we take a pre-branch snapshot of the current state for all symbols,
|
||||
@@ -207,8 +203,8 @@
|
||||
//! be the pre-if conditions; if we are entering the `else` clause, we know that the `if` test
|
||||
//! failed and we didn't execute the `if` body. So we first reset the builder to the pre-if state,
|
||||
//! using the snapshot we took previously (meaning we now have `x = 2` as the sole binding for `x`
|
||||
//! again), then visit the `else` clause, where `x = 4` replaces `x = 2` as the sole live binding
|
||||
//! of `x`.
|
||||
//! again), and record a *negative* `flag` constraint for all live bindings (`x = 2`). We then
|
||||
//! visit the `else` clause, where `x = 4` replaces `x = 2` as the sole live binding of `x`.
|
||||
//!
|
||||
//! Now we reach the end of the if/else, and want to visit the following code. The state here needs
|
||||
//! to reflect that we might have gone through the `if` branch, or we might have gone through the
|
||||
@@ -217,18 +213,58 @@
|
||||
//! snapshot (which has `x = 3` as the only live binding). The result of this merge is that we now
|
||||
//! have two live bindings of `x`: `x = 3` and `x = 4`.
|
||||
//!
|
||||
//! Another piece of information that the `UseDefMap` needs to provide are visibility constraints.
|
||||
//! These are similar to the narrowing constraints, but apply to bindings and declarations within a
|
||||
//! control flow path. Consider the following example:
|
||||
//! ```py
|
||||
//! x = 1
|
||||
//! if test:
|
||||
//! x = 2
|
||||
//! y = "y"
|
||||
//! ```
|
||||
//! In principle, there are two possible control flow paths here. However, if we can statically
|
||||
//! infer `test` to be always truthy or always falsy (that is, `__bool__` of `test` is of type
|
||||
//! `Literal[True]` or `Literal[False]`), we can rule out one of the possible paths. To support
|
||||
//! this feature, we record a visibility constraint of `test` to all live bindings and declarations
|
||||
//! *after* visiting the body of the `if` statement. And we record a negative visibility constraint
|
||||
//! `~test` to all live bindings/declarations in the (implicit) `else` branch. For the example
|
||||
//! above, we would record the following visibility constraints (adding the implicit "unbound"
|
||||
//! definitions for clarity):
|
||||
//! ```py
|
||||
//! x = <unbound> # not live, shadowed by `x = 1`
|
||||
//! y = <unbound> # visibility constraint: ~test
|
||||
//!
|
||||
//! x = 1 # visibility constraint: ~test
|
||||
//! if test:
|
||||
//! x = 2 # visibility constraint: test
|
||||
//! y = "y" # visibility constraint: test
|
||||
//! ```
|
||||
//! When we encounter a use of `x` after this `if` statement, we would record two live bindings: `x
|
||||
//! = 1` with a constraint of `~test`, and `x = 2` with a constraint of `test`. In type inference,
|
||||
//! when we iterate over all live bindings, we can evaluate these constraints to determine if a
|
||||
//! particular binding is actually visible. For example, if `test` is always truthy, we only see
|
||||
//! the `x = 2` binding. If `test` is always falsy, we only see the `x = 1` binding. And if the
|
||||
//! `__bool__` method of `test` returns type `bool`, we can see both bindings.
|
||||
//!
|
||||
//! Note that we also record visibility constraints for the start of the scope. This is important
|
||||
//! to determine if a symbol is definitely bound, possibly unbound, or definitely unbound. In the
|
||||
//! example above, The `y = <unbound>` binding is constrained by `~test`, so `y` would only be
|
||||
//! definitely-bound if `test` is always truthy.
|
||||
//!
|
||||
//! The [`UseDefMapBuilder`] itself just exposes methods for taking a snapshot, resetting to a
|
||||
//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in
|
||||
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it
|
||||
//! visits a `StmtIf` node.
|
||||
use self::symbol_state::{
|
||||
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
|
||||
ScopedConstraintId, ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
|
||||
ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
|
||||
};
|
||||
pub(crate) use self::symbol_state::{ScopedConstraintId, ScopedVisibilityConstraintId};
|
||||
use crate::semantic_index::ast_ids::ScopedUseId;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
use crate::symbol::Boundness;
|
||||
use crate::semantic_index::use_def::symbol_state::DeclarationIdWithConstraint;
|
||||
use crate::visibility_constraints::{VisibilityConstraint, VisibilityConstraints};
|
||||
use ruff_index::IndexVec;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -237,14 +273,20 @@ use super::constraint::Constraint;
|
||||
mod bitset;
|
||||
mod symbol_state;
|
||||
|
||||
type AllConstraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>;
|
||||
|
||||
/// Applicable definitions and constraints for every use of a name.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct UseDefMap<'db> {
|
||||
/// Array of [`Definition`] in this scope.
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
/// Array of [`Definition`] in this scope. Only the first entry should be `None`;
|
||||
/// this represents the implicit "unbound"/"undeclared" definition of every symbol.
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
|
||||
/// Array of [`Constraint`] in this scope.
|
||||
all_constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
all_constraints: AllConstraints<'db>,
|
||||
|
||||
/// Array of [`VisibilityConstraint`]s in this scope.
|
||||
visibility_constraints: VisibilityConstraints<'db>,
|
||||
|
||||
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
|
||||
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
|
||||
@@ -275,14 +317,6 @@ impl<'db> UseDefMap<'db> {
|
||||
self.bindings_iterator(&self.bindings_by_use[use_id])
|
||||
}
|
||||
|
||||
pub(crate) fn use_boundness(&self, use_id: ScopedUseId) -> Boundness {
|
||||
if self.bindings_by_use[use_id].may_be_unbound() {
|
||||
Boundness::PossiblyUnbound
|
||||
} else {
|
||||
Boundness::Bound
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn public_bindings(
|
||||
&self,
|
||||
symbol: ScopedSymbolId,
|
||||
@@ -290,14 +324,6 @@ impl<'db> UseDefMap<'db> {
|
||||
self.bindings_iterator(self.public_symbols[symbol].bindings())
|
||||
}
|
||||
|
||||
pub(crate) fn public_boundness(&self, symbol: ScopedSymbolId) -> Boundness {
|
||||
if self.public_symbols[symbol].may_be_unbound() {
|
||||
Boundness::PossiblyUnbound
|
||||
} else {
|
||||
Boundness::Bound
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bindings_at_declaration(
|
||||
&self,
|
||||
declaration: Definition<'db>,
|
||||
@@ -310,10 +336,10 @@ impl<'db> UseDefMap<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn declarations_at_binding(
|
||||
&self,
|
||||
pub(crate) fn declarations_at_binding<'map>(
|
||||
&'map self,
|
||||
binding: Definition<'db>,
|
||||
) -> DeclarationsIterator<'_, 'db> {
|
||||
) -> DeclarationsIterator<'map, 'db> {
|
||||
if let SymbolDefinitions::Declarations(declarations) =
|
||||
&self.definitions_by_definition[&binding]
|
||||
{
|
||||
@@ -323,37 +349,34 @@ impl<'db> UseDefMap<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn public_declarations(
|
||||
&self,
|
||||
pub(crate) fn public_declarations<'map>(
|
||||
&'map self,
|
||||
symbol: ScopedSymbolId,
|
||||
) -> DeclarationsIterator<'_, 'db> {
|
||||
) -> DeclarationsIterator<'map, 'db> {
|
||||
let declarations = self.public_symbols[symbol].declarations();
|
||||
self.declarations_iterator(declarations)
|
||||
}
|
||||
|
||||
pub(crate) fn has_public_declarations(&self, symbol: ScopedSymbolId) -> bool {
|
||||
!self.public_symbols[symbol].declarations().is_empty()
|
||||
}
|
||||
|
||||
fn bindings_iterator<'a>(
|
||||
&'a self,
|
||||
bindings: &'a SymbolBindings,
|
||||
) -> BindingWithConstraintsIterator<'a, 'db> {
|
||||
fn bindings_iterator<'map>(
|
||||
&'map self,
|
||||
bindings: &'map SymbolBindings,
|
||||
) -> BindingWithConstraintsIterator<'map, 'db> {
|
||||
BindingWithConstraintsIterator {
|
||||
all_definitions: &self.all_definitions,
|
||||
all_constraints: &self.all_constraints,
|
||||
visibility_constraints: &self.visibility_constraints,
|
||||
inner: bindings.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
fn declarations_iterator<'a>(
|
||||
&'a self,
|
||||
declarations: &'a SymbolDeclarations,
|
||||
) -> DeclarationsIterator<'a, 'db> {
|
||||
fn declarations_iterator<'map>(
|
||||
&'map self,
|
||||
declarations: &'map SymbolDeclarations,
|
||||
) -> DeclarationsIterator<'map, 'db> {
|
||||
DeclarationsIterator {
|
||||
all_definitions: &self.all_definitions,
|
||||
visibility_constraints: &self.visibility_constraints,
|
||||
inner: declarations.iter(),
|
||||
may_be_undeclared: declarations.may_be_undeclared(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,8 +390,9 @@ enum SymbolDefinitions {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
all_constraints: &'map AllConstraints<'db>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
|
||||
inner: BindingIdWithConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
@@ -376,14 +400,17 @@ impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
|
||||
type Item = BindingWithConstraints<'map, 'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let all_constraints = self.all_constraints;
|
||||
|
||||
self.inner
|
||||
.next()
|
||||
.map(|def_id_with_constraints| BindingWithConstraints {
|
||||
binding: self.all_definitions[def_id_with_constraints.definition],
|
||||
.map(|binding_id_with_constraints| BindingWithConstraints {
|
||||
binding: self.all_definitions[binding_id_with_constraints.definition],
|
||||
constraints: ConstraintsIterator {
|
||||
all_constraints: self.all_constraints,
|
||||
constraint_ids: def_id_with_constraints.constraint_ids,
|
||||
all_constraints,
|
||||
constraint_ids: binding_id_with_constraints.constraint_ids,
|
||||
},
|
||||
visibility_constraint: binding_id_with_constraints.visibility_constraint,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -391,12 +418,13 @@ impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
|
||||
impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
|
||||
|
||||
pub(crate) struct BindingWithConstraints<'map, 'db> {
|
||||
pub(crate) binding: Definition<'db>,
|
||||
pub(crate) binding: Option<Definition<'db>>,
|
||||
pub(crate) constraints: ConstraintsIterator<'map, 'db>,
|
||||
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
pub(crate) struct ConstraintsIterator<'map, 'db> {
|
||||
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
all_constraints: &'map AllConstraints<'db>,
|
||||
constraint_ids: ConstraintIdIterator<'map>,
|
||||
}
|
||||
|
||||
@@ -413,22 +441,31 @@ impl<'db> Iterator for ConstraintsIterator<'_, 'db> {
|
||||
impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
|
||||
|
||||
pub(crate) struct DeclarationsIterator<'map, 'db> {
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
|
||||
inner: DeclarationIdIterator<'map>,
|
||||
may_be_undeclared: bool,
|
||||
}
|
||||
|
||||
impl DeclarationsIterator<'_, '_> {
|
||||
pub(crate) fn may_be_undeclared(&self) -> bool {
|
||||
self.may_be_undeclared
|
||||
}
|
||||
pub(crate) struct DeclarationWithConstraint<'db> {
|
||||
pub(crate) declaration: Option<Definition<'db>>,
|
||||
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
impl<'db> Iterator for DeclarationsIterator<'_, 'db> {
|
||||
type Item = Definition<'db>;
|
||||
type Item = DeclarationWithConstraint<'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(|def_id| self.all_definitions[def_id])
|
||||
self.inner.next().map(
|
||||
|DeclarationIdWithConstraint {
|
||||
definition,
|
||||
visibility_constraint,
|
||||
}| {
|
||||
DeclarationWithConstraint {
|
||||
declaration: self.all_definitions[definition],
|
||||
visibility_constraint,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,15 +475,25 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct FlowSnapshot {
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
scope_start_visibility: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
pub(super) struct UseDefMapBuilder<'db> {
|
||||
/// Append-only array of [`Definition`].
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
|
||||
/// Append-only array of [`Constraint`].
|
||||
all_constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
all_constraints: AllConstraints<'db>,
|
||||
|
||||
/// Append-only array of [`VisibilityConstraint`].
|
||||
visibility_constraints: VisibilityConstraints<'db>,
|
||||
|
||||
/// A constraint which describes the visibility of the unbound/undeclared state, i.e.
|
||||
/// whether or not the start of the scope is visible. This is important for cases like
|
||||
/// `if True: x = 1; use(x)` where we need to hide the implicit "x = unbound" binding
|
||||
/// in the "else" branch.
|
||||
scope_start_visibility: ScopedVisibilityConstraintId,
|
||||
|
||||
/// Live bindings at each so-far-recorded use.
|
||||
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
|
||||
@@ -458,14 +505,30 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
}
|
||||
|
||||
impl Default for UseDefMapBuilder<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
all_definitions: IndexVec::from_iter([None]),
|
||||
all_constraints: IndexVec::new(),
|
||||
visibility_constraints: VisibilityConstraints::default(),
|
||||
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
bindings_by_use: IndexVec::new(),
|
||||
definitions_by_definition: FxHashMap::default(),
|
||||
symbol_states: IndexVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
||||
let new_symbol = self.symbol_states.push(SymbolState::undefined());
|
||||
let new_symbol = self
|
||||
.symbol_states
|
||||
.push(SymbolState::undefined(self.scope_start_visibility));
|
||||
debug_assert_eq!(symbol, new_symbol);
|
||||
}
|
||||
|
||||
pub(super) fn record_binding(&mut self, symbol: ScopedSymbolId, binding: Definition<'db>) {
|
||||
let def_id = self.all_definitions.push(binding);
|
||||
let def_id = self.all_definitions.push(Some(binding));
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
self.definitions_by_definition.insert(
|
||||
binding,
|
||||
@@ -474,10 +537,82 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
symbol_state.record_binding(def_id);
|
||||
}
|
||||
|
||||
pub(super) fn record_constraint(&mut self, constraint: Constraint<'db>) {
|
||||
let constraint_id = self.all_constraints.push(constraint);
|
||||
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
self.all_constraints.push(constraint)
|
||||
}
|
||||
|
||||
pub(super) fn record_constraint_id(&mut self, constraint: ScopedConstraintId) {
|
||||
for state in &mut self.symbol_states {
|
||||
state.record_constraint(constraint_id);
|
||||
state.record_constraint(constraint);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn record_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
let new_constraint_id = self.add_constraint(constraint);
|
||||
self.record_constraint_id(new_constraint_id);
|
||||
new_constraint_id
|
||||
}
|
||||
|
||||
pub(super) fn add_visibility_constraint(
|
||||
&mut self,
|
||||
constraint: VisibilityConstraint<'db>,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
self.visibility_constraints.add(constraint)
|
||||
}
|
||||
|
||||
pub(super) fn record_visibility_constraint_id(
|
||||
&mut self,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
for state in &mut self.symbol_states {
|
||||
state.record_visibility_constraint(&mut self.visibility_constraints, constraint);
|
||||
}
|
||||
|
||||
self.scope_start_visibility = self
|
||||
.visibility_constraints
|
||||
.add_and_constraint(self.scope_start_visibility, constraint);
|
||||
}
|
||||
|
||||
pub(super) fn record_visibility_constraint(
|
||||
&mut self,
|
||||
constraint: VisibilityConstraint<'db>,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
let new_constraint_id = self.add_visibility_constraint(constraint);
|
||||
self.record_visibility_constraint_id(new_constraint_id);
|
||||
new_constraint_id
|
||||
}
|
||||
|
||||
/// This method resets the visibility constraints for all symbols to a previous state
|
||||
/// *if* there have been no new declarations or bindings since then. Consider the
|
||||
/// following example:
|
||||
/// ```py
|
||||
/// x = 0
|
||||
/// y = 0
|
||||
/// if test_a:
|
||||
/// y = 1
|
||||
/// elif test_b:
|
||||
/// y = 2
|
||||
/// elif test_c:
|
||||
/// y = 3
|
||||
///
|
||||
/// # RESET
|
||||
/// ```
|
||||
/// We build a complex visibility constraint for the `y = 0` binding. We build the same
|
||||
/// constraint for the `x = 0` binding as well, but at the `RESET` point, we can get rid
|
||||
/// of it, as the `if`-`elif`-`elif` chain doesn't include any new bindings of `x`.
|
||||
pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
|
||||
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
|
||||
|
||||
self.scope_start_visibility = snapshot.scope_start_visibility;
|
||||
|
||||
// Note that this loop terminates when we reach a symbol not present in the snapshot.
|
||||
// This means we keep visibility constraints for all new symbols, which is intended,
|
||||
// since these symbols have been introduced in the corresponding branch, which might
|
||||
// be subject to visibility constraints. We only simplify/reset visibility constraints
|
||||
// for symbols that have the same bindings and declarations present compared to the
|
||||
// snapshot.
|
||||
for (current, snapshot) in self.symbol_states.iter_mut().zip(snapshot.symbol_states) {
|
||||
current.simplify_visibility_constraints(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +621,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
symbol: ScopedSymbolId,
|
||||
declaration: Definition<'db>,
|
||||
) {
|
||||
let def_id = self.all_definitions.push(declaration);
|
||||
let def_id = self.all_definitions.push(Some(declaration));
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
self.definitions_by_definition.insert(
|
||||
declaration,
|
||||
@@ -501,7 +636,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
// We don't need to store anything in self.definitions_by_definition.
|
||||
let def_id = self.all_definitions.push(definition);
|
||||
let def_id = self.all_definitions.push(Some(definition));
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
symbol_state.record_declaration(def_id);
|
||||
symbol_state.record_binding(def_id);
|
||||
@@ -520,6 +655,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn snapshot(&self) -> FlowSnapshot {
|
||||
FlowSnapshot {
|
||||
symbol_states: self.symbol_states.clone(),
|
||||
scope_start_visibility: self.scope_start_visibility,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,12 +669,15 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
|
||||
// Restore the current visible-definitions state to the given snapshot.
|
||||
self.symbol_states = snapshot.symbol_states;
|
||||
self.scope_start_visibility = snapshot.scope_start_visibility;
|
||||
|
||||
// If the snapshot we are restoring is missing some symbols we've recorded since, we need
|
||||
// to fill them in so the symbol IDs continue to line up. Since they don't exist in the
|
||||
// snapshot, the correct state to fill them in with is "undefined".
|
||||
self.symbol_states
|
||||
.resize(num_symbols, SymbolState::undefined());
|
||||
self.symbol_states.resize(
|
||||
num_symbols,
|
||||
SymbolState::undefined(self.scope_start_visibility),
|
||||
);
|
||||
}
|
||||
|
||||
/// Merge the given snapshot into the current state, reflecting that we might have taken either
|
||||
@@ -553,13 +692,19 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
let mut snapshot_definitions_iter = snapshot.symbol_states.into_iter();
|
||||
for current in &mut self.symbol_states {
|
||||
if let Some(snapshot) = snapshot_definitions_iter.next() {
|
||||
current.merge(snapshot);
|
||||
current.merge(snapshot, &mut self.visibility_constraints);
|
||||
} else {
|
||||
current.merge(
|
||||
SymbolState::undefined(snapshot.scope_start_visibility),
|
||||
&mut self.visibility_constraints,
|
||||
);
|
||||
// Symbol not present in snapshot, so it's unbound/undeclared from that path.
|
||||
current.set_may_be_unbound();
|
||||
current.set_may_be_undeclared();
|
||||
}
|
||||
}
|
||||
|
||||
self.scope_start_visibility = self
|
||||
.visibility_constraints
|
||||
.add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);
|
||||
}
|
||||
|
||||
pub(super) fn finish(mut self) -> UseDefMap<'db> {
|
||||
@@ -572,6 +717,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
UseDefMap {
|
||||
all_definitions: self.all_definitions,
|
||||
all_constraints: self.all_constraints,
|
||||
visibility_constraints: self.visibility_constraints,
|
||||
bindings_by_use: self.bindings_by_use,
|
||||
public_symbols: self.symbol_states,
|
||||
definitions_by_definition: self.definitions_by_definition,
|
||||
|
||||
@@ -32,10 +32,6 @@ impl<const B: usize> BitSet<B> {
|
||||
bitset
|
||||
}
|
||||
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.blocks().iter().all(|&b| b == 0)
|
||||
}
|
||||
|
||||
/// Convert from Inline to Heap, if needed, and resize the Heap vector, if needed.
|
||||
fn resize(&mut self, value: u32) {
|
||||
let num_blocks_needed = (value / 64) + 1;
|
||||
@@ -97,19 +93,6 @@ impl<const B: usize> BitSet<B> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Union in-place with another [`BitSet`].
|
||||
pub(super) fn union(&mut self, other: &BitSet<B>) {
|
||||
let mut max_len = self.blocks().len();
|
||||
let other_len = other.blocks().len();
|
||||
if other_len > max_len {
|
||||
max_len = other_len;
|
||||
self.resize_blocks(max_len);
|
||||
}
|
||||
for (my_block, other_block) in self.blocks_mut().iter_mut().zip(other.blocks()) {
|
||||
*my_block |= other_block;
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an iterator over the values (in ascending order) in this [`BitSet`].
|
||||
pub(super) fn iter(&self) -> BitSetIterator<'_, B> {
|
||||
let blocks = self.blocks();
|
||||
@@ -239,59 +222,6 @@ mod tests {
|
||||
assert_bitset(&b1, &[89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union() {
|
||||
let mut b1 = BitSet::<1>::with(2);
|
||||
let b2 = BitSet::<1>::with(4);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[2, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_mixed_1() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(5);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 5, 89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_mixed_2() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(23);
|
||||
b2.insert(89);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 23, 89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_heap() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(90);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 89, 90]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_heap_2() {
|
||||
let mut b1 = BitSet::<1>::with(89);
|
||||
let mut b2 = BitSet::<1>::with(89);
|
||||
b1.insert(91);
|
||||
b2.insert(90);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[89, 90, 91]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_blocks() {
|
||||
let mut b = BitSet::<2>::with(120);
|
||||
@@ -299,11 +229,4 @@ mod tests {
|
||||
assert!(matches!(b, BitSet::Inline(_)));
|
||||
assert_bitset(&b, &[45, 120]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let b = BitSet::<1>::default();
|
||||
|
||||
assert!(b.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
//!
|
||||
//! Tracking live declarations is simpler, since constraints are not involved, but otherwise very
|
||||
//! similar to tracking live bindings.
|
||||
use crate::semantic_index::use_def::VisibilityConstraints;
|
||||
|
||||
use super::bitset::{BitSet, BitSetIterator};
|
||||
use ruff_index::newtype_index;
|
||||
use smallvec::SmallVec;
|
||||
@@ -51,9 +53,18 @@ use smallvec::SmallVec;
|
||||
#[newtype_index]
|
||||
pub(super) struct ScopedDefinitionId;
|
||||
|
||||
impl ScopedDefinitionId {
|
||||
/// A special ID that is used to describe an implicit start-of-scope state. When
|
||||
/// we see that this definition is live, we know that the symbol is (possibly)
|
||||
/// unbound or undeclared at a given usage site.
|
||||
/// When creating a use-def-map builder, we always add an empty `None` definition
|
||||
/// at index 0, so this ID is always present.
|
||||
pub(super) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0);
|
||||
}
|
||||
|
||||
/// A newtype-index for a constraint expression in a particular scope.
|
||||
#[newtype_index]
|
||||
pub(super) struct ScopedConstraintId;
|
||||
pub(crate) struct ScopedConstraintId;
|
||||
|
||||
/// Can reference this * 64 total definitions inline; more will fall back to the heap.
|
||||
const INLINE_BINDING_BLOCKS: usize = 3;
|
||||
@@ -75,58 +86,97 @@ const INLINE_CONSTRAINT_BLOCKS: usize = 2;
|
||||
/// Can keep inline this many live bindings per symbol at a given time; more will go to heap.
|
||||
const INLINE_BINDINGS_PER_SYMBOL: usize = 4;
|
||||
|
||||
/// One [`BitSet`] of applicable [`ScopedConstraintId`] per live binding.
|
||||
type InlineConstraintArray = [BitSet<INLINE_CONSTRAINT_BLOCKS>; INLINE_BINDINGS_PER_SYMBOL];
|
||||
type Constraints = SmallVec<InlineConstraintArray>;
|
||||
type ConstraintsIterator<'a> = std::slice::Iter<'a, BitSet<INLINE_CONSTRAINT_BLOCKS>>;
|
||||
/// Which constraints apply to a given binding?
|
||||
type Constraints = BitSet<INLINE_CONSTRAINT_BLOCKS>;
|
||||
|
||||
type InlineConstraintArray = [Constraints; INLINE_BINDINGS_PER_SYMBOL];
|
||||
|
||||
/// One [`BitSet`] of applicable [`ScopedConstraintId`]s per live binding.
|
||||
type ConstraintsPerBinding = SmallVec<InlineConstraintArray>;
|
||||
|
||||
/// Iterate over all constraints for a single binding.
|
||||
type ConstraintsIterator<'a> = std::slice::Iter<'a, Constraints>;
|
||||
type ConstraintsIntoIterator = smallvec::IntoIter<InlineConstraintArray>;
|
||||
|
||||
/// Live declarations for a single symbol at some point in control flow.
|
||||
/// A newtype-index for a visibility constraint in a particular scope.
|
||||
#[newtype_index]
|
||||
pub(crate) struct ScopedVisibilityConstraintId;
|
||||
|
||||
impl ScopedVisibilityConstraintId {
|
||||
/// A special ID that is used for an "always true" / "always visible" constraint.
|
||||
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
|
||||
/// present at index 0.
|
||||
pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId =
|
||||
ScopedVisibilityConstraintId::from_u32(0);
|
||||
}
|
||||
|
||||
const INLINE_VISIBILITY_CONSTRAINTS: usize = 4;
|
||||
type InlineVisibilityConstraintsArray =
|
||||
[ScopedVisibilityConstraintId; INLINE_VISIBILITY_CONSTRAINTS];
|
||||
|
||||
/// One [`ScopedVisibilityConstraintId`] per live declaration.
|
||||
type VisibilityConstraintPerDeclaration = SmallVec<InlineVisibilityConstraintsArray>;
|
||||
|
||||
/// One [`ScopedVisibilityConstraintId`] per live binding.
|
||||
type VisibilityConstraintPerBinding = SmallVec<InlineVisibilityConstraintsArray>;
|
||||
|
||||
/// Iterator over the visibility constraints for all live bindings/declarations.
|
||||
type VisibilityConstraintsIterator<'a> = std::slice::Iter<'a, ScopedVisibilityConstraintId>;
|
||||
|
||||
type VisibilityConstraintsIntoIterator = smallvec::IntoIter<InlineVisibilityConstraintsArray>;
|
||||
|
||||
/// Live declarations for a single symbol at some point in control flow, with their
|
||||
/// corresponding visibility constraints.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct SymbolDeclarations {
|
||||
/// [`BitSet`]: which declarations (as [`ScopedDefinitionId`]) can reach the current location?
|
||||
live_declarations: Declarations,
|
||||
pub(crate) live_declarations: Declarations,
|
||||
|
||||
/// Could the symbol be un-declared at this point?
|
||||
may_be_undeclared: bool,
|
||||
/// For each live declaration, which visibility constraint applies to it?
|
||||
pub(crate) visibility_constraints: VisibilityConstraintPerDeclaration,
|
||||
}
|
||||
|
||||
impl SymbolDeclarations {
|
||||
fn undeclared() -> Self {
|
||||
fn undeclared(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
|
||||
Self {
|
||||
live_declarations: Declarations::default(),
|
||||
may_be_undeclared: true,
|
||||
live_declarations: Declarations::with(0),
|
||||
visibility_constraints: VisibilityConstraintPerDeclaration::from_iter([
|
||||
scope_start_visibility,
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a newly-encountered declaration for this symbol.
|
||||
fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
|
||||
self.live_declarations = Declarations::with(declaration_id.into());
|
||||
self.may_be_undeclared = false;
|
||||
|
||||
self.visibility_constraints = VisibilityConstraintPerDeclaration::with_capacity(1);
|
||||
self.visibility_constraints
|
||||
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
/// Add undeclared as a possibility for this symbol.
|
||||
fn set_may_be_undeclared(&mut self) {
|
||||
self.may_be_undeclared = true;
|
||||
/// Add given visibility constraint to all live declarations.
|
||||
pub(super) fn record_visibility_constraint(
|
||||
&mut self,
|
||||
visibility_constraints: &mut VisibilityConstraints,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
for existing in &mut self.visibility_constraints {
|
||||
*existing = visibility_constraints.add_and_constraint(*existing, constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an iterator over live declarations for this symbol.
|
||||
pub(super) fn iter(&self) -> DeclarationIdIterator {
|
||||
DeclarationIdIterator {
|
||||
inner: self.live_declarations.iter(),
|
||||
declarations: self.live_declarations.iter(),
|
||||
visibility_constraints: self.visibility_constraints.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.live_declarations.is_empty()
|
||||
}
|
||||
|
||||
pub(super) fn may_be_undeclared(&self) -> bool {
|
||||
self.may_be_undeclared
|
||||
}
|
||||
}
|
||||
|
||||
/// Live bindings and narrowing constraints for a single symbol at some point in control flow.
|
||||
/// Live bindings for a single symbol at some point in control flow. Each live binding comes
|
||||
/// with a set of narrowing constraints and a visibility constraint.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct SymbolBindings {
|
||||
/// [`BitSet`]: which bindings (as [`ScopedDefinitionId`]) can reach the current location?
|
||||
@@ -136,34 +186,34 @@ pub(super) struct SymbolBindings {
|
||||
///
|
||||
/// This is a [`smallvec::SmallVec`] which should always have one [`BitSet`] of constraints per
|
||||
/// binding in `live_bindings`.
|
||||
constraints: Constraints,
|
||||
constraints: ConstraintsPerBinding,
|
||||
|
||||
/// Could the symbol be unbound at this point?
|
||||
may_be_unbound: bool,
|
||||
/// For each live binding, which visibility constraint applies to it?
|
||||
visibility_constraints: VisibilityConstraintPerBinding,
|
||||
}
|
||||
|
||||
impl SymbolBindings {
|
||||
fn unbound() -> Self {
|
||||
fn unbound(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
|
||||
Self {
|
||||
live_bindings: Bindings::default(),
|
||||
constraints: Constraints::default(),
|
||||
may_be_unbound: true,
|
||||
live_bindings: Bindings::with(ScopedDefinitionId::UNBOUND.as_u32()),
|
||||
constraints: ConstraintsPerBinding::from_iter([Constraints::default()]),
|
||||
visibility_constraints: VisibilityConstraintPerBinding::from_iter([
|
||||
scope_start_visibility,
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add Unbound as a possibility for this symbol.
|
||||
fn set_may_be_unbound(&mut self) {
|
||||
self.may_be_unbound = true;
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
|
||||
// The new binding replaces all previous live bindings in this path, and has no
|
||||
// constraints.
|
||||
self.live_bindings = Bindings::with(binding_id.into());
|
||||
self.constraints = Constraints::with_capacity(1);
|
||||
self.constraints.push(BitSet::default());
|
||||
self.may_be_unbound = false;
|
||||
self.constraints = ConstraintsPerBinding::with_capacity(1);
|
||||
self.constraints.push(Constraints::default());
|
||||
|
||||
self.visibility_constraints = VisibilityConstraintPerBinding::with_capacity(1);
|
||||
self.visibility_constraints
|
||||
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
@@ -173,17 +223,25 @@ impl SymbolBindings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over currently live bindings for this symbol.
|
||||
/// Add given visibility constraint to all live bindings.
|
||||
pub(super) fn record_visibility_constraint(
|
||||
&mut self,
|
||||
visibility_constraints: &mut VisibilityConstraints,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
for existing in &mut self.visibility_constraints {
|
||||
*existing = visibility_constraints.add_and_constraint(*existing, constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over currently live bindings for this symbol
|
||||
pub(super) fn iter(&self) -> BindingIdWithConstraintsIterator {
|
||||
BindingIdWithConstraintsIterator {
|
||||
definitions: self.live_bindings.iter(),
|
||||
constraints: self.constraints.iter(),
|
||||
visibility_constraints: self.visibility_constraints.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn may_be_unbound(&self) -> bool {
|
||||
self.may_be_unbound
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -194,20 +252,16 @@ pub(super) struct SymbolState {
|
||||
|
||||
impl SymbolState {
|
||||
/// Return a new [`SymbolState`] representing an unbound, undeclared symbol.
|
||||
pub(super) fn undefined() -> Self {
|
||||
pub(super) fn undefined(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
|
||||
Self {
|
||||
declarations: SymbolDeclarations::undeclared(),
|
||||
bindings: SymbolBindings::unbound(),
|
||||
declarations: SymbolDeclarations::undeclared(scope_start_visibility),
|
||||
bindings: SymbolBindings::unbound(scope_start_visibility),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add Unbound as a possibility for this symbol.
|
||||
pub(super) fn set_may_be_unbound(&mut self) {
|
||||
self.bindings.set_may_be_unbound();
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
|
||||
debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND);
|
||||
self.bindings.record_binding(binding_id);
|
||||
}
|
||||
|
||||
@@ -216,9 +270,26 @@ impl SymbolState {
|
||||
self.bindings.record_constraint(constraint_id);
|
||||
}
|
||||
|
||||
/// Add undeclared as a possibility for this symbol.
|
||||
pub(super) fn set_may_be_undeclared(&mut self) {
|
||||
self.declarations.set_may_be_undeclared();
|
||||
/// Add given visibility constraint to all live bindings.
|
||||
pub(super) fn record_visibility_constraint(
|
||||
&mut self,
|
||||
visibility_constraints: &mut VisibilityConstraints,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
self.bindings
|
||||
.record_visibility_constraint(visibility_constraints, constraint);
|
||||
self.declarations
|
||||
.record_visibility_constraint(visibility_constraints, constraint);
|
||||
}
|
||||
|
||||
pub(super) fn simplify_visibility_constraints(&mut self, snapshot_state: SymbolState) {
|
||||
if self.bindings.live_bindings == snapshot_state.bindings.live_bindings {
|
||||
self.bindings.visibility_constraints = snapshot_state.bindings.visibility_constraints;
|
||||
}
|
||||
if self.declarations.live_declarations == snapshot_state.declarations.live_declarations {
|
||||
self.declarations.visibility_constraints =
|
||||
snapshot_state.declarations.visibility_constraints;
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a newly-encountered declaration of this symbol.
|
||||
@@ -227,29 +298,31 @@ impl SymbolState {
|
||||
}
|
||||
|
||||
/// Merge another [`SymbolState`] into this one.
|
||||
pub(super) fn merge(&mut self, b: SymbolState) {
|
||||
pub(super) fn merge(
|
||||
&mut self,
|
||||
b: SymbolState,
|
||||
visibility_constraints: &mut VisibilityConstraints,
|
||||
) {
|
||||
let mut a = Self {
|
||||
bindings: SymbolBindings {
|
||||
live_bindings: Bindings::default(),
|
||||
constraints: Constraints::default(),
|
||||
may_be_unbound: self.bindings.may_be_unbound || b.bindings.may_be_unbound,
|
||||
constraints: ConstraintsPerBinding::default(),
|
||||
visibility_constraints: VisibilityConstraintPerBinding::default(),
|
||||
},
|
||||
declarations: SymbolDeclarations {
|
||||
live_declarations: self.declarations.live_declarations.clone(),
|
||||
may_be_undeclared: self.declarations.may_be_undeclared
|
||||
|| b.declarations.may_be_undeclared,
|
||||
visibility_constraints: VisibilityConstraintPerDeclaration::default(),
|
||||
},
|
||||
};
|
||||
|
||||
std::mem::swap(&mut a, self);
|
||||
self.declarations
|
||||
.live_declarations
|
||||
.union(&b.declarations.live_declarations);
|
||||
|
||||
let mut a_defs_iter = a.bindings.live_bindings.iter();
|
||||
let mut b_defs_iter = b.bindings.live_bindings.iter();
|
||||
let mut a_constraints_iter = a.bindings.constraints.into_iter();
|
||||
let mut b_constraints_iter = b.bindings.constraints.into_iter();
|
||||
let mut a_vis_constraints_iter = a.bindings.visibility_constraints.into_iter();
|
||||
let mut b_vis_constraints_iter = b.bindings.visibility_constraints.into_iter();
|
||||
|
||||
let mut opt_a_def: Option<u32> = a_defs_iter.next();
|
||||
let mut opt_b_def: Option<u32> = b_defs_iter.next();
|
||||
@@ -261,17 +334,30 @@ impl SymbolState {
|
||||
// path is irrelevant.
|
||||
|
||||
// Helper to push `def`, with constraints in `constraints_iter`, onto `self`.
|
||||
let push = |def, constraints_iter: &mut ConstraintsIntoIterator, merged: &mut Self| {
|
||||
let push = |def,
|
||||
constraints_iter: &mut ConstraintsIntoIterator,
|
||||
visibility_constraints_iter: &mut VisibilityConstraintsIntoIterator,
|
||||
merged: &mut Self| {
|
||||
merged.bindings.live_bindings.insert(def);
|
||||
// SAFETY: we only ever create SymbolState with either no definitions and no constraint
|
||||
// bitsets (`::unbound`) or one definition and one constraint bitset (`::with`), and
|
||||
// `::merge` always pushes one definition and one constraint bitset together (just
|
||||
// below), so the number of definitions and the number of constraint bitsets can never
|
||||
// SAFETY: we only ever create SymbolState using [`SymbolState::undefined`], which adds
|
||||
// one "unbound" definition with corresponding narrowing and visibility constraints, or
|
||||
// using [`SymbolState::record_binding`] or [`SymbolState::record_declaration`], which
|
||||
// similarly add one definition with corresponding constraints. [`SymbolState::merge`]
|
||||
// always pushes one definition and one constraint bitset and one visibility constraint
|
||||
// together (just below), so the number of definitions and the number of constraints can
|
||||
// never get out of sync.
|
||||
// get out of sync.
|
||||
let constraints = constraints_iter
|
||||
.next()
|
||||
.expect("definitions and constraints length mismatch");
|
||||
let visibility_constraints = visibility_constraints_iter
|
||||
.next()
|
||||
.expect("definitions and visibility_constraints length mismatch");
|
||||
merged.bindings.constraints.push(constraints);
|
||||
merged
|
||||
.bindings
|
||||
.visibility_constraints
|
||||
.push(visibility_constraints);
|
||||
};
|
||||
|
||||
loop {
|
||||
@@ -279,50 +365,139 @@ impl SymbolState {
|
||||
(Some(a_def), Some(b_def)) => match a_def.cmp(&b_def) {
|
||||
std::cmp::Ordering::Less => {
|
||||
// Next definition ID is only in `a`, push it to `self` and advance `a`.
|
||||
push(a_def, &mut a_constraints_iter, self);
|
||||
push(
|
||||
a_def,
|
||||
&mut a_constraints_iter,
|
||||
&mut a_vis_constraints_iter,
|
||||
self,
|
||||
);
|
||||
opt_a_def = a_defs_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
// Next definition ID is only in `b`, push it to `self` and advance `b`.
|
||||
push(b_def, &mut b_constraints_iter, self);
|
||||
push(
|
||||
b_def,
|
||||
&mut b_constraints_iter,
|
||||
&mut b_vis_constraints_iter,
|
||||
self,
|
||||
);
|
||||
opt_b_def = b_defs_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
// Next definition is in both; push to `self` and intersect constraints.
|
||||
push(a_def, &mut b_constraints_iter, self);
|
||||
// SAFETY: we only ever create SymbolState with either no definitions and
|
||||
// no constraint bitsets (`::unbound`) or one definition and one constraint
|
||||
// bitset (`::with`), and `::merge` always pushes one definition and one
|
||||
// constraint bitset together (just below), so the number of definitions
|
||||
// and the number of constraint bitsets can never get out of sync.
|
||||
push(
|
||||
a_def,
|
||||
&mut b_constraints_iter,
|
||||
&mut b_vis_constraints_iter,
|
||||
self,
|
||||
);
|
||||
|
||||
// SAFETY: see comment in `push` above.
|
||||
let a_constraints = a_constraints_iter
|
||||
.next()
|
||||
.expect("definitions and constraints length mismatch");
|
||||
let current_constraints = self.bindings.constraints.last_mut().unwrap();
|
||||
|
||||
// If the same definition is visible through both paths, any constraint
|
||||
// that applies on only one path is irrelevant to the resulting type from
|
||||
// unioning the two paths, so we intersect the constraints.
|
||||
self.bindings
|
||||
.constraints
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.intersect(&a_constraints);
|
||||
current_constraints.intersect(&a_constraints);
|
||||
|
||||
// For visibility constraints, we merge them using a ternary OR operation:
|
||||
let a_vis_constraint = a_vis_constraints_iter
|
||||
.next()
|
||||
.expect("visibility_constraints length mismatch");
|
||||
let current_vis_constraint =
|
||||
self.bindings.visibility_constraints.last_mut().unwrap();
|
||||
*current_vis_constraint = visibility_constraints
|
||||
.add_or_constraint(*current_vis_constraint, a_vis_constraint);
|
||||
|
||||
opt_a_def = a_defs_iter.next();
|
||||
opt_b_def = b_defs_iter.next();
|
||||
}
|
||||
},
|
||||
(Some(a_def), None) => {
|
||||
// We've exhausted `b`, just push the def from `a` and move on to the next.
|
||||
push(a_def, &mut a_constraints_iter, self);
|
||||
push(
|
||||
a_def,
|
||||
&mut a_constraints_iter,
|
||||
&mut a_vis_constraints_iter,
|
||||
self,
|
||||
);
|
||||
opt_a_def = a_defs_iter.next();
|
||||
}
|
||||
(None, Some(b_def)) => {
|
||||
// We've exhausted `a`, just push the def from `b` and move on to the next.
|
||||
push(b_def, &mut b_constraints_iter, self);
|
||||
push(
|
||||
b_def,
|
||||
&mut b_constraints_iter,
|
||||
&mut b_vis_constraints_iter,
|
||||
self,
|
||||
);
|
||||
opt_b_def = b_defs_iter.next();
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Same as above, but for declarations.
|
||||
let mut a_decls_iter = a.declarations.live_declarations.iter();
|
||||
let mut b_decls_iter = b.declarations.live_declarations.iter();
|
||||
let mut a_vis_constraints_iter = a.declarations.visibility_constraints.into_iter();
|
||||
let mut b_vis_constraints_iter = b.declarations.visibility_constraints.into_iter();
|
||||
|
||||
let mut opt_a_decl: Option<u32> = a_decls_iter.next();
|
||||
let mut opt_b_decl: Option<u32> = b_decls_iter.next();
|
||||
|
||||
let push = |decl,
|
||||
vis_constraints_iter: &mut VisibilityConstraintsIntoIterator,
|
||||
merged: &mut Self| {
|
||||
merged.declarations.live_declarations.insert(decl);
|
||||
let vis_constraints = vis_constraints_iter
|
||||
.next()
|
||||
.expect("declarations and visibility_constraints length mismatch");
|
||||
merged
|
||||
.declarations
|
||||
.visibility_constraints
|
||||
.push(vis_constraints);
|
||||
};
|
||||
|
||||
loop {
|
||||
match (opt_a_decl, opt_b_decl) {
|
||||
(Some(a_decl), Some(b_decl)) => match a_decl.cmp(&b_decl) {
|
||||
std::cmp::Ordering::Less => {
|
||||
push(a_decl, &mut a_vis_constraints_iter, self);
|
||||
opt_a_decl = a_decls_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
push(b_decl, &mut b_vis_constraints_iter, self);
|
||||
opt_b_decl = b_decls_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
push(a_decl, &mut b_vis_constraints_iter, self);
|
||||
|
||||
let a_vis_constraint = a_vis_constraints_iter
|
||||
.next()
|
||||
.expect("declarations and visibility_constraints length mismatch");
|
||||
let current = self.declarations.visibility_constraints.last_mut().unwrap();
|
||||
*current =
|
||||
visibility_constraints.add_or_constraint(*current, a_vis_constraint);
|
||||
|
||||
opt_a_decl = a_decls_iter.next();
|
||||
opt_b_decl = b_decls_iter.next();
|
||||
}
|
||||
},
|
||||
(Some(a_decl), None) => {
|
||||
push(a_decl, &mut a_vis_constraints_iter, self);
|
||||
opt_a_decl = a_decls_iter.next();
|
||||
}
|
||||
(None, Some(b_decl)) => {
|
||||
push(b_decl, &mut b_vis_constraints_iter, self);
|
||||
opt_b_decl = b_decls_iter.next();
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn bindings(&self) -> &SymbolBindings {
|
||||
@@ -332,47 +507,44 @@ impl SymbolState {
|
||||
pub(super) fn declarations(&self) -> &SymbolDeclarations {
|
||||
&self.declarations
|
||||
}
|
||||
|
||||
/// Could the symbol be unbound?
|
||||
pub(super) fn may_be_unbound(&self) -> bool {
|
||||
self.bindings.may_be_unbound()
|
||||
}
|
||||
}
|
||||
|
||||
/// The default state of a symbol, if we've seen no definitions of it, is undefined (that is,
|
||||
/// both unbound and undeclared).
|
||||
impl Default for SymbolState {
|
||||
fn default() -> Self {
|
||||
SymbolState::undefined()
|
||||
}
|
||||
}
|
||||
|
||||
/// A single binding (as [`ScopedDefinitionId`]) with an iterator of its applicable
|
||||
/// [`ScopedConstraintId`].
|
||||
/// narrowing constraints ([`ScopedConstraintId`]) and a corresponding visibility
|
||||
/// visibility constraint ([`ScopedVisibilityConstraintId`]).
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BindingIdWithConstraints<'a> {
|
||||
pub(super) struct BindingIdWithConstraints<'map> {
|
||||
pub(super) definition: ScopedDefinitionId,
|
||||
pub(super) constraint_ids: ConstraintIdIterator<'a>,
|
||||
pub(super) constraint_ids: ConstraintIdIterator<'map>,
|
||||
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BindingIdWithConstraintsIterator<'a> {
|
||||
definitions: BindingsIterator<'a>,
|
||||
constraints: ConstraintsIterator<'a>,
|
||||
pub(super) struct BindingIdWithConstraintsIterator<'map> {
|
||||
definitions: BindingsIterator<'map>,
|
||||
constraints: ConstraintsIterator<'map>,
|
||||
visibility_constraints: VisibilityConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BindingIdWithConstraintsIterator<'a> {
|
||||
type Item = BindingIdWithConstraints<'a>;
|
||||
impl<'map> Iterator for BindingIdWithConstraintsIterator<'map> {
|
||||
type Item = BindingIdWithConstraints<'map>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.definitions.next(), self.constraints.next()) {
|
||||
(None, None) => None,
|
||||
(Some(def), Some(constraints)) => Some(BindingIdWithConstraints {
|
||||
definition: ScopedDefinitionId::from_u32(def),
|
||||
constraint_ids: ConstraintIdIterator {
|
||||
wrapped: constraints.iter(),
|
||||
},
|
||||
}),
|
||||
match (
|
||||
self.definitions.next(),
|
||||
self.constraints.next(),
|
||||
self.visibility_constraints.next(),
|
||||
) {
|
||||
(None, None, None) => None,
|
||||
(Some(def), Some(constraints), Some(visibility_constraint_id)) => {
|
||||
Some(BindingIdWithConstraints {
|
||||
definition: ScopedDefinitionId::from_u32(def),
|
||||
constraint_ids: ConstraintIdIterator {
|
||||
wrapped: constraints.iter(),
|
||||
},
|
||||
visibility_constraint: *visibility_constraint_id,
|
||||
})
|
||||
}
|
||||
// SAFETY: see above.
|
||||
_ => unreachable!("definitions and constraints length mismatch"),
|
||||
}
|
||||
@@ -396,16 +568,34 @@ impl Iterator for ConstraintIdIterator<'_> {
|
||||
|
||||
impl std::iter::FusedIterator for ConstraintIdIterator<'_> {}
|
||||
|
||||
/// A single declaration (as [`ScopedDefinitionId`]) with a corresponding visibility
|
||||
/// visibility constraint ([`ScopedVisibilityConstraintId`]).
|
||||
#[derive(Debug)]
|
||||
pub(super) struct DeclarationIdIterator<'a> {
|
||||
inner: DeclarationsIterator<'a>,
|
||||
pub(super) struct DeclarationIdWithConstraint {
|
||||
pub(super) definition: ScopedDefinitionId,
|
||||
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
pub(super) struct DeclarationIdIterator<'map> {
|
||||
pub(crate) declarations: DeclarationsIterator<'map>,
|
||||
pub(crate) visibility_constraints: VisibilityConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
impl Iterator for DeclarationIdIterator<'_> {
|
||||
type Item = ScopedDefinitionId;
|
||||
type Item = DeclarationIdWithConstraint;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(ScopedDefinitionId::from_u32)
|
||||
match (self.declarations.next(), self.visibility_constraints.next()) {
|
||||
(None, None) => None,
|
||||
(Some(declaration), Some(&visibility_constraint)) => {
|
||||
Some(DeclarationIdWithConstraint {
|
||||
definition: ScopedDefinitionId::from_u32(declaration),
|
||||
visibility_constraint,
|
||||
})
|
||||
}
|
||||
// SAFETY: see above.
|
||||
_ => unreachable!("declarations and visibility_constraints length mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,176 +603,172 @@ impl std::iter::FusedIterator for DeclarationIdIterator<'_> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ScopedConstraintId, ScopedDefinitionId, SymbolState};
|
||||
use super::*;
|
||||
|
||||
fn assert_bindings(symbol: &SymbolState, may_be_unbound: bool, expected: &[&str]) {
|
||||
assert_eq!(symbol.may_be_unbound(), may_be_unbound);
|
||||
#[track_caller]
|
||||
fn assert_bindings(symbol: &SymbolState, expected: &[&str]) {
|
||||
let actual = symbol
|
||||
.bindings()
|
||||
.iter()
|
||||
.map(|def_id_with_constraints| {
|
||||
format!(
|
||||
"{}<{}>",
|
||||
def_id_with_constraints.definition.as_u32(),
|
||||
def_id_with_constraints
|
||||
.constraint_ids
|
||||
.map(ScopedConstraintId::as_u32)
|
||||
.map(|idx| idx.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
let def_id = def_id_with_constraints.definition;
|
||||
let def = if def_id == ScopedDefinitionId::UNBOUND {
|
||||
"unbound".into()
|
||||
} else {
|
||||
def_id.as_u32().to_string()
|
||||
};
|
||||
let constraints = def_id_with_constraints
|
||||
.constraint_ids
|
||||
.map(ScopedConstraintId::as_u32)
|
||||
.map(|idx| idx.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("{def}<{constraints}>")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
pub(crate) fn assert_declarations(
|
||||
symbol: &SymbolState,
|
||||
may_be_undeclared: bool,
|
||||
expected: &[u32],
|
||||
) {
|
||||
assert_eq!(symbol.declarations.may_be_undeclared(), may_be_undeclared);
|
||||
#[track_caller]
|
||||
pub(crate) fn assert_declarations(symbol: &SymbolState, expected: &[&str]) {
|
||||
let actual = symbol
|
||||
.declarations()
|
||||
.iter()
|
||||
.map(ScopedDefinitionId::as_u32)
|
||||
.map(
|
||||
|DeclarationIdWithConstraint {
|
||||
definition,
|
||||
visibility_constraint: _,
|
||||
}| {
|
||||
if definition == ScopedDefinitionId::UNBOUND {
|
||||
"undeclared".into()
|
||||
} else {
|
||||
definition.as_u32().to_string()
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbound() {
|
||||
let sym = SymbolState::undefined();
|
||||
let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
|
||||
assert_bindings(&sym, true, &[]);
|
||||
assert_bindings(&sym, &["unbound<>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(0));
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
assert_bindings(&sym, false, &["0<>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_may_be_unbound() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(0));
|
||||
sym.set_may_be_unbound();
|
||||
|
||||
assert_bindings(&sym, true, &["0<>"]);
|
||||
assert_bindings(&sym, &["1<>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_constraint() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(0));
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
assert_bindings(&sym, false, &["0<0>"]);
|
||||
assert_bindings(&sym, &["1<0>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge() {
|
||||
let mut visibility_constraints = VisibilityConstraints::default();
|
||||
|
||||
// merging the same definition with the same constraint keeps the constraint
|
||||
let mut sym0a = SymbolState::undefined();
|
||||
sym0a.record_binding(ScopedDefinitionId::from_u32(0));
|
||||
sym0a.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1a.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
let mut sym0b = SymbolState::undefined();
|
||||
sym0b.record_binding(ScopedDefinitionId::from_u32(0));
|
||||
sym0b.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1b.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
sym0a.merge(sym0b);
|
||||
let mut sym0 = sym0a;
|
||||
assert_bindings(&sym0, false, &["0<0>"]);
|
||||
sym1a.merge(sym1b, &mut visibility_constraints);
|
||||
let mut sym1 = sym1a;
|
||||
assert_bindings(&sym1, &["1<0>"]);
|
||||
|
||||
// merging the same definition with differing constraints drops all constraints
|
||||
let mut sym1a = SymbolState::undefined();
|
||||
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1a.record_constraint(ScopedConstraintId::from_u32(1));
|
||||
let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
|
||||
sym2a.record_constraint(ScopedConstraintId::from_u32(1));
|
||||
|
||||
let mut sym1b = SymbolState::undefined();
|
||||
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1b.record_binding(ScopedDefinitionId::from_u32(2));
|
||||
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
|
||||
|
||||
sym1a.merge(sym1b);
|
||||
let sym1 = sym1a;
|
||||
assert_bindings(&sym1, false, &["1<>"]);
|
||||
sym2a.merge(sym1b, &mut visibility_constraints);
|
||||
let sym2 = sym2a;
|
||||
assert_bindings(&sym2, &["2<>"]);
|
||||
|
||||
// merging a constrained definition with unbound keeps both
|
||||
let mut sym2a = SymbolState::undefined();
|
||||
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
|
||||
sym2a.record_constraint(ScopedConstraintId::from_u32(3));
|
||||
let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym3a.record_binding(ScopedDefinitionId::from_u32(3));
|
||||
sym3a.record_constraint(ScopedConstraintId::from_u32(3));
|
||||
|
||||
let sym2b = SymbolState::undefined();
|
||||
let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
|
||||
sym2a.merge(sym2b);
|
||||
let sym2 = sym2a;
|
||||
assert_bindings(&sym2, true, &["2<3>"]);
|
||||
sym3a.merge(sym2b, &mut visibility_constraints);
|
||||
let sym3 = sym3a;
|
||||
assert_bindings(&sym3, &["unbound<>", "3<3>"]);
|
||||
|
||||
// merging different definitions keeps them each with their existing constraints
|
||||
sym0.merge(sym2);
|
||||
let sym = sym0;
|
||||
assert_bindings(&sym, true, &["0<0>", "2<3>"]);
|
||||
sym1.merge(sym3, &mut visibility_constraints);
|
||||
let sym = sym1;
|
||||
assert_bindings(&sym, &["unbound<>", "1<0>", "3<3>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_declaration() {
|
||||
let sym = SymbolState::undefined();
|
||||
let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
|
||||
assert_declarations(&sym, true, &[]);
|
||||
assert_declarations(&sym, &["undeclared"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_declaration() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
assert_declarations(&sym, false, &[1]);
|
||||
assert_declarations(&sym, &["1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_declaration_override() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(1));
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(2));
|
||||
|
||||
assert_declarations(&sym, false, &[2]);
|
||||
assert_declarations(&sym, &["2"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_declaration_merge() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
let mut visibility_constraints = VisibilityConstraints::default();
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
let mut sym2 = SymbolState::undefined();
|
||||
let mut sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym2.record_declaration(ScopedDefinitionId::from_u32(2));
|
||||
|
||||
sym.merge(sym2);
|
||||
sym.merge(sym2, &mut visibility_constraints);
|
||||
|
||||
assert_declarations(&sym, false, &[1, 2]);
|
||||
assert_declarations(&sym, &["1", "2"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_declaration_merge_partial_undeclared() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
let mut visibility_constraints = VisibilityConstraints::default();
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
let sym2 = SymbolState::undefined();
|
||||
let sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
|
||||
sym.merge(sym2);
|
||||
sym.merge(sym2, &mut visibility_constraints);
|
||||
|
||||
assert_declarations(&sym, true, &[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_may_be_undeclared() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(0));
|
||||
sym.set_may_be_undeclared();
|
||||
|
||||
assert_declarations(&sym, true, &[0]);
|
||||
assert_declarations(&sym, &["undeclared", "1"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,19 @@
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::module_resolver::{resolve_module, KnownModule};
|
||||
use crate::semantic_index::global_scope;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::symbol::Symbol;
|
||||
use crate::types::global_symbol;
|
||||
use crate::Db;
|
||||
|
||||
/// Enumeration of various core stdlib modules, for which we have dedicated Salsa queries.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CoreStdlibModule {
|
||||
Builtins,
|
||||
Types,
|
||||
Typeshed,
|
||||
TypingExtensions,
|
||||
Typing,
|
||||
Sys,
|
||||
#[allow(dead_code)]
|
||||
Abc, // currently only used in tests
|
||||
}
|
||||
|
||||
impl CoreStdlibModule {
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Builtins => "builtins",
|
||||
Self::Types => "types",
|
||||
Self::Typing => "typing",
|
||||
Self::Typeshed => "_typeshed",
|
||||
Self::TypingExtensions => "typing_extensions",
|
||||
Self::Sys => "sys",
|
||||
Self::Abc => "abc",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn name(self) -> ModuleName {
|
||||
let self_as_str = self.as_str();
|
||||
ModuleName::new_static(self_as_str)
|
||||
.unwrap_or_else(|| panic!("{self_as_str} should be a valid module name!"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in a given core module
|
||||
/// Lookup the type of `symbol` in a given known module
|
||||
///
|
||||
/// Returns `Symbol::Unbound` if the given core module cannot be resolved for some reason
|
||||
pub(crate) fn core_module_symbol<'db>(
|
||||
/// Returns `Symbol::Unbound` if the given known module cannot be resolved for some reason
|
||||
pub(crate) fn known_module_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
core_module: CoreStdlibModule,
|
||||
known_module: KnownModule,
|
||||
symbol: &str,
|
||||
) -> Symbol<'db> {
|
||||
resolve_module(db, &core_module.name())
|
||||
resolve_module(db, &known_module.name())
|
||||
.map(|module| global_symbol(db, module.file(), symbol))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
@@ -57,7 +23,7 @@ pub(crate) fn core_module_symbol<'db>(
|
||||
/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
core_module_symbol(db, CoreStdlibModule::Builtins, symbol)
|
||||
known_module_symbol(db, KnownModule::Builtins, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing` module namespace.
|
||||
@@ -66,7 +32,7 @@ pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db>
|
||||
#[inline]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
core_module_symbol(db, CoreStdlibModule::Typing, symbol)
|
||||
known_module_symbol(db, KnownModule::Typing, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing_extensions` module namespace.
|
||||
@@ -74,13 +40,13 @@ pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
/// Returns `Symbol::Unbound` if the `typing_extensions` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn typing_extensions_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
core_module_symbol(db, CoreStdlibModule::TypingExtensions, symbol)
|
||||
known_module_symbol(db, KnownModule::TypingExtensions, symbol)
|
||||
}
|
||||
|
||||
/// Get the scope of a core stdlib module.
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing the core module in question.
|
||||
fn core_module_scope(db: &dyn Db, core_module: CoreStdlibModule) -> Option<ScopeId<'_>> {
|
||||
fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option<ScopeId<'_>> {
|
||||
resolve_module(db, &core_module.name()).map(|module| global_scope(db, module.file()))
|
||||
}
|
||||
|
||||
@@ -88,5 +54,5 @@ fn core_module_scope(db: &dyn Db, core_module: CoreStdlibModule) -> Option<Scope
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`.
|
||||
pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
|
||||
core_module_scope(db, CoreStdlibModule::Builtins)
|
||||
core_module_scope(db, KnownModule::Builtins)
|
||||
}
|
||||
|
||||
1172
crates/red_knot_python_semantic/src/suppression.rs
Normal file
1172
crates/red_knot_python_semantic/src/suppression.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@ impl Boundness {
|
||||
/// possibly_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound),
|
||||
/// non_existent: Symbol::Unbound,
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum Symbol<'db> {
|
||||
Type(Type<'db>, Boundness),
|
||||
Unbound,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,8 @@ use crate::types::{InstanceType, IntersectionType, KnownClass, Type, UnionType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::Truthiness;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
@@ -243,15 +245,22 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
}
|
||||
} else {
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
// ~AlwaysTruthy & bool = Literal[False]
|
||||
if let Type::Instance(InstanceType { class }) = new_positive {
|
||||
if class.is_known(db, KnownClass::Bool) {
|
||||
if let Some(&Type::BooleanLiteral(value)) = self
|
||||
if let Some(new_type) = self
|
||||
.negative
|
||||
.iter()
|
||||
.find(|element| element.is_boolean_literal())
|
||||
.find(|element| {
|
||||
element.is_boolean_literal()
|
||||
| matches!(element, Type::AlwaysFalsy | Type::AlwaysTruthy)
|
||||
})
|
||||
.map(|element| {
|
||||
Type::BooleanLiteral(element.bool(db) != Truthiness::AlwaysTrue)
|
||||
})
|
||||
{
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::BooleanLiteral(!value));
|
||||
self.positive.insert(new_type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -318,15 +327,15 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
// simplify the representation.
|
||||
self.add_positive(db, ty);
|
||||
}
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
Type::BooleanLiteral(bool)
|
||||
if self
|
||||
.positive
|
||||
.iter()
|
||||
.any(|pos| *pos == KnownClass::Bool.to_instance(db)) =>
|
||||
// bool & ~Literal[True] = Literal[False]
|
||||
// bool & ~AlwaysTruthy = Literal[False]
|
||||
Type::BooleanLiteral(_) | Type::AlwaysFalsy | Type::AlwaysTruthy
|
||||
if self.positive.contains(&KnownClass::Bool.to_instance(db)) =>
|
||||
{
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::BooleanLiteral(!bool));
|
||||
self.positive.insert(Type::BooleanLiteral(
|
||||
new_negative.bool(db) != Truthiness::AlwaysTrue,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
@@ -380,7 +389,7 @@ mod tests {
|
||||
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
|
||||
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder};
|
||||
use crate::types::{global_symbol, todo_type, KnownClass, Truthiness, UnionBuilder};
|
||||
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
@@ -997,42 +1006,43 @@ mod tests {
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test_case(true)]
|
||||
#[test_case(false)]
|
||||
fn build_intersection_simplify_split_bool(bool_value: bool) {
|
||||
#[test_case(Type::BooleanLiteral(true))]
|
||||
#[test_case(Type::BooleanLiteral(false))]
|
||||
#[test_case(Type::AlwaysTruthy)]
|
||||
#[test_case(Type::AlwaysFalsy)]
|
||||
fn build_intersection_simplify_split_bool(t_splitter: Type) {
|
||||
let db = setup_db();
|
||||
|
||||
let t_bool = KnownClass::Bool.to_instance(&db);
|
||||
let t_boolean_literal = Type::BooleanLiteral(bool_value);
|
||||
let bool_value = t_splitter.bool(&db) == Truthiness::AlwaysTrue;
|
||||
|
||||
// We add t_object in various orders (in first or second position) in
|
||||
// the tests below to ensure that the boolean simplification eliminates
|
||||
// everything from the intersection, not just `bool`.
|
||||
let t_object = KnownClass::Object.to_instance(&db);
|
||||
let t_bool = KnownClass::Bool.to_instance(&db);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_object)
|
||||
.add_positive(t_bool)
|
||||
.add_negative(t_boolean_literal)
|
||||
.add_negative(t_splitter)
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_bool)
|
||||
.add_positive(t_object)
|
||||
.add_negative(t_boolean_literal)
|
||||
.add_negative(t_splitter)
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_object)
|
||||
.add_negative(t_boolean_literal)
|
||||
.add_negative(t_splitter)
|
||||
.add_positive(t_bool)
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t_boolean_literal)
|
||||
.add_negative(t_splitter)
|
||||
.add_positive(t_object)
|
||||
.add_positive(t_bool)
|
||||
.build();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE};
|
||||
use super::context::InferContext;
|
||||
use super::diagnostic::CALL_NON_CALLABLE;
|
||||
use super::{Severity, Type, TypeArrayDisplay, UnionBuilder};
|
||||
use crate::Db;
|
||||
use ruff_db::diagnostic::DiagnosticId;
|
||||
@@ -86,24 +87,23 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
|
||||
/// Get the return type of the call, emitting default diagnostics if needed.
|
||||
pub(super) fn unwrap_with_diagnostic<'a>(
|
||||
pub(super) fn unwrap_with_diagnostic(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
|
||||
) -> Type<'db> {
|
||||
match self.return_ty_result(db, node, diagnostics) {
|
||||
match self.return_ty_result(context, node) {
|
||||
Ok(return_ty) => return_ty,
|
||||
Err(NotCallableError::Type {
|
||||
not_callable_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add_lint(
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable",
|
||||
not_callable_ty.display(db)
|
||||
not_callable_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
@@ -113,13 +113,13 @@ impl<'db> CallOutcome<'db> {
|
||||
called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add_lint(
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (due to union element `{}`)",
|
||||
called_ty.display(db),
|
||||
not_callable_ty.display(db),
|
||||
called_ty.display(context.db()),
|
||||
not_callable_ty.display(context.db()),
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
@@ -129,13 +129,13 @@ impl<'db> CallOutcome<'db> {
|
||||
called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add_lint(
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (due to union elements {})",
|
||||
called_ty.display(db),
|
||||
not_callable_tys.display(db),
|
||||
called_ty.display(context.db()),
|
||||
not_callable_tys.display(context.db()),
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
@@ -144,12 +144,12 @@ impl<'db> CallOutcome<'db> {
|
||||
callable_ty: called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add_lint(
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
|
||||
called_ty.display(db)
|
||||
called_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
@@ -158,11 +158,10 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
|
||||
/// Get the return type of the call as a result.
|
||||
pub(super) fn return_ty_result<'a>(
|
||||
pub(super) fn return_ty_result(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
|
||||
) -> Result<Type<'db>, NotCallableError<'db>> {
|
||||
match self {
|
||||
Self::Callable { return_ty } => Ok(*return_ty),
|
||||
@@ -170,11 +169,11 @@ impl<'db> CallOutcome<'db> {
|
||||
return_ty,
|
||||
revealed_ty,
|
||||
} => {
|
||||
diagnostics.add(
|
||||
context.report_diagnostic(
|
||||
node,
|
||||
DiagnosticId::RevealedType,
|
||||
Severity::Info,
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(db)),
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
|
||||
);
|
||||
Ok(*return_ty)
|
||||
}
|
||||
@@ -187,14 +186,16 @@ impl<'db> CallOutcome<'db> {
|
||||
call_outcome,
|
||||
} => Err(NotCallableError::PossiblyUnboundDunderCall {
|
||||
callable_ty: *called_ty,
|
||||
return_ty: call_outcome.return_ty(db).unwrap_or(Type::Unknown),
|
||||
return_ty: call_outcome
|
||||
.return_ty(context.db())
|
||||
.unwrap_or(Type::Unknown),
|
||||
}),
|
||||
Self::Union {
|
||||
outcomes,
|
||||
called_ty,
|
||||
} => {
|
||||
let mut not_callable = vec![];
|
||||
let mut union_builder = UnionBuilder::new(db);
|
||||
let mut union_builder = UnionBuilder::new(context.db());
|
||||
let mut revealed = false;
|
||||
for outcome in outcomes {
|
||||
let return_ty = match outcome {
|
||||
@@ -210,10 +211,10 @@ impl<'db> CallOutcome<'db> {
|
||||
*return_ty
|
||||
} else {
|
||||
revealed = true;
|
||||
outcome.unwrap_with_diagnostic(db, node, diagnostics)
|
||||
outcome.unwrap_with_diagnostic(context, node)
|
||||
}
|
||||
}
|
||||
_ => outcome.unwrap_with_diagnostic(db, node, diagnostics),
|
||||
_ => outcome.unwrap_with_diagnostic(context, node),
|
||||
};
|
||||
union_builder = union_builder.add(return_ty);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,9 @@ impl<'db> ClassBase<'db> {
|
||||
| Type::Tuple(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::SubclassOf(_) => None,
|
||||
| Type::SubclassOf(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy => None,
|
||||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
KnownInstanceType::TypeVar(_)
|
||||
| KnownInstanceType::TypeAliasType(_)
|
||||
@@ -112,15 +114,24 @@ impl<'db> ClassBase<'db> {
|
||||
KnownInstanceType::FrozenSet => {
|
||||
Self::try_from_ty(db, KnownClass::FrozenSet.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Callable
|
||||
| KnownInstanceType::ChainMap
|
||||
| KnownInstanceType::Counter
|
||||
| KnownInstanceType::DefaultDict
|
||||
| KnownInstanceType::Deque
|
||||
| KnownInstanceType::OrderedDict => Self::try_from_ty(
|
||||
db,
|
||||
todo_type!("Support for more typing aliases as base classes"),
|
||||
),
|
||||
KnownInstanceType::ChainMap => {
|
||||
Self::try_from_ty(db, KnownClass::ChainMap.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Counter => {
|
||||
Self::try_from_ty(db, KnownClass::Counter.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::DefaultDict => {
|
||||
Self::try_from_ty(db, KnownClass::DefaultDict.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Deque => {
|
||||
Self::try_from_ty(db, KnownClass::Deque.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::OrderedDict => {
|
||||
Self::try_from_ty(db, KnownClass::OrderedDict.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Callable => {
|
||||
Self::try_from_ty(db, todo_type!("Support for Callable as a base class"))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
197
crates/red_knot_python_semantic/src/types/context.rs
Normal file
197
crates/red_knot_python_semantic/src/types/context.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
use std::fmt;
|
||||
|
||||
use drop_bomb::DebugDropBomb;
|
||||
use ruff_db::{
|
||||
diagnostic::{DiagnosticId, Severity},
|
||||
files::File,
|
||||
};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use super::{binding_ty, KnownFunction, TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::{
|
||||
lint::{LintId, LintMetadata},
|
||||
suppression::suppressions,
|
||||
Db,
|
||||
};
|
||||
|
||||
/// Context for inferring the types of a single file.
|
||||
///
|
||||
/// One context exists for at least for every inferred region but it's
|
||||
/// possible that inferring a sub-region, like an unpack assignment, creates
|
||||
/// a sub-context.
|
||||
///
|
||||
/// Tracks the reported diagnostics of the inferred region.
|
||||
///
|
||||
/// ## Consuming
|
||||
/// It's important that the context is explicitly consumed before dropping by calling
|
||||
/// [`InferContext::finish`] and the returned diagnostics must be stored
|
||||
/// on the current [`TypeInference`](super::infer::TypeInference) result.
|
||||
pub(crate) struct InferContext<'db> {
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
file: File,
|
||||
diagnostics: std::cell::RefCell<TypeCheckDiagnostics>,
|
||||
no_type_check: InNoTypeCheck,
|
||||
bomb: DebugDropBomb,
|
||||
}
|
||||
|
||||
impl<'db> InferContext<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
scope,
|
||||
file: scope.file(db),
|
||||
diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()),
|
||||
no_type_check: InNoTypeCheck::default(),
|
||||
bomb: DebugDropBomb::new("`InferContext` needs to be explicitly consumed by calling `::finish` to prevent accidental loss of diagnostics."),
|
||||
}
|
||||
}
|
||||
|
||||
/// The file for which the types are inferred.
|
||||
pub(crate) fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
pub(crate) fn db(&self) -> &'db dyn Db {
|
||||
self.db
|
||||
}
|
||||
|
||||
pub(crate) fn extend<T>(&mut self, other: &T)
|
||||
where
|
||||
T: WithDiagnostics,
|
||||
{
|
||||
self.diagnostics.get_mut().extend(other.diagnostics());
|
||||
}
|
||||
|
||||
/// Reports a lint located at `node`.
|
||||
pub(super) fn report_lint(
|
||||
&self,
|
||||
lint: &'static LintMetadata,
|
||||
node: AnyNodeRef,
|
||||
message: fmt::Arguments,
|
||||
) {
|
||||
if !self.db.is_file_open(self.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip over diagnostics if the rule is disabled.
|
||||
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if self.is_in_no_type_check() {
|
||||
return;
|
||||
}
|
||||
|
||||
let suppressions = suppressions(self.db, self.file);
|
||||
|
||||
if let Some(suppression) = suppressions.find_suppression(node.range(), LintId::of(lint)) {
|
||||
self.diagnostics.borrow_mut().mark_used(suppression.id());
|
||||
return;
|
||||
}
|
||||
|
||||
self.report_diagnostic(node, DiagnosticId::Lint(lint.name()), severity, message);
|
||||
}
|
||||
|
||||
/// Adds a new diagnostic.
|
||||
///
|
||||
/// The diagnostic does not get added if the rule isn't enabled for this file.
|
||||
pub(super) fn report_diagnostic(
|
||||
&self,
|
||||
node: AnyNodeRef,
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
message: fmt::Arguments,
|
||||
) {
|
||||
if !self.db.is_file_open(self.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Don't emit the diagnostic if:
|
||||
// * The enclosing node contains any syntax errors
|
||||
// * The rule is disabled for this file. We probably want to introduce a new query that
|
||||
// returns a rule selector for a given file that respects the package's settings,
|
||||
// any global pragma comments in the file, and any per-file-ignores.
|
||||
|
||||
self.diagnostics.borrow_mut().push(TypeCheckDiagnostic {
|
||||
file: self.file,
|
||||
id,
|
||||
message: message.to_string(),
|
||||
range: node.range(),
|
||||
severity,
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn set_in_no_type_check(&mut self, no_type_check: InNoTypeCheck) {
|
||||
self.no_type_check = no_type_check;
|
||||
}
|
||||
|
||||
fn is_in_no_type_check(&self) -> bool {
|
||||
match self.no_type_check {
|
||||
InNoTypeCheck::Possibly => {
|
||||
// Accessing the semantic index here is fine because
|
||||
// the index belongs to the same file as for which we emit the diagnostic.
|
||||
let index = semantic_index(self.db, self.file);
|
||||
|
||||
let scope_id = self.scope.file_scope_id(self.db);
|
||||
|
||||
// Inspect all ancestor function scopes by walking bottom up and infer the function's type.
|
||||
let mut function_scope_tys = index
|
||||
.ancestor_scopes(scope_id)
|
||||
.filter_map(|(_, scope)| scope.node().as_function())
|
||||
.filter_map(|function| {
|
||||
binding_ty(self.db, index.definition(function)).into_function_literal()
|
||||
});
|
||||
|
||||
// Iterate over all functions and test if any is decorated with `@no_type_check`.
|
||||
function_scope_tys.any(|function_ty| {
|
||||
function_ty
|
||||
.decorators(self.db)
|
||||
.iter()
|
||||
.filter_map(|decorator| decorator.into_function_literal())
|
||||
.any(|decorator_ty| {
|
||||
decorator_ty.is_known(self.db, KnownFunction::NoTypeCheck)
|
||||
})
|
||||
})
|
||||
}
|
||||
InNoTypeCheck::Yes => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
|
||||
self.bomb.defuse();
|
||||
let mut diagnostics = self.diagnostics.into_inner();
|
||||
diagnostics.shrink_to_fit();
|
||||
diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for InferContext<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("TyContext")
|
||||
.field("file", &self.file)
|
||||
.field("diagnostics", &self.diagnostics)
|
||||
.field("defused", &self.bomb)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub(crate) enum InNoTypeCheck {
|
||||
/// The inference might be in a `no_type_check` block but only if any
|
||||
/// ancestor function is decorated with `@no_type_check`.
|
||||
#[default]
|
||||
Possibly,
|
||||
|
||||
/// The inference is known to be in an `@no_type_check` decorated function.
|
||||
Yes,
|
||||
}
|
||||
|
||||
pub(crate) trait WithDiagnostics {
|
||||
fn diagnostics(&self) -> &TypeCheckDiagnostics;
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
use crate::lint::{Level, LintId, LintMetadata, LintRegistryBuilder, LintStatus};
|
||||
use super::context::InferContext;
|
||||
use crate::declare_lint;
|
||||
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
|
||||
use crate::suppression::FileSuppressionId;
|
||||
use crate::types::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
RAW_STRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use crate::types::{ClassLiteralType, Type};
|
||||
use crate::{declare_lint, Db};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
@@ -24,6 +27,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&CYCLIC_CLASS_DEFINITION);
|
||||
registry.register_lint(&DIVISION_BY_ZERO);
|
||||
registry.register_lint(&DUPLICATE_BASE);
|
||||
registry.register_lint(&INCOMPATIBLE_SLOTS);
|
||||
registry.register_lint(&INCONSISTENT_MRO);
|
||||
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
|
||||
registry.register_lint(&INVALID_ASSIGNMENT);
|
||||
@@ -32,6 +36,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&INVALID_DECLARATION);
|
||||
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
|
||||
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
|
||||
registry.register_lint(&INVALID_RAISE);
|
||||
registry.register_lint(&INVALID_TYPE_FORM);
|
||||
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
|
||||
registry.register_lint(&NON_SUBSCRIPTABLE);
|
||||
@@ -39,6 +44,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
|
||||
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
|
||||
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
|
||||
registry.register_lint(&SUBCLASS_OF_FINAL_CLASS);
|
||||
registry.register_lint(&UNDEFINED_REVEAL);
|
||||
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
|
||||
registry.register_lint(&UNRESOLVED_IMPORT);
|
||||
@@ -143,6 +149,64 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for classes whose bases define incompatible `__slots__`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Inheriting from bases with incompatible `__slots__`s
|
||||
/// will lead to a `TypeError` at runtime.
|
||||
///
|
||||
/// Classes with no or empty `__slots__` are always compatible:
|
||||
///
|
||||
/// ```python
|
||||
/// class A: ...
|
||||
/// class B:
|
||||
/// __slots__ = ()
|
||||
/// class C:
|
||||
/// __slots__ = ("a", "b")
|
||||
///
|
||||
/// # fine
|
||||
/// class D(A, B, C): ...
|
||||
/// ```
|
||||
///
|
||||
/// Multiple inheritance from more than one different class
|
||||
/// defining non-empty `__slots__` is not allowed:
|
||||
///
|
||||
/// ```python
|
||||
/// class A:
|
||||
/// __slots__ = ("a", "b")
|
||||
///
|
||||
/// class B:
|
||||
/// __slots__ = ("a", "b") # Even if the values are the same
|
||||
///
|
||||
/// # TypeError: multiple bases have instance lay-out conflict
|
||||
/// class C(A, B): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Dynamic (not tuple or string literal) `__slots__` are not checked.
|
||||
/// Additionally, classes inheriting from built-in classes with implicit layouts
|
||||
/// like `str` or `int` are also not checked.
|
||||
///
|
||||
/// ```pycon
|
||||
/// >>> hasattr(int, "__slots__")
|
||||
/// False
|
||||
/// >>> hasattr(str, "__slots__")
|
||||
/// False
|
||||
/// >>> class A(int, str): ...
|
||||
/// Traceback (most recent call last):
|
||||
/// File "<python-input-0>", line 1, in <module>
|
||||
/// class A(int, str): ...
|
||||
/// TypeError: multiple bases have instance lay-out conflict
|
||||
/// ```
|
||||
pub(crate) static INCOMPATIBLE_SLOTS = {
|
||||
summary: "detects class definitions whose MRO has conflicting `__slots__`",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO #14889
|
||||
pub(crate) static INCONSISTENT_MRO = {
|
||||
@@ -246,6 +310,49 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// Checks for `raise` statements that raise non-exceptions or use invalid
|
||||
/// causes for their raised exceptions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Only subclasses or instances of `BaseException` can be raised.
|
||||
/// For an exception's cause, the same rules apply, except that `None` is also
|
||||
/// permitted. Violating these rules results in a `TypeError` at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// try:
|
||||
/// something()
|
||||
/// except NameError:
|
||||
/// raise "oops!" from f
|
||||
///
|
||||
/// def g():
|
||||
/// raise NotImplemented from 42
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def f():
|
||||
/// try:
|
||||
/// something()
|
||||
/// except NameError as e:
|
||||
/// raise RuntimeError("oops!") from e
|
||||
///
|
||||
/// def g():
|
||||
/// raise NotImplementedError from None
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#raise)
|
||||
/// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)
|
||||
pub(crate) static INVALID_RAISE = {
|
||||
summary: "detects `raise` statements that raise invalid exceptions or use invalid causes",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for invalid type expressions.
|
||||
@@ -349,6 +456,29 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for classes that subclass final classes.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Decorating a class with `@final` declares to the type checker that it should not be subclassed.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import final
|
||||
///
|
||||
/// @final
|
||||
/// class A: ...
|
||||
/// class B(A): ... # Error raised here
|
||||
/// ```
|
||||
pub(crate) static SUBCLASS_OF_FINAL_CLASS = {
|
||||
summary: "detects subclasses of final classes",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls to `reveal_type` without importing it.
|
||||
@@ -442,11 +572,11 @@ declare_lint! {
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct TypeCheckDiagnostic {
|
||||
pub(super) id: DiagnosticId,
|
||||
pub(super) message: String,
|
||||
pub(super) range: TextRange,
|
||||
pub(super) severity: Severity,
|
||||
pub(super) file: File,
|
||||
pub(crate) id: DiagnosticId,
|
||||
pub(crate) message: String,
|
||||
pub(crate) range: TextRange,
|
||||
pub(crate) severity: Severity,
|
||||
pub(crate) file: File,
|
||||
}
|
||||
|
||||
impl TypeCheckDiagnostic {
|
||||
@@ -500,41 +630,41 @@ impl Ranged for TypeCheckDiagnostic {
|
||||
/// each Salsa-struct comes with an overhead.
|
||||
#[derive(Default, Eq, PartialEq)]
|
||||
pub struct TypeCheckDiagnostics {
|
||||
inner: Vec<std::sync::Arc<TypeCheckDiagnostic>>,
|
||||
diagnostics: Vec<Arc<TypeCheckDiagnostic>>,
|
||||
used_suppressions: FxHashSet<FileSuppressionId>,
|
||||
}
|
||||
|
||||
impl TypeCheckDiagnostics {
|
||||
pub(super) fn push(&mut self, diagnostic: TypeCheckDiagnostic) {
|
||||
self.inner.push(Arc::new(diagnostic));
|
||||
pub(crate) fn push(&mut self, diagnostic: TypeCheckDiagnostic) {
|
||||
self.diagnostics.push(Arc::new(diagnostic));
|
||||
}
|
||||
|
||||
pub(super) fn extend(&mut self, other: &TypeCheckDiagnostics) {
|
||||
self.diagnostics.extend_from_slice(&other.diagnostics);
|
||||
self.used_suppressions.extend(&other.used_suppressions);
|
||||
}
|
||||
|
||||
pub(crate) fn mark_used(&mut self, suppression_id: FileSuppressionId) {
|
||||
self.used_suppressions.insert(suppression_id);
|
||||
}
|
||||
|
||||
pub(crate) fn is_used(&self, suppression_id: FileSuppressionId) -> bool {
|
||||
self.used_suppressions.contains(&suppression_id)
|
||||
}
|
||||
|
||||
pub(crate) fn used_len(&self) -> usize {
|
||||
self.used_suppressions.len()
|
||||
}
|
||||
|
||||
pub(crate) fn shrink_to_fit(&mut self) {
|
||||
self.inner.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<TypeCheckDiagnostic> for TypeCheckDiagnostics {
|
||||
fn extend<T: IntoIterator<Item = TypeCheckDiagnostic>>(&mut self, iter: T) {
|
||||
self.inner.extend(iter.into_iter().map(std::sync::Arc::new));
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<std::sync::Arc<TypeCheckDiagnostic>> for TypeCheckDiagnostics {
|
||||
fn extend<T: IntoIterator<Item = Arc<TypeCheckDiagnostic>>>(&mut self, iter: T) {
|
||||
self.inner.extend(iter);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<&'a std::sync::Arc<TypeCheckDiagnostic>> for TypeCheckDiagnostics {
|
||||
fn extend<T: IntoIterator<Item = &'a Arc<TypeCheckDiagnostic>>>(&mut self, iter: T) {
|
||||
self.inner
|
||||
.extend(iter.into_iter().map(std::sync::Arc::clone));
|
||||
self.used_suppressions.shrink_to_fit();
|
||||
self.diagnostics.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TypeCheckDiagnostics {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
self.diagnostics.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,7 +672,7 @@ impl Deref for TypeCheckDiagnostics {
|
||||
type Target = [std::sync::Arc<TypeCheckDiagnostic>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
&self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,7 +681,7 @@ impl IntoIterator for TypeCheckDiagnostics {
|
||||
type IntoIter = std::vec::IntoIter<std::sync::Arc<TypeCheckDiagnostic>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.inner.into_iter()
|
||||
self.diagnostics.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,227 +690,193 @@ impl<'a> IntoIterator for &'a TypeCheckDiagnostics {
|
||||
type IntoIter = std::slice::Iter<'a, std::sync::Arc<TypeCheckDiagnostic>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.inner.iter()
|
||||
self.diagnostics.iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct TypeCheckDiagnosticsBuilder<'db> {
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
diagnostics: TypeCheckDiagnostics,
|
||||
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
|
||||
pub(super) fn report_not_iterable(context: &InferContext, node: AnyNodeRef, not_iterable_ty: Type) {
|
||||
context.report_lint(
|
||||
&NOT_ITERABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not iterable",
|
||||
not_iterable_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
pub(super) fn new(db: &'db dyn Db, file: File) -> Self {
|
||||
Self {
|
||||
db,
|
||||
file,
|
||||
diagnostics: TypeCheckDiagnostics::default(),
|
||||
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
|
||||
/// because its `__iter__` method is possibly unbound.
|
||||
pub(super) fn report_not_iterable_possibly_unbound(
|
||||
context: &InferContext,
|
||||
node: AnyNodeRef,
|
||||
element_ty: Type,
|
||||
) {
|
||||
context.report_lint(
|
||||
&NOT_ITERABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
|
||||
element_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Emit a diagnostic declaring that an index is out of bounds for a tuple.
|
||||
pub(super) fn report_index_out_of_bounds(
|
||||
context: &InferContext,
|
||||
kind: &'static str,
|
||||
node: AnyNodeRef,
|
||||
tuple_ty: Type,
|
||||
length: usize,
|
||||
index: i64,
|
||||
) {
|
||||
context.report_lint(
|
||||
&INDEX_OUT_OF_BOUNDS,
|
||||
node,
|
||||
format_args!(
|
||||
"Index {index} is out of bounds for {kind} `{}` with length {length}",
|
||||
tuple_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Emit a diagnostic declaring that a type does not support subscripting.
|
||||
pub(super) fn report_non_subscriptable(
|
||||
context: &InferContext,
|
||||
node: AnyNodeRef,
|
||||
non_subscriptable_ty: Type,
|
||||
method: &str,
|
||||
) {
|
||||
context.report_lint(
|
||||
&NON_SUBSCRIPTABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Cannot subscript object of type `{}` with no `{method}` method",
|
||||
non_subscriptable_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn report_unresolved_module<'db>(
|
||||
context: &InferContext,
|
||||
import_node: impl Into<AnyNodeRef<'db>>,
|
||||
level: u32,
|
||||
module: Option<&str>,
|
||||
) {
|
||||
context.report_lint(
|
||||
&UNRESOLVED_IMPORT,
|
||||
import_node.into(),
|
||||
format_args!(
|
||||
"Cannot resolve import `{}{}`",
|
||||
".".repeat(level as usize),
|
||||
module.unwrap_or_default()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeRef) {
|
||||
context.report_lint(
|
||||
&ZERO_STEPSIZE_IN_SLICE,
|
||||
node,
|
||||
format_args!("Slice step size can not be zero"),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn report_invalid_assignment(
|
||||
context: &InferContext,
|
||||
node: AnyNodeRef,
|
||||
declared_ty: Type,
|
||||
assigned_ty: Type,
|
||||
) {
|
||||
match declared_ty {
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||
context.report_lint(&INVALID_ASSIGNMENT, node, format_args!(
|
||||
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
|
||||
class.name(context.db())));
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
|
||||
pub(super) fn add_not_iterable(&mut self, node: AnyNodeRef, not_iterable_ty: Type<'db>) {
|
||||
self.add_lint(
|
||||
&NOT_ITERABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not iterable",
|
||||
not_iterable_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
|
||||
/// because its `__iter__` method is possibly unbound.
|
||||
pub(super) fn add_not_iterable_possibly_unbound(
|
||||
&mut self,
|
||||
node: AnyNodeRef,
|
||||
element_ty: Type<'db>,
|
||||
) {
|
||||
self.add_lint(
|
||||
&NOT_ITERABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
|
||||
element_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Emit a diagnostic declaring that an index is out of bounds for a tuple.
|
||||
pub(super) fn add_index_out_of_bounds(
|
||||
&mut self,
|
||||
kind: &'static str,
|
||||
node: AnyNodeRef,
|
||||
tuple_ty: Type<'db>,
|
||||
length: usize,
|
||||
index: i64,
|
||||
) {
|
||||
self.add_lint(
|
||||
&INDEX_OUT_OF_BOUNDS,
|
||||
node,
|
||||
format_args!(
|
||||
"Index {index} is out of bounds for {kind} `{}` with length {length}",
|
||||
tuple_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Emit a diagnostic declaring that a type does not support subscripting.
|
||||
pub(super) fn add_non_subscriptable(
|
||||
&mut self,
|
||||
node: AnyNodeRef,
|
||||
non_subscriptable_ty: Type<'db>,
|
||||
method: &str,
|
||||
) {
|
||||
self.add_lint(
|
||||
&NON_SUBSCRIPTABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Cannot subscript object of type `{}` with no `{method}` method",
|
||||
non_subscriptable_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn add_unresolved_module(
|
||||
&mut self,
|
||||
import_node: impl Into<AnyNodeRef<'db>>,
|
||||
level: u32,
|
||||
module: Option<&str>,
|
||||
) {
|
||||
self.add_lint(
|
||||
&UNRESOLVED_IMPORT,
|
||||
import_node.into(),
|
||||
format_args!(
|
||||
"Cannot resolve import `{}{}`",
|
||||
".".repeat(level as usize),
|
||||
module.unwrap_or_default()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn add_slice_step_size_zero(&mut self, node: AnyNodeRef) {
|
||||
self.add_lint(
|
||||
&ZERO_STEPSIZE_IN_SLICE,
|
||||
node,
|
||||
format_args!("Slice step size can not be zero"),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn add_invalid_assignment(
|
||||
&mut self,
|
||||
node: AnyNodeRef,
|
||||
declared_ty: Type<'db>,
|
||||
assigned_ty: Type<'db>,
|
||||
) {
|
||||
match declared_ty {
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||
self.add_lint(&INVALID_ASSIGNMENT, node, format_args!(
|
||||
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
|
||||
class.name(self.db)));
|
||||
}
|
||||
Type::FunctionLiteral(function) => {
|
||||
self.add_lint(&INVALID_ASSIGNMENT, node, format_args!(
|
||||
"Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional",
|
||||
function.name(self.db)));
|
||||
}
|
||||
_ => {
|
||||
self.add_lint(
|
||||
&INVALID_ASSIGNMENT,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not assignable to `{}`",
|
||||
assigned_ty.display(self.db),
|
||||
declared_ty.display(self.db),
|
||||
),
|
||||
);
|
||||
}
|
||||
Type::FunctionLiteral(function) => {
|
||||
context.report_lint(&INVALID_ASSIGNMENT, node, format_args!(
|
||||
"Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional",
|
||||
function.name(context.db())));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn add_possibly_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
|
||||
let ast::ExprName { id, .. } = expr_name_node;
|
||||
|
||||
self.add_lint(
|
||||
&POSSIBLY_UNRESOLVED_REFERENCE,
|
||||
expr_name_node.into(),
|
||||
format_args!("Name `{id}` used when possibly not defined"),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn add_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
|
||||
let ast::ExprName { id, .. } = expr_name_node;
|
||||
|
||||
self.add_lint(
|
||||
&UNRESOLVED_REFERENCE,
|
||||
expr_name_node.into(),
|
||||
format_args!("Name `{id}` used when not defined"),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn add_invalid_exception_caught(&mut self, db: &dyn Db, node: &ast::Expr, ty: Type) {
|
||||
self.add_lint(
|
||||
&INVALID_EXCEPTION_CAUGHT,
|
||||
node.into(),
|
||||
format_args!(
|
||||
"Cannot catch object of type `{}` in an exception handler \
|
||||
(must be a `BaseException` subclass or a tuple of `BaseException` subclasses)",
|
||||
ty.display(db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn add_lint(
|
||||
&mut self,
|
||||
lint: &'static LintMetadata,
|
||||
node: AnyNodeRef,
|
||||
message: std::fmt::Arguments,
|
||||
) {
|
||||
// Skip over diagnostics if the rule is disabled.
|
||||
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.add(node, DiagnosticId::Lint(lint.name()), severity, message);
|
||||
}
|
||||
|
||||
/// Adds a new diagnostic.
|
||||
///
|
||||
/// The diagnostic does not get added if the rule isn't enabled for this file.
|
||||
pub(super) fn add(
|
||||
&mut self,
|
||||
node: AnyNodeRef,
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
message: std::fmt::Arguments,
|
||||
) {
|
||||
if !self.db.is_file_open(self.file) {
|
||||
return;
|
||||
_ => {
|
||||
context.report_lint(
|
||||
&INVALID_ASSIGNMENT,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not assignable to `{}`",
|
||||
assigned_ty.display(context.db()),
|
||||
declared_ty.display(context.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Don't emit the diagnostic if:
|
||||
// * The enclosing node contains any syntax errors
|
||||
// * The rule is disabled for this file. We probably want to introduce a new query that
|
||||
// returns a rule selector for a given file that respects the package's settings,
|
||||
// any global pragma comments in the file, and any per-file-ignores.
|
||||
|
||||
self.diagnostics.push(TypeCheckDiagnostic {
|
||||
file: self.file,
|
||||
id,
|
||||
message: message.to_string(),
|
||||
range: node.range(),
|
||||
severity,
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn extend(&mut self, diagnostics: &TypeCheckDiagnostics) {
|
||||
self.diagnostics.extend(diagnostics);
|
||||
}
|
||||
|
||||
pub(super) fn finish(mut self) -> TypeCheckDiagnostics {
|
||||
self.diagnostics.shrink_to_fit();
|
||||
self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn report_possibly_unresolved_reference(
|
||||
context: &InferContext,
|
||||
expr_name_node: &ast::ExprName,
|
||||
) {
|
||||
let ast::ExprName { id, .. } = expr_name_node;
|
||||
|
||||
context.report_lint(
|
||||
&POSSIBLY_UNRESOLVED_REFERENCE,
|
||||
expr_name_node.into(),
|
||||
format_args!("Name `{id}` used when possibly not defined"),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node: &ast::ExprName) {
|
||||
let ast::ExprName { id, .. } = expr_name_node;
|
||||
|
||||
context.report_lint(
|
||||
&UNRESOLVED_REFERENCE,
|
||||
expr_name_node.into(),
|
||||
format_args!("Name `{id}` used when not defined"),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) {
|
||||
context.report_lint(
|
||||
&INVALID_EXCEPTION_CAUGHT,
|
||||
node.into(),
|
||||
format_args!(
|
||||
"Cannot catch object of type `{}` in an exception handler \
|
||||
(must be a `BaseException` subclass or a tuple of `BaseException` subclasses)",
|
||||
ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn report_invalid_exception_raised(context: &InferContext, node: &ast::Expr, ty: Type) {
|
||||
context.report_lint(
|
||||
&INVALID_RAISE,
|
||||
node.into(),
|
||||
format_args!(
|
||||
"Cannot raise object of type `{}` (must be a `BaseException` subclass or instance)",
|
||||
ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) {
|
||||
context.report_lint(
|
||||
&INVALID_RAISE,
|
||||
node.into(),
|
||||
format_args!(
|
||||
"Cannot use object of type `{}` as exception cause \
|
||||
(must be a `BaseException` subclass or instance or `None`)",
|
||||
ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node: &ast::Expr) {
|
||||
context.report_lint(
|
||||
&INCOMPATIBLE_SLOTS,
|
||||
node.into(),
|
||||
format_args!("Class base has incompatible `__slots__`"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ impl Display for DisplayRepresentation<'_> {
|
||||
// `[Type::Todo]`'s display should be explicit that is not a valid display of
|
||||
// any other type
|
||||
Type::Todo(todo) => write!(f, "@Todo{todo}"),
|
||||
Type::ModuleLiteral(file) => {
|
||||
write!(f, "<module '{:?}'>", file.path(self.db))
|
||||
Type::ModuleLiteral(module) => {
|
||||
write!(f, "<module '{}'>", module.module(self.db).name())
|
||||
}
|
||||
// TODO functions and classes should display using a fully qualified name
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)),
|
||||
@@ -140,6 +140,8 @@ impl Display for DisplayRepresentation<'_> {
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
|
||||
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,6 +324,12 @@ impl<'db> TypeArrayDisplay<'db> for Vec<Type<'db>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> TypeArrayDisplay<'db> for [Type<'db>] {
|
||||
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray {
|
||||
DisplayTypeArray { types: self, db }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DisplayTypeArray<'b, 'db> {
|
||||
types: &'b [Type<'db>],
|
||||
db: &'db dyn Db,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
use crate::semantic_index::ast_ids::HasScopedExpressionId;
|
||||
use crate::semantic_index::constraint::{Constraint, ConstraintNode, PatternConstraint};
|
||||
use crate::semantic_index::constraint::{
|
||||
Constraint, ConstraintNode, PatternConstraint, PatternConstraintKind,
|
||||
};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
|
||||
@@ -196,6 +198,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
is_positive: bool,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
match expression_node {
|
||||
ast::Expr::Name(name) => Some(self.evaluate_expr_name(name, is_positive)),
|
||||
ast::Expr::Compare(expr_compare) => {
|
||||
self.evaluate_expr_compare(expr_compare, expression, is_positive)
|
||||
}
|
||||
@@ -215,31 +218,12 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let subject = pattern.subject(self.db);
|
||||
|
||||
match pattern.pattern(self.db).node() {
|
||||
ast::Pattern::MatchValue(_) => {
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchSingleton(singleton_pattern) => {
|
||||
self.evaluate_match_pattern_singleton(subject, singleton_pattern)
|
||||
}
|
||||
ast::Pattern::MatchSequence(_) => {
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchMapping(_) => {
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchClass(_) => {
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchStar(_) => {
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchAs(_) => {
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchOr(_) => {
|
||||
None // TODO
|
||||
match pattern.kind(self.db) {
|
||||
PatternConstraintKind::Singleton(singleton, _guard) => {
|
||||
self.evaluate_match_pattern_singleton(*subject, *singleton)
|
||||
}
|
||||
// TODO: support more pattern kinds
|
||||
PatternConstraintKind::Value(..) | PatternConstraintKind::Unsupported => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +238,31 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_expr_name(
|
||||
&mut self,
|
||||
expr_name: &ast::ExprName,
|
||||
is_positive: bool,
|
||||
) -> NarrowingConstraints<'db> {
|
||||
let ast::ExprName { id, .. } = expr_name;
|
||||
|
||||
let symbol = self
|
||||
.symbols()
|
||||
.symbol_id_by_name(id)
|
||||
.expect("Should always have a symbol for every Name node");
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
|
||||
constraints.insert(
|
||||
symbol,
|
||||
if is_positive {
|
||||
Type::AlwaysFalsy.negate(self.db)
|
||||
} else {
|
||||
Type::AlwaysTruthy.negate(self.db)
|
||||
},
|
||||
);
|
||||
|
||||
constraints
|
||||
}
|
||||
|
||||
fn evaluate_expr_compare(
|
||||
&mut self,
|
||||
expr_compare: &ast::ExprCompare,
|
||||
@@ -457,14 +466,14 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
|
||||
fn evaluate_match_pattern_singleton(
|
||||
&mut self,
|
||||
subject: &ast::Expr,
|
||||
pattern: &ast::PatternMatchSingleton,
|
||||
subject: Expression<'db>,
|
||||
singleton: ast::Singleton,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
if let Some(ast::ExprName { id, .. }) = subject.as_name_expr() {
|
||||
if let Some(ast::ExprName { id, .. }) = subject.node_ref(self.db).as_name_expr() {
|
||||
// SAFETY: we should always have a symbol for every Name node.
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
|
||||
let ty = match pattern.value {
|
||||
let ty = match singleton {
|
||||
ast::Singleton::None => Type::none(self.db),
|
||||
ast::Singleton::True => Type::BooleanLiteral(true),
|
||||
ast::Singleton::False => Type::BooleanLiteral(false),
|
||||
|
||||
@@ -75,6 +75,8 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
|
||||
Ty::AbcClassLiteral("ABCMeta"),
|
||||
Ty::SubclassOfAbcClass("ABC"),
|
||||
Ty::SubclassOfAbcClass("ABCMeta"),
|
||||
Ty::AlwaysTruthy,
|
||||
Ty::AlwaysFalsy,
|
||||
])
|
||||
.unwrap()
|
||||
.clone()
|
||||
|
||||
103
crates/red_knot_python_semantic/src/types/slots.rs
Normal file
103
crates/red_knot_python_semantic/src/types/slots.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::symbol::{Boundness, Symbol};
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::diagnostic::report_base_with_incompatible_slots;
|
||||
use crate::types::{Class, ClassLiteralType, Type};
|
||||
|
||||
use super::InferContext;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum SlotsKind {
|
||||
/// `__slots__` is not found in the class.
|
||||
NotSpecified,
|
||||
/// `__slots__` is defined but empty: `__slots__ = ()`.
|
||||
Empty,
|
||||
/// `__slots__` is defined and is not empty: `__slots__ = ("a", "b")`.
|
||||
NotEmpty,
|
||||
/// `__slots__` is defined but its value is dynamic:
|
||||
/// * `__slots__ = tuple(a for a in b)`
|
||||
/// * `__slots__ = ["a", "b"]`
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
impl SlotsKind {
|
||||
fn from(db: &dyn Db, base: Class) -> Self {
|
||||
let Symbol::Type(slots_ty, bound) = base.own_class_member(db, "__slots__") else {
|
||||
return Self::NotSpecified;
|
||||
};
|
||||
|
||||
if matches!(bound, Boundness::PossiblyUnbound) {
|
||||
return Self::Dynamic;
|
||||
};
|
||||
|
||||
match slots_ty {
|
||||
// __slots__ = ("a", "b")
|
||||
Type::Tuple(tuple) => {
|
||||
if tuple.elements(db).is_empty() {
|
||||
Self::Empty
|
||||
} else {
|
||||
Self::NotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// __slots__ = "abc" # Same as `("abc",)`
|
||||
Type::StringLiteral(_) => Self::NotEmpty,
|
||||
|
||||
_ => Self::Dynamic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn check_class_slots(context: &InferContext, class: Class, node: &ast::StmtClassDef) {
|
||||
let db = context.db();
|
||||
|
||||
let mut first_with_solid_base = None;
|
||||
let mut common_solid_base = None;
|
||||
let mut found_second = false;
|
||||
|
||||
for (index, base) in class.explicit_bases(db).iter().enumerate() {
|
||||
let Type::ClassLiteral(ClassLiteralType { class: base }) = base else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let solid_base = base.iter_mro(db).find_map(|current| {
|
||||
let ClassBase::Class(current) = current else {
|
||||
return None;
|
||||
};
|
||||
|
||||
match SlotsKind::from(db, current) {
|
||||
SlotsKind::NotEmpty => Some(current),
|
||||
SlotsKind::NotSpecified | SlotsKind::Empty => None,
|
||||
SlotsKind::Dynamic => None,
|
||||
}
|
||||
});
|
||||
|
||||
if solid_base.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let base_node = &node.bases()[index];
|
||||
|
||||
if first_with_solid_base.is_none() {
|
||||
first_with_solid_base = Some(index);
|
||||
common_solid_base = solid_base;
|
||||
continue;
|
||||
}
|
||||
|
||||
if solid_base == common_solid_base {
|
||||
continue;
|
||||
}
|
||||
|
||||
found_second = true;
|
||||
report_base_with_incompatible_slots(context, base_node);
|
||||
}
|
||||
|
||||
if found_second {
|
||||
if let Some(index) = first_with_solid_base {
|
||||
let base_node = &node.bases()[index];
|
||||
report_base_with_incompatible_slots(context, base_node);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_python_ast::str::raw_contents;
|
||||
use ruff_python_ast::{self as ast, ModExpression, StringFlags};
|
||||
use ruff_python_parser::{parse_expression_range, Parsed};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::declare_lint;
|
||||
use crate::lint::{Level, LintStatus};
|
||||
use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
|
||||
use crate::{declare_lint, Db};
|
||||
|
||||
use super::context::InferContext;
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
@@ -127,24 +127,23 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
type AnnotationParseResult = Result<Parsed<ModExpression>, TypeCheckDiagnostics>;
|
||||
|
||||
/// Parses the given expression as a string annotation.
|
||||
pub(crate) fn parse_string_annotation(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
context: &InferContext,
|
||||
string_expr: &ast::ExprStringLiteral,
|
||||
) -> AnnotationParseResult {
|
||||
) -> Option<Parsed<ModExpression>> {
|
||||
let file = context.file();
|
||||
let db = context.db();
|
||||
|
||||
let _span = tracing::trace_span!("parse_string_annotation", string=?string_expr.range(), file=%file.path(db)).entered();
|
||||
|
||||
let source = source_text(db.upcast(), file);
|
||||
let node_text = &source[string_expr.range()];
|
||||
let mut diagnostics = TypeCheckDiagnosticsBuilder::new(db, file);
|
||||
|
||||
if let [string_literal] = string_expr.value.as_slice() {
|
||||
let prefix = string_literal.flags.prefix();
|
||||
if prefix.is_raw() {
|
||||
diagnostics.add_lint(
|
||||
context.report_lint(
|
||||
&RAW_STRING_TYPE_ANNOTATION,
|
||||
string_literal.into(),
|
||||
format_args!("Type expressions cannot use raw string literal"),
|
||||
@@ -167,8 +166,8 @@ pub(crate) fn parse_string_annotation(
|
||||
// """ = 1
|
||||
// ```
|
||||
match parse_expression_range(source.as_str(), range_excluding_quotes) {
|
||||
Ok(parsed) => return Ok(parsed),
|
||||
Err(parse_error) => diagnostics.add_lint(
|
||||
Ok(parsed) => return Some(parsed),
|
||||
Err(parse_error) => context.report_lint(
|
||||
&INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
string_literal.into(),
|
||||
format_args!("Syntax error in forward annotation: {}", parse_error.error),
|
||||
@@ -177,7 +176,7 @@ pub(crate) fn parse_string_annotation(
|
||||
} else {
|
||||
// The raw contents of the string doesn't match the parsed content. This could be the
|
||||
// case for annotations that contain escape sequences.
|
||||
diagnostics.add_lint(
|
||||
context.report_lint(
|
||||
&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION,
|
||||
string_expr.into(),
|
||||
format_args!("Type expressions cannot contain escape characters"),
|
||||
@@ -185,12 +184,12 @@ pub(crate) fn parse_string_annotation(
|
||||
}
|
||||
} else {
|
||||
// String is implicitly concatenated.
|
||||
diagnostics.add_lint(
|
||||
context.report_lint(
|
||||
&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION,
|
||||
string_expr.into(),
|
||||
format_args!("Type expressions cannot span multiple string literals"),
|
||||
);
|
||||
}
|
||||
|
||||
Err(diagnostics.finish())
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,126 +1,249 @@
|
||||
use std::borrow::Cow;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
|
||||
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::types::{todo_type, Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
|
||||
use crate::types::{infer_expression_types, todo_type, Type, TypeCheckDiagnostics};
|
||||
use crate::unpack::UnpackValue;
|
||||
use crate::Db;
|
||||
|
||||
use super::context::{InferContext, WithDiagnostics};
|
||||
use super::diagnostic::INVALID_ASSIGNMENT;
|
||||
use super::{TupleType, UnionType};
|
||||
|
||||
/// Unpacks the value expression type to their respective targets.
|
||||
pub(crate) struct Unpacker<'db> {
|
||||
db: &'db dyn Db,
|
||||
context: InferContext<'db>,
|
||||
scope: ScopeId<'db>,
|
||||
targets: FxHashMap<ScopedExpressionId, Type<'db>>,
|
||||
diagnostics: TypeCheckDiagnosticsBuilder<'db>,
|
||||
}
|
||||
|
||||
impl<'db> Unpacker<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db, file: File) -> Self {
|
||||
pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
context: InferContext::new(db, scope),
|
||||
targets: FxHashMap::default(),
|
||||
diagnostics: TypeCheckDiagnosticsBuilder::new(db, file),
|
||||
scope,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unpack(&mut self, target: &ast::Expr, value_ty: Type<'db>, scope: ScopeId<'db>) {
|
||||
fn db(&self) -> &'db dyn Db {
|
||||
self.context.db()
|
||||
}
|
||||
|
||||
/// Unpack the value to the target expression.
|
||||
pub(crate) fn unpack(&mut self, target: &ast::Expr, value: UnpackValue<'db>) {
|
||||
debug_assert!(
|
||||
matches!(target, ast::Expr::List(_) | ast::Expr::Tuple(_)),
|
||||
"Unpacking target must be a list or tuple expression"
|
||||
);
|
||||
|
||||
let mut value_ty = infer_expression_types(self.db(), value.expression())
|
||||
.expression_ty(value.scoped_expression_id(self.db(), self.scope));
|
||||
|
||||
if value.is_iterable() {
|
||||
// If the value is an iterable, then the type that needs to be unpacked is the iterator
|
||||
// type.
|
||||
value_ty = value_ty
|
||||
.iterate(self.db())
|
||||
.unwrap_with_diagnostic(&self.context, value.as_any_node_ref(self.db()));
|
||||
}
|
||||
|
||||
self.unpack_inner(target, value_ty);
|
||||
}
|
||||
|
||||
fn unpack_inner(&mut self, target: &ast::Expr, value_ty: Type<'db>) {
|
||||
match target {
|
||||
ast::Expr::Name(target_name) => {
|
||||
self.targets
|
||||
.insert(target_name.scoped_expression_id(self.db, scope), value_ty);
|
||||
self.targets.insert(
|
||||
target_name.scoped_expression_id(self.db(), self.scope),
|
||||
value_ty,
|
||||
);
|
||||
}
|
||||
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
|
||||
self.unpack(value, value_ty, scope);
|
||||
self.unpack_inner(value, value_ty);
|
||||
}
|
||||
ast::Expr::List(ast::ExprList { elts, .. })
|
||||
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => match value_ty {
|
||||
Type::Tuple(tuple_ty) => {
|
||||
let starred_index = elts.iter().position(ast::Expr::is_starred_expr);
|
||||
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
// Initialize the vector of target types, one for each target.
|
||||
//
|
||||
// This is mainly useful for the union type where the target type at index `n` is
|
||||
// going to be a union of types from every union type element at index `n`.
|
||||
//
|
||||
// For example, if the type is `tuple[int, int] | tuple[int, str]` and the target
|
||||
// has two elements `(a, b)`, then
|
||||
// * The type of `a` will be a union of `int` and `int` which are at index 0 in the
|
||||
// first and second tuple respectively which resolves to an `int`.
|
||||
// * Similarly, the type of `b` will be a union of `int` and `str` which are at
|
||||
// index 1 in the first and second tuple respectively which will be `int | str`.
|
||||
let mut target_types = vec![vec![]; elts.len()];
|
||||
|
||||
let element_types = if let Some(starred_index) = starred_index {
|
||||
if tuple_ty.len(self.db) >= elts.len() - 1 {
|
||||
let mut element_types = Vec::with_capacity(elts.len());
|
||||
element_types.extend_from_slice(
|
||||
// SAFETY: Safe because of the length check above.
|
||||
&tuple_ty.elements(self.db)[..starred_index],
|
||||
);
|
||||
let unpack_types = match value_ty {
|
||||
Type::Union(union_ty) => union_ty.elements(self.db()),
|
||||
_ => std::slice::from_ref(&value_ty),
|
||||
};
|
||||
|
||||
// E.g., in `(a, *b, c, d) = ...`, the index of starred element `b`
|
||||
// is 1 and the remaining elements after that are 2.
|
||||
let remaining = elts.len() - (starred_index + 1);
|
||||
// This index represents the type of the last element that belongs
|
||||
// to the starred expression, in an exclusive manner.
|
||||
let starred_end_index = tuple_ty.len(self.db) - remaining;
|
||||
// SAFETY: Safe because of the length check above.
|
||||
let _starred_element_types =
|
||||
&tuple_ty.elements(self.db)[starred_index..starred_end_index];
|
||||
// TODO: Combine the types into a list type. If the
|
||||
// starred_element_types is empty, then it should be `List[Any]`.
|
||||
// combine_types(starred_element_types);
|
||||
element_types.push(todo_type!("starred unpacking"));
|
||||
for ty in unpack_types.iter().copied() {
|
||||
// Deconstruct certain types to delegate the inference back to the tuple type
|
||||
// for correct handling of starred expressions.
|
||||
let ty = match ty {
|
||||
Type::StringLiteral(string_literal_ty) => {
|
||||
// We could go further and deconstruct to an array of `StringLiteral`
|
||||
// with each individual character, instead of just an array of
|
||||
// `LiteralString`, but there would be a cost and it's not clear that
|
||||
// it's worth it.
|
||||
Type::tuple(
|
||||
self.db(),
|
||||
std::iter::repeat(Type::LiteralString)
|
||||
.take(string_literal_ty.python_len(self.db())),
|
||||
)
|
||||
}
|
||||
_ => ty,
|
||||
};
|
||||
|
||||
element_types.extend_from_slice(
|
||||
// SAFETY: Safe because of the length check above.
|
||||
&tuple_ty.elements(self.db)[starred_end_index..],
|
||||
);
|
||||
Cow::Owned(element_types)
|
||||
} else {
|
||||
let mut element_types = tuple_ty.elements(self.db).to_vec();
|
||||
// Subtract 1 to insert the starred expression type at the correct
|
||||
// index.
|
||||
element_types.resize(elts.len() - 1, Type::Unknown);
|
||||
// TODO: This should be `list[Unknown]`
|
||||
element_types.insert(starred_index, todo_type!("starred unpacking"));
|
||||
Cow::Owned(element_types)
|
||||
if let Some(tuple_ty) = ty.into_tuple() {
|
||||
let tuple_ty_elements = self.tuple_ty_elements(target, elts, tuple_ty);
|
||||
|
||||
match elts.len().cmp(&tuple_ty_elements.len()) {
|
||||
Ordering::Less => {
|
||||
self.context.report_lint(
|
||||
&INVALID_ASSIGNMENT,
|
||||
target.into(),
|
||||
format_args!(
|
||||
"Too many values to unpack (expected {}, got {})",
|
||||
elts.len(),
|
||||
tuple_ty_elements.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
self.context.report_lint(
|
||||
&INVALID_ASSIGNMENT,
|
||||
target.into(),
|
||||
format_args!(
|
||||
"Not enough values to unpack (expected {}, got {})",
|
||||
elts.len(),
|
||||
tuple_ty_elements.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
}
|
||||
|
||||
for (index, ty) in tuple_ty_elements.iter().enumerate() {
|
||||
if let Some(element_types) = target_types.get_mut(index) {
|
||||
element_types.push(*ty);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(tuple_ty.elements(self.db).as_ref())
|
||||
};
|
||||
let ty = if ty.is_literal_string() {
|
||||
Type::LiteralString
|
||||
} else {
|
||||
ty.iterate(self.db())
|
||||
.unwrap_with_diagnostic(&self.context, AnyNodeRef::from(target))
|
||||
};
|
||||
for target_type in &mut target_types {
|
||||
target_type.push(ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (index, element) in elts.iter().enumerate() {
|
||||
self.unpack(
|
||||
element,
|
||||
element_types.get(index).copied().unwrap_or(Type::Unknown),
|
||||
scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
Type::StringLiteral(string_literal_ty) => {
|
||||
// Deconstruct the string literal to delegate the inference back to the
|
||||
// tuple type for correct handling of starred expressions. We could go
|
||||
// further and deconstruct to an array of `StringLiteral` with each
|
||||
// individual character, instead of just an array of `LiteralString`, but
|
||||
// there would be a cost and it's not clear that it's worth it.
|
||||
let value_ty = Type::tuple(
|
||||
self.db,
|
||||
std::iter::repeat(Type::LiteralString)
|
||||
.take(string_literal_ty.python_len(self.db)),
|
||||
);
|
||||
self.unpack(target, value_ty, scope);
|
||||
}
|
||||
_ => {
|
||||
let value_ty = if value_ty.is_literal_string() {
|
||||
Type::LiteralString
|
||||
} else {
|
||||
value_ty
|
||||
.iterate(self.db)
|
||||
.unwrap_with_diagnostic(AnyNodeRef::from(target), &mut self.diagnostics)
|
||||
for (index, element) in elts.iter().enumerate() {
|
||||
// SAFETY: `target_types` is initialized with the same length as `elts`.
|
||||
let element_ty = match target_types[index].as_slice() {
|
||||
[] => Type::Unknown,
|
||||
types => UnionType::from_elements(self.db(), types),
|
||||
};
|
||||
for element in elts {
|
||||
self.unpack(element, value_ty, scope);
|
||||
}
|
||||
self.unpack_inner(element, element_ty);
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`Type`] elements inside the given [`TupleType`] taking into account that there
|
||||
/// can be a starred expression in the `elements`.
|
||||
fn tuple_ty_elements(
|
||||
&self,
|
||||
expr: &ast::Expr,
|
||||
targets: &[ast::Expr],
|
||||
tuple_ty: TupleType<'db>,
|
||||
) -> Cow<'_, [Type<'db>]> {
|
||||
// If there is a starred expression, it will consume all of the types at that location.
|
||||
let Some(starred_index) = targets.iter().position(ast::Expr::is_starred_expr) else {
|
||||
// Otherwise, the types will be unpacked 1-1 to the targets.
|
||||
return Cow::Borrowed(tuple_ty.elements(self.db()).as_ref());
|
||||
};
|
||||
|
||||
if tuple_ty.len(self.db()) >= targets.len() - 1 {
|
||||
// This branch is only taken when there are enough elements in the tuple type to
|
||||
// combine for the starred expression. So, the arithmetic and indexing operations are
|
||||
// safe to perform.
|
||||
let mut element_types = Vec::with_capacity(targets.len());
|
||||
|
||||
// Insert all the elements before the starred expression.
|
||||
element_types.extend_from_slice(
|
||||
// SAFETY: Safe because of the length check above.
|
||||
&tuple_ty.elements(self.db())[..starred_index],
|
||||
);
|
||||
|
||||
// The number of target expressions that are remaining after the starred expression.
|
||||
// For example, in `(a, *b, c, d) = ...`, the index of starred element `b` is 1 and the
|
||||
// remaining elements after that are 2.
|
||||
let remaining = targets.len() - (starred_index + 1);
|
||||
|
||||
// This index represents the position of the last element that belongs to the starred
|
||||
// expression, in an exclusive manner. For example, in `(a, *b, c) = (1, 2, 3, 4)`, the
|
||||
// starred expression `b` will consume the elements `Literal[2]` and `Literal[3]` and
|
||||
// the index value would be 3.
|
||||
let starred_end_index = tuple_ty.len(self.db()) - remaining;
|
||||
|
||||
// SAFETY: Safe because of the length check above.
|
||||
let _starred_element_types =
|
||||
&tuple_ty.elements(self.db())[starred_index..starred_end_index];
|
||||
// TODO: Combine the types into a list type. If the
|
||||
// starred_element_types is empty, then it should be `List[Any]`.
|
||||
// combine_types(starred_element_types);
|
||||
element_types.push(todo_type!("starred unpacking"));
|
||||
|
||||
// Insert the types remaining that aren't consumed by the starred expression.
|
||||
element_types.extend_from_slice(
|
||||
// SAFETY: Safe because of the length check above.
|
||||
&tuple_ty.elements(self.db())[starred_end_index..],
|
||||
);
|
||||
|
||||
Cow::Owned(element_types)
|
||||
} else {
|
||||
self.context.report_lint(
|
||||
&INVALID_ASSIGNMENT,
|
||||
expr.into(),
|
||||
format_args!(
|
||||
"Not enough values to unpack (expected {} or more, got {})",
|
||||
targets.len() - 1,
|
||||
tuple_ty.len(self.db())
|
||||
),
|
||||
);
|
||||
|
||||
let mut element_types = tuple_ty.elements(self.db()).to_vec();
|
||||
|
||||
// Subtract 1 to insert the starred expression type at the correct
|
||||
// index.
|
||||
element_types.resize(targets.len() - 1, Type::Unknown);
|
||||
// TODO: This should be `list[Unknown]`
|
||||
element_types.insert(starred_index, todo_type!("starred unpacking"));
|
||||
|
||||
Cow::Owned(element_types)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(mut self) -> UnpackResult<'db> {
|
||||
self.targets.shrink_to_fit();
|
||||
UnpackResult {
|
||||
diagnostics: self.diagnostics.finish(),
|
||||
diagnostics: self.context.finish(),
|
||||
targets: self.targets,
|
||||
}
|
||||
}
|
||||
@@ -136,8 +259,10 @@ impl<'db> UnpackResult<'db> {
|
||||
pub(crate) fn get(&self, expr_id: ScopedExpressionId) -> Option<Type<'db>> {
|
||||
self.targets.get(&expr_id).copied()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics {
|
||||
impl WithDiagnostics for UnpackResult<'_> {
|
||||
fn diagnostics(&self) -> &TypeCheckDiagnostics {
|
||||
&self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
|
||||
use crate::Db;
|
||||
@@ -41,7 +43,7 @@ pub(crate) struct Unpack<'db> {
|
||||
/// The ingredient representing the value expression of the unpacking. For example, in
|
||||
/// `(a, b) = (1, 2)`, the value expression is `(1, 2)`.
|
||||
#[no_eq]
|
||||
pub(crate) value: Expression<'db>,
|
||||
pub(crate) value: UnpackValue<'db>,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<Unpack<'static>>,
|
||||
@@ -52,4 +54,48 @@ impl<'db> Unpack<'db> {
|
||||
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.
|
||||
pub(crate) fn range(self, db: &'db dyn Db) -> TextRange {
|
||||
self.target(db).range()
|
||||
}
|
||||
}
|
||||
|
||||
/// The expression that is being unpacked.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum UnpackValue<'db> {
|
||||
/// An iterable expression like the one in a `for` loop or a comprehension.
|
||||
Iterable(Expression<'db>),
|
||||
/// An expression that is being assigned to a target.
|
||||
Assign(Expression<'db>),
|
||||
}
|
||||
|
||||
impl<'db> UnpackValue<'db> {
|
||||
/// Returns `true` if the value is an iterable expression.
|
||||
pub(crate) const fn is_iterable(self) -> bool {
|
||||
matches!(self, UnpackValue::Iterable(_))
|
||||
}
|
||||
|
||||
/// Returns the underlying [`Expression`] that is being unpacked.
|
||||
pub(crate) const fn expression(self) -> Expression<'db> {
|
||||
match self {
|
||||
UnpackValue::Assign(expr) | UnpackValue::Iterable(expr) => expr,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`ScopedExpressionId`] of the underlying expression.
|
||||
pub(crate) fn scoped_expression_id(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
) -> ScopedExpressionId {
|
||||
self.expression()
|
||||
.node_ref(db)
|
||||
.scoped_expression_id(db, scope)
|
||||
}
|
||||
|
||||
/// Returns the expression as an [`AnyNodeRef`].
|
||||
pub(crate) fn as_any_node_ref(self, db: &'db dyn Db) -> AnyNodeRef<'db> {
|
||||
self.expression().node_ref(db).node().into()
|
||||
}
|
||||
}
|
||||
|
||||
338
crates/red_knot_python_semantic/src/visibility_constraints.rs
Normal file
338
crates/red_knot_python_semantic/src/visibility_constraints.rs
Normal file
@@ -0,0 +1,338 @@
|
||||
//! # Visibility constraints
|
||||
//!
|
||||
//! During semantic index building, we collect visibility constraints for each binding and
|
||||
//! declaration. These constraints are then used during type-checking to determine the static
|
||||
//! visibility of a certain definition. This allows us to re-analyze control flow during type
|
||||
//! checking, potentially "hiding" some branches that we can statically determine to never be
|
||||
//! taken. Consider the following example first. We added implicit "unbound" definitions at the
|
||||
//! start of the scope. Note how visibility constraints can apply to bindings outside of the
|
||||
//! if-statement:
|
||||
//! ```py
|
||||
//! x = <unbound> # not a live binding for the use of x below, shadowed by `x = 1`
|
||||
//! y = <unbound> # visibility constraint: ~test
|
||||
//!
|
||||
//! x = 1 # visibility constraint: ~test
|
||||
//! if test:
|
||||
//! x = 2 # visibility constraint: test
|
||||
//!
|
||||
//! y = 2 # visibility constraint: test
|
||||
//!
|
||||
//! use(x)
|
||||
//! use(y)
|
||||
//! ```
|
||||
//! The static truthiness of the `test` condition can either be always-false, ambiguous, or
|
||||
//! always-true. Similarly, we have the same three options when evaluating a visibility constraint.
|
||||
//! This outcome determines the visibility of a definition: always-true means that the definition
|
||||
//! is definitely visible for a given use, always-false means that the definition is definitely
|
||||
//! not visible, and ambiguous means that we might see this definition or not. In the latter case,
|
||||
//! we need to consider both options during type inference and boundness analysis. For the example
|
||||
//! above, these are the possible type inference / boundness results for the uses of `x` and `y`:
|
||||
//!
|
||||
//! ```text
|
||||
//! | `test` truthiness | `~test` truthiness | type of `x` | boundness of `y` |
|
||||
//! |-------------------|--------------------|-----------------|------------------|
|
||||
//! | always false | always true | `Literal[1]` | unbound |
|
||||
//! | ambiguous | ambiguous | `Literal[1, 2]` | possibly unbound |
|
||||
//! | always true | always false | `Literal[2]` | bound |
|
||||
//! ```
|
||||
//!
|
||||
//! ### Sequential constraints (ternary AND)
|
||||
//!
|
||||
//! As we have seen above, visibility constraints can apply outside of a control flow element.
|
||||
//! So we need to consider the possibility that multiple constraints apply to the same binding.
|
||||
//! Here, we consider what happens if multiple `if`-statements lead to a sequence of constraints.
|
||||
//! Consider the following example:
|
||||
//! ```py
|
||||
//! x = 0
|
||||
//!
|
||||
//! if test1:
|
||||
//! x = 1
|
||||
//!
|
||||
//! if test2:
|
||||
//! x = 2
|
||||
//! ```
|
||||
//! The binding `x = 2` is easy to analyze. Its visibility corresponds to the truthiness of `test2`.
|
||||
//! For the `x = 1` binding, things are a bit more interesting. It is always visible if `test1` is
|
||||
//! always-true *and* `test2` is always-false. It is never visible if `test1` is always-false *or*
|
||||
//! `test2` is always-true. And it is ambiguous otherwise. This corresponds to a ternary *test1 AND
|
||||
//! ~test2* operation in three-valued Kleene logic [Kleene]:
|
||||
//!
|
||||
//! ```text
|
||||
//! | AND | always-false | ambiguous | always-true |
|
||||
//! |--------------|--------------|--------------|--------------|
|
||||
//! | always false | always-false | always-false | always-false |
|
||||
//! | ambiguous | always-false | ambiguous | ambiguous |
|
||||
//! | always true | always-false | ambiguous | always-true |
|
||||
//! ```
|
||||
//!
|
||||
//! The `x = 0` binding can be handled similarly, with the difference that both `test1` and `test2`
|
||||
//! are negated:
|
||||
//! ```py
|
||||
//! x = 0 # ~test1 AND ~test2
|
||||
//!
|
||||
//! if test1:
|
||||
//! x = 1 # test1 AND ~test2
|
||||
//!
|
||||
//! if test2:
|
||||
//! x = 2 # test2
|
||||
//! ```
|
||||
//!
|
||||
//! ### Merged constraints (ternary OR)
|
||||
//!
|
||||
//! Finally, we consider what happens in "parallel" control flow. Consider the following example
|
||||
//! where we have omitted the test condition for the outer `if` for clarity:
|
||||
//! ```py
|
||||
//! x = 0
|
||||
//!
|
||||
//! if <…>:
|
||||
//! if test1:
|
||||
//! x = 1
|
||||
//! else:
|
||||
//! if test2:
|
||||
//! x = 2
|
||||
//!
|
||||
//! use(x)
|
||||
//! ```
|
||||
//! At the usage of `x`, i.e. after control flow has been merged again, the visibility of the `x =
|
||||
//! 0` binding behaves as follows: the binding is always visible if `test1` is always-false *or*
|
||||
//! `test2` is always-false; and it is never visible if `test1` is always-true *and* `test2` is
|
||||
//! always-true. This corresponds to a ternary *OR* operation in Kleene logic:
|
||||
//!
|
||||
//! ```text
|
||||
//! | OR | always-false | ambiguous | always-true |
|
||||
//! |--------------|--------------|--------------|--------------|
|
||||
//! | always false | always-false | ambiguous | always-true |
|
||||
//! | ambiguous | ambiguous | ambiguous | always-true |
|
||||
//! | always true | always-true | always-true | always-true |
|
||||
//! ```
|
||||
//!
|
||||
//! Using this, we can annotate the visibility constraints for the example above:
|
||||
//! ```py
|
||||
//! x = 0 # ~test1 OR ~test2
|
||||
//!
|
||||
//! if <…>:
|
||||
//! if test1:
|
||||
//! x = 1 # test1
|
||||
//! else:
|
||||
//! if test2:
|
||||
//! x = 2 # test2
|
||||
//!
|
||||
//! use(x)
|
||||
//! ```
|
||||
//!
|
||||
//! ### Explicit ambiguity
|
||||
//!
|
||||
//! In some cases, we explicitly add a `VisibilityConstraint::Ambiguous` constraint to all bindings
|
||||
//! in a certain control flow path. We do this when branching on something that we can not (or
|
||||
//! intentionally do not want to) analyze statically. `for` loops are one example:
|
||||
//! ```py
|
||||
//! x = <unbound>
|
||||
//!
|
||||
//! for _ in range(2):
|
||||
//! x = 1
|
||||
//! ```
|
||||
//! Here, we report an ambiguous visibility constraint before branching off. If we don't do this,
|
||||
//! the `x = <unbound>` binding would be considered unconditionally visible in the no-loop case.
|
||||
//! And since the other branch does not have the live `x = <unbound>` binding, we would incorrectly
|
||||
//! create a state where the `x = <unbound>` binding is always visible.
|
||||
//!
|
||||
//!
|
||||
//! ### Properties
|
||||
//!
|
||||
//! The ternary `AND` and `OR` operations have the property that `~a OR ~b = ~(a AND b)`. This
|
||||
//! means we could, in principle, get rid of either of these two to simplify the representation.
|
||||
//!
|
||||
//! However, we already apply negative constraints `~test1` and `~test2` to the "branches not
|
||||
//! taken" in the example above. This means that the tree-representation `~test1 OR ~test2` is much
|
||||
//! cheaper/shallower than basically creating `~(~(~test1) AND ~(~test2))`. Similarly, if we wanted
|
||||
//! to get rid of `AND`, we would also have to create additional nodes. So for performance reasons,
|
||||
//! there is a small "duplication" in the code between those two constraint types.
|
||||
//!
|
||||
//! [Kleene]: <https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics>
|
||||
|
||||
use ruff_index::IndexVec;
|
||||
|
||||
use crate::semantic_index::ScopedVisibilityConstraintId;
|
||||
use crate::semantic_index::{
|
||||
ast_ids::HasScopedExpressionId,
|
||||
constraint::{Constraint, ConstraintNode, PatternConstraintKind},
|
||||
};
|
||||
use crate::types::{infer_expression_types, Truthiness};
|
||||
use crate::Db;
|
||||
|
||||
/// The maximum depth of recursion when evaluating visibility constraints.
|
||||
///
|
||||
/// This is a performance optimization that prevents us from descending deeply in case of
|
||||
/// pathological cases. The actual limit here has been derived from performance testing on
|
||||
/// the `black` codebase. When increasing the limit beyond 32, we see a 5x runtime increase
|
||||
/// resulting from a few files with a lot of boolean expressions and `if`-statements.
|
||||
const MAX_RECURSION_DEPTH: usize = 24;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum VisibilityConstraint<'db> {
|
||||
AlwaysTrue,
|
||||
Ambiguous,
|
||||
VisibleIf(Constraint<'db>),
|
||||
VisibleIfNot(ScopedVisibilityConstraintId),
|
||||
KleeneAnd(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
|
||||
KleeneOr(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct VisibilityConstraints<'db> {
|
||||
constraints: IndexVec<ScopedVisibilityConstraintId, VisibilityConstraint<'db>>,
|
||||
}
|
||||
|
||||
impl Default for VisibilityConstraints<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
constraints: IndexVec::from_iter([VisibilityConstraint::AlwaysTrue]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> VisibilityConstraints<'db> {
|
||||
pub(crate) fn add(
|
||||
&mut self,
|
||||
constraint: VisibilityConstraint<'db>,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
self.constraints.push(constraint)
|
||||
}
|
||||
|
||||
pub(crate) fn add_or_constraint(
|
||||
&mut self,
|
||||
a: ScopedVisibilityConstraintId,
|
||||
b: ScopedVisibilityConstraintId,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
match (&self.constraints[a], &self.constraints[b]) {
|
||||
(_, VisibilityConstraint::VisibleIfNot(id)) if a == *id => {
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE
|
||||
}
|
||||
(VisibilityConstraint::VisibleIfNot(id), _) if *id == b => {
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE
|
||||
}
|
||||
_ => self.add(VisibilityConstraint::KleeneOr(a, b)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_and_constraint(
|
||||
&mut self,
|
||||
a: ScopedVisibilityConstraintId,
|
||||
b: ScopedVisibilityConstraintId,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
if a == ScopedVisibilityConstraintId::ALWAYS_TRUE {
|
||||
b
|
||||
} else if b == ScopedVisibilityConstraintId::ALWAYS_TRUE {
|
||||
a
|
||||
} else {
|
||||
self.add(VisibilityConstraint::KleeneAnd(a, b))
|
||||
}
|
||||
}
|
||||
|
||||
/// Analyze the statically known visibility for a given visibility constraint.
|
||||
pub(crate) fn evaluate(&self, db: &'db dyn Db, id: ScopedVisibilityConstraintId) -> Truthiness {
|
||||
self.evaluate_impl(db, id, MAX_RECURSION_DEPTH)
|
||||
}
|
||||
|
||||
fn evaluate_impl(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
id: ScopedVisibilityConstraintId,
|
||||
max_depth: usize,
|
||||
) -> Truthiness {
|
||||
if max_depth == 0 {
|
||||
return Truthiness::Ambiguous;
|
||||
}
|
||||
|
||||
let visibility_constraint = &self.constraints[id];
|
||||
match visibility_constraint {
|
||||
VisibilityConstraint::AlwaysTrue => Truthiness::AlwaysTrue,
|
||||
VisibilityConstraint::Ambiguous => Truthiness::Ambiguous,
|
||||
VisibilityConstraint::VisibleIf(constraint) => Self::analyze_single(db, constraint),
|
||||
VisibilityConstraint::VisibleIfNot(negated) => {
|
||||
self.evaluate_impl(db, *negated, max_depth - 1).negate()
|
||||
}
|
||||
VisibilityConstraint::KleeneAnd(lhs, rhs) => {
|
||||
let lhs = self.evaluate_impl(db, *lhs, max_depth - 1);
|
||||
|
||||
if lhs == Truthiness::AlwaysFalse {
|
||||
return Truthiness::AlwaysFalse;
|
||||
}
|
||||
|
||||
let rhs = self.evaluate_impl(db, *rhs, max_depth - 1);
|
||||
|
||||
if rhs == Truthiness::AlwaysFalse {
|
||||
Truthiness::AlwaysFalse
|
||||
} else if lhs == Truthiness::AlwaysTrue && rhs == Truthiness::AlwaysTrue {
|
||||
Truthiness::AlwaysTrue
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
VisibilityConstraint::KleeneOr(lhs_id, rhs_id) => {
|
||||
let lhs = self.evaluate_impl(db, *lhs_id, max_depth - 1);
|
||||
|
||||
if lhs == Truthiness::AlwaysTrue {
|
||||
return Truthiness::AlwaysTrue;
|
||||
}
|
||||
|
||||
let rhs = self.evaluate_impl(db, *rhs_id, max_depth - 1);
|
||||
|
||||
if rhs == Truthiness::AlwaysTrue {
|
||||
Truthiness::AlwaysTrue
|
||||
} else if lhs == Truthiness::AlwaysFalse && rhs == Truthiness::AlwaysFalse {
|
||||
Truthiness::AlwaysFalse
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_single(db: &dyn Db, constraint: &Constraint) -> Truthiness {
|
||||
match constraint.node {
|
||||
ConstraintNode::Expression(test_expr) => {
|
||||
let inference = infer_expression_types(db, test_expr);
|
||||
let scope = test_expr.scope(db);
|
||||
let ty =
|
||||
inference.expression_ty(test_expr.node_ref(db).scoped_expression_id(db, scope));
|
||||
|
||||
ty.bool(db).negate_if(!constraint.is_positive)
|
||||
}
|
||||
ConstraintNode::Pattern(inner) => match inner.kind(db) {
|
||||
PatternConstraintKind::Value(value, guard) => {
|
||||
let subject_expression = inner.subject(db);
|
||||
let inference = infer_expression_types(db, *subject_expression);
|
||||
let scope = subject_expression.scope(db);
|
||||
let subject_ty = inference.expression_ty(
|
||||
subject_expression
|
||||
.node_ref(db)
|
||||
.scoped_expression_id(db, scope),
|
||||
);
|
||||
|
||||
let inference = infer_expression_types(db, *value);
|
||||
let scope = value.scope(db);
|
||||
let value_ty =
|
||||
inference.expression_ty(value.node_ref(db).scoped_expression_id(db, scope));
|
||||
|
||||
if subject_ty.is_single_valued(db) {
|
||||
let truthiness =
|
||||
Truthiness::from(subject_ty.is_equivalent_to(db, value_ty));
|
||||
|
||||
if truthiness.is_always_true() && guard.is_some() {
|
||||
// Fall back to ambiguous, the guard might change the result.
|
||||
Truthiness::Ambiguous
|
||||
} else {
|
||||
truthiness
|
||||
}
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
PatternConstraintKind::Singleton(..) | PatternConstraintKind::Unsupported => {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,11 +91,11 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>(
|
||||
let db = match path {
|
||||
AnySystemPath::System(path) => {
|
||||
match session.workspace_db_for_path(path.as_std_path()) {
|
||||
Some(db) => db.snapshot(),
|
||||
None => session.default_workspace_db().snapshot(),
|
||||
Some(db) => db.clone(),
|
||||
None => session.default_workspace_db().clone(),
|
||||
}
|
||||
}
|
||||
AnySystemPath::SystemVirtual(_) => session.default_workspace_db().snapshot(),
|
||||
AnySystemPath::SystemVirtual(_) => session.default_workspace_db().clone(),
|
||||
};
|
||||
|
||||
let Some(snapshot) = session.take_snapshot(url) else {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//! ```
|
||||
|
||||
use anyhow::Context;
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
use red_knot_python_semantic::{PythonPlatform, PythonVersion};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
@@ -28,13 +28,22 @@ impl MarkdownTestConfig {
|
||||
pub(crate) fn python_version(&self) -> Option<PythonVersion> {
|
||||
self.environment.as_ref().and_then(|env| env.python_version)
|
||||
}
|
||||
|
||||
pub(crate) fn python_platform(&self) -> Option<PythonPlatform> {
|
||||
self.environment
|
||||
.as_ref()
|
||||
.and_then(|env| env.python_platform.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub(crate) struct Environment {
|
||||
/// Python version to assume when resolving types.
|
||||
/// Target Python version to assume when resolving types.
|
||||
pub(crate) python_version: Option<PythonVersion>,
|
||||
|
||||
/// Target platform to assume when resolving types.
|
||||
pub(crate) python_platform: Option<PythonPlatform>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use red_knot_python_semantic::lint::RuleSelection;
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{
|
||||
default_lint_registry, Db as SemanticDb, Program, ProgramSettings, PythonVersion,
|
||||
SearchPathSettings,
|
||||
default_lint_registry, Db as SemanticDb, Program, ProgramSettings, PythonPlatform,
|
||||
PythonVersion, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem};
|
||||
@@ -9,6 +9,7 @@ use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Db {
|
||||
workspace_root: SystemPathBuf,
|
||||
storage: salsa::Storage<Self>,
|
||||
@@ -20,7 +21,7 @@ pub(crate) struct Db {
|
||||
|
||||
impl Db {
|
||||
pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self {
|
||||
let rule_selection = RuleSelection::from_registry(&default_lint_registry());
|
||||
let rule_selection = RuleSelection::from_registry(default_lint_registry());
|
||||
|
||||
let db = Self {
|
||||
workspace_root,
|
||||
@@ -39,6 +40,7 @@ impl Db {
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings::new(db.workspace_root.clone()),
|
||||
},
|
||||
)
|
||||
@@ -95,6 +97,10 @@ impl SemanticDb for Db {
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
default_lint_registry()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -52,6 +52,9 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
|
||||
Program::get(&db)
|
||||
.set_python_version(&mut db)
|
||||
.to(test.configuration().python_version().unwrap_or_default());
|
||||
Program::get(&db)
|
||||
.set_python_platform(&mut db)
|
||||
.to(test.configuration().python_platform().unwrap_or_default());
|
||||
|
||||
// Remove all files so that the db is in a "fresh" state.
|
||||
db.memory_file_system().remove_all();
|
||||
@@ -97,7 +100,11 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
|
||||
let test_files: Vec<_> = test
|
||||
.files()
|
||||
.map(|embedded| {
|
||||
.filter_map(|embedded| {
|
||||
if embedded.lang == "ignore" {
|
||||
return None;
|
||||
}
|
||||
|
||||
assert!(
|
||||
matches!(embedded.lang, "py" | "pyi"),
|
||||
"Non-Python files not supported yet."
|
||||
@@ -106,10 +113,10 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
db.write_file(&full_path, embedded.code).unwrap();
|
||||
let file = system_path_to_file(db, full_path).unwrap();
|
||||
|
||||
TestFile {
|
||||
Some(TestFile {
|
||||
file,
|
||||
backtick_offset: embedded.md_offset,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
fc11e835108394728930059c8db5b436209bc957
|
||||
2047b820730fdd65d37e6e8efcf29ca9af7ec3e7
|
||||
|
||||
@@ -65,7 +65,7 @@ class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportIn
|
||||
self,
|
||||
coro: _TaskCompatibleCoro[_T_co],
|
||||
*,
|
||||
loop: AbstractEventLoop = ...,
|
||||
loop: AbstractEventLoop | None = None,
|
||||
name: str | None = ...,
|
||||
context: Context | None = None,
|
||||
eager_start: bool = False,
|
||||
@@ -75,13 +75,13 @@ class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportIn
|
||||
self,
|
||||
coro: _TaskCompatibleCoro[_T_co],
|
||||
*,
|
||||
loop: AbstractEventLoop = ...,
|
||||
loop: AbstractEventLoop | None = None,
|
||||
name: str | None = ...,
|
||||
context: Context | None = None,
|
||||
) -> None: ...
|
||||
else:
|
||||
def __init__(
|
||||
self, coro: _TaskCompatibleCoro[_T_co], *, loop: AbstractEventLoop = ..., name: str | None = ...
|
||||
self, coro: _TaskCompatibleCoro[_T_co], *, loop: AbstractEventLoop | None = None, name: str | None = ...
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
|
||||
@@ -22,8 +22,8 @@ class blake2b:
|
||||
digest_size: int
|
||||
name: str
|
||||
if sys.version_info >= (3, 9):
|
||||
def __init__(
|
||||
self,
|
||||
def __new__(
|
||||
cls,
|
||||
data: ReadableBuffer = b"",
|
||||
/,
|
||||
*,
|
||||
@@ -39,10 +39,10 @@ class blake2b:
|
||||
inner_size: int = 0,
|
||||
last_node: bool = False,
|
||||
usedforsecurity: bool = True,
|
||||
) -> None: ...
|
||||
) -> Self: ...
|
||||
else:
|
||||
def __init__(
|
||||
self,
|
||||
def __new__(
|
||||
cls,
|
||||
data: ReadableBuffer = b"",
|
||||
/,
|
||||
*,
|
||||
@@ -57,7 +57,7 @@ class blake2b:
|
||||
node_depth: int = 0,
|
||||
inner_size: int = 0,
|
||||
last_node: bool = False,
|
||||
) -> None: ...
|
||||
) -> Self: ...
|
||||
|
||||
def copy(self) -> Self: ...
|
||||
def digest(self) -> bytes: ...
|
||||
@@ -74,8 +74,8 @@ class blake2s:
|
||||
digest_size: int
|
||||
name: str
|
||||
if sys.version_info >= (3, 9):
|
||||
def __init__(
|
||||
self,
|
||||
def __new__(
|
||||
cls,
|
||||
data: ReadableBuffer = b"",
|
||||
/,
|
||||
*,
|
||||
@@ -91,10 +91,10 @@ class blake2s:
|
||||
inner_size: int = 0,
|
||||
last_node: bool = False,
|
||||
usedforsecurity: bool = True,
|
||||
) -> None: ...
|
||||
) -> Self: ...
|
||||
else:
|
||||
def __init__(
|
||||
self,
|
||||
def __new__(
|
||||
cls,
|
||||
data: ReadableBuffer = b"",
|
||||
/,
|
||||
*,
|
||||
@@ -109,7 +109,7 @@ class blake2s:
|
||||
node_depth: int = 0,
|
||||
inner_size: int = 0,
|
||||
last_node: bool = False,
|
||||
) -> None: ...
|
||||
) -> Self: ...
|
||||
|
||||
def copy(self) -> Self: ...
|
||||
def digest(self) -> bytes: ...
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import sys
|
||||
from _typeshed import ReadableBuffer
|
||||
from typing import final
|
||||
from typing_extensions import Self
|
||||
|
||||
@final
|
||||
class BZ2Compressor:
|
||||
def __init__(self, compresslevel: int = 9) -> None: ...
|
||||
if sys.version_info >= (3, 12):
|
||||
def __new__(cls, compresslevel: int = 9, /) -> Self: ...
|
||||
else:
|
||||
def __init__(self, compresslevel: int = 9, /) -> None: ...
|
||||
|
||||
def compress(self, data: ReadableBuffer, /) -> bytes: ...
|
||||
def flush(self) -> bytes: ...
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import ( # noqa: Y022,Y038
|
||||
AsyncIterator as AsyncIterator,
|
||||
Awaitable as Awaitable,
|
||||
Callable as Callable,
|
||||
ClassVar,
|
||||
Collection as Collection,
|
||||
Container as Container,
|
||||
Coroutine as Coroutine,
|
||||
@@ -74,6 +75,7 @@ _VT_co = TypeVar("_VT_co", covariant=True) # Value type covariant containers.
|
||||
class dict_keys(KeysView[_KT_co], Generic[_KT_co, _VT_co]): # undocumented
|
||||
def __eq__(self, value: object, /) -> bool: ...
|
||||
def __reversed__(self) -> Iterator[_KT_co]: ...
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
if sys.version_info >= (3, 13):
|
||||
def isdisjoint(self, other: Iterable[_KT_co], /) -> bool: ...
|
||||
if sys.version_info >= (3, 10):
|
||||
@@ -91,6 +93,7 @@ class dict_values(ValuesView[_VT_co], Generic[_KT_co, _VT_co]): # undocumented
|
||||
class dict_items(ItemsView[_KT_co, _VT_co]): # undocumented
|
||||
def __eq__(self, value: object, /) -> bool: ...
|
||||
def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ...
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
if sys.version_info >= (3, 13):
|
||||
def isdisjoint(self, other: Iterable[tuple[_KT_co, _VT_co]], /) -> bool: ...
|
||||
if sys.version_info >= (3, 10):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import sys
|
||||
from collections.abc import Callable, Iterator, Mapping
|
||||
from typing import Any, ClassVar, Generic, TypeVar, final, overload
|
||||
from typing_extensions import ParamSpec
|
||||
from typing_extensions import ParamSpec, Self
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
from types import GenericAlias
|
||||
@@ -13,9 +13,9 @@ _P = ParamSpec("_P")
|
||||
@final
|
||||
class ContextVar(Generic[_T]):
|
||||
@overload
|
||||
def __init__(self, name: str) -> None: ...
|
||||
def __new__(cls, name: str) -> Self: ...
|
||||
@overload
|
||||
def __init__(self, name: str, *, default: _T) -> None: ...
|
||||
def __new__(cls, name: str, *, default: _T) -> Self: ...
|
||||
def __hash__(self) -> int: ...
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
@@ -37,6 +37,7 @@ class Token(Generic[_T]):
|
||||
@property
|
||||
def old_value(self) -> Any: ... # returns either _T or MISSING, but that's hard to express
|
||||
MISSING: ClassVar[object]
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
if sys.version_info >= (3, 9):
|
||||
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
|
||||
|
||||
@@ -55,6 +56,7 @@ class Context(Mapping[ContextVar[Any], Any]):
|
||||
def get(self, key: ContextVar[_T], default: _D, /) -> _T | _D: ...
|
||||
def run(self, callable: Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs) -> _T: ...
|
||||
def copy(self) -> Context: ...
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
def __getitem__(self, key: ContextVar[_T], /) -> _T: ...
|
||||
def __iter__(self) -> Iterator[ContextVar[Any]]: ...
|
||||
def __len__(self) -> int: ...
|
||||
|
||||
@@ -32,8 +32,8 @@ class Dialect:
|
||||
lineterminator: str
|
||||
quoting: _QuotingType
|
||||
strict: bool
|
||||
def __init__(
|
||||
self,
|
||||
def __new__(
|
||||
cls,
|
||||
dialect: _DialectLike | None = ...,
|
||||
delimiter: str = ",",
|
||||
doublequote: bool = True,
|
||||
@@ -43,7 +43,7 @@ class Dialect:
|
||||
quoting: _QuotingType = 0,
|
||||
skipinitialspace: bool = False,
|
||||
strict: bool = False,
|
||||
) -> None: ...
|
||||
) -> Self: ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
# This class calls itself _csv.reader.
|
||||
@@ -115,7 +115,7 @@ def reader(
|
||||
) -> _reader: ...
|
||||
def register_dialect(
|
||||
name: str,
|
||||
dialect: type[Dialect] = ...,
|
||||
dialect: type[Dialect | csv.Dialect] = ...,
|
||||
*,
|
||||
delimiter: str = ",",
|
||||
quotechar: str | None = '"',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user