Compare commits
213 Commits
0.8.4
...
micha/dont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcde25773d | ||
|
|
c39ca8fe6d | ||
|
|
2d82445794 | ||
|
|
398f2e8b0c | ||
|
|
232fbc1300 | ||
|
|
c82932e580 | ||
|
|
12f86f39a4 | ||
|
|
2b28d566a4 | ||
|
|
adca7bd95c | ||
|
|
6b98a26452 | ||
|
|
c87463842a | ||
|
|
c364b586f9 | ||
|
|
73d424ee5e | ||
|
|
6e9ff445fd | ||
|
|
f2c3ddc5ea | ||
|
|
b861551b6a | ||
|
|
443bf38565 | ||
|
|
23ad319b55 | ||
|
|
3d9433ca66 | ||
|
|
baf068361a | ||
|
|
b33cf5baba | ||
|
|
b0905c4b04 | ||
|
|
d0b2bbd55e | ||
|
|
8628f169e9 | ||
|
|
8bc11c49b2 | ||
|
|
bf5b0c2688 | ||
|
|
097aa04c04 | ||
|
|
f706c3fdf2 | ||
|
|
29f6653318 | ||
|
|
d645525afc | ||
|
|
6dcf7b35b9 | ||
|
|
943d4fc160 | ||
|
|
3ea4c63d2c | ||
|
|
8e8a07144d | ||
|
|
225dd0a027 | ||
|
|
52aeb8ae11 | ||
|
|
71b6ac81a6 | ||
|
|
75fc2c3116 | ||
|
|
9c4d124ba0 | ||
|
|
8c620b9b4b | ||
|
|
1eda27d1a5 | ||
|
|
aaa86cf38d | ||
|
|
7821206b7b | ||
|
|
b76d05e283 | ||
|
|
424b720c19 | ||
|
|
a95deec00f | ||
|
|
21aa12a073 | ||
|
|
5f5eb7c0dd | ||
|
|
b6562ed57e | ||
|
|
4fd82d5f35 | ||
|
|
beb8e2dfe0 | ||
|
|
88d07202c1 | ||
|
|
2ca31e4b43 | ||
|
|
450d4e0e0c | ||
|
|
7284d68157 | ||
|
|
3820af2f1b | ||
|
|
ee9a912f47 | ||
|
|
1447553bc2 | ||
|
|
9a27b37a91 | ||
|
|
487f2f5df0 | ||
|
|
339167d372 | ||
|
|
235fdfc57a | ||
|
|
03ff883626 | ||
|
|
fdca2b422e | ||
|
|
71ad9a2ab1 | ||
|
|
3b3c2c5aa4 | ||
|
|
b2a0d68d70 | ||
|
|
f2a86fcfda | ||
|
|
ac72aca27c | ||
|
|
a876090715 | ||
|
|
e4139568b8 | ||
|
|
5567e7c26b | ||
|
|
95294e657c | ||
|
|
0dc00e63f4 | ||
|
|
ce9c4968ae | ||
|
|
066239fe5b | ||
|
|
1e948f739c | ||
|
|
78e26cec02 | ||
|
|
e7248ee43e | ||
|
|
065274d353 | ||
|
|
75a24bbc67 | ||
|
|
5e9259c96c | ||
|
|
d45c1ee44f | ||
|
|
636288038f | ||
|
|
832c0fa04b | ||
|
|
f29c9e48a4 | ||
|
|
bafe8714a8 | ||
|
|
e5270e2ac2 | ||
|
|
643fd7fe07 | ||
|
|
bd02beec85 | ||
|
|
7f299fceef | ||
|
|
6e2800df85 | ||
|
|
391332a835 | ||
|
|
84e13cea14 | ||
|
|
bcb5f621c5 | ||
|
|
47c8f1ad65 | ||
|
|
a4f8b9311e | ||
|
|
6097fd9bbe | ||
|
|
0743838438 | ||
|
|
980ce941c7 | ||
|
|
b26448926a | ||
|
|
2ea63620cf | ||
|
|
00aa387d9d | ||
|
|
eb82089551 | ||
|
|
f144b9684d | ||
|
|
df6e5c0293 | ||
|
|
8f0e01787f | ||
|
|
6b907c1305 | ||
|
|
f319531632 | ||
|
|
e4d9fe036a | ||
|
|
baf0d660eb | ||
|
|
bde8ecddca | ||
|
|
842f882ef0 | ||
|
|
75015b0ed9 | ||
|
|
706d87f239 | ||
|
|
0837cdd931 | ||
|
|
0dbfa8d0e0 | ||
|
|
1218bc65ed | ||
|
|
6180f78da4 | ||
|
|
835b453bfd | ||
|
|
a3d873ef66 | ||
|
|
d464ef67cf | ||
|
|
2355472d61 | ||
|
|
3c3f35a548 | ||
|
|
2327082c43 | ||
|
|
7671a3bbc7 | ||
|
|
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 |
10
.github/renovate.json5
vendored
10
.github/renovate.json5
vendored
@@ -45,7 +45,7 @@
|
||||
groupName: "Artifact GitHub Actions dependencies",
|
||||
matchManagers: ["github-actions"],
|
||||
matchDatasources: ["gitea-tags", "github-tags"],
|
||||
matchPackagePatterns: ["actions/.*-artifact"],
|
||||
matchPackageNames: ["actions/.*-artifact"],
|
||||
description: "Weekly update of artifact-related GitHub Actions dependencies",
|
||||
},
|
||||
{
|
||||
@@ -61,7 +61,7 @@
|
||||
{
|
||||
// Disable updates of `zip-rs`; intentionally pinned for now due to ownership change
|
||||
// See: https://github.com/astral-sh/uv/issues/3642
|
||||
matchPackagePatterns: ["zip"],
|
||||
matchPackageNames: ["zip"],
|
||||
matchManagers: ["cargo"],
|
||||
enabled: false,
|
||||
},
|
||||
@@ -70,7 +70,7 @@
|
||||
// with `mkdocs-material-insider`.
|
||||
// See: https://squidfunk.github.io/mkdocs-material/insiders/upgrade/
|
||||
matchManagers: ["pip_requirements"],
|
||||
matchPackagePatterns: ["mkdocs-material"],
|
||||
matchPackageNames: ["mkdocs-material"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
@@ -87,13 +87,13 @@
|
||||
{
|
||||
groupName: "Monaco",
|
||||
matchManagers: ["npm"],
|
||||
matchPackagePatterns: ["monaco"],
|
||||
matchPackageNames: ["monaco"],
|
||||
description: "Weekly update of the Monaco editor",
|
||||
},
|
||||
{
|
||||
groupName: "strum",
|
||||
matchManagers: ["cargo"],
|
||||
matchPackagePatterns: ["strum"],
|
||||
matchPackageNames: ["strum"],
|
||||
description: "Weekly update of strum dependencies",
|
||||
},
|
||||
{
|
||||
|
||||
12
.github/workflows/build-docker.yml
vendored
12
.github/workflows/build-docker.yml
vendored
@@ -48,11 +48,13 @@ jobs:
|
||||
|
||||
- name: Check tag consistency
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
env:
|
||||
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}
|
||||
run: |
|
||||
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
|
||||
if [ "${{ fromJson(inputs.plan).announcement_tag }}" != "${version}" ]; then
|
||||
if [ "${TAG}" != "${version}" ]; then
|
||||
echo "The input tag does not match the version from pyproject.toml:" >&2
|
||||
echo "${{ fromJson(inputs.plan).announcement_tag }}" >&2
|
||||
echo "${TAG}" >&2
|
||||
echo "${version}" >&2
|
||||
exit 1
|
||||
else
|
||||
@@ -175,6 +177,8 @@ jobs:
|
||||
|
||||
- name: Generate Dynamic Dockerfile Tags
|
||||
shell: bash
|
||||
env:
|
||||
TAG_VALUE: ${{ fromJson(inputs.plan).announcement_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -195,8 +199,8 @@ jobs:
|
||||
# Loop through all base tags and append its docker metadata pattern to the list
|
||||
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
|
||||
IFS=','; for TAG in ${BASE_TAGS}; do
|
||||
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
|
||||
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
|
||||
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${TAG_VALUE}\n"
|
||||
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${TAG_VALUE}\n"
|
||||
TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n"
|
||||
done
|
||||
|
||||
|
||||
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -349,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
|
||||
@@ -386,7 +386,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: ./scripts/add_rule.py --name DoTheThing --prefix PL --code C0999 --linter pylint
|
||||
- run: ./scripts/add_rule.py --name DoTheThing --prefix F --code 999 --linter pyflakes
|
||||
- run: cargo check
|
||||
- run: cargo fmt --all --check
|
||||
- run: |
|
||||
@@ -613,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' }}
|
||||
|
||||
4
.github/workflows/daily_fuzz.yaml
vendored
4
.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"
|
||||
@@ -73,6 +73,6 @@ jobs:
|
||||
owner: "astral-sh",
|
||||
repo: "ruff",
|
||||
title: `Daily parser fuzz failed on ${new Date().toDateString()}`,
|
||||
body: "Runs listed here: https://github.com/astral-sh/ruff/actions/workflows/daily_fuzz.yml",
|
||||
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
||||
labels: ["bug", "parser", "fuzzer"],
|
||||
})
|
||||
|
||||
71
.github/workflows/daily_property_tests.yaml
vendored
Normal file
71
.github/workflows/daily_property_tests.yaml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Daily property test run
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 12 * * *"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/daily_property_tests.yaml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
property_tests:
|
||||
name: Property tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
# Don't run the cron job on forks:
|
||||
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build Red Knot
|
||||
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
|
||||
# mode (1.5 min vs 14 min), so the overall time is shorter with a release build.
|
||||
run: cargo build --locked --release --package red_knot_python_semantic --tests
|
||||
- name: Run property tests
|
||||
shell: bash
|
||||
run: |
|
||||
export QUICKCHECK_TESTS=100000
|
||||
for _ in {1..5}; do
|
||||
cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable
|
||||
done
|
||||
|
||||
create-issue-on-failure:
|
||||
name: Create an issue if the daily property test run surfaced any bugs
|
||||
runs-on: ubuntu-latest
|
||||
needs: property_tests
|
||||
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.property_tests.result == 'failure' }}
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
await github.rest.issues.create({
|
||||
owner: "astral-sh",
|
||||
repo: "ruff",
|
||||
title: `Daily property test run failed on ${new Date().toDateString()}`,
|
||||
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
||||
labels: ["bug", "red-knot", "testing"],
|
||||
})
|
||||
3
.github/workflows/publish-docs.yml
vendored
3
.github/workflows/publish-docs.yml
vendored
@@ -33,8 +33,9 @@ jobs:
|
||||
python-version: 3.12
|
||||
|
||||
- name: "Set docs version"
|
||||
env:
|
||||
version: ${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}
|
||||
run: |
|
||||
version="${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}"
|
||||
# if version is missing, use 'latest'
|
||||
if [ -z "$version" ]; then
|
||||
echo "Using 'latest' as version"
|
||||
|
||||
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-*
|
||||
|
||||
6
.github/zizmor.yml
vendored
6
.github/zizmor.yml
vendored
@@ -1,6 +1,12 @@
|
||||
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
|
||||
# https://woodruffw.github.io/zizmor/configuration/
|
||||
#
|
||||
# TODO: can we remove the ignores here so that our workflows are more secure?
|
||||
rules:
|
||||
dangerous-triggers:
|
||||
ignore:
|
||||
- pr-comment.yaml
|
||||
cache-poisoning:
|
||||
ignore:
|
||||
- build-docker.yml
|
||||
- publish-playground.yml
|
||||
|
||||
@@ -23,7 +23,7 @@ repos:
|
||||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/executablebooks/mdformat
|
||||
rev: 0.7.19
|
||||
rev: 0.7.21
|
||||
hooks:
|
||||
- id: mdformat
|
||||
additional_dependencies:
|
||||
@@ -59,7 +59,7 @@ repos:
|
||||
- black==24.10.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.28.3
|
||||
rev: v1.29.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.3
|
||||
rev: v0.8.6
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -91,7 +91,7 @@ repos:
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v0.9.2
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
@@ -103,7 +103,7 @@ repos:
|
||||
# `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.4
|
||||
rev: v1.7.6
|
||||
hooks:
|
||||
- id: actionlint
|
||||
stages:
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.9.0
|
||||
|
||||
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the [changelog](./CHANGELOG.md#090) for a detailed list of changes.
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- **Default to Python 3.9**
|
||||
|
||||
175
CHANGELOG.md
175
CHANGELOG.md
@@ -1,5 +1,180 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`pycodestyle`\] Run `too-many-newlines-at-end-of-file` on each cell in notebooks (`W391`) ([#15308](https://github.com/astral-sh/ruff/pull/15308))
|
||||
- \[`ruff`\] Omit diagnostic for shadowed private function parameters in `used-dummy-variable` (`RUF052`) ([#15376](https://github.com/astral-sh/ruff/pull/15376))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-bugbear`\] Improve `assert-raises-exception` message (`B017`) ([#15389](https://github.com/astral-sh/ruff/pull/15389))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Preserve trailing end-of line comments for the last string literal in implicitly concatenated strings ([#15378](https://github.com/astral-sh/ruff/pull/15378))
|
||||
|
||||
### Server
|
||||
|
||||
- Fix a bug where the server and client notebooks were out of sync after reordering cells ([#15398](https://github.com/astral-sh/ruff/pull/15398))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-pie`\] Correctly remove wrapping parentheses (`PIE800`) ([#15394](https://github.com/astral-sh/ruff/pull/15394))
|
||||
- \[`pyupgrade`\] Handle comments and multiline expressions correctly (`UP037`) ([#15337](https://github.com/astral-sh/ruff/pull/15337))
|
||||
|
||||
## 0.9.0
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.9.0) for a migration guide and overview of the changes!
|
||||
|
||||
### Breaking changes
|
||||
|
||||
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the formatter section for a detailed list of changes.
|
||||
|
||||
This release doesn’t remove or remap any existing stable rules.
|
||||
|
||||
### Stabilization
|
||||
|
||||
The following rules have been stabilized and are no longer in preview:
|
||||
|
||||
- [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`).
|
||||
This rule has also been renamed: previously, it was called `builtin-module-shadowing`.
|
||||
- [`builtin-lambda-argument-shadowing`](https://docs.astral.sh/ruff/rules/builtin-lambda-argument-shadowing/) (`A006`)
|
||||
- [`slice-to-remove-prefix-or-suffix`](https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/) (`FURB188`)
|
||||
- [`boolean-chained-comparison`](https://docs.astral.sh/ruff/rules/boolean-chained-comparison/) (`PLR1716`)
|
||||
- [`decimal-from-float-literal`](https://docs.astral.sh/ruff/rules/decimal-from-float-literal/) (`RUF032`)
|
||||
- [`post-init-default`](https://docs.astral.sh/ruff/rules/post-init-default/) (`RUF033`)
|
||||
- [`useless-if-else`](https://docs.astral.sh/ruff/rules/useless-if-else/) (`RUF034`)
|
||||
|
||||
The following behaviors have been stabilized:
|
||||
|
||||
- [`pytest-parametrize-names-wrong-type`](https://docs.astral.sh/ruff/rules/pytest-parametrize-names-wrong-type/) (`PT006`): Detect [`pytest.parametrize`](https://docs.pytest.org/en/7.1.x/how-to/parametrize.html#parametrize) calls outside decorators and calls with keyword arguments.
|
||||
- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`): Ignore [`pytest.importorskip`](https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest-importorskip) calls between import statements.
|
||||
- [`mutable-dataclass-default`](https://docs.astral.sh/ruff/rules/mutable-dataclass-default/) (`RUF008`) and [`function-call-in-dataclass-default-argument`](https://docs.astral.sh/ruff/rules/function-call-in-dataclass-default-argument/) (`RUF009`): Add support for [`attrs`](https://www.attrs.org/en/stable/).
|
||||
- [`bad-version-info-comparison`](https://docs.astral.sh/ruff/rules/bad-version-info-comparison/) (`PYI006`): Extend the rule to check non-stub files.
|
||||
|
||||
The following fixes or improvements to fixes have been stabilized:
|
||||
|
||||
- [`redundant-numeric-union`](https://docs.astral.sh/ruff/rules/redundant-numeric-union/) (`PYI041`)
|
||||
- [`duplicate-union-members`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`)
|
||||
|
||||
### Formatter
|
||||
|
||||
This release introduces the new 2025 stable style ([#13371](https://github.com/astral-sh/ruff/issues/13371)), stabilizing the following changes:
|
||||
|
||||
- Format expressions in f-string elements ([#7594](https://github.com/astral-sh/ruff/issues/7594))
|
||||
- Alternate quotes for strings inside f-strings ([#13860](https://github.com/astral-sh/ruff/pull/13860))
|
||||
- Preserve the casing of hex codes in f-string debug expressions ([#14766](https://github.com/astral-sh/ruff/issues/14766))
|
||||
- Choose the quote style for each string literal in an implicitly concatenated f-string rather than for the entire string ([#13539](https://github.com/astral-sh/ruff/pull/13539))
|
||||
- Automatically join an implicitly concatenated string into a single string literal if it fits on a single line ([#9457](https://github.com/astral-sh/ruff/issues/9457))
|
||||
- Remove the [`ISC001`](https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/) incompatibility warning ([#15123](https://github.com/astral-sh/ruff/pull/15123))
|
||||
- Prefer parenthesizing the `assert` message over breaking the assertion expression ([#9457](https://github.com/astral-sh/ruff/issues/9457))
|
||||
- Automatically parenthesize over-long `if` guards in `match` `case` clauses ([#13513](https://github.com/astral-sh/ruff/pull/13513))
|
||||
- More consistent formatting for `match` `case` patterns ([#6933](https://github.com/astral-sh/ruff/issues/6933))
|
||||
- Avoid unnecessary parentheses around return type annotations ([#13381](https://github.com/astral-sh/ruff/pull/13381))
|
||||
- Keep the opening parentheses on the same line as the `if` keyword for comprehensions where the condition has a leading comment ([#12282](https://github.com/astral-sh/ruff/pull/12282))
|
||||
- More consistent formatting for `with` statements with a single context manager for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276))
|
||||
- Correctly calculate the line-width for code blocks in docstrings when using `max-doc-code-line-length = "dynamic"` ([#13523](https://github.com/astral-sh/ruff/pull/13523))
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-bugbear`\] Implement `class-as-data-structure` (`B903`) ([#9601](https://github.com/astral-sh/ruff/pull/9601))
|
||||
- \[`flake8-type-checking`\] Apply `quoted-type-alias` more eagerly in `TYPE_CHECKING` blocks and ignore it in stubs (`TC008`) ([#15180](https://github.com/astral-sh/ruff/pull/15180))
|
||||
- \[`pylint`\] Ignore `eq-without-hash` in stub files (`PLW1641`) ([#15310](https://github.com/astral-sh/ruff/pull/15310))
|
||||
- \[`pyupgrade`\] Split `UP007` into two individual rules: `UP007` for `Union` and `UP045` for `Optional` (`UP007`, `UP045`) ([#15313](https://github.com/astral-sh/ruff/pull/15313))
|
||||
- \[`ruff`\] New rule that detects classes that are both an enum and a `dataclass` (`RUF049`) ([#15299](https://github.com/astral-sh/ruff/pull/15299))
|
||||
- \[`ruff`\] Recode `RUF025` to `RUF037` (`RUF037`) ([#15258](https://github.com/astral-sh/ruff/pull/15258))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-builtins`\] Ignore [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) in stub files(`A005`) ([#15350](https://github.com/astral-sh/ruff/pull/15350))
|
||||
- \[`flake8-return`\] Add support for functions returning `typing.Never` (`RET503`) ([#15298](https://github.com/astral-sh/ruff/pull/15298))
|
||||
|
||||
### Server
|
||||
|
||||
- Improve the observability by removing the need for the ["trace" value](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#traceValue) to turn on or off logging. The server logging is solely controlled using the [`logLevel` server setting](https://docs.astral.sh/ruff/editors/settings/#loglevel)
|
||||
which defaults to `info`. This addresses the issue where users were notified about an error and told to consult the log, but it didn’t contain any messages. ([#15232](https://github.com/astral-sh/ruff/pull/15232))
|
||||
- Ignore diagnostics from other sources for code action requests ([#15373](https://github.com/astral-sh/ruff/pull/15373))
|
||||
|
||||
### CLI
|
||||
|
||||
- Improve the error message for `--config key=value` when the `key` is for a table and it’s a simple `value`
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`eradicate`\] Ignore metadata blocks directly followed by normal blocks (`ERA001`) ([#15330](https://github.com/astral-sh/ruff/pull/15330))
|
||||
- \[`flake8-django`\] Recognize other magic methods (`DJ012`) ([#15365](https://github.com/astral-sh/ruff/pull/15365))
|
||||
- \[`pycodestyle`\] Avoid false positives related to type aliases (`E252`) ([#15356](https://github.com/astral-sh/ruff/pull/15356))
|
||||
- \[`pydocstyle`\] Avoid treating newline-separated sections as sub-sections (`D405`) ([#15311](https://github.com/astral-sh/ruff/pull/15311))
|
||||
- \[`pyflakes`\] Remove call when removing final argument from `format` (`F523`) ([#15309](https://github.com/astral-sh/ruff/pull/15309))
|
||||
- \[`refurb`\] Mark fix as unsafe when the right-hand side is a string (`FURB171`) ([#15273](https://github.com/astral-sh/ruff/pull/15273))
|
||||
- \[`ruff`\] Treat `)` as a regex metacharacter (`RUF043`, `RUF055`) ([#15318](https://github.com/astral-sh/ruff/pull/15318))
|
||||
- \[`ruff`\] Parenthesize the `int`-call argument when removing the `int` call would change semantics (`RUF046`) ([#15277](https://github.com/astral-sh/ruff/pull/15277))
|
||||
|
||||
## 0.8.6
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`format`\]: Preserve multiline implicit concatenated strings in docstring positions ([#15126](https://github.com/astral-sh/ruff/pull/15126))
|
||||
- \[`ruff`\] Add rule to detect empty literal in deque call (`RUF025`) ([#15104](https://github.com/astral-sh/ruff/pull/15104))
|
||||
- \[`ruff`\] Avoid reporting when `ndigits` is possibly negative (`RUF057`) ([#15234](https://github.com/astral-sh/ruff/pull/15234))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-todos`\] remove issue code length restriction (`TD003`) ([#15175](https://github.com/astral-sh/ruff/pull/15175))
|
||||
- \[`pyflakes`\] Ignore errors in `@no_type_check` string annotations (`F722`, `F821`) ([#15215](https://github.com/astral-sh/ruff/pull/15215))
|
||||
|
||||
### CLI
|
||||
|
||||
- Show errors for attempted fixes only when passed `--verbose` ([#15237](https://github.com/astral-sh/ruff/pull/15237))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`ruff`\] Avoid syntax error when removing int over multiple lines (`RUF046`) ([#15230](https://github.com/astral-sh/ruff/pull/15230))
|
||||
- \[`pyupgrade`\] Revert "Add all PEP-585 names to `UP006` rule" ([#15250](https://github.com/astral-sh/ruff/pull/15250))
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
283
Cargo.lock
generated
283
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.1"
|
||||
version = "1.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
|
||||
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
@@ -291,12 +291,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
@@ -413,7 +407,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -424,15 +418,15 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "clearscreen"
|
||||
version = "3.0.0"
|
||||
version = "4.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f8c93eb5f77c9050c7750e14f13ef1033a40a0aac70c6371535b6763a01438c"
|
||||
checksum = "8c41dc435a7b98e4608224bbf65282309f5403719df9113621b30f8b6f74e2f4"
|
||||
dependencies = [
|
||||
"nix 0.28.0",
|
||||
"nix",
|
||||
"terminfo",
|
||||
"thiserror 1.0.67",
|
||||
"thiserror 2.0.9",
|
||||
"which",
|
||||
"winapi",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -470,14 +464,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[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",
|
||||
@@ -668,7 +662,7 @@ version = "3.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
|
||||
dependencies = [
|
||||
"nix 0.29.0",
|
||||
"nix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -693,7 +687,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -704,7 +698,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -774,16 +768,7 @@ dependencies = [
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||
dependencies = [
|
||||
"dirs-sys 0.3.7",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -792,18 +777,7 @@ version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -826,7 +800,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -876,10 +850,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.5"
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1019,9 +999,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 +1226,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1365,14 +1345,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.41.1"
|
||||
version = "1.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8"
|
||||
checksum = "6513e4067e16e69ed1db5ab56048ed65db32d10ba5fc1217f5393f8f17d8b5a5"
|
||||
dependencies = [
|
||||
"console",
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"linked-hash-map",
|
||||
"once_cell",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"regex",
|
||||
@@ -1420,7 +1400,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1462,6 +1442,15 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
@@ -1521,9 +1510,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.168"
|
||||
version = "0.2.169"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
|
||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -1547,7 +1536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1607,13 +1596,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",
|
||||
]
|
||||
|
||||
@@ -1646,9 +1636,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd0aa4b8ca861b08d68afc8702af3250776898c1508b278e1da9d01e01d4b45c"
|
||||
checksum = "2f926ade0c4e170215ae43342bf13b9310a437609c81f29f86c5df6657582ef9"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@@ -1718,18 +1708,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.1.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
@@ -1738,7 +1716,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.2.1",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -2014,7 +1992,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2161,7 +2139,7 @@ dependencies = [
|
||||
"newtype-uuid",
|
||||
"quick-xml",
|
||||
"strip-ansi-escapes",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.9",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2196,9 +2174,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",
|
||||
]
|
||||
@@ -2292,7 +2270,7 @@ dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
"indexmap",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"memchr",
|
||||
"ordermap",
|
||||
"quickcheck",
|
||||
@@ -2306,6 +2284,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",
|
||||
@@ -2315,7 +2294,7 @@ dependencies = [
|
||||
"static_assertions",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.9",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -2412,7 +2391,7 @@ dependencies = [
|
||||
"rustc-hash 2.1.0",
|
||||
"salsa",
|
||||
"serde",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.9",
|
||||
"toml",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2518,7 +2497,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.4"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2538,7 +2517,7 @@ dependencies = [
|
||||
"insta",
|
||||
"insta-cmd",
|
||||
"is-macro",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"mimalloc",
|
||||
"notify",
|
||||
@@ -2565,7 +2544,7 @@ dependencies = [
|
||||
"strum",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.9",
|
||||
"tikv-jemallocator",
|
||||
"toml",
|
||||
"tracing",
|
||||
@@ -2604,7 +2583,7 @@ dependencies = [
|
||||
"filetime",
|
||||
"glob",
|
||||
"globset",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"regex",
|
||||
"ruff_macros",
|
||||
"seahash",
|
||||
@@ -2635,7 +2614,7 @@ dependencies = [
|
||||
"salsa",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.9",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
@@ -2653,7 +2632,7 @@ dependencies = [
|
||||
"imara-diff",
|
||||
"indicatif",
|
||||
"indoc",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"libcst",
|
||||
"pretty_assertions",
|
||||
"rayon",
|
||||
@@ -2737,7 +2716,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.8.4"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2753,7 +2732,7 @@ dependencies = [
|
||||
"insta",
|
||||
"is-macro",
|
||||
"is-wsl",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"libcst",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -2766,6 +2745,7 @@ dependencies = [
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
"ruff_diagnostics",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -2787,7 +2767,7 @@ dependencies = [
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"test-case",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.9",
|
||||
"toml",
|
||||
"typed-arena",
|
||||
"unicode-normalization",
|
||||
@@ -2800,11 +2780,11 @@ dependencies = [
|
||||
name = "ruff_macros"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2812,7 +2792,7 @@ name = "ruff_notebook"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"rand",
|
||||
"ruff_diagnostics",
|
||||
"ruff_source_file",
|
||||
@@ -2821,7 +2801,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"test-case",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.9",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2833,7 +2813,7 @@ dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"compact_str",
|
||||
"is-macro",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"memchr",
|
||||
"ruff_cache",
|
||||
"ruff_macros",
|
||||
@@ -2875,7 +2855,7 @@ dependencies = [
|
||||
"clap",
|
||||
"countme",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"memchr",
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
@@ -2893,7 +2873,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.9",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -2913,7 +2893,7 @@ name = "ruff_python_literal"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"ruff_python_ast",
|
||||
"unic-ucd-category",
|
||||
]
|
||||
@@ -2967,6 +2947,7 @@ dependencies = [
|
||||
"rustc-hash 2.1.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2981,7 +2962,7 @@ dependencies = [
|
||||
name = "ruff_python_trivia"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"unicode-ident",
|
||||
@@ -3026,7 +3007,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.9",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
@@ -3052,7 +3033,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.8.4"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3086,7 +3067,7 @@ dependencies = [
|
||||
"globset",
|
||||
"ignore",
|
||||
"is-macro",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"matchit",
|
||||
"path-absolutize",
|
||||
@@ -3194,7 +3175,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
@@ -3202,7 +3183,6 @@ dependencies = [
|
||||
"dashmap 6.1.0",
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"rustc-hash 2.1.0",
|
||||
@@ -3215,17 +3195,17 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -3259,7 +3239,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3282,9 +3262,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.216"
|
||||
version = "1.0.217"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -3302,13 +3282,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.216"
|
||||
version = "1.0.217"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3319,14 +3299,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[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",
|
||||
@@ -3342,7 +3322,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3365,9 +3345,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",
|
||||
@@ -3376,14 +3356,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.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3412,7 +3392,7 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
|
||||
dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"dirs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3497,7 +3477,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3519,9 +3499,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.90"
|
||||
version = "2.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
||||
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3536,7 +3516,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3564,11 +3544,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "terminfo"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f"
|
||||
checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
|
||||
dependencies = [
|
||||
"dirs 4.0.0",
|
||||
"fnv",
|
||||
"nom",
|
||||
"phf",
|
||||
@@ -3599,7 +3578,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3610,7 +3589,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
@@ -3625,11 +3604,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.7"
|
||||
version = "2.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767"
|
||||
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.7",
|
||||
"thiserror-impl 2.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3640,18 +3619,18 @@ checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.7"
|
||||
version = "2.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36"
|
||||
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3773,7 +3752,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4043,7 +4022,7 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4138,7 +4117,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -4173,7 +4152,7 @@ checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -4207,7 +4186,7 @@ checksum = "222ebde6ea87fbfa6bdd2e9f1fd8a91d60aee5db68792632176c4e16a74fc7d8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4241,12 +4220,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "6.0.1"
|
||||
version = "7.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7"
|
||||
checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"env_home",
|
||||
"rustix",
|
||||
"winsafe",
|
||||
]
|
||||
@@ -4510,7 +4489,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4531,7 +4510,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4551,7 +4530,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4580,7 +4559,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -55,7 +55,7 @@ camino = { version = "1.1.7" }
|
||||
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.5.3", features = ["derive"] }
|
||||
clap_complete_command = { version = "0.6.0" }
|
||||
clearscreen = { version = "3.0.0" }
|
||||
clearscreen = { version = "4.0.0" }
|
||||
codspeed-criterion-compat = { version = "2.6.0", default-features = false }
|
||||
colored = { version = "2.1.0" }
|
||||
console_error_panic_hook = { version = "0.1.7" }
|
||||
@@ -89,7 +89,7 @@ insta = { version = "1.35.1" }
|
||||
insta-cmd = { version = "0.6.0" }
|
||||
is-macro = { version = "0.3.5" }
|
||||
is-wsl = { version = "0.4.0" }
|
||||
itertools = { version = "0.13.0" }
|
||||
itertools = { version = "0.14.0" }
|
||||
js-sys = { version = "0.3.69" }
|
||||
jod-thread = { version = "0.1.2" }
|
||||
libc = { version = "0.2.153" }
|
||||
@@ -119,7 +119,7 @@ rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3c7f1694c9efba751dbeeacfbc93b227586e316a" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -211,6 +211,9 @@ redundant_clone = "warn"
|
||||
debug_assert_with_mut_call = "warn"
|
||||
unused_peekable = "warn"
|
||||
|
||||
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
|
||||
large_stack_arrays = "allow"
|
||||
|
||||
[profile.release]
|
||||
# Note that we set these explicitly, and these values
|
||||
# were chosen based on a trade-off between compile times
|
||||
|
||||
23
README.md
23
README.md
@@ -116,12 +116,21 @@ For more, see the [documentation](https://docs.astral.sh/ruff/).
|
||||
|
||||
### Installation
|
||||
|
||||
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
|
||||
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI.
|
||||
|
||||
Invoke Ruff directly with [`uvx`](https://docs.astral.sh/uv/):
|
||||
|
||||
```shell
|
||||
uvx ruff check # Lint all files in the current directory.
|
||||
uvx ruff format # Format all files in the current directory.
|
||||
```
|
||||
|
||||
Or install Ruff with `uv` (recommended), `pip`, or `pipx`:
|
||||
|
||||
```shell
|
||||
# With uv.
|
||||
uv add --dev ruff # to add ruff to your project
|
||||
uv tool install ruff # to install ruff globally
|
||||
uv tool install ruff@latest # Install Ruff globally.
|
||||
uv add --dev ruff # Or add Ruff to your project.
|
||||
|
||||
# With pip.
|
||||
pip install ruff
|
||||
@@ -140,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.8.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.4/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.9.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.1/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 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.4
|
||||
rev: v0.9.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -196,7 +205,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>
|
||||
|
||||
@@ -20,6 +20,7 @@ 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 }
|
||||
|
||||
@@ -9,8 +9,6 @@ from typing import Literal
|
||||
from enum import Enum
|
||||
|
||||
mode: Literal["w", "r"]
|
||||
mode2: Literal["w"] | Literal["r"]
|
||||
union_var: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
|
||||
a1: Literal[26]
|
||||
a2: Literal[0x1A]
|
||||
a3: Literal[-4]
|
||||
@@ -19,7 +17,6 @@ a5: Literal[b"hello world"]
|
||||
a6: Literal[True]
|
||||
a7: Literal[None]
|
||||
a8: Literal[Literal[1]]
|
||||
a9: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]]
|
||||
|
||||
class Color(Enum):
|
||||
RED = 0
|
||||
@@ -30,9 +27,6 @@ b1: Literal[Color.RED]
|
||||
|
||||
def f():
|
||||
reveal_type(mode) # revealed: Literal["w", "r"]
|
||||
reveal_type(mode2) # revealed: Literal["w", "r"]
|
||||
# TODO: should be revealed: Literal[1, 2, 3, "foo", 5] | None
|
||||
reveal_type(union_var) # revealed: Literal[1, 2, 3, 5] | Literal["foo"] | None
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
reveal_type(a2) # revealed: Literal[26]
|
||||
reveal_type(a3) # revealed: Literal[-4]
|
||||
@@ -41,7 +35,6 @@ def f():
|
||||
reveal_type(a6) # revealed: Literal[True]
|
||||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
reveal_type(a9) # revealed: Literal["w", "r", "w+"]
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: Literal[0]
|
||||
|
||||
@@ -61,6 +54,63 @@ invalid4: Literal[
|
||||
]
|
||||
```
|
||||
|
||||
## Shortening unions of literals
|
||||
|
||||
When a Literal is parameterized with more than one value, it’s treated as exactly to equivalent to
|
||||
the union of those types.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def x(
|
||||
a1: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None],
|
||||
a2: Literal["w"] | Literal["r"],
|
||||
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
|
||||
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
|
||||
):
|
||||
reveal_type(a1) # revealed: Literal[1, 2, 3, "foo", 5] | None
|
||||
reveal_type(a2) # revealed: Literal["w", "r"]
|
||||
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
|
||||
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
|
||||
```
|
||||
|
||||
## Display of heterogeneous unions of literals
|
||||
|
||||
```py
|
||||
from typing import Literal, Union
|
||||
|
||||
def foo(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
def bar(s: str) -> str:
|
||||
return s
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def union_example(
|
||||
x: Union[
|
||||
# unknown type
|
||||
# error: [unresolved-reference]
|
||||
y,
|
||||
Literal[-1],
|
||||
Literal["A"],
|
||||
Literal[b"A"],
|
||||
Literal[b"\x00"],
|
||||
Literal[b"\x07"],
|
||||
Literal[0],
|
||||
Literal[1],
|
||||
Literal["B"],
|
||||
Literal["foo"],
|
||||
Literal["bar"],
|
||||
Literal["B"],
|
||||
Literal[True],
|
||||
None,
|
||||
]
|
||||
):
|
||||
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
|
||||
```
|
||||
|
||||
## Detecting Literal outside typing and typing_extensions
|
||||
|
||||
Only Literal that is defined in typing and typing_extension modules is detected as the special
|
||||
|
||||
@@ -107,7 +107,7 @@ def _(flag: bool):
|
||||
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
|
||||
|
||||
baz_3 = "foo" if flag else 1
|
||||
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
|
||||
reveal_type(baz_3) # revealed: Literal["foo", 1]
|
||||
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -105,7 +105,7 @@ def f1(
|
||||
from typing import Literal
|
||||
|
||||
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
|
||||
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
|
||||
reveal_type(v) # revealed: Literal["a", "b", b"c", "de", "f", "g", "h"]
|
||||
```
|
||||
|
||||
## Class variables
|
||||
|
||||
@@ -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]] = ([], [])
|
||||
@@ -124,3 +122,10 @@ class Foo: ...
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
|
||||
## Annotated assignments in stub files are inferred correctly
|
||||
|
||||
```pyi path=main.pyi
|
||||
x: int = 1
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -40,9 +40,9 @@ class C:
|
||||
return 42
|
||||
|
||||
x = C()
|
||||
# error: [invalid-argument-type]
|
||||
x -= 1
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
reveal_type(x) # revealed: 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)
|
||||
```
|
||||
|
||||
@@ -46,3 +46,50 @@ reveal_type(a | b) # revealed: Literal[True]
|
||||
reveal_type(b | a) # revealed: Literal[True]
|
||||
reveal_type(b | b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Arithmetic with a variable
|
||||
|
||||
```py
|
||||
a = True
|
||||
b = False
|
||||
|
||||
def lhs_is_int(x: int):
|
||||
reveal_type(x + a) # revealed: int
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: float
|
||||
reveal_type(x % a) # revealed: int
|
||||
|
||||
def rhs_is_int(x: int):
|
||||
reveal_type(a + x) # revealed: int
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def lhs_is_bool(x: bool):
|
||||
reveal_type(x + a) # revealed: int
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: float
|
||||
reveal_type(x % a) # revealed: int
|
||||
|
||||
def rhs_is_bool(x: bool):
|
||||
reveal_type(a + x) # revealed: int
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def both_are_bool(x: bool, y: bool):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(x - y) # revealed: int
|
||||
reveal_type(x * y) # revealed: int
|
||||
reveal_type(x // y) # revealed: int
|
||||
reveal_type(x / y) # revealed: float
|
||||
reveal_type(x % y) # revealed: int
|
||||
```
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Binary operations on classes
|
||||
|
||||
## Union of two classes
|
||||
|
||||
Unioning two classes via the `|` operator is only available in Python 3.10 and later.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
reveal_type(A | B) # revealed: UnionType
|
||||
```
|
||||
|
||||
## Union of two classes (prior to 3.10)
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A | B) # revealed: Unknown
|
||||
```
|
||||
@@ -0,0 +1,371 @@
|
||||
# Custom binary operations
|
||||
|
||||
## Class instances
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
def __sub__(self, other) -> Literal["-"]:
|
||||
return "-"
|
||||
|
||||
def __mul__(self, other) -> Literal["*"]:
|
||||
return "*"
|
||||
|
||||
def __matmul__(self, other) -> Literal["@"]:
|
||||
return "@"
|
||||
|
||||
def __truediv__(self, other) -> Literal["/"]:
|
||||
return "/"
|
||||
|
||||
def __mod__(self, other) -> Literal["%"]:
|
||||
return "%"
|
||||
|
||||
def __pow__(self, other) -> Literal["**"]:
|
||||
return "**"
|
||||
|
||||
def __lshift__(self, other) -> Literal["<<"]:
|
||||
return "<<"
|
||||
|
||||
def __rshift__(self, other) -> Literal[">>"]:
|
||||
return ">>"
|
||||
|
||||
def __or__(self, other) -> Literal["|"]:
|
||||
return "|"
|
||||
|
||||
def __xor__(self, other) -> Literal["^"]:
|
||||
return "^"
|
||||
|
||||
def __and__(self, other) -> Literal["&"]:
|
||||
return "&"
|
||||
|
||||
def __floordiv__(self, other) -> Literal["//"]:
|
||||
return "//"
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
# Yes implements all of the dunder methods.
|
||||
reveal_type(Yes() + Yes()) # revealed: Literal["+"]
|
||||
reveal_type(Yes() - Yes()) # revealed: Literal["-"]
|
||||
reveal_type(Yes() * Yes()) # revealed: Literal["*"]
|
||||
reveal_type(Yes() @ Yes()) # revealed: Literal["@"]
|
||||
reveal_type(Yes() / Yes()) # revealed: Literal["/"]
|
||||
reveal_type(Yes() % Yes()) # revealed: Literal["%"]
|
||||
reveal_type(Yes() ** Yes()) # revealed: Literal["**"]
|
||||
reveal_type(Yes() << Yes()) # revealed: Literal["<<"]
|
||||
reveal_type(Yes() >> Yes()) # revealed: Literal[">>"]
|
||||
reveal_type(Yes() | Yes()) # revealed: Literal["|"]
|
||||
reveal_type(Yes() ^ Yes()) # revealed: Literal["^"]
|
||||
reveal_type(Yes() & Yes()) # revealed: Literal["&"]
|
||||
reveal_type(Yes() // Yes()) # revealed: Literal["//"]
|
||||
|
||||
# Sub inherits Yes's implementation of the dunder methods.
|
||||
reveal_type(Sub() + Sub()) # revealed: Literal["+"]
|
||||
reveal_type(Sub() - Sub()) # revealed: Literal["-"]
|
||||
reveal_type(Sub() * Sub()) # revealed: Literal["*"]
|
||||
reveal_type(Sub() @ Sub()) # revealed: Literal["@"]
|
||||
reveal_type(Sub() / Sub()) # revealed: Literal["/"]
|
||||
reveal_type(Sub() % Sub()) # revealed: Literal["%"]
|
||||
reveal_type(Sub() ** Sub()) # revealed: Literal["**"]
|
||||
reveal_type(Sub() << Sub()) # revealed: Literal["<<"]
|
||||
reveal_type(Sub() >> Sub()) # revealed: Literal[">>"]
|
||||
reveal_type(Sub() | Sub()) # revealed: Literal["|"]
|
||||
reveal_type(Sub() ^ Sub()) # revealed: Literal["^"]
|
||||
reveal_type(Sub() & Sub()) # revealed: Literal["&"]
|
||||
reveal_type(Sub() // Sub()) # revealed: Literal["//"]
|
||||
|
||||
# No does not implement any of the dunder methods.
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() + No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() - No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() * No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() @ No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() / No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() % No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() ** No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() << No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() >> No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() | No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() ^ No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() & No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() // No()) # revealed: Unknown
|
||||
|
||||
# Yes does not implement any of the reflected dunder methods.
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() + Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() - Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() * Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() @ Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() / Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() % Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() ** Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() << Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() >> Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() | Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() ^ Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() & Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() // Yes()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Subclass reflections override superclass dunders
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
def __sub__(self, other) -> Literal["-"]:
|
||||
return "-"
|
||||
|
||||
def __mul__(self, other) -> Literal["*"]:
|
||||
return "*"
|
||||
|
||||
def __matmul__(self, other) -> Literal["@"]:
|
||||
return "@"
|
||||
|
||||
def __truediv__(self, other) -> Literal["/"]:
|
||||
return "/"
|
||||
|
||||
def __mod__(self, other) -> Literal["%"]:
|
||||
return "%"
|
||||
|
||||
def __pow__(self, other) -> Literal["**"]:
|
||||
return "**"
|
||||
|
||||
def __lshift__(self, other) -> Literal["<<"]:
|
||||
return "<<"
|
||||
|
||||
def __rshift__(self, other) -> Literal[">>"]:
|
||||
return ">>"
|
||||
|
||||
def __or__(self, other) -> Literal["|"]:
|
||||
return "|"
|
||||
|
||||
def __xor__(self, other) -> Literal["^"]:
|
||||
return "^"
|
||||
|
||||
def __and__(self, other) -> Literal["&"]:
|
||||
return "&"
|
||||
|
||||
def __floordiv__(self, other) -> Literal["//"]:
|
||||
return "//"
|
||||
|
||||
class Sub(Yes):
|
||||
def __radd__(self, other) -> Literal["r+"]:
|
||||
return "r+"
|
||||
|
||||
def __rsub__(self, other) -> Literal["r-"]:
|
||||
return "r-"
|
||||
|
||||
def __rmul__(self, other) -> Literal["r*"]:
|
||||
return "r*"
|
||||
|
||||
def __rmatmul__(self, other) -> Literal["r@"]:
|
||||
return "r@"
|
||||
|
||||
def __rtruediv__(self, other) -> Literal["r/"]:
|
||||
return "r/"
|
||||
|
||||
def __rmod__(self, other) -> Literal["r%"]:
|
||||
return "r%"
|
||||
|
||||
def __rpow__(self, other) -> Literal["r**"]:
|
||||
return "r**"
|
||||
|
||||
def __rlshift__(self, other) -> Literal["r<<"]:
|
||||
return "r<<"
|
||||
|
||||
def __rrshift__(self, other) -> Literal["r>>"]:
|
||||
return "r>>"
|
||||
|
||||
def __ror__(self, other) -> Literal["r|"]:
|
||||
return "r|"
|
||||
|
||||
def __rxor__(self, other) -> Literal["r^"]:
|
||||
return "r^"
|
||||
|
||||
def __rand__(self, other) -> Literal["r&"]:
|
||||
return "r&"
|
||||
|
||||
def __rfloordiv__(self, other) -> Literal["r//"]:
|
||||
return "r//"
|
||||
|
||||
class No:
|
||||
def __radd__(self, other) -> Literal["r+"]:
|
||||
return "r+"
|
||||
|
||||
def __rsub__(self, other) -> Literal["r-"]:
|
||||
return "r-"
|
||||
|
||||
def __rmul__(self, other) -> Literal["r*"]:
|
||||
return "r*"
|
||||
|
||||
def __rmatmul__(self, other) -> Literal["r@"]:
|
||||
return "r@"
|
||||
|
||||
def __rtruediv__(self, other) -> Literal["r/"]:
|
||||
return "r/"
|
||||
|
||||
def __rmod__(self, other) -> Literal["r%"]:
|
||||
return "r%"
|
||||
|
||||
def __rpow__(self, other) -> Literal["r**"]:
|
||||
return "r**"
|
||||
|
||||
def __rlshift__(self, other) -> Literal["r<<"]:
|
||||
return "r<<"
|
||||
|
||||
def __rrshift__(self, other) -> Literal["r>>"]:
|
||||
return "r>>"
|
||||
|
||||
def __ror__(self, other) -> Literal["r|"]:
|
||||
return "r|"
|
||||
|
||||
def __rxor__(self, other) -> Literal["r^"]:
|
||||
return "r^"
|
||||
|
||||
def __rand__(self, other) -> Literal["r&"]:
|
||||
return "r&"
|
||||
|
||||
def __rfloordiv__(self, other) -> Literal["r//"]:
|
||||
return "r//"
|
||||
|
||||
# Subclass reflected dunder methods take precedence over the superclass's regular dunders.
|
||||
reveal_type(Yes() + Sub()) # revealed: Literal["r+"]
|
||||
reveal_type(Yes() - Sub()) # revealed: Literal["r-"]
|
||||
reveal_type(Yes() * Sub()) # revealed: Literal["r*"]
|
||||
reveal_type(Yes() @ Sub()) # revealed: Literal["r@"]
|
||||
reveal_type(Yes() / Sub()) # revealed: Literal["r/"]
|
||||
reveal_type(Yes() % Sub()) # revealed: Literal["r%"]
|
||||
reveal_type(Yes() ** Sub()) # revealed: Literal["r**"]
|
||||
reveal_type(Yes() << Sub()) # revealed: Literal["r<<"]
|
||||
reveal_type(Yes() >> Sub()) # revealed: Literal["r>>"]
|
||||
reveal_type(Yes() | Sub()) # revealed: Literal["r|"]
|
||||
reveal_type(Yes() ^ Sub()) # revealed: Literal["r^"]
|
||||
reveal_type(Yes() & Sub()) # revealed: Literal["r&"]
|
||||
reveal_type(Yes() // Sub()) # revealed: Literal["r//"]
|
||||
|
||||
# But for an unrelated class, the superclass regular dunders are used.
|
||||
reveal_type(Yes() + No()) # revealed: Literal["+"]
|
||||
reveal_type(Yes() - No()) # revealed: Literal["-"]
|
||||
reveal_type(Yes() * No()) # revealed: Literal["*"]
|
||||
reveal_type(Yes() @ No()) # revealed: Literal["@"]
|
||||
reveal_type(Yes() / No()) # revealed: Literal["/"]
|
||||
reveal_type(Yes() % No()) # revealed: Literal["%"]
|
||||
reveal_type(Yes() ** No()) # revealed: Literal["**"]
|
||||
reveal_type(Yes() << No()) # revealed: Literal["<<"]
|
||||
reveal_type(Yes() >> No()) # revealed: Literal[">>"]
|
||||
reveal_type(Yes() | No()) # revealed: Literal["|"]
|
||||
reveal_type(Yes() ^ No()) # revealed: Literal["^"]
|
||||
reveal_type(Yes() & No()) # revealed: Literal["&"]
|
||||
reveal_type(Yes() // No()) # revealed: Literal["//"]
|
||||
```
|
||||
|
||||
## Classes
|
||||
|
||||
Dunder methods defined in a class are available to instances of that class, but not to the class
|
||||
itself. (For these operators to work on the class itself, they would have to be defined on the
|
||||
class's type, i.e. `type`.)
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`"
|
||||
reveal_type(Yes + Yes) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`"
|
||||
reveal_type(Sub + Sub) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`"
|
||||
reveal_type(No + No) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Subclass
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
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] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`"
|
||||
reveal_type(yes() + yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`"
|
||||
reveal_type(sub() + sub()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`"
|
||||
reveal_type(no() + no()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Function literals
|
||||
|
||||
```py
|
||||
def f():
|
||||
pass
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f + f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f - f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f * f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f @ f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f / f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f % f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f**f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f << f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f >> f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f | f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f ^ f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f & f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f // f) # revealed: Unknown
|
||||
```
|
||||
@@ -9,6 +9,34 @@ reveal_type(3 * -1) # revealed: Literal[-3]
|
||||
reveal_type(-3 // 3) # revealed: Literal[-1]
|
||||
reveal_type(-3 / 3) # revealed: float
|
||||
reveal_type(5 % 3) # revealed: Literal[2]
|
||||
|
||||
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
|
||||
# formal parameter type.
|
||||
reveal_type(2 + "f") # revealed: int
|
||||
|
||||
def lhs(x: int):
|
||||
reveal_type(x + 1) # revealed: int
|
||||
reveal_type(x - 4) # revealed: int
|
||||
reveal_type(x * -1) # revealed: int
|
||||
reveal_type(x // 3) # revealed: int
|
||||
reveal_type(x / 3) # revealed: float
|
||||
reveal_type(x % 3) # revealed: int
|
||||
|
||||
def rhs(x: int):
|
||||
reveal_type(2 + x) # revealed: int
|
||||
reveal_type(3 - x) # revealed: int
|
||||
reveal_type(3 * x) # revealed: int
|
||||
reveal_type(-3 // x) # revealed: int
|
||||
reveal_type(-3 / x) # revealed: float
|
||||
reveal_type(5 % x) # revealed: int
|
||||
|
||||
def both(x: int):
|
||||
reveal_type(x + x) # revealed: int
|
||||
reveal_type(x - x) # revealed: int
|
||||
reveal_type(x * x) # revealed: int
|
||||
reveal_type(x // x) # revealed: int
|
||||
reveal_type(x / x) # revealed: float
|
||||
reveal_type(x % x) # revealed: int
|
||||
```
|
||||
|
||||
## Power
|
||||
@@ -21,6 +49,11 @@ largest_u32 = 4_294_967_295
|
||||
reveal_type(2**2) # revealed: Literal[4]
|
||||
reveal_type(1 ** (largest_u32 + 1)) # revealed: int
|
||||
reveal_type(2**largest_u32) # revealed: int
|
||||
|
||||
def variable(x: int):
|
||||
reveal_type(x**2) # revealed: @Todo(return type)
|
||||
reveal_type(2**x) # revealed: @Todo(return type)
|
||||
reveal_type(x**x) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Division by Zero
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
|
||||
|
||||
@@ -67,6 +67,35 @@ def _(flag: bool):
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Literal[__call__] | Literal[1]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: int | Unknown
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Call binding errors
|
||||
|
||||
### Wrong argument type
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __call__(self, x: int) -> int:
|
||||
return 1
|
||||
|
||||
c = C()
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`"
|
||||
reveal_type(c("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Wrong argument type on `self`
|
||||
|
||||
```py
|
||||
class C:
|
||||
# TODO this definition should also be an error; `C` must be assignable to type of `self`
|
||||
def __call__(self: int) -> int:
|
||||
return 1
|
||||
|
||||
c = C()
|
||||
|
||||
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
|
||||
reveal_type(c()) # revealed: int
|
||||
```
|
||||
|
||||
@@ -64,3 +64,260 @@ def _(flag: bool):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
## Wrong argument type
|
||||
|
||||
### Positional argument, positional-or-keyword parameter
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Positional argument, positional-only parameter
|
||||
|
||||
```py
|
||||
def f(x: int, /) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Positional argument, variadic parameter
|
||||
|
||||
```py
|
||||
def f(*args: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `*args` of function `f`; expected type `int`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Keyword argument, positional-or-keyword parameter
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Keyword argument, keyword-only parameter
|
||||
|
||||
```py
|
||||
def f(*, x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Keyword argument, keywords parameter
|
||||
|
||||
```py
|
||||
def f(**kwargs: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `**kwargs` of function `f`; expected type `int`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Correctly match keyword out-of-order
|
||||
|
||||
```py
|
||||
def f(x: int = 1, y: str = "foo") -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal[2]` cannot be assigned to parameter `y` of function `f`; expected type `str`"
|
||||
# error: 20 [invalid-argument-type] "Object of type `Literal["bar"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
|
||||
reveal_type(f(y=2, x="bar")) # revealed: int
|
||||
```
|
||||
|
||||
## Too many positional arguments
|
||||
|
||||
### One too many
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Two too many
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2"
|
||||
reveal_type(f("foo", "bar")) # revealed: int
|
||||
```
|
||||
|
||||
### No too-many-positional if variadic is taken
|
||||
|
||||
```py
|
||||
def f(*args: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f(1, 2, 3)) # revealed: int
|
||||
```
|
||||
|
||||
## Missing arguments
|
||||
|
||||
### No defaults or variadic
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### With default
|
||||
|
||||
```py
|
||||
def f(x: int, y: str = "foo") -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Defaulted argument is not required
|
||||
|
||||
```py
|
||||
def f(x: int = 1) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### With variadic
|
||||
|
||||
```py
|
||||
def f(x: int, *y: str) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Variadic argument is not required
|
||||
|
||||
```py
|
||||
def f(*args: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Keywords argument is not required
|
||||
|
||||
```py
|
||||
def f(**kwargs: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Multiple
|
||||
|
||||
```py
|
||||
def f(x: int, y: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
## Unknown argument
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`"
|
||||
reveal_type(f(x=1, y=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Parameter already assigned
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
|
||||
reveal_type(f(1, x=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Special functions
|
||||
|
||||
Some functions require special handling in type inference. Here, we make sure that we still emit
|
||||
proper diagnostics in case of missing or superfluous arguments.
|
||||
|
||||
### `reveal_type`
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
|
||||
reveal_type() # revealed: Unknown
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
|
||||
reveal_type(1, 2) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### `static_assert`
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
|
||||
# error: [static-assert-error]
|
||||
static_assert()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"
|
||||
static_assert(True, 2, 3)
|
||||
```
|
||||
|
||||
### `len`
|
||||
|
||||
```py
|
||||
# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`"
|
||||
len()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2"
|
||||
len([], 1)
|
||||
```
|
||||
|
||||
### Type API predicates
|
||||
|
||||
```py
|
||||
from knot_extensions import is_subtype_of, is_fully_static
|
||||
|
||||
# error: [missing-argument]
|
||||
is_subtype_of()
|
||||
|
||||
# error: [missing-argument]
|
||||
is_subtype_of(int)
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_subtype_of(int, int, int)
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_subtype_of(int, int, int, int)
|
||||
|
||||
# error: [missing-argument]
|
||||
is_fully_static()
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_fully_static(int, int)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Invalid signatures
|
||||
|
||||
## Multiple arguments with the same name
|
||||
|
||||
We always map a keyword argument to the first parameter of that name.
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "Duplicate parameter "x""
|
||||
def f(x: int, x: str) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
|
||||
reveal_type(f(1, x=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Positional after non-positional
|
||||
|
||||
When parameter kinds are given in an invalid order, we emit a diagnostic and implicitly reorder them
|
||||
to the valid order:
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "Parameter cannot follow var-keyword parameter"
|
||||
def f(**kw: int, x: str) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `str`"
|
||||
reveal_type(f(1)) # revealed: int
|
||||
```
|
||||
|
||||
## Non-defaulted after defaulted
|
||||
|
||||
We emit a syntax diagnostic for this, but it doesn't cause any problems for binding.
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "Parameter without a default cannot follow a parameter with a default"
|
||||
def f(x: int = 1, y: str) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f(y="foo")) # revealed: int
|
||||
# error: [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
|
||||
# error: [missing-argument] "No argument provided for required parameter `y` of function `f`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
@@ -56,7 +56,7 @@ def _(flag: bool, flag2: bool):
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
# revealed: Unknown | int
|
||||
reveal_type(f())
|
||||
```
|
||||
@@ -72,6 +72,6 @@ def _(flag: bool):
|
||||
else:
|
||||
f = "foo"
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
|
||||
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -22,7 +22,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
reveal_type(d) # revealed: bool
|
||||
|
||||
int_literal_or_str_literal = 1 if flag else "foo"
|
||||
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
|
||||
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
reveal_type(e) # revealed: bool
|
||||
|
||||
|
||||
@@ -115,3 +115,35 @@ def _(flag: bool, flag2: bool):
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## if-elif with assignment expressions in tests
|
||||
|
||||
```py
|
||||
def check(x: int) -> bool:
|
||||
return bool(x)
|
||||
|
||||
if check(x := 1):
|
||||
x = 2
|
||||
elif check(x := 3):
|
||||
x = 4
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## constraints apply to later test expressions
|
||||
|
||||
```py
|
||||
def check(x) -> bool:
|
||||
return bool(x)
|
||||
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else None
|
||||
y = 0
|
||||
|
||||
if x is None:
|
||||
pass
|
||||
elif check(y := x):
|
||||
pass
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# `assert_type`
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(x: int):
|
||||
assert_type(x, int) # fine
|
||||
assert_type(x, str) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
The asserted type is checked against the inferred type, not the declared type.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(x: int | str):
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: int
|
||||
assert_type(x, int) # fine
|
||||
```
|
||||
|
||||
## Equivalence
|
||||
|
||||
The actual type must match the asserted type precisely.
|
||||
|
||||
```py
|
||||
from typing import Any, Type, Union
|
||||
from typing_extensions import assert_type
|
||||
|
||||
# Subtype does not count
|
||||
def _(x: bool):
|
||||
assert_type(x, int) # error: [type-assertion-failure]
|
||||
|
||||
def _(a: type[int], b: type[Any]):
|
||||
assert_type(a, type[Any]) # error: [type-assertion-failure]
|
||||
assert_type(b, type[int]) # error: [type-assertion-failure]
|
||||
|
||||
# The expression constructing the type is not taken into account
|
||||
def _(a: type[int]):
|
||||
assert_type(a, Type[int]) # fine
|
||||
```
|
||||
|
||||
## Gradual types
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import Literal, assert_type
|
||||
|
||||
from knot_extensions import Unknown
|
||||
|
||||
# Any and Unknown are considered equivalent
|
||||
def _(a: Unknown, b: Any):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
assert_type(a, Any) # fine
|
||||
|
||||
reveal_type(b) # revealed: Any
|
||||
assert_type(b, Unknown) # fine
|
||||
|
||||
def _(a: type[Unknown], b: type[Any]):
|
||||
reveal_type(a) # revealed: type[Unknown]
|
||||
assert_type(a, type[Any]) # fine
|
||||
|
||||
reveal_type(b) # revealed: type[Any]
|
||||
assert_type(b, type[Unknown]) # fine
|
||||
```
|
||||
|
||||
## Tuples
|
||||
|
||||
Tuple types with the same elements are the same.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(a: tuple[int, str, bytes]):
|
||||
assert_type(a, tuple[int, str, bytes]) # fine
|
||||
|
||||
assert_type(a, tuple[int, str]) # error: [type-assertion-failure]
|
||||
assert_type(a, tuple[int, str, bytes, None]) # error: [type-assertion-failure]
|
||||
assert_type(a, tuple[int, bytes, str]) # error: [type-assertion-failure]
|
||||
|
||||
def _(a: tuple[Any, ...], b: tuple[Unknown, ...]):
|
||||
assert_type(a, tuple[Any, ...]) # fine
|
||||
assert_type(a, tuple[Unknown, ...]) # fine
|
||||
|
||||
assert_type(b, tuple[Unknown, ...]) # fine
|
||||
assert_type(b, tuple[Any, ...]) # fine
|
||||
```
|
||||
|
||||
## Unions
|
||||
|
||||
Unions with the same elements are the same, regardless of order.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(a: str | int):
|
||||
assert_type(a, str | int) # fine
|
||||
|
||||
# TODO: Order-independent union handling in type equivalence
|
||||
assert_type(a, int | str) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
## Intersections
|
||||
|
||||
Intersections are the same when their positive and negative parts are respectively the same,
|
||||
regardless of order.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
|
||||
def _(a: A):
|
||||
if isinstance(a, B) and not isinstance(a, C) and not isinstance(a, D):
|
||||
reveal_type(a) # revealed: A & B & ~C & ~D
|
||||
|
||||
assert_type(a, Intersection[A, B, Not[C], Not[D]]) # fine
|
||||
|
||||
# TODO: Order-independent intersection handling in type equivalence
|
||||
assert_type(a, Intersection[B, A, Not[D], Not[C]]) # error: [type-assertion-failure]
|
||||
```
|
||||
@@ -17,7 +17,7 @@ def _(flag: bool):
|
||||
|
||||
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||
|
||||
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
|
||||
reveal_type(A.union) # revealed: Literal[1, "abc"]
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||
|
||||
@@ -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,9 +31,9 @@ The test inside an if expression should not affect code outside of the expressio
|
||||
def _(flag: bool):
|
||||
x: Literal[42, "hello"] = 42 if flag else "hello"
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
reveal_type(x) # revealed: Literal[42, "hello"]
|
||||
|
||||
_ = ... if isinstance(x, str) else ...
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
reveal_type(x) # revealed: Literal[42, "hello"]
|
||||
```
|
||||
|
||||
@@ -119,7 +119,7 @@ class ZeroOrStr:
|
||||
reveal_type(len(Zero())) # revealed: Literal[0]
|
||||
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(OneOrFalse())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(OneOrFalse())) # revealed: Literal[1, 0]
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(OneOrFoo())) # revealed: int
|
||||
|
||||
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,
|
||||
): ...
|
||||
```
|
||||
@@ -0,0 +1,748 @@
|
||||
# Intersection types
|
||||
|
||||
## Introduction
|
||||
|
||||
This test suite covers certain properties of intersection types and makes sure that we can apply
|
||||
various simplification strategies. We use `Intersection` (`&`) and `Not` (`~`) to construct
|
||||
intersection types (note that we display negative contributions at the end; the order does not
|
||||
matter):
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Q],
|
||||
i2: Intersection[P, Not[Q]],
|
||||
i3: Intersection[Not[P], Q],
|
||||
i4: Intersection[Not[P], Not[Q]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P & Q
|
||||
reveal_type(i2) # revealed: P & ~Q
|
||||
reveal_type(i3) # revealed: Q & ~P
|
||||
reveal_type(i4) # revealed: ~P & ~Q
|
||||
```
|
||||
|
||||
## Notation
|
||||
|
||||
Throughout this document, we use the following types as representatives for certain equivalence
|
||||
classes.
|
||||
|
||||
### Non-disjoint types
|
||||
|
||||
We use `P`, `Q`, `R`, … to denote types that are non-disjoint:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
static_assert(not is_disjoint_from(P, Q))
|
||||
static_assert(not is_disjoint_from(P, R))
|
||||
static_assert(not is_disjoint_from(Q, R))
|
||||
```
|
||||
|
||||
Although `P` is not a subtype of `Q` and `Q` is not a subtype of `P`, the two types are not disjoint
|
||||
because it would be possible to create a class `S` that inherits from both `P` and `Q` using
|
||||
multiple inheritance. An instance of `S` would be a member of the `P` type _and_ the `Q` type.
|
||||
|
||||
### Disjoint types
|
||||
|
||||
We use `Literal[1]`, `Literal[2]`, … as examples of pairwise-disjoint types, and `int` as a joint
|
||||
supertype of these:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_disjoint_from, is_subtype_of
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_disjoint_from(Literal[1], Literal[2]))
|
||||
static_assert(is_disjoint_from(Literal[1], Literal[3]))
|
||||
static_assert(is_disjoint_from(Literal[2], Literal[3]))
|
||||
|
||||
static_assert(is_subtype_of(Literal[1], int))
|
||||
static_assert(is_subtype_of(Literal[2], int))
|
||||
static_assert(is_subtype_of(Literal[3], int))
|
||||
```
|
||||
|
||||
### Subtypes
|
||||
|
||||
Finally, we use `A <: B <: C` and `A <: B1`, `A <: B2` to denote hierarchies of (proper) subtypes:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_subtype_of, is_disjoint_from
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
|
||||
static_assert(is_subtype_of(B, A))
|
||||
static_assert(is_subtype_of(C, B))
|
||||
static_assert(is_subtype_of(C, A))
|
||||
|
||||
static_assert(not is_subtype_of(A, B))
|
||||
static_assert(not is_subtype_of(B, C))
|
||||
static_assert(not is_subtype_of(A, C))
|
||||
|
||||
class B1(A): ...
|
||||
class B2(A): ...
|
||||
|
||||
static_assert(is_subtype_of(B1, A))
|
||||
static_assert(is_subtype_of(B2, A))
|
||||
|
||||
static_assert(not is_subtype_of(A, B1))
|
||||
static_assert(not is_subtype_of(A, B2))
|
||||
|
||||
static_assert(not is_subtype_of(B1, B2))
|
||||
static_assert(not is_subtype_of(B2, B1))
|
||||
```
|
||||
|
||||
## Structural properties
|
||||
|
||||
This section covers structural properties of intersection types and documents some decisions on how
|
||||
to represent mixtures of intersections and unions.
|
||||
|
||||
### Single-element intersections
|
||||
|
||||
If we have an intersection with a single element, we can simplify to that element. Similarly, we
|
||||
show an intersection with a single negative contribution as just the negation of that element.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P],
|
||||
i2: Intersection[Not[P]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P
|
||||
reveal_type(i2) # revealed: ~P
|
||||
```
|
||||
|
||||
### Flattening of nested intersections
|
||||
|
||||
We eagerly flatten nested intersections types.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
class S: ...
|
||||
|
||||
def positive_contributions(
|
||||
i1: Intersection[P, Intersection[Q, R]],
|
||||
i2: Intersection[Intersection[P, Q], R],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P & Q & R
|
||||
reveal_type(i2) # revealed: P & Q & R
|
||||
|
||||
def negative_contributions(
|
||||
i1: Intersection[Not[P], Intersection[Not[Q], Not[R]]],
|
||||
i2: Intersection[Intersection[Not[P], Not[Q]], Not[R]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: ~P & ~Q & ~R
|
||||
reveal_type(i2) # revealed: ~P & ~Q & ~R
|
||||
|
||||
def mixed(
|
||||
i1: Intersection[P, Intersection[Not[Q], R]],
|
||||
i2: Intersection[Intersection[P, Not[Q]], R],
|
||||
i3: Intersection[Not[P], Intersection[Q, Not[R]]],
|
||||
i4: Intersection[Intersection[Q, Not[R]], Not[P]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P & R & ~Q
|
||||
reveal_type(i2) # revealed: P & R & ~Q
|
||||
reveal_type(i3) # revealed: Q & ~P & ~R
|
||||
reveal_type(i4) # revealed: Q & ~R & ~P
|
||||
|
||||
def multiple(
|
||||
i1: Intersection[Intersection[P, Q], Intersection[R, S]],
|
||||
):
|
||||
reveal_type(i1) # revealed: P & Q & R & S
|
||||
|
||||
def nested(
|
||||
i1: Intersection[Intersection[Intersection[P, Q], R], S],
|
||||
i2: Intersection[P, Intersection[Q, Intersection[R, S]]],
|
||||
):
|
||||
reveal_type(i1) # revealed: P & Q & R & S
|
||||
reveal_type(i2) # revealed: P & Q & R & S
|
||||
```
|
||||
|
||||
### Union of intersections
|
||||
|
||||
We always normalize our representation to a _union of intersections_, so when we add a _union to an
|
||||
intersection_, we distribute the union over the respective elements:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
class S: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Q | R | S],
|
||||
i2: Intersection[P | Q | R, S],
|
||||
i3: Intersection[P | Q, R | S],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P & Q | P & R | P & S
|
||||
reveal_type(i2) # revealed: P & S | Q & S | R & S
|
||||
reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S
|
||||
|
||||
def simplifications_for_same_elements(
|
||||
i1: Intersection[P, Q | P],
|
||||
i2: Intersection[Q, P | Q],
|
||||
i3: Intersection[P | Q, Q | R],
|
||||
i4: Intersection[P | Q, P | Q],
|
||||
i5: Intersection[P | Q, Q | P],
|
||||
) -> None:
|
||||
# P & (Q | P)
|
||||
# = P & Q | P & P
|
||||
# = P & Q | P
|
||||
# = P
|
||||
# (because P is a supertype of P & Q)
|
||||
reveal_type(i1) # revealed: P
|
||||
# similar here:
|
||||
reveal_type(i2) # revealed: Q
|
||||
|
||||
# (P | Q) & (Q | R)
|
||||
# = P & Q | P & R | Q & Q | Q & R
|
||||
# = P & Q | P & R | Q | Q & R
|
||||
# = Q | P & R
|
||||
# (again, because Q is a supertype of P & Q and of Q & R)
|
||||
reveal_type(i3) # revealed: Q | P & R
|
||||
|
||||
# (P | Q) & (P | Q)
|
||||
# = P & P | P & Q | Q & P | Q & Q
|
||||
# = P | P & Q | Q
|
||||
# = P | Q
|
||||
reveal_type(i4) # revealed: P | Q
|
||||
```
|
||||
|
||||
### Negation distributes over union
|
||||
|
||||
Distribution also applies to a negation operation. This is a manifestation of one of
|
||||
[De Morgan's laws], namely `~(P | Q) = ~P & ~Q`:
|
||||
|
||||
```py
|
||||
from knot_extensions import Not
|
||||
from typing import Literal
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
def _(i1: Not[P | Q], i2: Not[P | Q | R]) -> None:
|
||||
reveal_type(i1) # revealed: ~P & ~Q
|
||||
reveal_type(i2) # revealed: ~P & ~Q & ~R
|
||||
|
||||
def example_literals(i: Not[Literal[1, 2]]) -> None:
|
||||
reveal_type(i) # revealed: ~Literal[1] & ~Literal[2]
|
||||
```
|
||||
|
||||
### Negation of intersections
|
||||
|
||||
The other of [De Morgan's laws], `~(P & Q) = ~P | ~Q`, also holds:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
def _(
|
||||
i1: Not[Intersection[P, Q]],
|
||||
i2: Not[Intersection[P, Q, R]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: ~P | ~Q
|
||||
reveal_type(i2) # revealed: ~P | ~Q | ~R
|
||||
```
|
||||
|
||||
### `Never` is dual to `object`
|
||||
|
||||
`Never` represents the empty set of values, while `object` represents the set of all values, so
|
||||
`~Never` is equivalent to `object`, and `~object` is equivalent to `Never`. This is a manifestation
|
||||
of the [complement laws] of set theory.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing_extensions import Never
|
||||
|
||||
def _(
|
||||
not_never: Not[Never],
|
||||
not_object: Not[object],
|
||||
) -> None:
|
||||
reveal_type(not_never) # revealed: object
|
||||
reveal_type(not_object) # revealed: Never
|
||||
```
|
||||
|
||||
### Intersection of a type and its negation
|
||||
|
||||
Continuing with more [complement laws], if we see both `P` and `~P` in an intersection, we can
|
||||
simplify to `Never`, even in the presence of other types:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Any
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Not[P]],
|
||||
i2: Intersection[Not[P], P],
|
||||
i3: Intersection[P, Q, Not[P]],
|
||||
i4: Intersection[Not[P], Q, P],
|
||||
i5: Intersection[P, Any, Not[P]],
|
||||
i6: Intersection[Not[P], Any, P],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Never
|
||||
reveal_type(i2) # revealed: Never
|
||||
reveal_type(i3) # revealed: Never
|
||||
reveal_type(i4) # revealed: Never
|
||||
reveal_type(i5) # revealed: Never
|
||||
reveal_type(i6) # revealed: Never
|
||||
```
|
||||
|
||||
### Union of a type and its negation
|
||||
|
||||
Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `object`.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
def _(
|
||||
i1: P | Not[P],
|
||||
i2: Not[P] | P,
|
||||
i3: P | Q | Not[P],
|
||||
i4: Not[P] | Q | P,
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: object
|
||||
reveal_type(i2) # revealed: object
|
||||
reveal_type(i3) # revealed: object
|
||||
reveal_type(i4) # revealed: object
|
||||
```
|
||||
|
||||
### Negation is an involution
|
||||
|
||||
The final of the [complement laws] states that negating twice is equivalent to not negating at all:
|
||||
|
||||
```py
|
||||
from knot_extensions import Not
|
||||
|
||||
class P: ...
|
||||
|
||||
def _(
|
||||
i1: Not[P],
|
||||
i2: Not[Not[P]],
|
||||
i3: Not[Not[Not[P]]],
|
||||
i4: Not[Not[Not[Not[P]]]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: ~P
|
||||
reveal_type(i2) # revealed: P
|
||||
reveal_type(i3) # revealed: ~P
|
||||
reveal_type(i4) # revealed: P
|
||||
```
|
||||
|
||||
## Simplification strategies
|
||||
|
||||
In this section, we present various simplification strategies that go beyond the structure of the
|
||||
representation.
|
||||
|
||||
### `Never` in intersections
|
||||
|
||||
If we intersect with `Never`, we can simplify the whole intersection to `Never`, even if there are
|
||||
dynamic types involved:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing_extensions import Never, Any
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Never],
|
||||
i2: Intersection[Never, P],
|
||||
i3: Intersection[Any, Never],
|
||||
i4: Intersection[Never, Not[Any]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Never
|
||||
reveal_type(i2) # revealed: Never
|
||||
reveal_type(i3) # revealed: Never
|
||||
reveal_type(i4) # revealed: Never
|
||||
```
|
||||
|
||||
### Simplifications using disjointness
|
||||
|
||||
#### Positive contributions
|
||||
|
||||
If we intersect disjoint types, we can simplify to `Never`, even in the presence of other types:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Literal, Any
|
||||
|
||||
class P: ...
|
||||
|
||||
def _(
|
||||
i01: Intersection[Literal[1], Literal[2]],
|
||||
i02: Intersection[Literal[2], Literal[1]],
|
||||
i03: Intersection[Literal[1], Literal[2], P],
|
||||
i04: Intersection[Literal[1], P, Literal[2]],
|
||||
i05: Intersection[P, Literal[1], Literal[2]],
|
||||
i06: Intersection[Literal[1], Literal[2], Any],
|
||||
i07: Intersection[Literal[1], Any, Literal[2]],
|
||||
i08: Intersection[Any, Literal[1], Literal[2]],
|
||||
) -> None:
|
||||
reveal_type(i01) # revealed: Never
|
||||
reveal_type(i02) # revealed: Never
|
||||
reveal_type(i03) # revealed: Never
|
||||
reveal_type(i04) # revealed: Never
|
||||
reveal_type(i05) # revealed: Never
|
||||
reveal_type(i06) # revealed: Never
|
||||
reveal_type(i07) # revealed: Never
|
||||
reveal_type(i08) # revealed: Never
|
||||
|
||||
# `bool` is final and can not be subclassed, so `type[bool]` is equivalent to `Literal[bool]`, which
|
||||
# is disjoint from `type[str]`:
|
||||
def example_type_bool_type_str(
|
||||
i: Intersection[type[bool], type[str]],
|
||||
) -> None:
|
||||
reveal_type(i) # revealed: Never
|
||||
```
|
||||
|
||||
#### Positive and negative contributions
|
||||
|
||||
If we intersect a type `X` with the negation `~Y` of a disjoint type `Y`, we can remove the negative
|
||||
contribution `~Y`, as `~Y` must fully contain the positive contribution `X` as a subtype:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Literal
|
||||
|
||||
def _(
|
||||
i1: Intersection[Literal[1], Not[Literal[2]]],
|
||||
i2: Intersection[Not[Literal[2]], Literal[1]],
|
||||
i3: Intersection[Literal[1], Not[Literal[2]], int],
|
||||
i4: Intersection[Literal[1], int, Not[Literal[2]]],
|
||||
i5: Intersection[int, Literal[1], Not[Literal[2]]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Literal[1]
|
||||
reveal_type(i2) # revealed: Literal[1]
|
||||
reveal_type(i3) # revealed: Literal[1]
|
||||
reveal_type(i4) # revealed: Literal[1]
|
||||
reveal_type(i5) # revealed: Literal[1]
|
||||
|
||||
# None is disjoint from int, so this simplification applies here
|
||||
def example_none(
|
||||
i1: Intersection[int, Not[None]],
|
||||
i2: Intersection[Not[None], int],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: int
|
||||
reveal_type(i2) # revealed: int
|
||||
```
|
||||
|
||||
### Simplifications using subtype relationships
|
||||
|
||||
#### Positive type and positive subtype
|
||||
|
||||
Subtypes are contained within their supertypes, so we can simplify intersections by removing
|
||||
superfluous supertypes:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _(
|
||||
i01: Intersection[A, B],
|
||||
i02: Intersection[B, A],
|
||||
i03: Intersection[A, C],
|
||||
i04: Intersection[C, A],
|
||||
i05: Intersection[B, C],
|
||||
i06: Intersection[C, B],
|
||||
i07: Intersection[A, B, C],
|
||||
i08: Intersection[C, B, A],
|
||||
i09: Intersection[B, C, A],
|
||||
i10: Intersection[A, B, Unrelated],
|
||||
i11: Intersection[B, A, Unrelated],
|
||||
i12: Intersection[B, Unrelated, A],
|
||||
i13: Intersection[A, Unrelated, B],
|
||||
i14: Intersection[Unrelated, A, B],
|
||||
i15: Intersection[Unrelated, B, A],
|
||||
i16: Intersection[A, B, Any],
|
||||
i17: Intersection[B, A, Any],
|
||||
i18: Intersection[B, Any, A],
|
||||
i19: Intersection[A, Any, B],
|
||||
i20: Intersection[Any, A, B],
|
||||
i21: Intersection[Any, B, A],
|
||||
) -> None:
|
||||
reveal_type(i01) # revealed: B
|
||||
reveal_type(i02) # revealed: B
|
||||
reveal_type(i03) # revealed: C
|
||||
reveal_type(i04) # revealed: C
|
||||
reveal_type(i05) # revealed: C
|
||||
reveal_type(i06) # revealed: C
|
||||
reveal_type(i07) # revealed: C
|
||||
reveal_type(i08) # revealed: C
|
||||
reveal_type(i09) # revealed: C
|
||||
reveal_type(i10) # revealed: B & Unrelated
|
||||
reveal_type(i11) # revealed: B & Unrelated
|
||||
reveal_type(i12) # revealed: B & Unrelated
|
||||
reveal_type(i13) # revealed: Unrelated & B
|
||||
reveal_type(i14) # revealed: Unrelated & B
|
||||
reveal_type(i15) # revealed: Unrelated & B
|
||||
reveal_type(i16) # revealed: B & Any
|
||||
reveal_type(i17) # revealed: B & Any
|
||||
reveal_type(i18) # revealed: B & Any
|
||||
reveal_type(i19) # revealed: Any & B
|
||||
reveal_type(i20) # revealed: Any & B
|
||||
reveal_type(i21) # revealed: Any & B
|
||||
```
|
||||
|
||||
#### Negative type and negative subtype
|
||||
|
||||
For negative contributions, this property is reversed. Here we can remove superfluous _subtypes_:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _(
|
||||
i01: Intersection[Not[B], Not[A]],
|
||||
i02: Intersection[Not[A], Not[B]],
|
||||
i03: Intersection[Not[A], Not[C]],
|
||||
i04: Intersection[Not[C], Not[A]],
|
||||
i05: Intersection[Not[B], Not[C]],
|
||||
i06: Intersection[Not[C], Not[B]],
|
||||
i07: Intersection[Not[A], Not[B], Not[C]],
|
||||
i08: Intersection[Not[C], Not[B], Not[A]],
|
||||
i09: Intersection[Not[B], Not[C], Not[A]],
|
||||
i10: Intersection[Not[B], Not[A], Unrelated],
|
||||
i11: Intersection[Not[A], Not[B], Unrelated],
|
||||
i12: Intersection[Not[A], Unrelated, Not[B]],
|
||||
i13: Intersection[Not[B], Unrelated, Not[A]],
|
||||
i14: Intersection[Unrelated, Not[A], Not[B]],
|
||||
i15: Intersection[Unrelated, Not[B], Not[A]],
|
||||
i16: Intersection[Not[B], Not[A], Any],
|
||||
i17: Intersection[Not[A], Not[B], Any],
|
||||
i18: Intersection[Not[A], Any, Not[B]],
|
||||
i19: Intersection[Not[B], Any, Not[A]],
|
||||
i20: Intersection[Any, Not[A], Not[B]],
|
||||
i21: Intersection[Any, Not[B], Not[A]],
|
||||
) -> None:
|
||||
reveal_type(i01) # revealed: ~A
|
||||
reveal_type(i02) # revealed: ~A
|
||||
reveal_type(i03) # revealed: ~A
|
||||
reveal_type(i04) # revealed: ~A
|
||||
reveal_type(i05) # revealed: ~B
|
||||
reveal_type(i06) # revealed: ~B
|
||||
reveal_type(i07) # revealed: ~A
|
||||
reveal_type(i08) # revealed: ~A
|
||||
reveal_type(i09) # revealed: ~A
|
||||
reveal_type(i10) # revealed: Unrelated & ~A
|
||||
reveal_type(i11) # revealed: Unrelated & ~A
|
||||
reveal_type(i12) # revealed: Unrelated & ~A
|
||||
reveal_type(i13) # revealed: Unrelated & ~A
|
||||
reveal_type(i14) # revealed: Unrelated & ~A
|
||||
reveal_type(i15) # revealed: Unrelated & ~A
|
||||
reveal_type(i16) # revealed: Any & ~A
|
||||
reveal_type(i17) # revealed: Any & ~A
|
||||
reveal_type(i18) # revealed: Any & ~A
|
||||
reveal_type(i19) # revealed: Any & ~A
|
||||
reveal_type(i20) # revealed: Any & ~A
|
||||
reveal_type(i21) # revealed: Any & ~A
|
||||
```
|
||||
|
||||
#### Negative type and multiple negative subtypes
|
||||
|
||||
If there are multiple negative subtypes, all of them can be removed:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class A: ...
|
||||
class B1(A): ...
|
||||
class B2(A): ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[Not[A], Not[B1], Not[B2]],
|
||||
i2: Intersection[Not[A], Not[B2], Not[B1]],
|
||||
i3: Intersection[Not[B1], Not[A], Not[B2]],
|
||||
i4: Intersection[Not[B1], Not[B2], Not[A]],
|
||||
i5: Intersection[Not[B2], Not[A], Not[B1]],
|
||||
i6: Intersection[Not[B2], Not[B1], Not[A]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: ~A
|
||||
reveal_type(i2) # revealed: ~A
|
||||
reveal_type(i3) # revealed: ~A
|
||||
reveal_type(i4) # revealed: ~A
|
||||
reveal_type(i5) # revealed: ~A
|
||||
reveal_type(i6) # revealed: ~A
|
||||
```
|
||||
|
||||
#### Negative type and positive subtype
|
||||
|
||||
When `A` is a supertype of `B`, its negation `~A` is disjoint from `B`, so we can simplify the
|
||||
intersection to `Never`:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[Not[A], B],
|
||||
i2: Intersection[B, Not[A]],
|
||||
i3: Intersection[Not[A], C],
|
||||
i4: Intersection[C, Not[A]],
|
||||
i5: Intersection[Unrelated, Not[A], B],
|
||||
i6: Intersection[B, Not[A], Not[Unrelated]],
|
||||
i7: Intersection[Any, Not[A], B],
|
||||
i8: Intersection[B, Not[A], Not[Any]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Never
|
||||
reveal_type(i2) # revealed: Never
|
||||
reveal_type(i3) # revealed: Never
|
||||
reveal_type(i4) # revealed: Never
|
||||
reveal_type(i5) # revealed: Never
|
||||
reveal_type(i6) # revealed: Never
|
||||
reveal_type(i7) # revealed: Never
|
||||
reveal_type(i8) # revealed: Never
|
||||
```
|
||||
|
||||
## Non fully-static types
|
||||
|
||||
### Negation of dynamic types
|
||||
|
||||
`Any` represents the dynamic type, an unknown set of runtime values. The negation of that, `~Any`,
|
||||
is still an unknown set of runtime values, so `~Any` is equivalent to `Any`. We therefore eagerly
|
||||
simplify `~Any` to `Any` in intersections. The same applies to `Unknown`.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, Unknown
|
||||
from typing_extensions import Any, Never
|
||||
|
||||
class P: ...
|
||||
|
||||
def any(
|
||||
i1: Not[Any],
|
||||
i2: Intersection[P, Not[Any]],
|
||||
i3: Intersection[Never, Not[Any]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Any
|
||||
reveal_type(i2) # revealed: P & Any
|
||||
reveal_type(i3) # revealed: Never
|
||||
|
||||
def unknown(
|
||||
i1: Not[Unknown],
|
||||
i2: Intersection[P, Not[Unknown]],
|
||||
i3: Intersection[Never, Not[Unknown]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Unknown
|
||||
reveal_type(i2) # revealed: P & Unknown
|
||||
reveal_type(i3) # revealed: Never
|
||||
```
|
||||
|
||||
### Collapsing of multiple `Any`/`Unknown` contributions
|
||||
|
||||
The intersection of an unknown set of runtime values with (another) unknown set of runtime values is
|
||||
still an unknown set of runtime values:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, Unknown
|
||||
from typing_extensions import Any
|
||||
|
||||
class P: ...
|
||||
|
||||
def any(
|
||||
i1: Intersection[Any, Any],
|
||||
i2: Intersection[P, Any, Any],
|
||||
i3: Intersection[Any, P, Any],
|
||||
i4: Intersection[Any, Any, P],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Any
|
||||
reveal_type(i2) # revealed: P & Any
|
||||
reveal_type(i3) # revealed: Any & P
|
||||
reveal_type(i4) # revealed: Any & P
|
||||
|
||||
def unknown(
|
||||
i1: Intersection[Unknown, Unknown],
|
||||
i2: Intersection[P, Unknown, Unknown],
|
||||
i3: Intersection[Unknown, P, Unknown],
|
||||
i4: Intersection[Unknown, Unknown, P],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Unknown
|
||||
reveal_type(i2) # revealed: P & Unknown
|
||||
reveal_type(i3) # revealed: Unknown & P
|
||||
reveal_type(i4) # revealed: Unknown & P
|
||||
```
|
||||
|
||||
### No self-cancellation
|
||||
|
||||
Dynamic types do not cancel each other out. Intersecting an unknown set of values with the negation
|
||||
of another unknown set of values is not necessarily empty, so we keep the positive contribution:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, Unknown
|
||||
|
||||
def any(
|
||||
i1: Intersection[Any, Not[Any]],
|
||||
i2: Intersection[Not[Any], Any],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Any
|
||||
reveal_type(i2) # revealed: Any
|
||||
|
||||
def unknown(
|
||||
i1: Intersection[Unknown, Not[Unknown]],
|
||||
i2: Intersection[Not[Unknown], Unknown],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Unknown
|
||||
reveal_type(i2) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Mixed dynamic types
|
||||
|
||||
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, Unknown
|
||||
|
||||
def mixed(
|
||||
i1: Intersection[Any, Unknown],
|
||||
i2: Intersection[Any, Not[Unknown]],
|
||||
i3: Intersection[Not[Any], Unknown],
|
||||
i4: Intersection[Not[Any], Not[Unknown]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Any & Unknown
|
||||
reveal_type(i2) # revealed: Any & Unknown
|
||||
reveal_type(i3) # revealed: Any & Unknown
|
||||
reveal_type(i4) # revealed: Any & Unknown
|
||||
```
|
||||
|
||||
[complement laws]: https://en.wikipedia.org/wiki/Complement_(set_theory)
|
||||
[de morgan's laws]: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -98,7 +98,7 @@ reveal_type(x)
|
||||
for x in (1, "a", b"foo"):
|
||||
pass
|
||||
|
||||
# revealed: Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
# revealed: Literal[1, "a", b"foo"]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
@@ -41,7 +41,7 @@ def _(flag: bool, flag2: bool):
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
reveal_type(y) # revealed: Literal[4, 1, 2]
|
||||
```
|
||||
|
||||
## Nested `while` loops
|
||||
|
||||
@@ -170,8 +170,35 @@ def f(*args, **kwargs) -> int: ...
|
||||
|
||||
class A(metaclass=f): ...
|
||||
|
||||
# TODO should be `type[int]`
|
||||
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
|
||||
# TODO: Should be `int`
|
||||
reveal_type(A) # revealed: Literal[A]
|
||||
reveal_type(A.__class__) # revealed: type[int]
|
||||
|
||||
def _(n: int):
|
||||
# error: [invalid-metaclass]
|
||||
class B(metaclass=n): ...
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(B) # revealed: Literal[B]
|
||||
reveal_type(B.__class__) # revealed: type[Unknown]
|
||||
|
||||
def _(flag: bool):
|
||||
m = f if flag else 42
|
||||
|
||||
# error: [invalid-metaclass]
|
||||
class C(metaclass=m): ...
|
||||
# TODO: Should be `int | Unknown`
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
reveal_type(C.__class__) # revealed: type[Unknown]
|
||||
|
||||
class SignatureMismatch: ...
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
class D(metaclass=SignatureMismatch): ...
|
||||
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(D) # revealed: Literal[D]
|
||||
# TODO: Should be `type[Unknown]`
|
||||
reveal_type(D.__class__) # revealed: Literal[SignatureMismatch]
|
||||
```
|
||||
|
||||
## Cyclic
|
||||
|
||||
@@ -56,7 +56,7 @@ def _(x_flag: bool, y_flag: bool):
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = None if flag1 else (1 if flag2 else True)
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
|
||||
reveal_type(x) # revealed: None | Literal[1, True]
|
||||
if x is None:
|
||||
reveal_type(x) # revealed: None
|
||||
elif x is True:
|
||||
|
||||
@@ -17,7 +17,7 @@ def _(flag: bool):
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
```
|
||||
|
||||
## `classinfo` is a tuple of types
|
||||
@@ -30,7 +30,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, (int, str)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
@@ -43,19 +43,19 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
# No narrowing should occur if a larger type is also
|
||||
# one of the possibilities:
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
y = 1 if flag1 else "a" if flag2 else b"b"
|
||||
if isinstance(y, (int, str)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(y) # revealed: Literal[1, "a"]
|
||||
|
||||
if isinstance(y, (int, bytes)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
|
||||
reveal_type(y) # revealed: Literal[1, b"b"]
|
||||
|
||||
if isinstance(y, (str, bytes)):
|
||||
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
|
||||
reveal_type(y) # revealed: Literal["a", b"b"]
|
||||
```
|
||||
|
||||
## `classinfo` is a nested tuple of types
|
||||
@@ -107,7 +107,7 @@ def _(flag: bool):
|
||||
x = 1 if flag else "foo"
|
||||
|
||||
if isinstance(x, t):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
|
||||
reveal_type(x) # revealed: Literal[1, "foo"]
|
||||
```
|
||||
|
||||
## Do not use custom `isinstance` for narrowing
|
||||
@@ -119,7 +119,7 @@ def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
```
|
||||
|
||||
## Do support narrowing if `isinstance` is aliased
|
||||
@@ -155,12 +155,12 @@ def _(flag: bool):
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "a"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "int"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
```
|
||||
|
||||
## Do not narrow if there are keyword arguments
|
||||
@@ -169,8 +169,15 @@ def _(flag: bool):
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`isinstance` has no `foo` parameter)
|
||||
# error: [unknown-argument]
|
||||
if isinstance(x, int, foo="bar"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
```
|
||||
|
||||
## `type[]` types are narrowed as well as class-literal types
|
||||
|
||||
```py
|
||||
def _(x: object, y: type[int]):
|
||||
if isinstance(x, y):
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
@@ -90,15 +90,19 @@ def _(t: type[object]):
|
||||
if issubclass(t, B):
|
||||
reveal_type(t) # revealed: type[A] & type[B]
|
||||
else:
|
||||
reveal_type(t) # revealed: type[object] & ~type[A]
|
||||
reveal_type(t) # revealed: type & ~type[A]
|
||||
```
|
||||
|
||||
### 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):
|
||||
@@ -142,7 +146,7 @@ class A: ...
|
||||
|
||||
t = object()
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
# error: [invalid-argument-type]
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
```
|
||||
@@ -156,7 +160,7 @@ branch:
|
||||
```py
|
||||
t = 1
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
# error: [invalid-argument-type]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
@@ -230,8 +234,15 @@ def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`issubclass` has no `foo` parameter)
|
||||
# error: [unknown-argument]
|
||||
if issubclass(t, int, foo="bar"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
|
||||
### `type[]` types are narrowed as well as class-literal types
|
||||
|
||||
```py
|
||||
def _(x: type, y: type[int]):
|
||||
if issubclass(x, y):
|
||||
reveal_type(x) # revealed: type[int]
|
||||
```
|
||||
|
||||
@@ -16,3 +16,48 @@ def _(flag: bool):
|
||||
|
||||
reveal_type(y) # revealed: Literal[0] | None
|
||||
```
|
||||
|
||||
## Class patterns
|
||||
|
||||
```py
|
||||
def get_object() -> object: ...
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case A():
|
||||
reveal_type(x) # revealed: A
|
||||
case B():
|
||||
# TODO could be `B & ~A`
|
||||
reveal_type(x) # revealed: B
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
## Class pattern with guard
|
||||
|
||||
```py
|
||||
def get_object() -> object: ...
|
||||
|
||||
class A:
|
||||
def y() -> int: ...
|
||||
|
||||
class B: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case A() if reveal_type(x): # revealed: A
|
||||
pass
|
||||
case B() if reveal_type(x): # revealed: B
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
@@ -9,39 +9,39 @@ def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[(
|
||||
x = foo()
|
||||
|
||||
if x:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
|
||||
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
|
||||
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
|
||||
|
||||
if not x:
|
||||
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
|
||||
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
|
||||
reveal_type(x) # revealed: Literal[-1, True, "foo", 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[()]
|
||||
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
|
||||
|
||||
if not (x and not x):
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
|
||||
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | 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[()]
|
||||
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | 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[()]
|
||||
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
|
||||
|
||||
if (isinstance(x, int) or isinstance(x, str)) and x:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"]
|
||||
reveal_type(x) # revealed: Literal[-1, True, "foo"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[b"", b"bar"] | None | tuple[()] | Literal[0] | Literal[False] | Literal[""]
|
||||
reveal_type(x) # revealed: Literal[b"", b"bar", 0, False, ""] | None | tuple[()]
|
||||
```
|
||||
|
||||
## Function Literals
|
||||
@@ -87,10 +87,10 @@ def f(x: A | B):
|
||||
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
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if x or not x:
|
||||
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy | A & ~AlwaysTruthy | B & ~AlwaysTruthy
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
|
||||
```
|
||||
@@ -166,16 +166,16 @@ 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"]
|
||||
reveal_type(y) # revealed: Literal[0, 42, "", "hello"]
|
||||
|
||||
z = x if flag() else y
|
||||
|
||||
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42] | Literal["", "hello"]
|
||||
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"]
|
||||
|
||||
if z:
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42] | Literal["hello"]
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"]
|
||||
else:
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0] | Literal[""]
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""]
|
||||
```
|
||||
|
||||
## Narrowing Multiple Variables
|
||||
@@ -214,8 +214,101 @@ if x and not x:
|
||||
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
|
||||
else:
|
||||
y = x
|
||||
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
|
||||
reveal_type(y) # revealed: A
|
||||
|
||||
# TODO: It should be A. We should improve UnionBuilder or IntersectionBuilder. (issue #15023)
|
||||
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
|
||||
reveal_type(y) # revealed: A
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -37,7 +37,7 @@ class C:
|
||||
# error: [possibly-unresolved-reference]
|
||||
y = x
|
||||
|
||||
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
|
||||
reveal_type(C.y) # revealed: Literal[1, "abc"]
|
||||
```
|
||||
|
||||
## Unbound function local
|
||||
|
||||
@@ -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(return type)
|
||||
|
||||
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
@@ -0,0 +1,78 @@
|
||||
# Ellipsis
|
||||
|
||||
## Function and methods
|
||||
|
||||
The ellipsis literal `...` can be used as a placeholder default value for a function parameter, in a
|
||||
stub file only, regardless of the type of the parameter.
|
||||
|
||||
```py path=test.pyi
|
||||
def f(x: int = ...) -> None:
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def f2(x: str = ...) -> None:
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
## Class and module symbols
|
||||
|
||||
The ellipsis literal can be assigned to a class or module symbol, regardless of its declared type,
|
||||
in a stub file only.
|
||||
|
||||
```py path=test.pyi
|
||||
y: bytes = ...
|
||||
reveal_type(y) # revealed: bytes
|
||||
x = ...
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
class Foo:
|
||||
y: int = ...
|
||||
|
||||
reveal_type(Foo.y) # revealed: int
|
||||
```
|
||||
|
||||
## Unpacking ellipsis literal in assignment
|
||||
|
||||
No diagnostic is emitted if an ellipsis literal is "unpacked" in a stub file as part of an
|
||||
assignment statement:
|
||||
|
||||
```py path=test.pyi
|
||||
x, y = ...
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unpacking ellipsis literal in for loops
|
||||
|
||||
Iterating over an ellipsis literal as part of a `for` loop in a stub is invalid, however, and
|
||||
results in a diagnostic:
|
||||
|
||||
```py path=test.pyi
|
||||
# error: [not-iterable] "Object of type `ellipsis` is not iterable"
|
||||
for a, b in ...:
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Ellipsis usage in non stub file
|
||||
|
||||
In a non-stub file, there's no special treatment of ellipsis literals. An ellipsis literal can only
|
||||
be assigned if `EllipsisType` is actually assignable to the annotated type.
|
||||
|
||||
```py
|
||||
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
|
||||
def f(x: int = ...) -> None: ...
|
||||
|
||||
# error: 1 [invalid-assignment] "Object of type `ellipsis` is not assignable to `int`"
|
||||
a: int = ...
|
||||
b = ...
|
||||
reveal_type(b) # revealed: ellipsis
|
||||
```
|
||||
|
||||
## Use of `Ellipsis` symbol
|
||||
|
||||
There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals.
|
||||
|
||||
```py path=test.pyi
|
||||
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
|
||||
def f(x: int = Ellipsis) -> None: ...
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
352
crates/red_knot_python_semantic/resources/mdtest/type_api.md
Normal file
352
crates/red_knot_python_semantic/resources/mdtest/type_api.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# Type API (`knot_extensions`)
|
||||
|
||||
This document describes the internal `knot_extensions` API for creating and manipulating types as
|
||||
well as testing various type system properties.
|
||||
|
||||
## Type extensions
|
||||
|
||||
The Python language itself allows us to perform a variety of operations on types. For example, we
|
||||
can build a union of types like `int | None`, or we can use type constructors such as `list[int]`
|
||||
and `type[int]` to create new types. But some type-level operations that we rely on in Red Knot,
|
||||
like intersections, cannot yet be expressed in Python. The `knot_extensions` module provides the
|
||||
`Intersection` and `Not` type constructors (special forms) which allow us to construct these types
|
||||
directly.
|
||||
|
||||
### Negation
|
||||
|
||||
```py
|
||||
from knot_extensions import Not, static_assert
|
||||
|
||||
def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None:
|
||||
reveal_type(n1) # revealed: ~int
|
||||
reveal_type(n2) # revealed: int
|
||||
reveal_type(n3) # revealed: ~int
|
||||
|
||||
def static_truthiness(not_one: Not[Literal[1]]) -> None:
|
||||
static_assert(not_one != 1)
|
||||
static_assert(not (not_one == 1))
|
||||
|
||||
# error: "Special form `knot_extensions.Not` expected exactly one type parameter"
|
||||
n: Not[int, str]
|
||||
```
|
||||
|
||||
### Intersection
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
|
||||
from typing_extensions import Never
|
||||
|
||||
class S: ...
|
||||
class T: ...
|
||||
|
||||
def x(x1: Intersection[S, T], x2: Intersection[S, Not[T]]) -> None:
|
||||
reveal_type(x1) # revealed: S & T
|
||||
reveal_type(x2) # revealed: S & ~T
|
||||
|
||||
def y(y1: Intersection[int, object], y2: Intersection[int, bool], y3: Intersection[int, Never]) -> None:
|
||||
reveal_type(y1) # revealed: int
|
||||
reveal_type(y2) # revealed: bool
|
||||
reveal_type(y3) # revealed: Never
|
||||
|
||||
def z(z1: Intersection[int, Not[Literal[1]], Not[Literal[2]]]) -> None:
|
||||
reveal_type(z1) # revealed: int & ~Literal[1] & ~Literal[2]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
type ABC = Intersection[A, B, C]
|
||||
|
||||
static_assert(is_subtype_of(ABC, A))
|
||||
static_assert(is_subtype_of(ABC, B))
|
||||
static_assert(is_subtype_of(ABC, C))
|
||||
|
||||
class D: ...
|
||||
|
||||
static_assert(not is_subtype_of(ABC, D))
|
||||
```
|
||||
|
||||
### Unknown type
|
||||
|
||||
The `Unknown` type is a special type that we use to represent actually unknown types (no
|
||||
annotation), as opposed to `Any` which represents an explicitly unknown type.
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown, static_assert, is_assignable_to, is_fully_static
|
||||
|
||||
static_assert(is_assignable_to(Unknown, int))
|
||||
static_assert(is_assignable_to(int, Unknown))
|
||||
|
||||
static_assert(not is_fully_static(Unknown))
|
||||
|
||||
def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: tuple[str, Unknown]
|
||||
reveal_type(z) # revealed: Unknown | Literal[1]
|
||||
|
||||
# Unknown can be subclassed, just like Any
|
||||
class C(Unknown): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
reveal_type(C.__mro__)
|
||||
|
||||
# error: "Special form `knot_extensions.Unknown` expected no type parameter"
|
||||
u: Unknown[str]
|
||||
```
|
||||
|
||||
## Static assertions
|
||||
|
||||
### Basics
|
||||
|
||||
The `knot_extensions` module provides a `static_assert` function that can be used to enforce
|
||||
properties at type-check time. The function takes an arbitrary expression and raises a type error if
|
||||
the expression is not of statically known truthiness.
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
from typing import TYPE_CHECKING
|
||||
import sys
|
||||
|
||||
static_assert(True)
|
||||
static_assert(False) # error: "Static assertion error: argument evaluates to `False`"
|
||||
|
||||
static_assert(False or True)
|
||||
static_assert(True and True)
|
||||
static_assert(False or False) # error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(False and True) # error: "Static assertion error: argument evaluates to `False`"
|
||||
|
||||
static_assert(1 + 1 == 2)
|
||||
static_assert(1 + 1 == 3) # error: "Static assertion error: argument evaluates to `False`"
|
||||
|
||||
static_assert("a" in "abc")
|
||||
static_assert("d" in "abc") # error: "Static assertion error: argument evaluates to `False`"
|
||||
|
||||
n = None
|
||||
static_assert(n is None)
|
||||
|
||||
static_assert(TYPE_CHECKING)
|
||||
|
||||
static_assert(sys.version_info >= (3, 6))
|
||||
```
|
||||
|
||||
### Narrowing constraints
|
||||
|
||||
Static assertions can be used to enforce narrowing constraints:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
|
||||
def f(x: int) -> None:
|
||||
if x != 0:
|
||||
static_assert(x != 0)
|
||||
else:
|
||||
# `int` can be subclassed, so we cannot assert that `x == 0` here:
|
||||
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
|
||||
static_assert(x == 0)
|
||||
```
|
||||
|
||||
### Truthy expressions
|
||||
|
||||
See also: <https://docs.python.org/3/library/stdtypes.html#truth-value-testing>
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
|
||||
static_assert(True)
|
||||
static_assert(False) # error: "Static assertion error: argument evaluates to `False`"
|
||||
|
||||
static_assert(None) # error: "Static assertion error: argument of type `None` is statically known to be falsy"
|
||||
|
||||
static_assert(1)
|
||||
static_assert(0) # error: "Static assertion error: argument of type `Literal[0]` is statically known to be falsy"
|
||||
|
||||
static_assert((0,))
|
||||
static_assert(()) # error: "Static assertion error: argument of type `tuple[()]` is statically known to be falsy"
|
||||
|
||||
static_assert("a")
|
||||
static_assert("") # error: "Static assertion error: argument of type `Literal[""]` is statically known to be falsy"
|
||||
|
||||
static_assert(b"a")
|
||||
static_assert(b"") # error: "Static assertion error: argument of type `Literal[b""]` is statically known to be falsy"
|
||||
```
|
||||
|
||||
### Error messages
|
||||
|
||||
We provide various tailored error messages for wrong argument types to `static_assert`:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
|
||||
static_assert(2 * 3 == 6)
|
||||
|
||||
# error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(2 * 3 == 7)
|
||||
|
||||
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
|
||||
static_assert(int(2.0 * 3.0) == 6)
|
||||
|
||||
class InvalidBoolDunder:
|
||||
def __bool__(self) -> int:
|
||||
return 1
|
||||
|
||||
# error: "Static assertion error: argument of type `InvalidBoolDunder` has an ambiguous static truthiness"
|
||||
static_assert(InvalidBoolDunder())
|
||||
```
|
||||
|
||||
### Custom error messages
|
||||
|
||||
Alternatively, users can provide custom error messages:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
|
||||
# error: "Static assertion error: I really want this to be true"
|
||||
static_assert(1 + 1 == 3, "I really want this to be true")
|
||||
|
||||
error_message = "A custom message "
|
||||
error_message += "constructed from multiple string literals"
|
||||
# error: "Static assertion error: A custom message constructed from multiple string literals"
|
||||
static_assert(False, error_message)
|
||||
|
||||
# There are limitations to what we can still infer as a string literal. In those cases,
|
||||
# we simply fall back to the default message.
|
||||
shouted_message = "A custom message".upper()
|
||||
# error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(False, shouted_message)
|
||||
```
|
||||
|
||||
## Type predicates
|
||||
|
||||
The `knot_extensions` module also provides predicates to test various properties of types. These are
|
||||
implemented as functions that return `Literal[True]` or `Literal[False]` depending on the result of
|
||||
the test.
|
||||
|
||||
### Equivalence
|
||||
|
||||
```py
|
||||
from knot_extensions import is_equivalent_to, static_assert
|
||||
from typing_extensions import Never, Union
|
||||
|
||||
static_assert(is_equivalent_to(type, type[object]))
|
||||
static_assert(is_equivalent_to(tuple[int, Never], Never))
|
||||
static_assert(is_equivalent_to(int | str, Union[int, str]))
|
||||
|
||||
static_assert(not is_equivalent_to(int, str))
|
||||
static_assert(not is_equivalent_to(int | str, int | str | bytes))
|
||||
```
|
||||
|
||||
### Subtyping
|
||||
|
||||
```py
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(bool, int))
|
||||
static_assert(not is_subtype_of(str, int))
|
||||
|
||||
static_assert(is_subtype_of(bool, int | str))
|
||||
static_assert(is_subtype_of(str, int | str))
|
||||
static_assert(not is_subtype_of(bytes, int | str))
|
||||
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
static_assert(is_subtype_of(Derived, Base))
|
||||
static_assert(not is_subtype_of(Base, Derived))
|
||||
static_assert(is_subtype_of(Base, Base))
|
||||
|
||||
static_assert(not is_subtype_of(Unrelated, Base))
|
||||
static_assert(not is_subtype_of(Base, Unrelated))
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
```py
|
||||
from knot_extensions import is_assignable_to, static_assert
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_assignable_to(int, Any))
|
||||
static_assert(is_assignable_to(Any, str))
|
||||
static_assert(not is_assignable_to(int, str))
|
||||
```
|
||||
|
||||
### Disjointness
|
||||
|
||||
```py
|
||||
from knot_extensions import is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(None, int))
|
||||
static_assert(not is_disjoint_from(Literal[2] | str, int))
|
||||
```
|
||||
|
||||
### Fully static types
|
||||
|
||||
```py
|
||||
from knot_extensions import is_fully_static, static_assert
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_fully_static(int | str))
|
||||
static_assert(is_fully_static(type[int]))
|
||||
|
||||
static_assert(not is_fully_static(int | Any))
|
||||
static_assert(not is_fully_static(type[Any]))
|
||||
```
|
||||
|
||||
### Singleton types
|
||||
|
||||
```py
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(None))
|
||||
static_assert(is_singleton(Literal[True]))
|
||||
|
||||
static_assert(not is_singleton(int))
|
||||
static_assert(not is_singleton(Literal["a"]))
|
||||
```
|
||||
|
||||
### Single-valued types
|
||||
|
||||
```py
|
||||
from knot_extensions import is_single_valued, static_assert
|
||||
|
||||
static_assert(is_single_valued(None))
|
||||
static_assert(is_single_valued(Literal[True]))
|
||||
static_assert(is_single_valued(Literal["a"]))
|
||||
|
||||
static_assert(not is_single_valued(int))
|
||||
static_assert(not is_single_valued(Literal["a"] | Literal["b"]))
|
||||
```
|
||||
|
||||
## `TypeOf`
|
||||
|
||||
We use `TypeOf` to get the inferred type of an expression. This is useful when we want to refer to
|
||||
it in a type expression. For example, if we want to make sure that the class literal type `str` is a
|
||||
subtype of `type[str]`, we can not use `is_subtype_of(str, type[str])`, as that would test if the
|
||||
type `str` itself is a subtype of `type[str]`. Instead, we can use `TypeOf[str]` to get the type of
|
||||
the expression `str`:
|
||||
|
||||
```py
|
||||
from knot_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
# This is incorrect and therefore fails with ...
|
||||
# error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(is_subtype_of(str, type[str]))
|
||||
|
||||
# Correct, returns True:
|
||||
static_assert(is_subtype_of(TypeOf[str], type[str]))
|
||||
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
|
||||
# `TypeOf` can be used in annotations:
|
||||
def type_of_annotation() -> None:
|
||||
t1: TypeOf[Base] = Base
|
||||
t2: TypeOf[Base] = Derived # error: [invalid-assignment]
|
||||
|
||||
# Note how this is different from `type[…]` which includes subclasses:
|
||||
s1: type[Base] = Base
|
||||
s2: type[Base] = Derived # no error here
|
||||
|
||||
# error: "Special form `knot_extensions.TypeOf` expected exactly one type parameter"
|
||||
t: TypeOf[int, str, bytes]
|
||||
```
|
||||
@@ -142,3 +142,25 @@ class Foo(type[int]): ...
|
||||
# TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `@final` classes
|
||||
|
||||
`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is
|
||||
used as the type argument. This applies to standard-library classes and user-defined classes:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from types import EllipsisType
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class Foo: ...
|
||||
|
||||
def _(x: type[Foo], y: type[EllipsisType]):
|
||||
reveal_type(x) # revealed: Literal[Foo]
|
||||
reveal_type(y) # revealed: Literal[EllipsisType]
|
||||
```
|
||||
|
||||
@@ -47,9 +47,8 @@ x: type = A() # error: [invalid-assignment]
|
||||
|
||||
```py
|
||||
def f(x: type[object]):
|
||||
reveal_type(x) # revealed: type[object]
|
||||
# TODO: bound method types
|
||||
reveal_type(x.__repr__) # revealed: Literal[__repr__]
|
||||
reveal_type(x) # revealed: type
|
||||
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
|
||||
|
||||
class A: ...
|
||||
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
# Assignable-to relation
|
||||
|
||||
The `is_assignable_to(S, T)` relation below checks if type `S` is assignable to type `T` (target).
|
||||
This allows us to check if a type `S` can be used in a context where a type `T` is expected
|
||||
(function arguments, variable assignments). See the [typing documentation] for a precise definition
|
||||
of this concept.
|
||||
|
||||
## Basic types
|
||||
|
||||
### Fully static
|
||||
|
||||
Fully static types participate in subtyping. If a type `S` is a subtype of `T`, `S` will also be
|
||||
assignable to `T`. Two equivalent types are subtypes of each other:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
|
||||
class Parent: ...
|
||||
class Child1(Parent): ...
|
||||
class Child2(Parent): ...
|
||||
class Grandchild(Child1, Child2): ...
|
||||
class Unrelated: ...
|
||||
|
||||
static_assert(is_assignable_to(int, int))
|
||||
static_assert(is_assignable_to(Parent, Parent))
|
||||
static_assert(is_assignable_to(Child1, Parent))
|
||||
static_assert(is_assignable_to(Grandchild, Parent))
|
||||
static_assert(is_assignable_to(Unrelated, Unrelated))
|
||||
|
||||
static_assert(not is_assignable_to(str, int))
|
||||
static_assert(not is_assignable_to(object, int))
|
||||
static_assert(not is_assignable_to(Parent, Child1))
|
||||
static_assert(not is_assignable_to(Unrelated, Parent))
|
||||
static_assert(not is_assignable_to(Child1, Child2))
|
||||
```
|
||||
|
||||
### Gradual types
|
||||
|
||||
Gradual types do not participate in subtyping, but can still be assignable to other types (and
|
||||
static types can be assignable to gradual types):
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_assignable_to(Unknown, Literal[1]))
|
||||
static_assert(is_assignable_to(Any, Literal[1]))
|
||||
static_assert(is_assignable_to(Literal[1], Unknown))
|
||||
static_assert(is_assignable_to(Literal[1], Any))
|
||||
```
|
||||
|
||||
## Literal types
|
||||
|
||||
### Boolean literals
|
||||
|
||||
`Literal[True]` and `Literal[False]` are both subtypes of (and therefore assignable to) `bool`,
|
||||
which is in turn a subtype of `int`:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_assignable_to(Literal[True], Literal[True]))
|
||||
static_assert(is_assignable_to(Literal[True], bool))
|
||||
static_assert(is_assignable_to(Literal[True], int))
|
||||
|
||||
static_assert(not is_assignable_to(Literal[True], Literal[False]))
|
||||
static_assert(not is_assignable_to(bool, Literal[True]))
|
||||
```
|
||||
|
||||
### Integer literals
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_assignable_to(Literal[1], Literal[1]))
|
||||
static_assert(is_assignable_to(Literal[1], int))
|
||||
|
||||
static_assert(not is_assignable_to(Literal[1], Literal[2]))
|
||||
static_assert(not is_assignable_to(int, Literal[1]))
|
||||
static_assert(not is_assignable_to(Literal[1], str))
|
||||
```
|
||||
|
||||
### String literals and `LiteralString`
|
||||
|
||||
All string-literal types are subtypes of (and therefore assignable to) `LiteralString`, which is in
|
||||
turn a subtype of `str`:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
static_assert(is_assignable_to(Literal["foo"], Literal["foo"]))
|
||||
static_assert(is_assignable_to(Literal["foo"], LiteralString))
|
||||
static_assert(is_assignable_to(Literal["foo"], str))
|
||||
|
||||
static_assert(is_assignable_to(LiteralString, str))
|
||||
|
||||
static_assert(not is_assignable_to(Literal["foo"], Literal["bar"]))
|
||||
static_assert(not is_assignable_to(str, Literal["foo"]))
|
||||
static_assert(not is_assignable_to(str, LiteralString))
|
||||
```
|
||||
|
||||
### Byte literals
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
static_assert(is_assignable_to(Literal[b"foo"], bytes))
|
||||
static_assert(is_assignable_to(Literal[b"foo"], Literal[b"foo"]))
|
||||
|
||||
static_assert(not is_assignable_to(Literal[b"foo"], str))
|
||||
static_assert(not is_assignable_to(Literal[b"foo"], LiteralString))
|
||||
static_assert(not is_assignable_to(Literal[b"foo"], Literal[b"bar"]))
|
||||
static_assert(not is_assignable_to(Literal[b"foo"], Literal["foo"]))
|
||||
static_assert(not is_assignable_to(Literal["foo"], Literal[b"foo"]))
|
||||
```
|
||||
|
||||
## `type[…]` and class literals
|
||||
|
||||
In the following tests, `TypeOf[str]` is a singleton type with a single inhabitant, the class `str`.
|
||||
This contrasts with `type[str]`, which represents "all possible subclasses of `str`".
|
||||
|
||||
Both `TypeOf[str]` and `type[str]` are subtypes of `type` and `type[object]`, which both represent
|
||||
"all possible instances of `type`"; therefore both `type[str]` and `TypeOf[str]` are assignable to
|
||||
`type`. `type[Any]`, on the other hand, represents a type of unknown size or inhabitants, but which
|
||||
is known to be no larger than the set of possible objects represented by `type`.
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown, TypeOf
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_assignable_to(type, type))
|
||||
static_assert(is_assignable_to(type[object], type[object]))
|
||||
|
||||
static_assert(is_assignable_to(type, type[object]))
|
||||
static_assert(is_assignable_to(type[object], type))
|
||||
|
||||
static_assert(is_assignable_to(type[str], type[object]))
|
||||
static_assert(is_assignable_to(TypeOf[str], type[object]))
|
||||
static_assert(is_assignable_to(type[str], type))
|
||||
static_assert(is_assignable_to(TypeOf[str], type))
|
||||
|
||||
static_assert(is_assignable_to(type[str], type[str]))
|
||||
static_assert(is_assignable_to(TypeOf[str], type[str]))
|
||||
|
||||
static_assert(not is_assignable_to(TypeOf[int], type[str]))
|
||||
static_assert(not is_assignable_to(type, type[str]))
|
||||
static_assert(not is_assignable_to(type[object], type[str]))
|
||||
|
||||
static_assert(is_assignable_to(type[Any], type[Any]))
|
||||
static_assert(is_assignable_to(type[Any], type[object]))
|
||||
static_assert(is_assignable_to(type[object], type[Any]))
|
||||
static_assert(is_assignable_to(type, type[Any]))
|
||||
static_assert(is_assignable_to(type[Any], type[str]))
|
||||
static_assert(is_assignable_to(type[str], type[Any]))
|
||||
static_assert(is_assignable_to(TypeOf[str], type[Any]))
|
||||
|
||||
static_assert(is_assignable_to(type[Unknown], type[Unknown]))
|
||||
static_assert(is_assignable_to(type[Unknown], type[object]))
|
||||
static_assert(is_assignable_to(type[object], type[Unknown]))
|
||||
static_assert(is_assignable_to(type, type[Unknown]))
|
||||
static_assert(is_assignable_to(type[Unknown], type[str]))
|
||||
static_assert(is_assignable_to(type[str], type[Unknown]))
|
||||
static_assert(is_assignable_to(TypeOf[str], type[Unknown]))
|
||||
|
||||
static_assert(is_assignable_to(type[Unknown], type[Any]))
|
||||
static_assert(is_assignable_to(type[Any], type[Unknown]))
|
||||
|
||||
static_assert(not is_assignable_to(object, type[Any]))
|
||||
static_assert(not is_assignable_to(str, type[Any]))
|
||||
|
||||
class Meta(type): ...
|
||||
|
||||
static_assert(is_assignable_to(type[Any], Meta))
|
||||
static_assert(is_assignable_to(type[Unknown], Meta))
|
||||
static_assert(is_assignable_to(Meta, type[Any]))
|
||||
static_assert(is_assignable_to(Meta, type[Unknown]))
|
||||
```
|
||||
|
||||
## Tuple types
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
from typing import Literal, Any
|
||||
|
||||
static_assert(is_assignable_to(tuple[()], tuple[()]))
|
||||
static_assert(is_assignable_to(tuple[int], tuple[int]))
|
||||
static_assert(is_assignable_to(tuple[int], tuple[Any]))
|
||||
static_assert(is_assignable_to(tuple[Any], tuple[int]))
|
||||
static_assert(is_assignable_to(tuple[int, str], tuple[int, str]))
|
||||
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, int]))
|
||||
static_assert(is_assignable_to(tuple[Any, Literal[2]], tuple[int, int]))
|
||||
static_assert(is_assignable_to(tuple[Literal[1], Any], tuple[int, int]))
|
||||
|
||||
static_assert(not is_assignable_to(tuple[()], tuple[int]))
|
||||
static_assert(not is_assignable_to(tuple[int], tuple[str]))
|
||||
static_assert(not is_assignable_to(tuple[int], tuple[int, str]))
|
||||
static_assert(not is_assignable_to(tuple[int, str], tuple[int]))
|
||||
static_assert(not is_assignable_to(tuple[int, int], tuple[Literal[1], int]))
|
||||
static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str]))
|
||||
```
|
||||
|
||||
## Union types
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing import Literal, Any
|
||||
|
||||
static_assert(is_assignable_to(int, int | str))
|
||||
static_assert(is_assignable_to(str, int | str))
|
||||
static_assert(is_assignable_to(int | str, int | str))
|
||||
static_assert(is_assignable_to(str | int, int | str))
|
||||
static_assert(is_assignable_to(Literal[1], int | str))
|
||||
static_assert(is_assignable_to(Literal[1], Unknown | str))
|
||||
static_assert(is_assignable_to(Literal[1] | Literal[2], Literal[1] | Literal[2]))
|
||||
static_assert(is_assignable_to(Literal[1] | Literal[2], int))
|
||||
static_assert(is_assignable_to(Literal[1] | None, int | None))
|
||||
static_assert(is_assignable_to(Any, int | str))
|
||||
static_assert(is_assignable_to(Any | int, int))
|
||||
static_assert(is_assignable_to(str, int | Any))
|
||||
|
||||
static_assert(not is_assignable_to(int | None, int))
|
||||
static_assert(not is_assignable_to(int | None, str | None))
|
||||
static_assert(not is_assignable_to(Literal[1] | None, int))
|
||||
static_assert(not is_assignable_to(Literal[1] | None, str | None))
|
||||
static_assert(not is_assignable_to(Any | int | str, int))
|
||||
```
|
||||
|
||||
## Intersection types
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Intersection, Not
|
||||
from typing_extensions import Any, Literal
|
||||
|
||||
class Parent: ...
|
||||
class Child1(Parent): ...
|
||||
class Child2(Parent): ...
|
||||
class Grandchild(Child1, Child2): ...
|
||||
class Unrelated: ...
|
||||
|
||||
static_assert(is_assignable_to(Intersection[Child1, Child2], Child1))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Child2], Child2))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Child2], Parent))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Parent], Parent))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[Parent, Unrelated], Parent))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Unrelated], Child1))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Child1))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Parent))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Not[Grandchild]], Parent))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child1, Child2]))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child2, Child1]))
|
||||
static_assert(is_assignable_to(Grandchild, Intersection[Child1, Child2]))
|
||||
|
||||
static_assert(not is_assignable_to(Parent, Intersection[Parent, Unrelated]))
|
||||
static_assert(not is_assignable_to(int, Intersection[int, Not[Literal[1]]]))
|
||||
static_assert(not is_assignable_to(int, Not[int]))
|
||||
static_assert(not is_assignable_to(int, Not[Literal[1]]))
|
||||
|
||||
static_assert(not is_assignable_to(Intersection[Any, Parent], Unrelated))
|
||||
|
||||
# TODO: The following assertions should not fail (see https://github.com/astral-sh/ruff/issues/14899)
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Any, int], int))
|
||||
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]]))
|
||||
```
|
||||
|
||||
## General properties
|
||||
|
||||
See also: our property tests in `property_tests.rs`.
|
||||
|
||||
### Everything is assignable to `object`
|
||||
|
||||
`object` is Python's top type; the set of all possible objects at runtime:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing import Literal, Any
|
||||
|
||||
static_assert(is_assignable_to(str, object))
|
||||
static_assert(is_assignable_to(Literal[1], object))
|
||||
static_assert(is_assignable_to(object, object))
|
||||
static_assert(is_assignable_to(type, object))
|
||||
static_assert(is_assignable_to(Any, object))
|
||||
static_assert(is_assignable_to(Unknown, object))
|
||||
static_assert(is_assignable_to(type[object], object))
|
||||
static_assert(is_assignable_to(type[str], object))
|
||||
static_assert(is_assignable_to(type[Any], object))
|
||||
```
|
||||
|
||||
### Every type is assignable to `Any` / `Unknown`
|
||||
|
||||
`Any` and `Unknown` are gradual types. They could materialize to any given type at runtime, and so
|
||||
any type is assignable to them:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing import Literal, Any
|
||||
|
||||
static_assert(is_assignable_to(str, Any))
|
||||
static_assert(is_assignable_to(Literal[1], Any))
|
||||
static_assert(is_assignable_to(object, Any))
|
||||
static_assert(is_assignable_to(type, Any))
|
||||
static_assert(is_assignable_to(Any, Any))
|
||||
static_assert(is_assignable_to(Unknown, Any))
|
||||
static_assert(is_assignable_to(type[object], Any))
|
||||
static_assert(is_assignable_to(type[str], Any))
|
||||
static_assert(is_assignable_to(type[Any], Any))
|
||||
|
||||
static_assert(is_assignable_to(str, Unknown))
|
||||
static_assert(is_assignable_to(Literal[1], Unknown))
|
||||
static_assert(is_assignable_to(object, Unknown))
|
||||
static_assert(is_assignable_to(type, Unknown))
|
||||
static_assert(is_assignable_to(Any, Unknown))
|
||||
static_assert(is_assignable_to(Unknown, Unknown))
|
||||
static_assert(is_assignable_to(type[object], Unknown))
|
||||
static_assert(is_assignable_to(type[str], Unknown))
|
||||
static_assert(is_assignable_to(type[Any], Unknown))
|
||||
```
|
||||
|
||||
### `Never` is assignable to every type
|
||||
|
||||
`Never` is Python's bottom type: the empty set, a type with no inhabitants. It is therefore
|
||||
assignable to any arbitrary type.
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing_extensions import Never, Any
|
||||
|
||||
static_assert(is_assignable_to(Never, str))
|
||||
static_assert(is_assignable_to(Never, Literal[1]))
|
||||
static_assert(is_assignable_to(Never, object))
|
||||
static_assert(is_assignable_to(Never, type))
|
||||
static_assert(is_assignable_to(Never, Any))
|
||||
static_assert(is_assignable_to(Never, Unknown))
|
||||
static_assert(is_assignable_to(Never, type[object]))
|
||||
static_assert(is_assignable_to(Never, type[str]))
|
||||
static_assert(is_assignable_to(Never, type[Any]))
|
||||
```
|
||||
|
||||
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
@@ -0,0 +1,33 @@
|
||||
# Tuples containing `Never`
|
||||
|
||||
A heterogeneous `tuple[…]` type that contains `Never` as a type argument simplifies to `Never`. One
|
||||
way to think about this is the following: in order to construct a tuple, you need to have an object
|
||||
of every element type. But since there is no object of type `Never`, you cannot construct the tuple.
|
||||
Such a tuple type is therefore uninhabited and equivalent to `Never`.
|
||||
|
||||
In the language of algebraic data types, a tuple type is a product type and `Never` acts like the
|
||||
zero element in multiplication, similar to how a Cartesian product with the empty set is the empty
|
||||
set.
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_equivalent_to
|
||||
from typing_extensions import Never, NoReturn
|
||||
|
||||
static_assert(is_equivalent_to(Never, tuple[Never]))
|
||||
static_assert(is_equivalent_to(Never, tuple[Never, int]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, Never]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, Never, str]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, tuple[str, Never]]))
|
||||
static_assert(is_equivalent_to(Never, tuple[tuple[str, Never], int]))
|
||||
|
||||
# The empty tuple is *not* equivalent to Never!
|
||||
static_assert(not is_equivalent_to(Never, tuple[()]))
|
||||
|
||||
# NoReturn is just a different spelling of Never, so the same is true for NoReturn
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn, int]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn, str]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[int, tuple[str, NoReturn]]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[tuple[str, NoReturn], int]))
|
||||
```
|
||||
@@ -34,6 +34,10 @@ reveal_type(~No()) # revealed: Unknown
|
||||
|
||||
## Classes
|
||||
|
||||
Dunder methods defined in a class are available to instances of that class, but not to the class
|
||||
itself. (For these operators to work on the class itself, they would have to be defined on the
|
||||
class's type, i.e. `type`.)
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __pos__(self) -> bool:
|
||||
|
||||
143
crates/red_knot_python_semantic/resources/mdtest/union_types.md
Normal file
143
crates/red_knot_python_semantic/resources/mdtest/union_types.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Union types
|
||||
|
||||
This test suite covers certain basic properties and simplification strategies for union types.
|
||||
|
||||
## Basic unions
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(u1: int | str, u2: Literal[0] | Literal[1]) -> None:
|
||||
reveal_type(u1) # revealed: int | str
|
||||
reveal_type(u2) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
## Duplicate elements are collapsed
|
||||
|
||||
```py
|
||||
def _(u1: int | int | str, u2: int | str | int) -> None:
|
||||
reveal_type(u1) # revealed: int | str
|
||||
reveal_type(u2) # revealed: int | str
|
||||
```
|
||||
|
||||
## `Never` is removed
|
||||
|
||||
`Never` is an empty set, a type with no inhabitants. Its presence in a union is always redundant,
|
||||
and so we eagerly simplify it away. `NoReturn` is equivalent to `Never`.
|
||||
|
||||
```py
|
||||
from typing_extensions import Never, NoReturn
|
||||
|
||||
def never(u1: int | Never, u2: int | Never | str) -> None:
|
||||
reveal_type(u1) # revealed: int
|
||||
reveal_type(u2) # revealed: int | str
|
||||
|
||||
def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None:
|
||||
reveal_type(u1) # revealed: int
|
||||
reveal_type(u2) # revealed: int | str
|
||||
```
|
||||
|
||||
## Flattening of nested unions
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(
|
||||
u1: (int | str) | bytes,
|
||||
u2: int | (str | bytes),
|
||||
u3: int | (str | (bytes | complex)),
|
||||
) -> None:
|
||||
reveal_type(u1) # revealed: int | str | bytes
|
||||
reveal_type(u2) # revealed: int | str | bytes
|
||||
reveal_type(u3) # revealed: int | str | bytes | complex
|
||||
```
|
||||
|
||||
## Simplification using subtyping
|
||||
|
||||
The type `S | T` can be simplified to `T` if `S` is a subtype of `T`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
def _(
|
||||
u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString
|
||||
) -> None:
|
||||
reveal_type(u1) # revealed: str
|
||||
reveal_type(u2) # revealed: str
|
||||
reveal_type(u3) # revealed: str
|
||||
reveal_type(u4) # revealed: str | bytes
|
||||
```
|
||||
|
||||
## Boolean literals
|
||||
|
||||
The union `Literal[True] | Literal[False]` is exactly equivalent to `bool`:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(
|
||||
u1: Literal[True, False],
|
||||
u2: bool | Literal[True],
|
||||
u3: Literal[True] | bool,
|
||||
u4: Literal[True] | Literal[True, 17],
|
||||
u5: Literal[True, False, True, 17],
|
||||
) -> None:
|
||||
reveal_type(u1) # revealed: bool
|
||||
reveal_type(u2) # revealed: bool
|
||||
reveal_type(u3) # revealed: bool
|
||||
reveal_type(u4) # revealed: Literal[True, 17]
|
||||
reveal_type(u5) # revealed: bool | Literal[17]
|
||||
```
|
||||
|
||||
## Do not erase `Unknown`
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(u1: Unknown | str, u2: str | Unknown) -> None:
|
||||
reveal_type(u1) # revealed: Unknown | str
|
||||
reveal_type(u2) # revealed: str | Unknown
|
||||
```
|
||||
|
||||
## Collapse multiple `Unknown`s
|
||||
|
||||
Since `Unknown` is a gradual type, it is not a subtype of anything, but multiple `Unknown`s in a
|
||||
union are still redundant:
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None:
|
||||
reveal_type(u1) # revealed: Unknown | str
|
||||
reveal_type(u2) # revealed: Unknown | str
|
||||
reveal_type(u3) # revealed: str | Unknown
|
||||
```
|
||||
|
||||
## Subsume multiple elements
|
||||
|
||||
Simplifications still apply when `Unknown` is present.
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(u1: str | Unknown | int | object):
|
||||
reveal_type(u1) # revealed: Unknown | object
|
||||
```
|
||||
|
||||
## Union of intersections
|
||||
|
||||
We can simplify unions of intersections:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Q] | Intersection[P, Q],
|
||||
i2: Intersection[P, Q] | Intersection[Q, P],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P & Q
|
||||
reveal_type(i2) # revealed: P & Q
|
||||
```
|
||||
@@ -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, "a"]
|
||||
reveal_type(b) # revealed: Literal[2, "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, "a"]
|
||||
reveal_type(b) # revealed: Literal[2, "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
|
||||
```
|
||||
|
||||
@@ -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};
|
||||
@@ -45,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())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,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]
|
||||
@@ -127,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
|
||||
@@ -137,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![],
|
||||
}
|
||||
@@ -173,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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -67,6 +79,7 @@ struct ModuleInner {
|
||||
kind: ModuleKind,
|
||||
search_path: SearchPath,
|
||||
file: File,
|
||||
known: Option<KnownModule>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
@@ -83,3 +96,69 @@ 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,
|
||||
KnotExtensions,
|
||||
}
|
||||
|
||||
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",
|
||||
Self::KnotExtensions => "knot_extensions",
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
"knot_extensions" => Some(Self::KnotExtensions),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_typing(self) -> bool {
|
||||
matches!(self, Self::Typing)
|
||||
}
|
||||
|
||||
pub const fn is_knot_extensions(self) -> bool {
|
||||
matches!(self, Self::KnotExtensions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,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::*;
|
||||
|
||||
@@ -1262,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;
|
||||
@@ -1296,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(),
|
||||
@@ -1801,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),
|
||||
}
|
||||
@@ -29,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>;
|
||||
@@ -378,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +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,
|
||||
@@ -24,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};
|
||||
@@ -285,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),
|
||||
@@ -297,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>) {
|
||||
@@ -324,30 +400,52 @@ 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> {
|
||||
// This is called for the top-level pattern of each match arm. We need to create a
|
||||
// standalone expression for each arm of a match statement, since they can introduce
|
||||
// constraints on the match subject. (Or more accurately, for the match arm's pattern,
|
||||
// since its the pattern that introduces any constraints, not the body.) Ideally, that
|
||||
// standalone expression would wrap the match arm's pattern as a whole. But a standalone
|
||||
// expression can currently only wrap an ast::Expr, which patterns are not. So, we need to
|
||||
// choose an Expr that can “stand in” for the pattern, which we can wrap in a standalone
|
||||
// expression.
|
||||
//
|
||||
// See the comment in TypeInferenceBuilder::infer_match_pattern for more details.
|
||||
|
||||
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)
|
||||
}
|
||||
ast::Pattern::MatchClass(pattern) => {
|
||||
let cls = self.add_standalone_expression(&pattern.cls);
|
||||
PatternConstraintKind::Class(cls, 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
|
||||
@@ -726,7 +824,7 @@ where
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
},
|
||||
value,
|
||||
UnpackValue::Assign(value),
|
||||
countme::Count::default(),
|
||||
)),
|
||||
})
|
||||
@@ -795,10 +893,13 @@ where
|
||||
}
|
||||
ast::Stmt::If(node) => {
|
||||
self.visit_expr(&node.test);
|
||||
let pre_if = self.flow_snapshot();
|
||||
let constraint = self.record_expression_constraint(&node.test);
|
||||
let mut constraints = vec![constraint];
|
||||
let mut no_branch_taken = self.flow_snapshot();
|
||||
let mut last_constraint = self.record_expression_constraint(&node.test);
|
||||
self.visit_body(&node.body);
|
||||
|
||||
let visibility_constraint_id = self.record_visibility_constraint(last_constraint);
|
||||
let mut vis_constraints = vec![visibility_constraint_id];
|
||||
|
||||
let mut post_clauses: Vec<FlowSnapshot> = vec![];
|
||||
let elif_else_clauses = node
|
||||
.elif_else_clauses
|
||||
@@ -820,20 +921,37 @@ where
|
||||
// the state that we merge the other snapshots into
|
||||
post_clauses.push(self.flow_snapshot());
|
||||
// we can only take an elif/else branch if none of the previous ones were
|
||||
// taken, so the block entry state is always `pre_if`
|
||||
self.flow_restore(pre_if.clone());
|
||||
for constraint in &constraints {
|
||||
self.record_negated_constraint(*constraint);
|
||||
}
|
||||
if let Some(elif_test) = clause_test {
|
||||
// taken
|
||||
self.flow_restore(no_branch_taken.clone());
|
||||
self.record_negated_constraint(last_constraint);
|
||||
|
||||
let elif_constraint = if let Some(elif_test) = clause_test {
|
||||
self.visit_expr(elif_test);
|
||||
constraints.push(self.record_expression_constraint(elif_test));
|
||||
}
|
||||
// A test expression is evaluated whether the branch is taken or not
|
||||
no_branch_taken = self.flow_snapshot();
|
||||
let constraint = self.record_expression_constraint(elif_test);
|
||||
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 {
|
||||
last_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(no_branch_taken);
|
||||
}
|
||||
ast::Stmt::While(ast::StmtWhile {
|
||||
test,
|
||||
@@ -856,6 +974,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 =
|
||||
@@ -863,15 +983,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,
|
||||
@@ -909,16 +1035,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)
|
||||
@@ -947,32 +1104,61 @@ where
|
||||
cases,
|
||||
range: _,
|
||||
}) => {
|
||||
self.add_standalone_expression(subject);
|
||||
self.visit_expr(subject);
|
||||
debug_assert_eq!(self.current_match_case, None);
|
||||
|
||||
let after_subject = self.flow_snapshot();
|
||||
let Some((first, remaining)) = cases.split_first() else {
|
||||
let subject_expr = self.add_standalone_expression(subject);
|
||||
self.visit_expr(subject);
|
||||
if cases.is_empty() {
|
||||
return;
|
||||
};
|
||||
self.add_pattern_constraint(subject, &first.pattern);
|
||||
self.visit_match_case(first);
|
||||
|
||||
let after_subject = self.flow_snapshot();
|
||||
let mut vis_constraints = vec![];
|
||||
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);
|
||||
self.visit_match_case(case);
|
||||
}
|
||||
for post_clause_state in post_case_snapshots {
|
||||
self.flow_merge(post_clause_state);
|
||||
for (i, case) in cases.iter().enumerate() {
|
||||
if i != 0 {
|
||||
post_case_snapshots.push(self.flow_snapshot());
|
||||
self.flow_restore(after_subject.clone());
|
||||
}
|
||||
|
||||
self.current_match_case = Some(CurrentMatchCase::new(&case.pattern));
|
||||
self.visit_pattern(&case.pattern);
|
||||
self.current_match_case = None;
|
||||
let constraint_id = self.add_pattern_constraint(
|
||||
subject_expr,
|
||||
&case.pattern,
|
||||
case.guard.as_deref(),
|
||||
);
|
||||
if let Some(expr) = &case.guard {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
self.visit_body(&case.body);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -982,6 +1168,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
|
||||
@@ -1136,12 +1324,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,
|
||||
},
|
||||
);
|
||||
@@ -1177,7 +1371,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;
|
||||
}
|
||||
@@ -1222,19 +1418,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 {
|
||||
@@ -1291,27 +1487,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);
|
||||
@@ -1327,18 +1551,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_match_case(&mut self, match_case: &'ast ast::MatchCase) {
|
||||
debug_assert!(self.current_match_case.is_none());
|
||||
self.current_match_case = Some(CurrentMatchCase::new(&match_case.pattern));
|
||||
self.visit_pattern(&match_case.pattern);
|
||||
self.current_match_case = None;
|
||||
|
||||
if let Some(expr) = &match_case.guard {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
self.visit_body(&match_case.body);
|
||||
}
|
||||
|
||||
fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) {
|
||||
if let ast::Pattern::MatchStar(ast::PatternMatchStar {
|
||||
name: Some(name),
|
||||
@@ -1391,7 +1603,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,
|
||||
@@ -1415,18 +1631,13 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct CurrentMatchCase<'a> {
|
||||
/// The pattern that's part of the current match case.
|
||||
pattern: &'a ast::Pattern,
|
||||
|
||||
@@ -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,15 @@ 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>>),
|
||||
Class(Expression<'db>, Option<Expression<'db>>),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
pub(crate) struct PatternConstraint<'db> {
|
||||
#[id]
|
||||
@@ -28,11 +36,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,21 @@ 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)
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_knot_extensions_definition(self, db: &'db dyn Db) -> bool {
|
||||
file_to_module(db, self.file(db))
|
||||
.is_some_and(|module| module.is_known(KnownModule::KnotExtensions))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,8 +230,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 +305,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 +393,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 +465,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 +478,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 +496,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 +509,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 +682,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 +783,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,
|
||||
live_declarations: Declarations::default(),
|
||||
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,55 +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
|
||||
Collections,
|
||||
}
|
||||
|
||||
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",
|
||||
Self::Collections => "collections",
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -59,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.
|
||||
@@ -68,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.
|
||||
@@ -76,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()))
|
||||
}
|
||||
|
||||
@@ -90,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)
|
||||
}
|
||||
|
||||
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
@@ -65,6 +65,8 @@ impl<'db> UnionBuilder<'db> {
|
||||
|
||||
let mut to_add = ty;
|
||||
let mut to_remove = SmallVec::<[usize; 2]>::new();
|
||||
let ty_negated = ty.negate(self.db);
|
||||
|
||||
for (index, element) in self.elements.iter().enumerate() {
|
||||
if Some(*element) == bool_pair {
|
||||
to_add = KnownClass::Bool.to_instance(self.db);
|
||||
@@ -80,6 +82,17 @@ impl<'db> UnionBuilder<'db> {
|
||||
return self;
|
||||
} else if element.is_subtype_of(self.db, ty) {
|
||||
to_remove.push(index);
|
||||
} else if ty_negated.is_subtype_of(self.db, *element) {
|
||||
// We add `ty` to the union. We just checked that `~ty` is a subtype of an existing `element`.
|
||||
// This also means that `~ty | ty` is a subtype of `element | ty`, because both elements in the
|
||||
// first union are subtypes of the corresponding elements in the second union. But `~ty | ty` is
|
||||
// just `object`. Since `object` is a subtype of `element | ty`, we can only conclude that
|
||||
// `element | ty` must be `object` (object has no other supertypes). This means we can simplify
|
||||
// the whole union to just `object`, since all other potential elements would also be subtypes of
|
||||
// `object`.
|
||||
self.elements.clear();
|
||||
self.elements.push(KnownClass::Object.to_instance(self.db));
|
||||
return self;
|
||||
}
|
||||
}
|
||||
match to_remove[..] {
|
||||
@@ -321,7 +334,15 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
self.add_positive(db, *neg);
|
||||
}
|
||||
}
|
||||
ty @ (Type::Any | Type::Unknown | Type::Todo(_)) => {
|
||||
Type::Never => {
|
||||
// Adding ~Never to an intersection is a no-op.
|
||||
}
|
||||
Type::Instance(instance) if instance.class.is_known(db, KnownClass::Object) => {
|
||||
// Adding ~object to an intersection results in Never.
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::Never);
|
||||
}
|
||||
ty @ Type::Dynamic(_) => {
|
||||
// Adding any of these types to the negative side of an intersection
|
||||
// is equivalent to adding it to the positive side. We do this to
|
||||
// simplify the representation.
|
||||
@@ -386,18 +407,34 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
|
||||
use super::{IntersectionBuilder, Type, UnionBuilder, UnionType};
|
||||
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{global_symbol, todo_type, KnownClass, Truthiness, UnionBuilder};
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::{KnownClass, Truthiness};
|
||||
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use test_case::test_case;
|
||||
|
||||
#[test]
|
||||
fn build_union() {
|
||||
fn build_union_no_elements() {
|
||||
let db = setup_db();
|
||||
|
||||
let empty_union = UnionBuilder::new(&db).build();
|
||||
assert_eq!(empty_union, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_single_element() {
|
||||
let db = setup_db();
|
||||
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let union = UnionType::from_elements(&db, [t0]);
|
||||
assert_eq!(union, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_two_elements() {
|
||||
let db = setup_db();
|
||||
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
|
||||
@@ -405,605 +442,12 @@ mod tests {
|
||||
assert_eq!(union.elements(&db), &[t0, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_single() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ty = UnionType::from_elements(&db, [t0]);
|
||||
assert_eq!(ty, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_empty() {
|
||||
let db = setup_db();
|
||||
let ty = UnionBuilder::new(&db).build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_never() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ty = UnionType::from_elements(&db, [t0, Type::Never]);
|
||||
assert_eq!(ty, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_bool() {
|
||||
let db = setup_db();
|
||||
let bool_instance_ty = KnownClass::Bool.to_instance(&db);
|
||||
|
||||
let t0 = Type::BooleanLiteral(true);
|
||||
let t1 = Type::BooleanLiteral(true);
|
||||
let t2 = Type::BooleanLiteral(false);
|
||||
let t3 = Type::IntLiteral(17);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t3]).expect_union();
|
||||
assert_eq!(union.elements(&db), &[t0, t3]);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
|
||||
assert_eq!(union.elements(&db), &[bool_instance_ty, t3]);
|
||||
|
||||
let result_ty = UnionType::from_elements(&db, [bool_instance_ty, t0]);
|
||||
assert_eq!(result_ty, bool_instance_ty);
|
||||
|
||||
let result_ty = UnionType::from_elements(&db, [t0, bool_instance_ty]);
|
||||
assert_eq!(result_ty, bool_instance_ty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_flatten() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::IntLiteral(2);
|
||||
let u1 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let union = UnionType::from_elements(&db, [u1, t2]).expect_union();
|
||||
|
||||
assert_eq!(union.elements(&db), &[t0, t1, t2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_simplify_subtype() {
|
||||
let db = setup_db();
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::LiteralString;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
|
||||
assert_eq!(u0, t0);
|
||||
assert_eq!(u1, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_no_simplify_unknown() {
|
||||
let db = setup_db();
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::Unknown;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
|
||||
assert_eq!(u0.expect_union().elements(&db), &[t0, t1]);
|
||||
assert_eq!(u1.expect_union().elements(&db), &[t1, t0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_simplify_multiple_unknown() {
|
||||
let db = setup_db();
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::Unknown;
|
||||
|
||||
let u = UnionType::from_elements(&db, [t0, t1, t1]);
|
||||
|
||||
assert_eq!(u.expect_union().elements(&db), &[t0, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_subsume_multiple() {
|
||||
let db = setup_db();
|
||||
let str_ty = KnownClass::Str.to_instance(&db);
|
||||
let int_ty = KnownClass::Int.to_instance(&db);
|
||||
let object_ty = KnownClass::Object.to_instance(&db);
|
||||
let unknown_ty = Type::Unknown;
|
||||
|
||||
let u0 = UnionType::from_elements(&db, [str_ty, unknown_ty, int_ty, object_ty]);
|
||||
|
||||
assert_eq!(u0.expect_union().elements(&db), &[unknown_ty, object_ty]);
|
||||
}
|
||||
|
||||
impl<'db> IntersectionType<'db> {
|
||||
fn pos_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
|
||||
self.positive(db).into_iter().copied().collect()
|
||||
}
|
||||
|
||||
fn neg_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
|
||||
self.negative(db).into_iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ta = Type::Any;
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t0)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[ta]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[t0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_empty_intersection_equals_object() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db).build();
|
||||
|
||||
assert_eq!(ty, KnownClass::Object.to_instance(&db));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_flatten_positive() {
|
||||
let db = setup_db();
|
||||
let ta = Type::Any;
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::IntLiteral(2);
|
||||
let i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t1)
|
||||
.build();
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(t2)
|
||||
.add_positive(i0)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[t2, ta]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_flatten_negative() {
|
||||
let db = setup_db();
|
||||
let ta = Type::Any;
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = KnownClass::Int.to_instance(&db);
|
||||
// i0 = Any & ~Literal[1]
|
||||
let i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t1)
|
||||
.build();
|
||||
// ta_not_i0 = int & ~(Any & ~Literal[1])
|
||||
// -> int & (~Any | Literal[1])
|
||||
// (~Any is equivalent to Any)
|
||||
// -> (int & Any) | (int & Literal[1])
|
||||
// -> (int & Any) | Literal[1]
|
||||
let ta_not_i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(t2)
|
||||
.add_negative(i0)
|
||||
.build();
|
||||
|
||||
assert_eq!(ta_not_i0.display(&db).to_string(), "int & Any | Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_any() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Any)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Any);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Never)
|
||||
.add_negative(Type::Any)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_multiple_unknown() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_positive(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Unknown);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_negative(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Unknown);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Unknown)
|
||||
.add_negative(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Unknown);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_positive(Type::IntLiteral(0))
|
||||
.add_negative(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(
|
||||
ty,
|
||||
IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_positive(Type::IntLiteral(0))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let ta = Type::Any;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
|
||||
let union = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_positive(u0)
|
||||
.build()
|
||||
.expect_union();
|
||||
let [Type::Intersection(i0), Type::Intersection(i1)] = union.elements(&db)[..] else {
|
||||
panic!("expected a union of two intersections");
|
||||
};
|
||||
assert_eq!(i0.pos_vec(&db), &[ta, t0]);
|
||||
assert_eq!(i1.pos_vec(&db), &[ta, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_negation_distributes_over_union() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
r#"
|
||||
class A: ...
|
||||
class B: ...
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let a = global_symbol(&db, module, "A")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
let b = global_symbol(&db, module, "B")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
|
||||
// intersection: A & B
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_positive(b)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
assert_eq!(intersection.pos_vec(&db), &[a, b]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[]);
|
||||
|
||||
// ~intersection => ~A | ~B
|
||||
let negated_intersection = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Intersection(intersection))
|
||||
.build()
|
||||
.expect_union();
|
||||
|
||||
// should have as elements ~A and ~B
|
||||
let not_a = a.negate(&db);
|
||||
let not_b = b.negate(&db);
|
||||
assert_eq!(negated_intersection.elements(&db), &[not_a, not_b]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_intersection_negation_distributes_over_union() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
r#"
|
||||
class A: ...
|
||||
class B: ...
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let a = global_symbol(&db, module, "A")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
let b = global_symbol(&db, module, "B")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
let int = KnownClass::Int.to_instance(&db);
|
||||
|
||||
// a_not_b: A & ~B
|
||||
let a_not_b = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_negative(b)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
assert_eq!(a_not_b.pos_vec(&db), &[a]);
|
||||
assert_eq!(a_not_b.neg_vec(&db), &[b]);
|
||||
|
||||
// let's build
|
||||
// int & ~(A & ~B)
|
||||
// = int & ~(A & ~B)
|
||||
// = int & (~A | B)
|
||||
// = (int & ~A) | (int & B)
|
||||
let t = IntersectionBuilder::new(&db)
|
||||
.add_positive(int)
|
||||
.add_negative(Type::Intersection(a_not_b))
|
||||
.build();
|
||||
assert_eq!(t.display(&db).to_string(), "int & ~A | int & B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_self_negation() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_negative(Type::none(&db))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_never() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_negative(Type::Never)
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::none(&db));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_never() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_positive(Type::Never)
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_none() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::none(&db))
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.add_negative(Type::none(&db))
|
||||
.build();
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_negative_union_de_morgan() {
|
||||
let db = setup_db();
|
||||
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(Type::IntLiteral(1))
|
||||
.add(Type::IntLiteral(2))
|
||||
.build();
|
||||
assert_eq!(union.display(&db).to_string(), "Literal[1, 2]");
|
||||
|
||||
let ty = IntersectionBuilder::new(&db).add_negative(union).build();
|
||||
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::IntLiteral(1))
|
||||
.add_negative(Type::IntLiteral(2))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty.display(&db).to_string(), "~Literal[1] & ~Literal[2]");
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_type_and_positive_subtype() {
|
||||
let db = setup_db();
|
||||
|
||||
let t = KnownClass::Str.to_instance(&db);
|
||||
let s = Type::LiteralString;
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, s);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_positive(t)
|
||||
.build();
|
||||
assert_eq!(ty, s);
|
||||
|
||||
let literal = Type::string_literal(&db, "a");
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(literal)
|
||||
.build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t)
|
||||
.add_negative(literal)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(literal)
|
||||
.add_positive(t)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_type_and_negative_subtype() {
|
||||
let db = setup_db();
|
||||
|
||||
let t = KnownClass::Str.to_instance(&db);
|
||||
let s = Type::LiteralString;
|
||||
|
||||
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_negative(s)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(s)
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let object = KnownClass::Object.to_instance(&db);
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_positive(object)
|
||||
.build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_positive(object)
|
||||
.add_negative(s)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_type_and_multiple_negative_subtypes() {
|
||||
let db = setup_db();
|
||||
|
||||
let s1 = Type::IntLiteral(1);
|
||||
let s2 = Type::IntLiteral(2);
|
||||
let t = KnownClass::Int.to_instance(&db);
|
||||
|
||||
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(s1)
|
||||
.add_negative(s2)
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_type_and_positive_subtype() {
|
||||
let db = setup_db();
|
||||
|
||||
let t = KnownClass::Str.to_instance(&db);
|
||||
let s = Type::LiteralString;
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
// This should also work in the presence of additional contributions:
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(KnownClass::Object.to_instance(&db))
|
||||
.add_negative(t)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(Type::string_literal(&db, "a"))
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_disjoint_positive_types() {
|
||||
let db = setup_db();
|
||||
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::none(&db);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t1)
|
||||
.add_positive(t2)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
// If there are any negative contributions, they should
|
||||
// be removed too.
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(KnownClass::Str.to_instance(&db))
|
||||
.add_negative(Type::LiteralString)
|
||||
.add_positive(t2)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_disjoint_positive_and_negative_types() {
|
||||
let db = setup_db();
|
||||
|
||||
let t_p = KnownClass::Int.to_instance(&db);
|
||||
let t_n = Type::string_literal(&db, "t_n");
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_p)
|
||||
.add_negative(t_n)
|
||||
.build();
|
||||
assert_eq!(ty, t_p);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t_n)
|
||||
.add_positive(t_p)
|
||||
.build();
|
||||
assert_eq!(ty, t_p);
|
||||
|
||||
let int_literal = Type::IntLiteral(1);
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_p)
|
||||
.add_negative(int_literal)
|
||||
.build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_p)
|
||||
.add_negative(int_literal)
|
||||
.add_negative(t_n)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t_n)
|
||||
.add_negative(int_literal)
|
||||
.add_positive(t_p)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
let intersection = IntersectionBuilder::new(&db).build();
|
||||
assert_eq!(intersection, KnownClass::Object.to_instance(&db));
|
||||
}
|
||||
|
||||
#[test_case(Type::BooleanLiteral(true))]
|
||||
@@ -1048,85 +492,4 @@ mod tests {
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
}
|
||||
|
||||
#[test_case(Type::Any)]
|
||||
#[test_case(Type::Unknown)]
|
||||
#[test_case(todo_type!())]
|
||||
fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) {
|
||||
let db = setup_db();
|
||||
|
||||
let result = IntersectionBuilder::new(&db)
|
||||
.add_positive(ty)
|
||||
.add_negative(ty)
|
||||
.build();
|
||||
assert_eq!(result, ty);
|
||||
|
||||
let result = IntersectionBuilder::new(&db)
|
||||
.add_negative(ty)
|
||||
.add_positive(ty)
|
||||
.build();
|
||||
assert_eq!(result, ty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_of_two_unions_simplify() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
"
|
||||
class A: ...
|
||||
class B: ...
|
||||
a = A()
|
||||
b = B()
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
|
||||
|
||||
let a = global_symbol(&db, file, "a").expect_type();
|
||||
let b = global_symbol(&db, file, "b").expect_type();
|
||||
let union = UnionBuilder::new(&db).add(a).add(b).build();
|
||||
assert_eq!(union.display(&db).to_string(), "A | B");
|
||||
let reversed_union = UnionBuilder::new(&db).add(b).add(a).build();
|
||||
assert_eq!(reversed_union.display(&db).to_string(), "B | A");
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(union)
|
||||
.add_positive(reversed_union)
|
||||
.build();
|
||||
assert_eq!(intersection.display(&db).to_string(), "B | A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_of_two_intersections_simplify() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
"
|
||||
class A: ...
|
||||
class B: ...
|
||||
a = A()
|
||||
b = B()
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
|
||||
|
||||
let a = global_symbol(&db, file, "a").expect_type();
|
||||
let b = global_symbol(&db, file, "b").expect_type();
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_positive(b)
|
||||
.build();
|
||||
let reversed_intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(b)
|
||||
.add_positive(a)
|
||||
.build();
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(intersection)
|
||||
.add(reversed_intersection)
|
||||
.build();
|
||||
assert_eq!(union.display(&db).to_string(), "A & B");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
use super::context::InferContext;
|
||||
use super::diagnostic::CALL_NON_CALLABLE;
|
||||
use super::{Severity, Type, TypeArrayDisplay, UnionBuilder};
|
||||
use super::diagnostic::{CALL_NON_CALLABLE, TYPE_ASSERTION_FAILURE};
|
||||
use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder};
|
||||
use crate::types::diagnostic::STATIC_ASSERT_ERROR;
|
||||
use crate::Db;
|
||||
use ruff_db::diagnostic::DiagnosticId;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
mod arguments;
|
||||
mod bind;
|
||||
|
||||
pub(super) use arguments::{Argument, CallArguments};
|
||||
pub(super) use bind::{bind_call, CallBinding};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum StaticAssertionErrorKind<'db> {
|
||||
ArgumentIsFalse,
|
||||
ArgumentIsFalsy(Type<'db>),
|
||||
ArgumentTruthinessIsAmbiguous(Type<'db>),
|
||||
CustomError(&'db str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum CallOutcome<'db> {
|
||||
Callable {
|
||||
return_ty: Type<'db>,
|
||||
binding: CallBinding<'db>,
|
||||
},
|
||||
RevealType {
|
||||
return_ty: Type<'db>,
|
||||
binding: CallBinding<'db>,
|
||||
revealed_ty: Type<'db>,
|
||||
},
|
||||
NotCallable {
|
||||
@@ -25,12 +40,20 @@ pub(super) enum CallOutcome<'db> {
|
||||
called_ty: Type<'db>,
|
||||
call_outcome: Box<CallOutcome<'db>>,
|
||||
},
|
||||
StaticAssertionError {
|
||||
binding: CallBinding<'db>,
|
||||
error_kind: StaticAssertionErrorKind<'db>,
|
||||
},
|
||||
AssertType {
|
||||
binding: CallBinding<'db>,
|
||||
asserted_ty: Type<'db>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'db> CallOutcome<'db> {
|
||||
/// Create a new `CallOutcome::Callable` with given return type.
|
||||
pub(super) fn callable(return_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::Callable { return_ty }
|
||||
pub(super) fn callable(binding: CallBinding<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::Callable { binding }
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::NotCallable` with given not-callable type.
|
||||
@@ -39,9 +62,9 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::RevealType` with given revealed and return types.
|
||||
pub(super) fn revealed(return_ty: Type<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
pub(super) fn revealed(binding: CallBinding<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::RevealType {
|
||||
return_ty,
|
||||
binding,
|
||||
revealed_ty,
|
||||
}
|
||||
}
|
||||
@@ -57,14 +80,22 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::AssertType` with given revealed and return types.
|
||||
pub(super) fn asserted(binding: CallBinding<'db>, asserted_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::AssertType {
|
||||
binding,
|
||||
asserted_ty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the return type of the call, or `None` if not callable.
|
||||
pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Self::Callable { return_ty } => Some(*return_ty),
|
||||
Self::Callable { binding } => Some(binding.return_ty()),
|
||||
Self::RevealType {
|
||||
return_ty,
|
||||
binding,
|
||||
revealed_ty: _,
|
||||
} => Some(*return_ty),
|
||||
} => Some(binding.return_ty()),
|
||||
Self::NotCallable { not_callable_ty: _ } => None,
|
||||
Self::Union {
|
||||
outcomes,
|
||||
@@ -78,11 +109,16 @@ impl<'db> CallOutcome<'db> {
|
||||
match (acc, ty) {
|
||||
(None, None) => None,
|
||||
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
|
||||
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::Unknown))),
|
||||
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::unknown()))),
|
||||
}
|
||||
})
|
||||
.map(UnionBuilder::build),
|
||||
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db),
|
||||
Self::StaticAssertionError { .. } => Some(Type::none(db)),
|
||||
Self::AssertType {
|
||||
binding,
|
||||
asserted_ty: _,
|
||||
} => Some(binding.return_ty()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,23 +199,30 @@ impl<'db> CallOutcome<'db> {
|
||||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
) -> Result<Type<'db>, NotCallableError<'db>> {
|
||||
// TODO should this method emit diagnostics directly, or just return results that allow the
|
||||
// caller to decide about emitting diagnostics? Currently it emits binding diagnostics, but
|
||||
// only non-callable diagnostics in the union case, which is inconsistent.
|
||||
match self {
|
||||
Self::Callable { return_ty } => Ok(*return_ty),
|
||||
Self::Callable { binding } => {
|
||||
binding.report_diagnostics(context, node);
|
||||
Ok(binding.return_ty())
|
||||
}
|
||||
Self::RevealType {
|
||||
return_ty,
|
||||
binding,
|
||||
revealed_ty,
|
||||
} => {
|
||||
binding.report_diagnostics(context, node);
|
||||
context.report_diagnostic(
|
||||
node,
|
||||
DiagnosticId::RevealedType,
|
||||
Severity::Info,
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
|
||||
);
|
||||
Ok(*return_ty)
|
||||
Ok(binding.return_ty())
|
||||
}
|
||||
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
|
||||
not_callable_ty: *not_callable_ty,
|
||||
return_ty: Type::Unknown,
|
||||
return_ty: Type::unknown(),
|
||||
}),
|
||||
Self::PossiblyUnboundDunderCall {
|
||||
called_ty,
|
||||
@@ -188,7 +231,7 @@ impl<'db> CallOutcome<'db> {
|
||||
callable_ty: *called_ty,
|
||||
return_ty: call_outcome
|
||||
.return_ty(context.db())
|
||||
.unwrap_or(Type::Unknown),
|
||||
.unwrap_or(Type::unknown()),
|
||||
}),
|
||||
Self::Union {
|
||||
outcomes,
|
||||
@@ -201,14 +244,14 @@ impl<'db> CallOutcome<'db> {
|
||||
let return_ty = match outcome {
|
||||
Self::NotCallable { not_callable_ty } => {
|
||||
not_callable.push(*not_callable_ty);
|
||||
Type::Unknown
|
||||
Type::unknown()
|
||||
}
|
||||
Self::RevealType {
|
||||
return_ty,
|
||||
binding,
|
||||
revealed_ty: _,
|
||||
} => {
|
||||
if revealed {
|
||||
*return_ty
|
||||
binding.return_ty()
|
||||
} else {
|
||||
revealed = true;
|
||||
outcome.unwrap_with_diagnostic(context, node)
|
||||
@@ -237,6 +280,73 @@ impl<'db> CallOutcome<'db> {
|
||||
}),
|
||||
}
|
||||
}
|
||||
CallOutcome::StaticAssertionError {
|
||||
binding,
|
||||
error_kind,
|
||||
} => {
|
||||
binding.report_diagnostics(context, node);
|
||||
|
||||
match error_kind {
|
||||
StaticAssertionErrorKind::ArgumentIsFalse => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!("Static assertion error: argument evaluates to `False`"),
|
||||
);
|
||||
}
|
||||
StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty) => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!(
|
||||
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
|
||||
parameter_ty=parameter_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(parameter_ty) => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!(
|
||||
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
|
||||
parameter_ty=parameter_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
StaticAssertionErrorKind::CustomError(message) => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!("Static assertion error: {message}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Type::unknown())
|
||||
}
|
||||
CallOutcome::AssertType {
|
||||
binding,
|
||||
asserted_ty,
|
||||
} => {
|
||||
let [actual_ty, _asserted] = binding.parameter_tys() else {
|
||||
return Ok(binding.return_ty());
|
||||
};
|
||||
|
||||
if !actual_ty.is_gradual_equivalent_to(context.db(), *asserted_ty) {
|
||||
context.report_lint(
|
||||
&TYPE_ASSERTION_FAILURE,
|
||||
node,
|
||||
format_args!(
|
||||
"Actual type `{}` is not the same as asserted type `{}`",
|
||||
actual_ty.display(context.db()),
|
||||
asserted_ty.display(context.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(binding.return_ty())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
73
crates/red_knot_python_semantic/src/types/call/arguments.rs
Normal file
73
crates/red_knot_python_semantic/src/types/call/arguments.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use super::Type;
|
||||
|
||||
/// Typed arguments for a single call, in source order.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct CallArguments<'a, 'db>(Vec<Argument<'a, 'db>>);
|
||||
|
||||
impl<'a, 'db> CallArguments<'a, 'db> {
|
||||
/// Create a [`CallArguments`] from an iterator over non-variadic positional argument types.
|
||||
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
|
||||
positional_tys
|
||||
.into_iter()
|
||||
.map(Argument::Positional)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Prepend an extra positional argument.
|
||||
pub(crate) fn with_self(&self, self_ty: Type<'db>) -> Self {
|
||||
let mut arguments = Vec::with_capacity(self.0.len() + 1);
|
||||
arguments.push(Argument::Synthetic(self_ty));
|
||||
arguments.extend_from_slice(&self.0);
|
||||
Self(arguments)
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &Argument<'a, 'db>> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
// TODO this should be eliminated in favor of [`bind_call`]
|
||||
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.first().map(Argument::ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
|
||||
type Item = &'b Argument<'a, 'db>;
|
||||
type IntoIter = std::slice::Iter<'b, Argument<'a, 'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> FromIterator<Argument<'a, 'db>> for CallArguments<'a, 'db> {
|
||||
fn from_iter<T: IntoIterator<Item = Argument<'a, 'db>>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Argument<'a, 'db> {
|
||||
/// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site.
|
||||
Synthetic(Type<'db>),
|
||||
/// A positional argument.
|
||||
Positional(Type<'db>),
|
||||
/// A starred positional argument (e.g. `*args`).
|
||||
Variadic(Type<'db>),
|
||||
/// A keyword argument (e.g. `a=1`).
|
||||
Keyword { name: &'a str, ty: Type<'db> },
|
||||
/// The double-starred keywords argument (e.g. `**kwargs`).
|
||||
Keywords(Type<'db>),
|
||||
}
|
||||
|
||||
impl<'db> Argument<'_, 'db> {
|
||||
fn ty(&self) -> Type<'db> {
|
||||
match self {
|
||||
Self::Synthetic(ty) => *ty,
|
||||
Self::Positional(ty) => *ty,
|
||||
Self::Variadic(ty) => *ty,
|
||||
Self::Keyword { name: _, ty } => *ty,
|
||||
Self::Keywords(ty) => *ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
411
crates/red_knot_python_semantic/src/types/call/bind.rs
Normal file
411
crates/red_knot_python_semantic/src/types/call/bind.rs
Normal file
@@ -0,0 +1,411 @@
|
||||
use super::{Argument, CallArguments, InferContext, Signature, Type};
|
||||
use crate::db::Db;
|
||||
use crate::types::diagnostic::{
|
||||
INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED,
|
||||
TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
|
||||
};
|
||||
use crate::types::signatures::Parameter;
|
||||
use crate::types::UnionType;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
/// Bind a [`CallArguments`] against a callable [`Signature`].
|
||||
///
|
||||
/// The returned [`CallBinding`] provides the return type of the call, the bound types for all
|
||||
/// parameters, and any errors resulting from binding the call.
|
||||
pub(crate) fn bind_call<'db>(
|
||||
db: &'db dyn Db,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
signature: &Signature<'db>,
|
||||
callable_ty: Option<Type<'db>>,
|
||||
) -> CallBinding<'db> {
|
||||
let parameters = signature.parameters();
|
||||
// The type assigned to each parameter at this call site.
|
||||
let mut parameter_tys = vec![None; parameters.len()];
|
||||
let mut errors = vec![];
|
||||
let mut next_positional = 0;
|
||||
let mut first_excess_positional = None;
|
||||
let mut num_synthetic_args = 0;
|
||||
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
|
||||
if argument_index >= num_synthetic_args {
|
||||
// Adjust the argument index to skip synthetic args, which don't appear at the call
|
||||
// site and thus won't be in the Call node arguments list.
|
||||
Some(argument_index - num_synthetic_args)
|
||||
} else {
|
||||
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
|
||||
// entire Call node, since there's no argument node for this argument at the call site
|
||||
None
|
||||
}
|
||||
};
|
||||
for (argument_index, argument) in arguments.iter().enumerate() {
|
||||
let (index, parameter, argument_ty, positional) = match argument {
|
||||
Argument::Positional(ty) | Argument::Synthetic(ty) => {
|
||||
if matches!(argument, Argument::Synthetic(_)) {
|
||||
num_synthetic_args += 1;
|
||||
}
|
||||
let Some((index, parameter)) = parameters
|
||||
.get_positional(next_positional)
|
||||
.map(|param| (next_positional, param))
|
||||
.or_else(|| parameters.variadic())
|
||||
else {
|
||||
first_excess_positional.get_or_insert(argument_index);
|
||||
next_positional += 1;
|
||||
continue;
|
||||
};
|
||||
next_positional += 1;
|
||||
(index, parameter, ty, !parameter.is_variadic())
|
||||
}
|
||||
Argument::Keyword { name, ty } => {
|
||||
let Some((index, parameter)) = parameters
|
||||
.keyword_by_name(name)
|
||||
.or_else(|| parameters.keyword_variadic())
|
||||
else {
|
||||
errors.push(CallBindingError::UnknownArgument {
|
||||
argument_name: ast::name::Name::new(name),
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
});
|
||||
continue;
|
||||
};
|
||||
(index, parameter, ty, false)
|
||||
}
|
||||
|
||||
Argument::Variadic(_) | Argument::Keywords(_) => {
|
||||
// TODO
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(expected_ty) = parameter.annotated_ty() {
|
||||
if !argument_ty.is_assignable_to(db, expected_ty) {
|
||||
errors.push(CallBindingError::InvalidArgumentType {
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
expected_ty,
|
||||
provided_ty: *argument_ty,
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
|
||||
if parameter.is_variadic() {
|
||||
let union = UnionType::from_elements(db, [existing, *argument_ty]);
|
||||
parameter_tys[index].replace(union);
|
||||
} else {
|
||||
errors.push(CallBindingError::ParameterAlreadyAssigned {
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(first_excess_argument_index) = first_excess_positional {
|
||||
errors.push(CallBindingError::TooManyPositionalArguments {
|
||||
first_excess_argument_index: get_argument_index(
|
||||
first_excess_argument_index,
|
||||
num_synthetic_args,
|
||||
),
|
||||
expected_positional_count: parameters.positional().count(),
|
||||
provided_positional_count: next_positional,
|
||||
});
|
||||
}
|
||||
let mut missing = vec![];
|
||||
for (index, bound_ty) in parameter_tys.iter().enumerate() {
|
||||
if bound_ty.is_none() {
|
||||
let param = ¶meters[index];
|
||||
if param.is_variadic() || param.is_keyword_variadic() || param.default_ty().is_some() {
|
||||
// variadic/keywords and defaulted arguments are not required
|
||||
continue;
|
||||
}
|
||||
missing.push(ParameterContext::new(param, index, false));
|
||||
}
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
errors.push(CallBindingError::MissingArguments {
|
||||
parameters: ParameterContexts(missing),
|
||||
});
|
||||
}
|
||||
|
||||
CallBinding {
|
||||
callable_ty,
|
||||
return_ty: signature.return_ty.unwrap_or(Type::unknown()),
|
||||
parameter_tys: parameter_tys
|
||||
.into_iter()
|
||||
.map(|opt_ty| opt_ty.unwrap_or(Type::unknown()))
|
||||
.collect(),
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct CallBinding<'db> {
|
||||
/// Type of the callable object (function, class...)
|
||||
callable_ty: Option<Type<'db>>,
|
||||
|
||||
/// Return type of the call.
|
||||
return_ty: Type<'db>,
|
||||
|
||||
/// Bound types for parameters, in parameter source order.
|
||||
parameter_tys: Box<[Type<'db>]>,
|
||||
|
||||
/// Call binding errors, if any.
|
||||
errors: Vec<CallBindingError<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> CallBinding<'db> {
|
||||
// TODO remove this constructor and construct always from `bind_call`
|
||||
pub(crate) fn from_return_ty(return_ty: Type<'db>) -> Self {
|
||||
Self {
|
||||
callable_ty: None,
|
||||
return_ty,
|
||||
parameter_tys: Box::default(),
|
||||
errors: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_return_ty(&mut self, return_ty: Type<'db>) {
|
||||
self.return_ty = return_ty;
|
||||
}
|
||||
|
||||
pub(crate) fn return_ty(&self) -> Type<'db> {
|
||||
self.return_ty
|
||||
}
|
||||
|
||||
pub(crate) fn parameter_tys(&self) -> &[Type<'db>] {
|
||||
&self.parameter_tys
|
||||
}
|
||||
|
||||
pub(crate) fn one_parameter_ty(&self) -> Option<Type<'db>> {
|
||||
match self.parameter_tys() {
|
||||
[ty] => Some(*ty),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn two_parameter_tys(&self) -> Option<(Type<'db>, Type<'db>)> {
|
||||
match self.parameter_tys() {
|
||||
[first, second] => Some((*first, *second)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn callable_name(&self, db: &'db dyn Db) -> Option<&str> {
|
||||
match self.callable_ty {
|
||||
Some(Type::FunctionLiteral(function)) => Some(function.name(db)),
|
||||
Some(Type::ClassLiteral(class_type)) => Some(class_type.class.name(db)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
|
||||
let callable_name = self.callable_name(context.db());
|
||||
for error in &self.errors {
|
||||
error.report_diagnostic(context, node, callable_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information needed to emit a diagnostic regarding a parameter.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ParameterContext {
|
||||
name: Option<ast::name::Name>,
|
||||
index: usize,
|
||||
|
||||
/// Was the argument for this parameter passed positionally, and matched to a non-variadic
|
||||
/// positional parameter? (If so, we will provide the index in the diagnostic, not just the
|
||||
/// name.)
|
||||
positional: bool,
|
||||
}
|
||||
|
||||
impl ParameterContext {
|
||||
fn new(parameter: &Parameter, index: usize, positional: bool) -> Self {
|
||||
Self {
|
||||
name: parameter.display_name(),
|
||||
index,
|
||||
positional,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ParameterContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(name) = &self.name {
|
||||
if self.positional {
|
||||
write!(f, "{} (`{name}`)", self.index + 1)
|
||||
} else {
|
||||
write!(f, "`{name}`")
|
||||
}
|
||||
} else {
|
||||
write!(f, "{}", self.index + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ParameterContexts(Vec<ParameterContext>);
|
||||
|
||||
impl std::fmt::Display for ParameterContexts {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut iter = self.0.iter();
|
||||
if let Some(first) = iter.next() {
|
||||
write!(f, "{first}")?;
|
||||
for param in iter {
|
||||
f.write_str(", ")?;
|
||||
write!(f, "{param}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum CallBindingError<'db> {
|
||||
/// The type of an argument is not assignable to the annotated type of its corresponding
|
||||
/// parameter.
|
||||
InvalidArgumentType {
|
||||
parameter: ParameterContext,
|
||||
argument_index: Option<usize>,
|
||||
expected_ty: Type<'db>,
|
||||
provided_ty: Type<'db>,
|
||||
},
|
||||
/// One or more required parameters (that is, with no default) is not supplied by any argument.
|
||||
MissingArguments { parameters: ParameterContexts },
|
||||
/// A call argument can't be matched to any parameter.
|
||||
UnknownArgument {
|
||||
argument_name: ast::name::Name,
|
||||
argument_index: Option<usize>,
|
||||
},
|
||||
/// More positional arguments are provided in the call than can be handled by the signature.
|
||||
TooManyPositionalArguments {
|
||||
first_excess_argument_index: Option<usize>,
|
||||
expected_positional_count: usize,
|
||||
provided_positional_count: usize,
|
||||
},
|
||||
/// Multiple arguments were provided for a single parameter.
|
||||
ParameterAlreadyAssigned {
|
||||
argument_index: Option<usize>,
|
||||
parameter: ParameterContext,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'db> CallBindingError<'db> {
|
||||
pub(super) fn report_diagnostic(
|
||||
&self,
|
||||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
callable_name: Option<&str>,
|
||||
) {
|
||||
match self {
|
||||
Self::InvalidArgumentType {
|
||||
parameter,
|
||||
argument_index,
|
||||
expected_ty,
|
||||
provided_ty,
|
||||
} => {
|
||||
let provided_ty_display = provided_ty.display(context.db());
|
||||
let expected_ty_display = expected_ty.display(context.db());
|
||||
context.report_lint(
|
||||
&INVALID_ARGUMENT_TYPE,
|
||||
Self::get_node(node, *argument_index),
|
||||
format_args!(
|
||||
"Object of type `{provided_ty_display}` cannot be assigned to \
|
||||
parameter {parameter}{}; expected type `{expected_ty_display}`",
|
||||
if let Some(callable_name) = callable_name {
|
||||
format!(" of function `{callable_name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Self::TooManyPositionalArguments {
|
||||
first_excess_argument_index,
|
||||
expected_positional_count,
|
||||
provided_positional_count,
|
||||
} => {
|
||||
context.report_lint(
|
||||
&TOO_MANY_POSITIONAL_ARGUMENTS,
|
||||
Self::get_node(node, *first_excess_argument_index),
|
||||
format_args!(
|
||||
"Too many positional arguments{}: expected \
|
||||
{expected_positional_count}, got {provided_positional_count}",
|
||||
if let Some(callable_name) = callable_name {
|
||||
format!(" to function `{callable_name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Self::MissingArguments { parameters } => {
|
||||
let s = if parameters.0.len() == 1 { "" } else { "s" };
|
||||
context.report_lint(
|
||||
&MISSING_ARGUMENT,
|
||||
node,
|
||||
format_args!(
|
||||
"No argument{s} provided for required parameter{s} {parameters}{}",
|
||||
if let Some(callable_name) = callable_name {
|
||||
format!(" of function `{callable_name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Self::UnknownArgument {
|
||||
argument_name,
|
||||
argument_index,
|
||||
} => {
|
||||
context.report_lint(
|
||||
&UNKNOWN_ARGUMENT,
|
||||
Self::get_node(node, *argument_index),
|
||||
format_args!(
|
||||
"Argument `{argument_name}` does not match any known parameter{}",
|
||||
if let Some(callable_name) = callable_name {
|
||||
format!(" of function `{callable_name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Self::ParameterAlreadyAssigned {
|
||||
argument_index,
|
||||
parameter,
|
||||
} => {
|
||||
context.report_lint(
|
||||
&PARAMETER_ALREADY_ASSIGNED,
|
||||
Self::get_node(node, *argument_index),
|
||||
format_args!(
|
||||
"Multiple values provided for parameter {parameter}{}",
|
||||
if let Some(callable_name) = callable_name {
|
||||
format!(" of function `{callable_name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_node(node: ast::AnyNodeRef, argument_index: Option<usize>) -> ast::AnyNodeRef {
|
||||
// If we have a Call node and an argument index, report the diagnostic on the correct
|
||||
// argument node; otherwise, report it on the entire provided node.
|
||||
match (node, argument_index) {
|
||||
(ast::AnyNodeRef::ExprCall(call_node), Some(argument_index)) => {
|
||||
match call_node
|
||||
.arguments
|
||||
.arguments_source_order()
|
||||
.nth(argument_index)
|
||||
.expect("argument index should not be out of range")
|
||||
{
|
||||
ast::ArgOrKeyword::Arg(expr) => expr.into(),
|
||||
ast::ArgOrKeyword::Keyword(keyword) => keyword.into(),
|
||||
}
|
||||
}
|
||||
_ => node,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::types::{
|
||||
todo_type, Class, ClassLiteralType, KnownClass, KnownInstanceType, TodoType, Type,
|
||||
todo_type, Class, ClassLiteralType, DynamicType, KnownClass, KnownInstanceType, Type,
|
||||
};
|
||||
use crate::Db;
|
||||
use itertools::Either;
|
||||
@@ -8,16 +8,29 @@ use itertools::Either;
|
||||
///
|
||||
/// This is much more limited than the [`Type`] enum:
|
||||
/// all types that would be invalid to have as a class base are
|
||||
/// transformed into [`ClassBase::Unknown`]
|
||||
/// transformed into [`ClassBase::unknown`]
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
|
||||
pub enum ClassBase<'db> {
|
||||
Any,
|
||||
Unknown,
|
||||
Todo(TodoType),
|
||||
Dynamic(DynamicType),
|
||||
Class(Class<'db>),
|
||||
}
|
||||
|
||||
impl<'db> ClassBase<'db> {
|
||||
pub const fn any() -> Self {
|
||||
Self::Dynamic(DynamicType::Any)
|
||||
}
|
||||
|
||||
pub const fn unknown() -> Self {
|
||||
Self::Dynamic(DynamicType::Unknown)
|
||||
}
|
||||
|
||||
pub const fn is_dynamic(self) -> bool {
|
||||
match self {
|
||||
ClassBase::Dynamic(_) => true,
|
||||
ClassBase::Class(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
|
||||
struct Display<'db> {
|
||||
base: ClassBase<'db>,
|
||||
@@ -27,9 +40,7 @@ impl<'db> ClassBase<'db> {
|
||||
impl std::fmt::Display for Display<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.base {
|
||||
ClassBase::Any => f.write_str("Any"),
|
||||
ClassBase::Todo(todo) => todo.fmt(f),
|
||||
ClassBase::Unknown => f.write_str("Unknown"),
|
||||
ClassBase::Dynamic(dynamic) => dynamic.fmt(f),
|
||||
ClassBase::Class(class) => write!(f, "<class '{}'>", class.name(self.db)),
|
||||
}
|
||||
}
|
||||
@@ -43,7 +54,7 @@ impl<'db> ClassBase<'db> {
|
||||
KnownClass::Object
|
||||
.to_class_literal(db)
|
||||
.into_class_literal()
|
||||
.map_or(Self::Unknown, |ClassLiteralType { class }| {
|
||||
.map_or(Self::unknown(), |ClassLiteralType { class }| {
|
||||
Self::Class(class)
|
||||
})
|
||||
}
|
||||
@@ -53,9 +64,7 @@ impl<'db> ClassBase<'db> {
|
||||
/// Return `None` if `ty` is not an acceptable type for a class base.
|
||||
pub(super) fn try_from_ty(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
|
||||
match ty {
|
||||
Type::Any => Some(Self::Any),
|
||||
Type::Unknown => Some(Self::Unknown),
|
||||
Type::Todo(todo) => Some(Self::Todo(todo)),
|
||||
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
|
||||
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
|
||||
Type::Intersection(_) => None, // TODO -- probably incorrect?
|
||||
@@ -93,8 +102,12 @@ impl<'db> ClassBase<'db> {
|
||||
| KnownInstanceType::Required
|
||||
| KnownInstanceType::TypeAlias
|
||||
| KnownInstanceType::ReadOnly
|
||||
| KnownInstanceType::Optional => None,
|
||||
KnownInstanceType::Any => Some(Self::Any),
|
||||
| KnownInstanceType::Optional
|
||||
| KnownInstanceType::Not
|
||||
| KnownInstanceType::Intersection
|
||||
| KnownInstanceType::TypeOf => None,
|
||||
KnownInstanceType::Unknown => Some(Self::unknown()),
|
||||
KnownInstanceType::Any => Some(Self::any()),
|
||||
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
|
||||
KnownInstanceType::Dict => {
|
||||
Self::try_from_ty(db, KnownClass::Dict.to_class_literal(db))
|
||||
@@ -139,7 +152,7 @@ impl<'db> ClassBase<'db> {
|
||||
pub(super) fn into_class(self) -> Option<Class<'db>> {
|
||||
match self {
|
||||
Self::Class(class) => Some(class),
|
||||
_ => None,
|
||||
Self::Dynamic(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,13 +162,7 @@ impl<'db> ClassBase<'db> {
|
||||
db: &'db dyn Db,
|
||||
) -> Either<impl Iterator<Item = ClassBase<'db>>, impl Iterator<Item = ClassBase<'db>>> {
|
||||
match self {
|
||||
ClassBase::Any => Either::Left([ClassBase::Any, ClassBase::object(db)].into_iter()),
|
||||
ClassBase::Unknown => {
|
||||
Either::Left([ClassBase::Unknown, ClassBase::object(db)].into_iter())
|
||||
}
|
||||
ClassBase::Todo(todo) => {
|
||||
Either::Left([ClassBase::Todo(todo), ClassBase::object(db)].into_iter())
|
||||
}
|
||||
ClassBase::Dynamic(_) => Either::Left([self, ClassBase::object(db)].into_iter()),
|
||||
ClassBase::Class(class) => Either::Right(class.iter_mro(db)),
|
||||
}
|
||||
}
|
||||
@@ -170,9 +177,7 @@ impl<'db> From<Class<'db>> for ClassBase<'db> {
|
||||
impl<'db> From<ClassBase<'db>> for Type<'db> {
|
||||
fn from(value: ClassBase<'db>) -> Self {
|
||||
match value {
|
||||
ClassBase::Any => Type::Any,
|
||||
ClassBase::Todo(todo) => Type::Todo(todo),
|
||||
ClassBase::Unknown => Type::Unknown,
|
||||
ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic),
|
||||
ClassBase::Class(class) => Type::class_literal(class),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,16 @@ use ruff_db::{
|
||||
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,
|
||||
};
|
||||
|
||||
use super::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
|
||||
/// Context for inferring the types of a single file.
|
||||
///
|
||||
/// One context exists for at least for every inferred region but it's
|
||||
@@ -29,17 +32,21 @@ use super::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
/// 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, file: File) -> Self {
|
||||
pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
file,
|
||||
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."),
|
||||
}
|
||||
}
|
||||
@@ -57,9 +64,7 @@ impl<'db> InferContext<'db> {
|
||||
where
|
||||
T: WithDiagnostics,
|
||||
{
|
||||
self.diagnostics
|
||||
.get_mut()
|
||||
.extend(other.diagnostics().iter().cloned());
|
||||
self.diagnostics.get_mut().extend(other.diagnostics());
|
||||
}
|
||||
|
||||
/// Reports a lint located at `node`.
|
||||
@@ -67,13 +72,28 @@ impl<'db> InferContext<'db> {
|
||||
&self,
|
||||
lint: &'static LintMetadata,
|
||||
node: AnyNodeRef,
|
||||
message: std::fmt::Arguments,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -85,7 +105,7 @@ impl<'db> InferContext<'db> {
|
||||
node: AnyNodeRef,
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
message: std::fmt::Arguments,
|
||||
message: fmt::Arguments,
|
||||
) {
|
||||
if !self.db.is_file_open(self.file) {
|
||||
return;
|
||||
@@ -96,7 +116,6 @@ impl<'db> InferContext<'db> {
|
||||
// * 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.
|
||||
// * Check for suppression comments, bump a counter if the diagnostic is suppressed.
|
||||
|
||||
self.diagnostics.borrow_mut().push(TypeCheckDiagnostic {
|
||||
file: self.file,
|
||||
@@ -107,6 +126,47 @@ impl<'db> InferContext<'db> {
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Are we currently inferring types in a stub file?
|
||||
pub(crate) fn in_stub(&self) -> bool {
|
||||
self.file.is_stub(self.db().upcast())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
|
||||
self.bomb.defuse();
|
||||
@@ -126,6 +186,17 @@ impl fmt::Debug for InferContext<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[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,5 +1,7 @@
|
||||
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,
|
||||
@@ -10,13 +12,12 @@ 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;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::context::InferContext;
|
||||
|
||||
/// Registers all known type check lints.
|
||||
pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&CALL_NON_CALLABLE);
|
||||
@@ -26,28 +27,38 @@ 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_ARGUMENT_TYPE);
|
||||
registry.register_lint(&INVALID_ASSIGNMENT);
|
||||
registry.register_lint(&INVALID_BASE);
|
||||
registry.register_lint(&INVALID_CONTEXT_MANAGER);
|
||||
registry.register_lint(&INVALID_DECLARATION);
|
||||
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
|
||||
registry.register_lint(&INVALID_METACLASS);
|
||||
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(&MISSING_ARGUMENT);
|
||||
registry.register_lint(&NON_SUBSCRIPTABLE);
|
||||
registry.register_lint(&NOT_ITERABLE);
|
||||
registry.register_lint(&PARAMETER_ALREADY_ASSIGNED);
|
||||
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(&TYPE_ASSERTION_FAILURE);
|
||||
registry.register_lint(&TOO_MANY_POSITIONAL_ARGUMENTS);
|
||||
registry.register_lint(&UNDEFINED_REVEAL);
|
||||
registry.register_lint(&UNKNOWN_ARGUMENT);
|
||||
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
|
||||
registry.register_lint(&UNRESOLVED_IMPORT);
|
||||
registry.register_lint(&UNRESOLVED_REFERENCE);
|
||||
registry.register_lint(&UNSUPPORTED_OPERATOR);
|
||||
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
|
||||
registry.register_lint(&STATIC_ASSERT_ERROR);
|
||||
|
||||
// String annotations
|
||||
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
||||
@@ -146,6 +157,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 = {
|
||||
@@ -165,6 +234,27 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Detects call arguments whose type is not assignable to the corresponding typed parameter.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Passing an argument of a type the function (or callable object) does not accept violates
|
||||
/// the expectations of the function author and may cause unexpected runtime errors within the
|
||||
/// body of the function.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// def func(x: int): ...
|
||||
/// func("foo") # error: [invalid-argument-type]
|
||||
/// ```
|
||||
pub(crate) static INVALID_ARGUMENT_TYPE = {
|
||||
summary: "detects call arguments whose type is not assignable to the corresponding typed parameter",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO #14889
|
||||
pub(crate) static INVALID_ASSIGNMENT = {
|
||||
@@ -202,6 +292,7 @@ declare_lint! {
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for exception handlers that catch non-exception classes.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
@@ -236,6 +327,33 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for arguments to `metaclass=` that are invalid.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python allows arbitrary expressions to be used as the argument to `metaclass=`.
|
||||
/// These expressions, however, need to be callable and accept the same arguments
|
||||
/// as `type.__new__`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def f(): ...
|
||||
///
|
||||
/// # TypeError: f() takes 0 positional arguments but 3 were given
|
||||
/// class B(metaclass=f): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
|
||||
pub(crate) static INVALID_METACLASS = {
|
||||
summary: "detects invalid `metaclass=` arguments",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for default values that can't be assigned to the parameter's annotated type.
|
||||
@@ -314,6 +432,25 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for missing required arguments in a call.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Failing to provide a required argument will raise a `TypeError` at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// def func(x: int): ...
|
||||
/// func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
/// ```
|
||||
pub(crate) static MISSING_ARGUMENT = {
|
||||
summary: "detects missing required arguments in a call",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for subscripting objects that do not support subscripting.
|
||||
@@ -352,6 +489,27 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls which provide more than one argument for a single parameter.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Providing multiple values for a single parameter will raise a `TypeError` at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```python
|
||||
/// def f(x: int) -> int: ...
|
||||
///
|
||||
/// f(1, x=2) # Error raised here
|
||||
/// ```
|
||||
pub(crate) static PARAMETER_ALREADY_ASSIGNED = {
|
||||
summary: "detects multiple arguments for the same parameter",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for possibly unbound attributes.
|
||||
@@ -395,6 +553,72 @@ 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 `assert_type()` calls where the actual type
|
||||
/// is not the same as the asserted type.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `assert_type()` allows confirming the inferred type of a certain value.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def _(x: int):
|
||||
/// assert_type(x, int) # fine
|
||||
/// assert_type(x, str) # error: Actual type does not match asserted type
|
||||
/// ```
|
||||
pub(crate) static TYPE_ASSERTION_FAILURE = {
|
||||
summary: "detects failed type assertions",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls that pass more positional arguments than the callable can accept.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Passing too many positional arguments will raise `TypeError` at runtime.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def f(): ...
|
||||
///
|
||||
/// f("foo") # Error raised here
|
||||
/// ```
|
||||
pub(crate) static TOO_MANY_POSITIONAL_ARGUMENTS = {
|
||||
summary: "detects calls passing too many positional arguments",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls to `reveal_type` without importing it.
|
||||
@@ -411,6 +635,27 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for keyword arguments in calls that don't match any parameter of the callable.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Providing an unknown keyword argument will raise `TypeError` at runtime.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def f(x: int) -> int: ...
|
||||
///
|
||||
/// f(x=1, y=2) # Error raised here
|
||||
/// ```
|
||||
pub(crate) static UNKNOWN_ARGUMENT = {
|
||||
summary: "detects unknown keyword arguments in calls",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for unresolved attributes.
|
||||
@@ -486,13 +731,32 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Makes sure that the argument of `static_assert` is statically known to be true.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// from knot_extensions import static_assert
|
||||
///
|
||||
/// static_assert(1 + 1 == 3) # error: evaluates to `False`
|
||||
///
|
||||
/// static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness
|
||||
/// ```
|
||||
pub(crate) static STATIC_ASSERT_ERROR = {
|
||||
summary: "Failed static assertion",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
@@ -546,41 +810,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,7 +852,7 @@ impl Deref for TypeCheckDiagnostics {
|
||||
type Target = [std::sync::Arc<TypeCheckDiagnostic>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
&self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,7 +861,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,7 +870,7 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -788,3 +1052,11 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast:
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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__`"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::{
|
||||
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
|
||||
SubclassOfType, Type, UnionType,
|
||||
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, Type,
|
||||
UnionType,
|
||||
};
|
||||
use crate::Db;
|
||||
use rustc_hash::FxHashMap;
|
||||
@@ -65,9 +65,8 @@ struct DisplayRepresentation<'db> {
|
||||
impl Display for DisplayRepresentation<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self.ty {
|
||||
Type::Any => f.write_str("Any"),
|
||||
Type::Dynamic(dynamic) => dynamic.fmt(f),
|
||||
Type::Never => f.write_str("Never"),
|
||||
Type::Unknown => f.write_str("Unknown"),
|
||||
Type::Instance(InstanceType { class }) => {
|
||||
let representation = match class.known(self.db) {
|
||||
Some(KnownClass::NoneType) => "None",
|
||||
@@ -76,24 +75,17 @@ impl Display for DisplayRepresentation<'_> {
|
||||
};
|
||||
f.write_str(representation)
|
||||
}
|
||||
// `[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(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)),
|
||||
Type::SubclassOf(SubclassOfType {
|
||||
base: ClassBase::Class(class),
|
||||
}) => {
|
||||
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
|
||||
// Only show the bare class name here; ClassBase::display would render this as
|
||||
// type[<class 'Foo'>] instead of type[Foo].
|
||||
write!(f, "type[{}]", class.name(self.db))
|
||||
}
|
||||
Type::SubclassOf(SubclassOfType { base }) => {
|
||||
write!(f, "type[{}]", base.display(self.db))
|
||||
}
|
||||
ClassBase::Class(class) => write!(f, "type[{}]", class.name(self.db)),
|
||||
ClassBase::Dynamic(dynamic) => write!(f, "type[{dynamic}]"),
|
||||
},
|
||||
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
|
||||
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
@@ -177,12 +169,9 @@ impl Display for DisplayUnionType<'_> {
|
||||
|
||||
for element in elements {
|
||||
if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) {
|
||||
let Some(mut condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
|
||||
let Some(condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
|
||||
continue;
|
||||
};
|
||||
if kind == CondensedDisplayTypeKind::Int {
|
||||
condensed_kind.sort_unstable_by_key(|ty| ty.expect_int_literal());
|
||||
}
|
||||
join.entry(&DisplayLiteralGroup {
|
||||
literals: condensed_kind,
|
||||
db: self.db,
|
||||
@@ -223,17 +212,12 @@ impl Display for DisplayLiteralGroup<'_> {
|
||||
|
||||
/// Enumeration of literal types that are displayed in a "condensed way" inside `Literal` slices.
|
||||
///
|
||||
/// For example, `Literal[1] | Literal[2]` is displayed as `"Literal[1, 2]"`.
|
||||
/// Not all `Literal` types are displayed using `Literal` slices
|
||||
/// (e.g. it would be inappropriate to display `LiteralString`
|
||||
/// as `Literal[LiteralString]`).
|
||||
/// For example, `Literal[1] | Literal[2] | Literal["s"]` is displayed as `"Literal[1, 2, "s"]"`.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
enum CondensedDisplayTypeKind {
|
||||
Class,
|
||||
Function,
|
||||
Int,
|
||||
String,
|
||||
Bytes,
|
||||
LiteralExpression,
|
||||
}
|
||||
|
||||
impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
|
||||
@@ -243,9 +227,10 @@ impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
|
||||
match value {
|
||||
Type::ClassLiteral(_) => Ok(Self::Class),
|
||||
Type::FunctionLiteral(_) => Ok(Self::Function),
|
||||
Type::IntLiteral(_) => Ok(Self::Int),
|
||||
Type::StringLiteral(_) => Ok(Self::String),
|
||||
Type::BytesLiteral(_) => Ok(Self::Bytes),
|
||||
Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::BooleanLiteral(_) => Ok(Self::LiteralExpression),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
@@ -324,6 +309,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,
|
||||
@@ -366,64 +357,8 @@ impl Display for DisplayStringLiteralType<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::{global_symbol, SliceLiteralType, StringLiteralType, Type, UnionType};
|
||||
|
||||
#[test]
|
||||
fn test_condense_literal_display_by_type() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/main.py",
|
||||
"
|
||||
def foo(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
def bar(s: str) -> str:
|
||||
return s
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
",
|
||||
)?;
|
||||
let mod_file = system_path_to_file(&db, "src/main.py").expect("file to exist");
|
||||
|
||||
let union_elements = &[
|
||||
Type::Unknown,
|
||||
Type::IntLiteral(-1),
|
||||
global_symbol(&db, mod_file, "A").expect_type(),
|
||||
Type::string_literal(&db, "A"),
|
||||
Type::bytes_literal(&db, &[0u8]),
|
||||
Type::bytes_literal(&db, &[7u8]),
|
||||
Type::IntLiteral(0),
|
||||
Type::IntLiteral(1),
|
||||
Type::string_literal(&db, "B"),
|
||||
global_symbol(&db, mod_file, "foo").expect_type(),
|
||||
global_symbol(&db, mod_file, "bar").expect_type(),
|
||||
global_symbol(&db, mod_file, "B").expect_type(),
|
||||
Type::BooleanLiteral(true),
|
||||
Type::none(&db),
|
||||
];
|
||||
let union = UnionType::from_elements(&db, union_elements).expect_union();
|
||||
let display = format!("{}", union.display(&db));
|
||||
assert_eq!(
|
||||
display,
|
||||
concat!(
|
||||
"Unknown | ",
|
||||
"Literal[-1, 0, 1] | ",
|
||||
"Literal[A, B] | ",
|
||||
"Literal[\"A\", \"B\"] | ",
|
||||
"Literal[b\"\\x00\", b\"\\x07\"] | ",
|
||||
"Literal[foo, bar] | ",
|
||||
"Literal[True] | ",
|
||||
"None"
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
use crate::types::{SliceLiteralType, StringLiteralType, Type};
|
||||
|
||||
#[test]
|
||||
fn test_slice_literal_display() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user