Compare commits
1 Commits
0.11.3
...
dcreager/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eff2734bb |
34
.github/workflows/build-binaries.yml
vendored
34
.github/workflows/build-binaries.yml
vendored
@@ -28,7 +28,7 @@ permissions: {}
|
||||
env:
|
||||
PACKAGE_NAME: ruff
|
||||
MODULE_NAME: ruff
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.11"
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -43,13 +43,13 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build sdist"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
command: sdist
|
||||
args: --out dist
|
||||
@@ -72,14 +72,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - x86_64"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: x86_64
|
||||
args: --release --locked --out dist
|
||||
@@ -114,14 +114,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: arm64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - aarch64"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: aarch64
|
||||
args: --release --locked --out dist
|
||||
@@ -170,14 +170,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: ${{ matrix.platform.arch }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: --release --locked --out dist
|
||||
@@ -223,14 +223,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: auto
|
||||
@@ -298,13 +298,13 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: auto
|
||||
@@ -363,14 +363,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: musllinux_1_2
|
||||
@@ -429,13 +429,13 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: musllinux_1_2
|
||||
|
||||
85
.github/workflows/ci.yaml
vendored
85
.github/workflows/ci.yaml
vendored
@@ -18,7 +18,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
PACKAGE_NAME: ruff
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.12"
|
||||
|
||||
jobs:
|
||||
determine_changes:
|
||||
@@ -36,8 +36,6 @@ jobs:
|
||||
code: ${{ steps.check_code.outputs.changed }}
|
||||
# Flag that is raised when any code that affects the fuzzer is changed
|
||||
fuzz: ${{ steps.check_fuzzer.outputs.changed }}
|
||||
# Flag that is set to "true" when code related to red-knot changes.
|
||||
red_knot: ${{ steps.check_red_knot.outputs.changed }}
|
||||
|
||||
# Flag that is set to "true" when code related to the playground changes.
|
||||
playground: ${{ steps.check_playground.outputs.changed }}
|
||||
@@ -168,29 +166,6 @@ jobs:
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check if the red-knot code changed
|
||||
id: check_red_knot
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
|
||||
':Cargo.toml' \
|
||||
':Cargo.lock' \
|
||||
':crates/red_knot*/**' \
|
||||
':crates/ruff_db/**' \
|
||||
':crates/ruff_annotate_snippets/**' \
|
||||
':crates/ruff_python_ast/**' \
|
||||
':crates/ruff_python_parser/**' \
|
||||
':crates/ruff_python_trivia/**' \
|
||||
':crates/ruff_source_file/**' \
|
||||
':crates/ruff_text_size/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
cargo-fmt:
|
||||
name: "cargo fmt"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -239,21 +214,13 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: Red-knot mdtests (GitHub annotations)
|
||||
if: ${{ needs.determine_changes.outputs.red_knot == 'true' }}
|
||||
env:
|
||||
NO_COLOR: 1
|
||||
MDTEST_GITHUB_ANNOTATIONS_FORMAT: 1
|
||||
# Ignore errors if this step fails; we want to continue to later steps in the workflow anyway.
|
||||
# This step is just to get nice GitHub annotations on the PR diff in the files-changed tab.
|
||||
run: cargo test -p red_knot_python_semantic --test mdtest || true
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -293,11 +260,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -320,7 +287,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -403,11 +370,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -455,7 +422,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
- uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
@@ -521,7 +488,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -654,7 +621,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -662,7 +629,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
args: --out dist
|
||||
- name: "Test wheel"
|
||||
@@ -675,13 +642,20 @@ jobs:
|
||||
|
||||
pre-commit:
|
||||
name: "pre-commit"
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install pre-commit"
|
||||
run: pip install pre-commit
|
||||
- name: "Cache pre-commit"
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
@@ -692,7 +666,7 @@ jobs:
|
||||
echo '```console' > "$GITHUB_STEP_SUMMARY"
|
||||
# Enable color output for pre-commit and remove it for the summary
|
||||
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all uvx --python="${PYTHON_VERSION}" pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
|
||||
exit_code="${PIPESTATUS[0]}"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -708,19 +682,19 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
uses: webfactory/ssh-agent@dc588b651fe13675774614f8e6a936a468676387 # v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
@@ -779,10 +753,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
repository: "astral-sh/ruff-lsp"
|
||||
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
# installation fails on 3.13 and newer
|
||||
python-version: "3.12"
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
name: Download development ruff binary
|
||||
@@ -857,7 +830,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -865,7 +838,7 @@ jobs:
|
||||
run: cargo codspeed build --features codspeed -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0
|
||||
uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
4
.github/workflows/daily_fuzz.yaml
vendored
4
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
- uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/daily_property_tests.yaml
vendored
2
.github/workflows/daily_property_tests.yaml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
8
.github/workflows/mypy_primer.yaml
vendored
8
.github/workflows/mypy_primer.yaml
vendored
@@ -25,7 +25,7 @@ env:
|
||||
jobs:
|
||||
mypy_primer:
|
||||
name: Run mypy_primer
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
with:
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
- name: Install mypy_primer
|
||||
run: |
|
||||
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v2"
|
||||
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
|
||||
|
||||
- name: Run mypy_primer
|
||||
shell: bash
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
--type-checker knot \
|
||||
--old base_commit \
|
||||
--new "$GITHUB_SHA" \
|
||||
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument|typeshed-stats|scrapy)$' \
|
||||
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument)$' \
|
||||
--output concise \
|
||||
--debug > mypy_primer.diff || [ $? -eq 1 ]
|
||||
|
||||
|
||||
2
.github/workflows/notify-dependents.yml
vendored
2
.github/workflows/notify-dependents.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Update pre-commit mirror"
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
|
||||
script: |
|
||||
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
uses: webfactory/ssh-agent@dc588b651fe13675774614f8e6a936a468676387 # v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
|
||||
|
||||
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@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
2
.github/workflows/sync_typeshed.yaml
vendored
2
.github/workflows/sync_typeshed.yaml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -19,7 +19,7 @@ exclude: |
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.24.1
|
||||
rev: v0.24
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
@@ -60,7 +60,7 @@ repos:
|
||||
- black==25.1.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.31.0
|
||||
rev: v1.30.2
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -74,7 +74,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.2
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -92,12 +92,12 @@ repos:
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.5.2
|
||||
rev: v1.5.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.32.1
|
||||
rev: 0.31.3
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
|
||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,39 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.11.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Add more autofixes for `AIR302` ([#16876](https://github.com/astral-sh/ruff/pull/16876), [#16977](https://github.com/astral-sh/ruff/pull/16977), [#16976](https://github.com/astral-sh/ruff/pull/16976), [#16965](https://github.com/astral-sh/ruff/pull/16965))
|
||||
- \[`airflow`\] Move `AIR301` to `AIR002` ([#16978](https://github.com/astral-sh/ruff/pull/16978))
|
||||
- \[`airflow`\] Move `AIR302` to `AIR301` and `AIR303` to `AIR302` ([#17151](https://github.com/astral-sh/ruff/pull/17151))
|
||||
- \[`flake8-bandit`\] Mark `str` and `list[str]` literals as trusted input (`S603`) ([#17136](https://github.com/astral-sh/ruff/pull/17136))
|
||||
- \[`ruff`\] Support slices in `RUF005` ([#17078](https://github.com/astral-sh/ruff/pull/17078))
|
||||
- [syntax-errors] Start detecting compile-time syntax errors ([#16106](https://github.com/astral-sh/ruff/pull/16106))
|
||||
- [syntax-errors] Duplicate type parameter names ([#16858](https://github.com/astral-sh/ruff/pull/16858))
|
||||
- [syntax-errors] Irrefutable `case` pattern before final case ([#16905](https://github.com/astral-sh/ruff/pull/16905))
|
||||
- [syntax-errors] Multiple assignments in `case` pattern ([#16957](https://github.com/astral-sh/ruff/pull/16957))
|
||||
- [syntax-errors] Single starred assignment target ([#17024](https://github.com/astral-sh/ruff/pull/17024))
|
||||
- [syntax-errors] Starred expressions in `return`, `yield`, and `for` ([#17134](https://github.com/astral-sh/ruff/pull/17134))
|
||||
- [syntax-errors] Store to or delete `__debug__` ([#16984](https://github.com/astral-sh/ruff/pull/16984))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Error instead of `panic!` when running Ruff from a deleted directory (#16903) ([#17054](https://github.com/astral-sh/ruff/pull/17054))
|
||||
- [syntax-errors] Fix false positive for parenthesized tuple index ([#16948](https://github.com/astral-sh/ruff/pull/16948))
|
||||
|
||||
### CLI
|
||||
|
||||
- Check `pyproject.toml` correctly when it is passed via stdin ([#16971](https://github.com/astral-sh/ruff/pull/16971))
|
||||
|
||||
### Configuration
|
||||
|
||||
- \[`flake8-import-conventions`\] Add import `numpy.typing as npt` to default `flake8-import-conventions.aliases` ([#17133](https://github.com/astral-sh/ruff/pull/17133))
|
||||
|
||||
### Documentation
|
||||
|
||||
- \[`refurb`\] Document why `UserDict`, `UserList`, and `UserString` are preferred over `dict`, `list`, and `str` (`FURB189`) ([#16927](https://github.com/astral-sh/ruff/pull/16927))
|
||||
|
||||
## 0.11.2
|
||||
|
||||
### Preview features
|
||||
|
||||
51
Cargo.lock
generated
51
Cargo.lock
generated
@@ -334,9 +334,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.34"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
|
||||
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -344,9 +344,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.34"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
|
||||
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -918,7 +918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1499,7 +1499,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.0",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1726,9 +1726,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
@@ -2503,22 +2503,6 @@ dependencies = [
|
||||
"wild",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_ide"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"insta",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_vendored",
|
||||
"ruff_db",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_text_size",
|
||||
"salsa",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_project"
|
||||
version = "0.0.0"
|
||||
@@ -2530,7 +2514,6 @@ dependencies = [
|
||||
"notify",
|
||||
"pep440_rs",
|
||||
"rayon",
|
||||
"red_knot_ide",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_vendored",
|
||||
"ruff_cache",
|
||||
@@ -2602,9 +2585,7 @@ dependencies = [
|
||||
"libc",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"red_knot_ide",
|
||||
"red_knot_project",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -2665,7 +2646,6 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"js-sys",
|
||||
"log",
|
||||
"red_knot_ide",
|
||||
"red_knot_project",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_db",
|
||||
@@ -2754,7 +2734,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.3"
|
||||
version = "0.11.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2989,7 +2969,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.3"
|
||||
version = "0.11.2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3018,6 +2998,7 @@ dependencies = [
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_cache",
|
||||
"ruff_diagnostics",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -3212,7 +3193,6 @@ name = "ruff_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"ruff_cache",
|
||||
"ruff_index",
|
||||
@@ -3225,7 +3205,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"test-case",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3313,7 +3292,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.11.3"
|
||||
version = "0.11.2"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3407,7 +3386,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3420,7 +3399,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.3",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3805,7 +3784,7 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"once_cell",
|
||||
"rustix 1.0.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -38,11 +38,10 @@ ruff_text_size = { path = "crates/ruff_text_size" }
|
||||
red_knot_vendored = { path = "crates/red_knot_vendored" }
|
||||
ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
|
||||
red_knot_ide = { path = "crates/red_knot_ide" }
|
||||
red_knot_project = { path = "crates/red_knot_project", default-features = false }
|
||||
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
|
||||
red_knot_server = { path = "crates/red_knot_server" }
|
||||
red_knot_test = { path = "crates/red_knot_test" }
|
||||
red_knot_project = { path = "crates/red_knot_project", default-features = false }
|
||||
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
anstream = { version = "0.6.18" }
|
||||
|
||||
@@ -149,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.11.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.3/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.11.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.2/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -183,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.11.3
|
||||
rev: v0.11.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -15,7 +15,7 @@ use red_knot_project::watch::ProjectWatcher;
|
||||
use red_knot_project::{watch, Db};
|
||||
use red_knot_project::{ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_server::run_server;
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
|
||||
use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait, Severity};
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
@@ -288,7 +288,7 @@ impl MainLoop {
|
||||
let diagnostics_count = result.len();
|
||||
|
||||
for diagnostic in result {
|
||||
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
|
||||
writeln!(stdout, "{}", diagnostic.display(db, &display_config))?;
|
||||
|
||||
failed |= diagnostic.severity() >= min_error_severity;
|
||||
}
|
||||
@@ -359,7 +359,7 @@ enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted {
|
||||
/// The diagnostics that were found during the check.
|
||||
result: Vec<Diagnostic>,
|
||||
result: Vec<Box<dyn OldDiagnosticTrait>>,
|
||||
revision: u64,
|
||||
},
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
|
||||
@@ -1125,11 +1125,11 @@ print(sys.last_exc, os.getegid())
|
||||
|
||||
assert_eq!(diagnostics.len(), 2);
|
||||
assert_eq!(
|
||||
diagnostics[0].primary_message(),
|
||||
diagnostics[0].message(),
|
||||
"Type `<module 'sys'>` has no attribute `last_exc`"
|
||||
);
|
||||
assert_eq!(
|
||||
diagnostics[1].primary_message(),
|
||||
diagnostics[1].message(),
|
||||
"Type `<module 'os'>` has no attribute `getegid`"
|
||||
);
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "red_knot_ide"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
|
||||
salsa = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,134 +0,0 @@
|
||||
use red_knot_python_semantic::Db as SemanticDb;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
pub trait Db: SemanticDb + Upcast<dyn SemanticDb> + Upcast<dyn SourceDb> {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::Db;
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
events: Arc::default(),
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots");
|
||||
|
||||
let events = inner.get_mut().unwrap();
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
/// Clears the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn clear_salsa_events(&mut self) {
|
||||
self.take_salsa_events();
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SourceDb> for TestDb {
|
||||
fn upcast(&self) -> &(dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SemanticDb> for TestDb {
|
||||
fn upcast(&self) -> &(dyn SemanticDb + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
fn upcast_mut(&mut self) -> &mut dyn SemanticDb {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for TestDb {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
default_lint_registry()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for TestDb {}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
/// Returns the node with a minimal range that fully contains `range`.
|
||||
///
|
||||
/// If `range` is empty and falls within a parser *synthesized* node generated during error recovery,
|
||||
/// then the first node with the given range is returned.
|
||||
///
|
||||
/// ## Panics
|
||||
/// Panics if `range` is not contained within `root`.
|
||||
pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
|
||||
struct Visitor<'a> {
|
||||
range: TextRange,
|
||||
found: bool,
|
||||
ancestors: Vec<AnyNodeRef<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> SourceOrderVisitor<'a> for Visitor<'a> {
|
||||
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
|
||||
// If the node fully contains the range, than it is a possible match but traverse into its children
|
||||
// to see if there's a node with a narrower range.
|
||||
if !self.found && node.range().contains_range(self.range) {
|
||||
self.ancestors.push(node);
|
||||
TraversalSignal::Traverse
|
||||
} else {
|
||||
TraversalSignal::Skip
|
||||
}
|
||||
}
|
||||
|
||||
fn leave_node(&mut self, node: AnyNodeRef<'a>) {
|
||||
if !self.found && self.ancestors.last() == Some(&node) {
|
||||
self.found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
root.range().contains_range(range),
|
||||
"Range is not contained within root"
|
||||
);
|
||||
|
||||
let mut visitor = Visitor {
|
||||
range,
|
||||
found: false,
|
||||
ancestors: Vec::new(),
|
||||
};
|
||||
|
||||
root.visit_source_order(&mut visitor);
|
||||
|
||||
let minimal = visitor.ancestors.pop().unwrap_or(root);
|
||||
CoveringNode {
|
||||
node: minimal,
|
||||
ancestors: visitor.ancestors,
|
||||
}
|
||||
}
|
||||
|
||||
/// The node with a minimal range that fully contains the search range.
|
||||
pub(crate) struct CoveringNode<'a> {
|
||||
/// The node with a minimal range that fully contains the search range.
|
||||
node: AnyNodeRef<'a>,
|
||||
|
||||
/// The node's ancestor (the spine up to the root).
|
||||
ancestors: Vec<AnyNodeRef<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> CoveringNode<'a> {
|
||||
pub(crate) fn node(&self) -> AnyNodeRef<'a> {
|
||||
self.node
|
||||
}
|
||||
|
||||
/// Returns the node's parent.
|
||||
pub(crate) fn parent(&self) -> Option<AnyNodeRef<'a>> {
|
||||
self.ancestors.last().copied()
|
||||
}
|
||||
|
||||
/// Finds the minimal node that fully covers the range and fulfills the given predicate.
|
||||
pub(crate) fn find(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
|
||||
if f(self.node) {
|
||||
return Ok(self);
|
||||
}
|
||||
|
||||
match self.ancestors.iter().rposition(|node| f(*node)) {
|
||||
Some(index) => {
|
||||
let node = self.ancestors[index];
|
||||
self.ancestors.truncate(index);
|
||||
|
||||
Ok(Self {
|
||||
node,
|
||||
ancestors: self.ancestors,
|
||||
})
|
||||
}
|
||||
None => Err(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for CoveringNode<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("NodeWithAncestors")
|
||||
.field(&self.node)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -1,912 +0,0 @@
|
||||
use crate::find_node::covering_node;
|
||||
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
|
||||
use red_knot_python_semantic::{HasType, SemanticModel};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::{parsed_module, ParsedModule};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
pub fn goto_type_definition(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
offset: TextSize,
|
||||
) -> Option<RangedValue<NavigationTargets>> {
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
let goto_target = find_goto_target(parsed, offset)?;
|
||||
|
||||
let model = SemanticModel::new(db.upcast(), file);
|
||||
|
||||
let ty = match goto_target {
|
||||
GotoTarget::Expression(expression) => expression.inferred_type(&model),
|
||||
GotoTarget::FunctionDef(function) => function.inferred_type(&model),
|
||||
GotoTarget::ClassDef(class) => class.inferred_type(&model),
|
||||
GotoTarget::Parameter(parameter) => parameter.inferred_type(&model),
|
||||
GotoTarget::Alias(alias) => alias.inferred_type(&model),
|
||||
GotoTarget::ExceptVariable(except) => except.inferred_type(&model),
|
||||
GotoTarget::KeywordArgument(argument) => {
|
||||
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
|
||||
// than using the inferred value.
|
||||
argument.value.inferred_type(&model)
|
||||
}
|
||||
// TODO: Support identifier targets
|
||||
GotoTarget::PatternMatchRest(_)
|
||||
| GotoTarget::PatternKeywordArgument(_)
|
||||
| GotoTarget::PatternMatchStarName(_)
|
||||
| GotoTarget::PatternMatchAsName(_)
|
||||
| GotoTarget::ImportedModule(_)
|
||||
| GotoTarget::TypeParamTypeVarName(_)
|
||||
| GotoTarget::TypeParamParamSpecName(_)
|
||||
| GotoTarget::TypeParamTypeVarTupleName(_)
|
||||
| GotoTarget::NonLocal { .. }
|
||||
| GotoTarget::Globals { .. } => return None,
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
"Inferred type of covering node is {}",
|
||||
ty.display(db.upcast())
|
||||
);
|
||||
|
||||
Some(RangedValue {
|
||||
range: FileRange::new(file, goto_target.range()),
|
||||
value: ty.navigation_targets(db),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum GotoTarget<'a> {
|
||||
Expression(ast::ExprRef<'a>),
|
||||
FunctionDef(&'a ast::StmtFunctionDef),
|
||||
ClassDef(&'a ast::StmtClassDef),
|
||||
Parameter(&'a ast::Parameter),
|
||||
Alias(&'a ast::Alias),
|
||||
|
||||
/// Go to on the module name of an import from
|
||||
/// ```py
|
||||
/// from foo import bar
|
||||
/// ^^^
|
||||
/// ```
|
||||
ImportedModule(&'a ast::StmtImportFrom),
|
||||
|
||||
/// Go to on the exception handler variable
|
||||
/// ```py
|
||||
/// try: ...
|
||||
/// except Exception as e: ...
|
||||
/// ^
|
||||
/// ```
|
||||
ExceptVariable(&'a ast::ExceptHandlerExceptHandler),
|
||||
|
||||
/// Go to on a keyword argument
|
||||
/// ```py
|
||||
/// test(a = 1)
|
||||
/// ^
|
||||
/// ```
|
||||
KeywordArgument(&'a ast::Keyword),
|
||||
|
||||
/// Go to on the rest parameter of a pattern match
|
||||
///
|
||||
/// ```py
|
||||
/// match x:
|
||||
/// case {"a": a, "b": b, **rest}: ...
|
||||
/// ^^^^
|
||||
/// ```
|
||||
PatternMatchRest(&'a ast::PatternMatchMapping),
|
||||
|
||||
/// Go to on a keyword argument of a class pattern
|
||||
///
|
||||
/// ```py
|
||||
/// match Point3D(0, 0, 0):
|
||||
/// case Point3D(x=0, y=0, z=0): ...
|
||||
/// ^ ^ ^
|
||||
/// ```
|
||||
PatternKeywordArgument(&'a ast::PatternKeyword),
|
||||
|
||||
/// Go to on a pattern star argument
|
||||
///
|
||||
/// ```py
|
||||
/// match array:
|
||||
/// case [*args]: ...
|
||||
/// ^^^^
|
||||
PatternMatchStarName(&'a ast::PatternMatchStar),
|
||||
|
||||
/// Go to on the name of a pattern match as pattern
|
||||
///
|
||||
/// ```py
|
||||
/// match x:
|
||||
/// case [x] as y: ...
|
||||
/// ^
|
||||
PatternMatchAsName(&'a ast::PatternMatchAs),
|
||||
|
||||
/// Go to on the name of a type variable
|
||||
///
|
||||
/// ```py
|
||||
/// type Alias[T: int = bool] = list[T]
|
||||
/// ^
|
||||
/// ```
|
||||
TypeParamTypeVarName(&'a ast::TypeParamTypeVar),
|
||||
|
||||
/// Go to on the name of a type param spec
|
||||
///
|
||||
/// ```py
|
||||
/// type Alias[**P = [int, str]] = Callable[P, int]
|
||||
/// ^
|
||||
/// ```
|
||||
TypeParamParamSpecName(&'a ast::TypeParamParamSpec),
|
||||
|
||||
/// Go to on the name of a type var tuple
|
||||
///
|
||||
/// ```py
|
||||
/// type Alias[*Ts = ()] = tuple[*Ts]
|
||||
/// ^^
|
||||
/// ```
|
||||
TypeParamTypeVarTupleName(&'a ast::TypeParamTypeVarTuple),
|
||||
|
||||
NonLocal {
|
||||
identifier: &'a ast::Identifier,
|
||||
},
|
||||
Globals {
|
||||
identifier: &'a ast::Identifier,
|
||||
},
|
||||
}
|
||||
|
||||
impl Ranged for GotoTarget<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
GotoTarget::Expression(expression) => expression.range(),
|
||||
GotoTarget::FunctionDef(function) => function.name.range,
|
||||
GotoTarget::ClassDef(class) => class.name.range,
|
||||
GotoTarget::Parameter(parameter) => parameter.name.range,
|
||||
GotoTarget::Alias(alias) => alias.name.range,
|
||||
GotoTarget::ImportedModule(module) => module.module.as_ref().unwrap().range,
|
||||
GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range,
|
||||
GotoTarget::KeywordArgument(keyword) => keyword.arg.as_ref().unwrap().range,
|
||||
GotoTarget::PatternMatchRest(rest) => rest.rest.as_ref().unwrap().range,
|
||||
GotoTarget::PatternKeywordArgument(keyword) => keyword.attr.range,
|
||||
GotoTarget::PatternMatchStarName(star) => star.name.as_ref().unwrap().range,
|
||||
GotoTarget::PatternMatchAsName(as_name) => as_name.name.as_ref().unwrap().range,
|
||||
GotoTarget::TypeParamTypeVarName(type_var) => type_var.name.range,
|
||||
GotoTarget::TypeParamParamSpecName(spec) => spec.name.range,
|
||||
GotoTarget::TypeParamTypeVarTupleName(tuple) => tuple.name.range,
|
||||
GotoTarget::NonLocal { identifier, .. } => identifier.range,
|
||||
GotoTarget::Globals { identifier, .. } => identifier.range,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option<GotoTarget> {
|
||||
let token = parsed.tokens().at_offset(offset).find(|token| {
|
||||
matches!(
|
||||
token.kind(),
|
||||
TokenKind::Name
|
||||
| TokenKind::String
|
||||
| TokenKind::Complex
|
||||
| TokenKind::Float
|
||||
| TokenKind::Int
|
||||
)
|
||||
})?;
|
||||
let covering_node = covering_node(parsed.syntax().into(), token.range())
|
||||
.find(|node| node.is_identifier() || node.is_expression())
|
||||
.ok()?;
|
||||
|
||||
tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind());
|
||||
|
||||
match covering_node.node() {
|
||||
AnyNodeRef::Identifier(identifier) => match covering_node.parent() {
|
||||
Some(AnyNodeRef::StmtFunctionDef(function)) => Some(GotoTarget::FunctionDef(function)),
|
||||
Some(AnyNodeRef::StmtClassDef(class)) => Some(GotoTarget::ClassDef(class)),
|
||||
Some(AnyNodeRef::Parameter(parameter)) => Some(GotoTarget::Parameter(parameter)),
|
||||
Some(AnyNodeRef::Alias(alias)) => Some(GotoTarget::Alias(alias)),
|
||||
Some(AnyNodeRef::StmtImportFrom(from)) => Some(GotoTarget::ImportedModule(from)),
|
||||
Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => {
|
||||
Some(GotoTarget::ExceptVariable(handler))
|
||||
}
|
||||
Some(AnyNodeRef::Keyword(keyword)) => Some(GotoTarget::KeywordArgument(keyword)),
|
||||
Some(AnyNodeRef::PatternMatchMapping(mapping)) => {
|
||||
Some(GotoTarget::PatternMatchRest(mapping))
|
||||
}
|
||||
Some(AnyNodeRef::PatternKeyword(keyword)) => {
|
||||
Some(GotoTarget::PatternKeywordArgument(keyword))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchStar(star)) => {
|
||||
Some(GotoTarget::PatternMatchStarName(star))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchAs(as_pattern)) => {
|
||||
Some(GotoTarget::PatternMatchAsName(as_pattern))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVar(var)) => Some(GotoTarget::TypeParamTypeVarName(var)),
|
||||
Some(AnyNodeRef::TypeParamParamSpec(bound)) => {
|
||||
Some(GotoTarget::TypeParamParamSpecName(bound))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVarTuple(var_tuple)) => {
|
||||
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
||||
}
|
||||
Some(AnyNodeRef::ExprAttribute(attribute)) => {
|
||||
Some(GotoTarget::Expression(attribute.into()))
|
||||
}
|
||||
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
|
||||
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
||||
None => None,
|
||||
Some(parent) => {
|
||||
tracing::debug!(
|
||||
"Missing `GoToTarget` for identifier with parent {:?}",
|
||||
parent.kind()
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
|
||||
node => node.as_expr_ref().map(GotoTarget::Expression),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::{goto_type_definition, NavigationTarget};
|
||||
use insta::assert_snapshot;
|
||||
use insta::internals::SettingsBindDropGuard;
|
||||
use red_knot_python_semantic::{
|
||||
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
|
||||
Severity, Span, SubDiagnostic,
|
||||
};
|
||||
use ruff_db::files::{system_path_to_file, File, FileRange};
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_class_type() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
class Test: ...
|
||||
|
||||
a<CURSOR>b = Test()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:19
|
||||
|
|
||||
2 | class Test: ...
|
||||
| ^^^^
|
||||
3 |
|
||||
4 | ab = Test()
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | class Test: ...
|
||||
3 |
|
||||
4 | ab = Test()
|
||||
| ^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_function_type() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
ab = foo
|
||||
|
||||
a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:17
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
| ^^^
|
||||
3 |
|
||||
4 | ab = foo
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:6:13
|
||||
|
|
||||
4 | ab = foo
|
||||
5 |
|
||||
6 | ab
|
||||
| ^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_union_type() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
|
||||
def foo(a, b): ...
|
||||
|
||||
def bar(a, b): ...
|
||||
|
||||
if random.choice():
|
||||
a = foo
|
||||
else:
|
||||
a = bar
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:3:17
|
||||
|
|
||||
3 | def foo(a, b): ...
|
||||
| ^^^
|
||||
4 |
|
||||
5 | def bar(a, b): ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:5:17
|
||||
|
|
||||
3 | def foo(a, b): ...
|
||||
4 |
|
||||
5 | def bar(a, b): ...
|
||||
| ^^^
|
||||
6 |
|
||||
7 | if random.choice():
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_module() {
|
||||
let mut test = goto_test(
|
||||
r#"
|
||||
import lib
|
||||
|
||||
lib<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /lib.py:1:1
|
||||
|
|
||||
1 | a = 10
|
||||
| ^
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | import lib
|
||||
3 |
|
||||
4 | lib
|
||||
| ^^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_type() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
a: str = "test"
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | a: str = "test"
|
||||
3 |
|
||||
4 | a
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_node() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
a: str = "te<CURSOR>st"
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:2:22
|
||||
|
|
||||
2 | a: str = "test"
|
||||
| ^^^^^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_type() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
type Alias[T: int = bool] = list[T<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:24
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:2:46
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_param_spec() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: Goto type definition currently doesn't work for type param specs
|
||||
// because the inference doesn't support them yet.
|
||||
// This snapshot should show a single target pointing to `T`
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_tuple() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: Goto type definition currently doesn't work for type var tuples
|
||||
// because the inference doesn't support them yet.
|
||||
// This snapshot should show a single target pointing to `T`
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_keyword_argument() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
test(a<CURSOR>= "123")
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:18
|
||||
|
|
||||
2 | def test(a: str): ...
|
||||
3 |
|
||||
4 | test(a= "123")
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_incorrectly_typed_keyword_argument() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
test(a<CURSOR>= 123)
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: This should jump to `str` and not `int` because
|
||||
// the keyword is typed as a string. It's only the passed argument that
|
||||
// is an int. Navigating to `str` would match pyright's behavior.
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:234:7
|
||||
|
|
||||
232 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
233 |
|
||||
234 | class int:
|
||||
| ^^^
|
||||
235 | @overload
|
||||
236 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:18
|
||||
|
|
||||
2 | def test(a: str): ...
|
||||
3 |
|
||||
4 | test(a= 123)
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_kwargs() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
def f(name: str): ...
|
||||
|
||||
kwargs = { "name": "test"}
|
||||
|
||||
f(**kwargs<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:1098:7
|
||||
|
|
||||
1096 | def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
|
||||
1097 |
|
||||
1098 | class dict(MutableMapping[_KT, _VT]):
|
||||
| ^^^^
|
||||
1099 | # __init__ should be kept roughly in line with `collections.UserDict.__init__`, which has similar semantics
|
||||
1100 | # Also multiprocessing.managers.SyncManager.dict()
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:6:5
|
||||
|
|
||||
4 | kwargs = { "name": "test"}
|
||||
5 |
|
||||
6 | f(**kwargs)
|
||||
| ^^^^^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_builtin() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
def foo(a: str):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
// FIXME: This should go to `str`
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_definition_cursor_between_object_and_attribute() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
class X:
|
||||
def foo(a, b): ...
|
||||
|
||||
x = X()
|
||||
|
||||
x<CURSOR>.foo()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:19
|
||||
|
|
||||
2 | class X:
|
||||
| ^
|
||||
3 | def foo(a, b): ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:7:13
|
||||
|
|
||||
5 | x = X()
|
||||
6 |
|
||||
7 | x.foo()
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_between_call_arguments() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
foo<CURSOR>()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:17
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
| ^^^
|
||||
3 |
|
||||
4 | foo()
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
3 |
|
||||
4 | foo()
|
||||
| ^^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_narrowing() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
if a is not None:
|
||||
print(a<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:27
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | if a is not None:
|
||||
4 | print(a)
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_none() {
|
||||
let test = goto_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/types.pyi:677:11
|
||||
|
|
||||
675 | if sys.version_info >= (3, 10):
|
||||
676 | @final
|
||||
677 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
678 | def __bool__(self) -> Literal[False]: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
fn goto_test(source: &str) -> GotoTest {
|
||||
let mut db = TestDb::new();
|
||||
let cursor_offset = source.find("<CURSOR>").expect(
|
||||
"`source`` should contain a `<CURSOR>` marker, indicating the position of the cursor.",
|
||||
);
|
||||
|
||||
let mut content = source[..cursor_offset].to_string();
|
||||
content.push_str(&source[cursor_offset + "<CURSOR>".len()..]);
|
||||
|
||||
db.write_file("main.py", &content)
|
||||
.expect("write to memory file system to be successful");
|
||||
|
||||
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
python_version: PythonVersion::latest(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![SystemPathBuf::from("/")],
|
||||
custom_typeshed: None,
|
||||
python_path: PythonPath::KnownSitePackages(vec![]),
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("Default settings to be valid");
|
||||
|
||||
let mut insta_settings = insta::Settings::clone_current();
|
||||
insta_settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
||||
|
||||
let insta_settings_guard = insta_settings.bind_to_scope();
|
||||
|
||||
GotoTest {
|
||||
db,
|
||||
cursor_offset: TextSize::try_from(cursor_offset)
|
||||
.expect("source to be smaller than 4GB"),
|
||||
file,
|
||||
_insta_settings_guard: insta_settings_guard,
|
||||
}
|
||||
}
|
||||
|
||||
struct GotoTest {
|
||||
db: TestDb,
|
||||
cursor_offset: TextSize,
|
||||
file: File,
|
||||
_insta_settings_guard: SettingsBindDropGuard,
|
||||
}
|
||||
|
||||
impl GotoTest {
|
||||
fn write_file(
|
||||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
content: &str,
|
||||
) -> std::io::Result<()> {
|
||||
self.db.write_file(path, content)
|
||||
}
|
||||
|
||||
fn goto_type_definition(&self) -> String {
|
||||
let Some(targets) = goto_type_definition(&self.db, self.file, self.cursor_offset)
|
||||
else {
|
||||
return "No goto target found".to_string();
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return "No type definitions found".to_string();
|
||||
}
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
let source = targets.range;
|
||||
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.color(false)
|
||||
.format(DiagnosticFormat::Full);
|
||||
for target in &*targets {
|
||||
let diag = GotoTypeDefinitionDiagnostic::new(source, target).into_diagnostic();
|
||||
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
struct GotoTypeDefinitionDiagnostic {
|
||||
source: FileRange,
|
||||
target: FileRange,
|
||||
}
|
||||
|
||||
impl GotoTypeDefinitionDiagnostic {
|
||||
fn new(source: FileRange, target: &NavigationTarget) -> Self {
|
||||
Self {
|
||||
source,
|
||||
target: FileRange::new(target.file(), target.focus_range()),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(Severity::Info, "Source");
|
||||
source.annotate(Annotation::primary(
|
||||
Span::from(self.source.file()).with_range(self.source.range()),
|
||||
));
|
||||
|
||||
let mut main = Diagnostic::new(
|
||||
DiagnosticId::Lint(LintName::of("goto-type-definition")),
|
||||
Severity::Info,
|
||||
"Type definition".to_string(),
|
||||
);
|
||||
main.annotate(Annotation::primary(
|
||||
Span::from(self.target.file()).with_range(self.target.range()),
|
||||
));
|
||||
main.sub(source);
|
||||
|
||||
main
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
mod db;
|
||||
mod find_node;
|
||||
mod goto;
|
||||
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub use db::Db;
|
||||
pub use goto::goto_type_definition;
|
||||
use red_knot_python_semantic::types::{
|
||||
Class, ClassBase, ClassLiteralType, FunctionType, InstanceType, IntersectionType,
|
||||
KnownInstanceType, ModuleLiteralType, Type,
|
||||
};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
/// Information associated with a text range.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct RangedValue<T> {
|
||||
pub range: FileRange,
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
impl<T> RangedValue<T> {
|
||||
pub fn file_range(&self) -> FileRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for RangedValue<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for RangedValue<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIterator for RangedValue<T>
|
||||
where
|
||||
T: IntoIterator,
|
||||
{
|
||||
type Item = T::Item;
|
||||
type IntoIter = T::IntoIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.value.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Target to which the editor can navigate to.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NavigationTarget {
|
||||
file: File,
|
||||
|
||||
/// The range that should be focused when navigating to the target.
|
||||
///
|
||||
/// This is typically not the full range of the node. For example, it's the range of the class's name in a class definition.
|
||||
///
|
||||
/// The `focus_range` must be fully covered by `full_range`.
|
||||
focus_range: TextRange,
|
||||
|
||||
/// The range covering the entire target.
|
||||
full_range: TextRange,
|
||||
}
|
||||
|
||||
impl NavigationTarget {
|
||||
pub fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
pub fn focus_range(&self) -> TextRange {
|
||||
self.focus_range
|
||||
}
|
||||
|
||||
pub fn full_range(&self) -> TextRange {
|
||||
self.full_range
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NavigationTargets(smallvec::SmallVec<[NavigationTarget; 1]>);
|
||||
|
||||
impl NavigationTargets {
|
||||
fn single(target: NavigationTarget) -> Self {
|
||||
Self(smallvec::smallvec![target])
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self(smallvec::SmallVec::new())
|
||||
}
|
||||
|
||||
fn iter(&self) -> std::slice::Iter<'_, NavigationTarget> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for NavigationTargets {
|
||||
type Item = NavigationTarget;
|
||||
type IntoIter = smallvec::IntoIter<[NavigationTarget; 1]>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a NavigationTargets {
|
||||
type Item = &'a NavigationTarget;
|
||||
type IntoIter = std::slice::Iter<'a, NavigationTarget>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<NavigationTarget> for NavigationTargets {
|
||||
fn from_iter<T: IntoIterator<Item = NavigationTarget>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasNavigationTargets {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets;
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for Type<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
match self {
|
||||
Type::BoundMethod(method) => method.function(db).navigation_targets(db),
|
||||
Type::FunctionLiteral(function) => function.navigation_targets(db),
|
||||
Type::ModuleLiteral(module) => module.navigation_targets(db),
|
||||
Type::Union(union) => union
|
||||
.iter(db.upcast())
|
||||
.flat_map(|target| target.navigation_targets(db))
|
||||
.collect(),
|
||||
Type::ClassLiteral(class) => class.navigation_targets(db),
|
||||
Type::Instance(instance) => instance.navigation_targets(db),
|
||||
Type::KnownInstance(instance) => instance.navigation_targets(db),
|
||||
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
||||
ClassBase::Class(class) => class.navigation_targets(db),
|
||||
ClassBase::Dynamic(_) => NavigationTargets::empty(),
|
||||
},
|
||||
|
||||
Type::StringLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::Tuple(_) => self.to_meta_type(db.upcast()).navigation_targets(db),
|
||||
|
||||
Type::Intersection(intersection) => intersection.navigation_targets(db),
|
||||
|
||||
Type::Dynamic(_)
|
||||
| Type::Never
|
||||
| Type::Callable(_)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy => NavigationTargets::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for FunctionType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
let function_range = self.focus_range(db.upcast());
|
||||
NavigationTargets::single(NavigationTarget {
|
||||
file: function_range.file(),
|
||||
focus_range: function_range.range(),
|
||||
full_range: self.full_range(db.upcast()).range(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for Class<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
let class_range = self.focus_range(db.upcast());
|
||||
NavigationTargets::single(NavigationTarget {
|
||||
file: class_range.file(),
|
||||
focus_range: class_range.range(),
|
||||
full_range: self.full_range(db.upcast()).range(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for ClassLiteralType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
self.class().navigation_targets(db)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for InstanceType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
self.class().navigation_targets(db)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for ModuleLiteralType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
let file = self.module(db).file();
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
NavigationTargets::single(NavigationTarget {
|
||||
file,
|
||||
focus_range: TextRange::default(),
|
||||
full_range: TextRange::up_to(source.text_len()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for KnownInstanceType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
match self {
|
||||
KnownInstanceType::TypeVar(var) => {
|
||||
let definition = var.definition(db);
|
||||
let full_range = definition.full_range(db.upcast());
|
||||
|
||||
NavigationTargets::single(NavigationTarget {
|
||||
file: full_range.file(),
|
||||
focus_range: definition.focus_range(db.upcast()).range(),
|
||||
full_range: full_range.range(),
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Track the definition of `KnownInstance` and navigate to their definition.
|
||||
_ => NavigationTargets::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for IntersectionType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
// Only consider the positive elements because the negative elements are mainly from narrowing constraints.
|
||||
let mut targets = self
|
||||
.iter_positive(db.upcast())
|
||||
.filter(|ty| !ty.is_unknown());
|
||||
|
||||
let Some(first) = targets.next() else {
|
||||
return NavigationTargets::empty();
|
||||
};
|
||||
|
||||
match targets.next() {
|
||||
Some(_) => {
|
||||
// If there are multiple types in the intersection, we can't navigate to a single one
|
||||
// because the type is the intersection of all those types.
|
||||
NavigationTargets::empty()
|
||||
}
|
||||
None => first.navigation_targets(db),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ ruff_db = { workspace = true, features = ["cache", "serde"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true, features = ["serde"] }
|
||||
ruff_text_size = { workspace = true }
|
||||
red_knot_ide = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true, features = ["serde"] }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ use ruff_python_ast::PythonVersion;
|
||||
/// resolved extra search path of `["b", "c", "a"]`, which means `a` will be tried last.
|
||||
///
|
||||
/// There's an argument here that the user should be able to specify the order of the paths,
|
||||
/// because only then is the user in full control of where to insert the path when specifying `extra-paths`
|
||||
/// because only then is the user in full control of where to insert the path when specyifing `extra-paths`
|
||||
/// in multiple sources.
|
||||
///
|
||||
/// ## Macro
|
||||
|
||||
@@ -3,10 +3,9 @@ use std::sync::Arc;
|
||||
|
||||
use crate::DEFAULT_LINT_REGISTRY;
|
||||
use crate::{Project, ProjectMetadata};
|
||||
use red_knot_ide::Db as IdeDb;
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{Db as SemanticDb, Program};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::OldDiagnosticTrait;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
@@ -56,12 +55,12 @@ impl ProjectDatabase {
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
pub fn check(&self) -> Result<Vec<Diagnostic>, Cancelled> {
|
||||
pub fn check(&self) -> Result<Vec<Box<dyn OldDiagnosticTrait>>, Cancelled> {
|
||||
self.with_db(|db| db.project().check(db))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Diagnostic>, Cancelled> {
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Box<dyn OldDiagnosticTrait>>, Cancelled> {
|
||||
self.with_db(|db| self.project().check_file(db, file))
|
||||
}
|
||||
|
||||
@@ -104,19 +103,6 @@ impl Upcast<dyn SourceDb> for ProjectDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn IdeDb> for ProjectDatabase {
|
||||
fn upcast(&self) -> &(dyn IdeDb + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
fn upcast_mut(&mut self) -> &mut (dyn IdeDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl IdeDb for ProjectDatabase {}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for ProjectDatabase {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
|
||||
@@ -9,9 +9,7 @@ pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
|
||||
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
|
||||
use red_knot_python_semantic::register_lints;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::{
|
||||
create_parse_diagnostic, Annotation, Diagnostic, DiagnosticId, Severity, Span,
|
||||
};
|
||||
use ruff_db::diagnostic::{DiagnosticId, OldDiagnosticTrait, OldParseDiagnostic, Severity, Span};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{source_text, SourceTextError};
|
||||
@@ -19,6 +17,7 @@ use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use rustc_hash::FxHashSet;
|
||||
use salsa::Durability;
|
||||
use salsa::Setter;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -164,27 +163,24 @@ impl Project {
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Diagnostic> {
|
||||
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
let project_span = tracing::debug_span!("Project::check");
|
||||
let _span = project_span.enter();
|
||||
|
||||
tracing::debug!("Checking project '{name}'", name = self.name(db));
|
||||
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
diagnostics.extend(
|
||||
self.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(OptionDiagnostic::to_diagnostic),
|
||||
);
|
||||
let mut diagnostics: Vec<Box<dyn OldDiagnosticTrait>> = Vec::new();
|
||||
diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
let files = ProjectFiles::new(db, self);
|
||||
|
||||
diagnostics.extend(
|
||||
files
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.map(IOErrorDiagnostic::to_diagnostic),
|
||||
);
|
||||
diagnostics.extend(files.diagnostics().iter().cloned().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic);
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
let result = Arc::new(std::sync::Mutex::new(diagnostics));
|
||||
let inner_result = Arc::clone(&result);
|
||||
@@ -212,11 +208,14 @@ impl Project {
|
||||
Arc::into_inner(result).unwrap().into_inner().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
let mut file_diagnostics: Vec<_> = self
|
||||
.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(OptionDiagnostic::to_diagnostic)
|
||||
.map(|diagnostic| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
})
|
||||
.collect();
|
||||
|
||||
let check_diagnostics = check_file_impl(db, file);
|
||||
@@ -399,36 +398,35 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
let mut diagnostics: Vec<Box<dyn OldDiagnosticTrait>> = Vec::new();
|
||||
|
||||
// Abort checking if there are IO errors.
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
if let Some(read_error) = source.read_error() {
|
||||
diagnostics.push(
|
||||
IOErrorDiagnostic {
|
||||
file: Some(file),
|
||||
error: read_error.clone().into(),
|
||||
}
|
||||
.to_diagnostic(),
|
||||
);
|
||||
diagnostics.push(Box::new(IOErrorDiagnostic {
|
||||
file: Some(file),
|
||||
error: read_error.clone().into(),
|
||||
}));
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
diagnostics.extend(
|
||||
parsed
|
||||
.errors()
|
||||
.iter()
|
||||
.map(|error| create_parse_diagnostic(file, error)),
|
||||
);
|
||||
diagnostics.extend(parsed.errors().iter().map(|error| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> =
|
||||
Box::new(OldParseDiagnostic::new(file, error.clone()));
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned());
|
||||
diagnostics.extend(check_types(db.upcast(), file).iter().map(|diagnostic| {
|
||||
let boxed: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
boxed
|
||||
}));
|
||||
|
||||
diagnostics.sort_unstable_by_key(|diagnostic| {
|
||||
diagnostic
|
||||
.primary_span()
|
||||
.span()
|
||||
.and_then(|span| span.range())
|
||||
.unwrap_or_default()
|
||||
.start()
|
||||
@@ -496,13 +494,21 @@ pub struct IOErrorDiagnostic {
|
||||
error: IOErrorKind,
|
||||
}
|
||||
|
||||
impl IOErrorDiagnostic {
|
||||
fn to_diagnostic(&self) -> Diagnostic {
|
||||
let mut diag = Diagnostic::new(DiagnosticId::Io, Severity::Error, &self.error);
|
||||
if let Some(file) = self.file {
|
||||
diag.annotate(Annotation::primary(Span::from(file)));
|
||||
}
|
||||
diag
|
||||
impl OldDiagnosticTrait for IOErrorDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::Io
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
self.error.to_string().into()
|
||||
}
|
||||
|
||||
fn span(&self) -> Option<Span> {
|
||||
self.file.map(Span::from)
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,6 +526,7 @@ mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::{check_file_impl, ProjectMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::OldDiagnosticTrait;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
|
||||
@@ -543,7 +550,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
check_file_impl(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.primary_message().to_string())
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Failed to read file: No such file or directory".to_string()]
|
||||
);
|
||||
@@ -559,7 +566,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
check_file_impl(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.primary_message().to_string())
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![] as Vec<String>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,14 @@ use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSou
|
||||
use crate::Db;
|
||||
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
|
||||
use red_knot_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span};
|
||||
use ruff_db::diagnostic::{DiagnosticFormat, DiagnosticId, OldDiagnosticTrait, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::Combine;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -120,7 +121,7 @@ impl Options {
|
||||
.ok()
|
||||
.map(PythonPath::from_virtual_env_var)
|
||||
})
|
||||
.unwrap_or_else(|| PythonPath::Discover(project_root.to_path_buf())),
|
||||
.unwrap_or_else(|| PythonPath::KnownSitePackages(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,14 +397,22 @@ impl OptionDiagnostic {
|
||||
fn with_span(self, span: Option<Span>) -> Self {
|
||||
OptionDiagnostic { span, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_diagnostic(&self) -> Diagnostic {
|
||||
if let Some(ref span) = self.span {
|
||||
let mut diag = Diagnostic::new(self.id, self.severity, "");
|
||||
diag.annotate(Annotation::primary(span.clone()).message(&self.message));
|
||||
diag
|
||||
} else {
|
||||
Diagnostic::new(self.id, self.severity, &self.message)
|
||||
}
|
||||
impl OldDiagnosticTrait for OptionDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
Cow::Borrowed(&self.message)
|
||||
}
|
||||
|
||||
fn span(&self) -> Option<Span> {
|
||||
self.span.clone()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
self.severity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,6 @@ class MDTestRunner:
|
||||
|
||||
def watch(self) -> Never:
|
||||
self._recompile_tests("Compiling tests...", message_on_success=False)
|
||||
self._run_mdtest()
|
||||
self.console.print("[dim]Ready to watch for changes...[/dim]")
|
||||
|
||||
for changes in watch(CRATE_ROOT):
|
||||
|
||||
@@ -45,104 +45,3 @@ class Foo: ...
|
||||
|
||||
reveal_type(get_foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## Deferred self-reference annotations in a class definition
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class Foo:
|
||||
this: Foo
|
||||
# error: [unresolved-reference]
|
||||
_ = Foo()
|
||||
# error: [unresolved-reference]
|
||||
[Foo for _ in range(1)]
|
||||
a = int
|
||||
|
||||
def f(self, x: Foo):
|
||||
reveal_type(x) # revealed: Foo
|
||||
|
||||
def g(self) -> Foo:
|
||||
_: Foo = self
|
||||
return self
|
||||
|
||||
class Bar:
|
||||
foo: Foo
|
||||
b = int
|
||||
|
||||
def f(self, x: Foo):
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Bar:
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def h[T: Bar](self):
|
||||
pass
|
||||
|
||||
class Baz[T: Foo]:
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference]
|
||||
type S = a
|
||||
type T = b
|
||||
|
||||
def h[T: Bar]():
|
||||
# error: [unresolved-reference]
|
||||
return Bar()
|
||||
type Baz = Foo
|
||||
```
|
||||
|
||||
## Non-deferred self-reference annotations in a class definition
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
# error: [unresolved-reference]
|
||||
this: Foo
|
||||
ok: "Foo"
|
||||
# error: [unresolved-reference]
|
||||
_ = Foo()
|
||||
# error: [unresolved-reference]
|
||||
[Foo for _ in range(1)]
|
||||
a = int
|
||||
|
||||
# error: [unresolved-reference]
|
||||
def f(self, x: Foo):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Foo:
|
||||
_: Foo = self
|
||||
return self
|
||||
|
||||
class Bar:
|
||||
# error: [unresolved-reference]
|
||||
foo: Foo
|
||||
b = int
|
||||
|
||||
# error: [unresolved-reference]
|
||||
def f(self, x: Foo):
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Bar:
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def h[T: Bar](self):
|
||||
pass
|
||||
|
||||
class Baz[T: Foo]:
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference]
|
||||
type S = a
|
||||
type T = b
|
||||
|
||||
def h[T: Bar]():
|
||||
# error: [unresolved-reference]
|
||||
return Bar()
|
||||
type Qux = Foo
|
||||
|
||||
def _():
|
||||
class C:
|
||||
# error: [unresolved-reference]
|
||||
def f(self) -> C:
|
||||
return self
|
||||
```
|
||||
|
||||
@@ -36,7 +36,7 @@ def f():
|
||||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(b1) # revealed: Unknown | Literal[0]
|
||||
|
||||
# error: [invalid-type-form]
|
||||
invalid1: Literal[3 + 4]
|
||||
|
||||
@@ -73,12 +73,12 @@ qux = (foo, bar)
|
||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||
|
||||
# TODO: Infer "LiteralString"
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
template: LiteralString = "{}, {}"
|
||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||
# TODO: Infer `LiteralString`
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
@@ -56,7 +56,7 @@ def _(
|
||||
reveal_type(d) # revealed: Unknown
|
||||
|
||||
def foo(a_: e) -> None:
|
||||
reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec`)
|
||||
reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec` instances in type expressions)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
@@ -52,11 +52,13 @@ reveal_type(b) # revealed: tuple[int]
|
||||
reveal_type(c) # revealed: tuple[str, int]
|
||||
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
|
||||
|
||||
# TODO: homogeneous tuples, PEP-646 tuples, generics
|
||||
# TODO: homogeneous tuples, PEP-646 tuples
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(f) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(h) # revealed: tuple[@Todo(generics), @Todo(generics)]
|
||||
|
||||
# TODO: support more kinds of type expressions in annotations
|
||||
reveal_type(h) # revealed: @Todo(full tuple[...] support)
|
||||
|
||||
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||
reveal_type(j) # revealed: tuple[str | int]
|
||||
|
||||
@@ -1541,7 +1541,7 @@ integers are instances of that class:
|
||||
|
||||
```py
|
||||
reveal_type((2).bit_length) # revealed: <bound method `bit_length` of `Literal[2]`>
|
||||
reveal_type((2).denominator) # revealed: Literal[1]
|
||||
reveal_type((2).denominator) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
@@ -1709,37 +1709,6 @@ reveal_type(C.a_type) # revealed: type
|
||||
reveal_type(C.a_none) # revealed: None
|
||||
```
|
||||
|
||||
## Enum classes
|
||||
|
||||
Enums are not supported yet; attribute access on an enum class is inferred as `Todo`.
|
||||
|
||||
```py
|
||||
import enum
|
||||
|
||||
reveal_type(enum.Enum.__members__) # revealed: @Todo(Attribute access on enum classes)
|
||||
|
||||
class Foo(enum.Enum):
|
||||
BAR = 1
|
||||
|
||||
reveal_type(Foo.BAR) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
|
||||
```
|
||||
|
||||
## `super()`
|
||||
|
||||
`super()` is not supported yet, but we do not emit false positives on `super()` calls.
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def bar(self) -> int:
|
||||
return 42
|
||||
|
||||
class Bar(Foo):
|
||||
def bar(self) -> int:
|
||||
return super().bar()
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||
|
||||
@@ -14,43 +14,43 @@ We support inference for all Python's binary operators: `+`, `-`, `*`, `@`, `/`,
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> "A":
|
||||
def __add__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __sub__(self, other) -> "A":
|
||||
def __sub__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __mul__(self, other) -> "A":
|
||||
def __mul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __matmul__(self, other) -> "A":
|
||||
def __matmul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __truediv__(self, other) -> "A":
|
||||
def __truediv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __floordiv__(self, other) -> "A":
|
||||
def __floordiv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __mod__(self, other) -> "A":
|
||||
def __mod__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __pow__(self, other) -> "A":
|
||||
def __pow__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __lshift__(self, other) -> "A":
|
||||
def __lshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rshift__(self, other) -> "A":
|
||||
def __rshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __and__(self, other) -> "A":
|
||||
def __and__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __xor__(self, other) -> "A":
|
||||
def __xor__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __or__(self, other) -> "A":
|
||||
def __or__(self, other) -> A:
|
||||
return self
|
||||
|
||||
class B: ...
|
||||
@@ -76,43 +76,43 @@ We also support inference for reflected operations:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __radd__(self, other) -> "A":
|
||||
def __radd__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rsub__(self, other) -> "A":
|
||||
def __rsub__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rmul__(self, other) -> "A":
|
||||
def __rmul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rmatmul__(self, other) -> "A":
|
||||
def __rmatmul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rtruediv__(self, other) -> "A":
|
||||
def __rtruediv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rfloordiv__(self, other) -> "A":
|
||||
def __rfloordiv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rmod__(self, other) -> "A":
|
||||
def __rmod__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rpow__(self, other) -> "A":
|
||||
def __rpow__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rlshift__(self, other) -> "A":
|
||||
def __rlshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rrshift__(self, other) -> "A":
|
||||
def __rrshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rand__(self, other) -> "A":
|
||||
def __rand__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rxor__(self, other) -> "A":
|
||||
def __rxor__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __ror__(self, other) -> "A":
|
||||
def __ror__(self, other) -> A:
|
||||
return self
|
||||
|
||||
class B: ...
|
||||
@@ -157,11 +157,11 @@ the right-hand side is not a subtype of the left-hand side, `lhs.__add__` will t
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other: "B") -> int:
|
||||
def __add__(self, other: B) -> int:
|
||||
return 42
|
||||
|
||||
class B:
|
||||
def __radd__(self, other: "A") -> str:
|
||||
def __radd__(self, other: A) -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(A() + B()) # revealed: int
|
||||
@@ -169,10 +169,10 @@ reveal_type(A() + B()) # revealed: int
|
||||
# Edge case: C is a subtype of C, *but* if the two sides are of *equal* types,
|
||||
# the lhs *still* takes precedence
|
||||
class C:
|
||||
def __add__(self, other: "C") -> int:
|
||||
def __add__(self, other: C) -> int:
|
||||
return 42
|
||||
|
||||
def __radd__(self, other: "C") -> str:
|
||||
def __radd__(self, other: C) -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(C() + C()) # revealed: int
|
||||
@@ -237,11 +237,11 @@ well.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __sub__(self, other: "A") -> "A":
|
||||
def __sub__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
class B:
|
||||
def __rsub__(self, other: A) -> "B":
|
||||
def __rsub__(self, other: A) -> B:
|
||||
return B()
|
||||
|
||||
reveal_type(A() - B()) # revealed: B
|
||||
@@ -300,10 +300,10 @@ its instance super-type.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> "A":
|
||||
def __add__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __radd__(self, other) -> "A":
|
||||
def __radd__(self, other) -> A:
|
||||
return self
|
||||
|
||||
reveal_type(A() + 1) # revealed: A
|
||||
@@ -312,7 +312,7 @@ reveal_type(1 + A()) # revealed: A
|
||||
reveal_type(A() + "foo") # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type("foo" + A()) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type("foo" + A()) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
reveal_type(A() + b"foo") # revealed: A
|
||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||
@@ -320,7 +320,7 @@ reveal_type(b"foo" + A()) # revealed: bytes
|
||||
|
||||
reveal_type(A() + ()) # revealed: A
|
||||
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
||||
reveal_type(() + A()) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(() + A()) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
literal_string_instance = "foo" * 1_000_000_000
|
||||
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
||||
@@ -329,7 +329,7 @@ reveal_type(literal_string_instance) # revealed: LiteralString
|
||||
reveal_type(A() + literal_string_instance) # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
@@ -371,39 +371,6 @@ a = NotBoolable()
|
||||
10 and a and True
|
||||
```
|
||||
|
||||
## Operations on class objects
|
||||
|
||||
When operating on class objects, the corresponding dunder methods are looked up on the metaclass.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class Meta(type):
|
||||
def __add__(self, other: Meta) -> int:
|
||||
return 1
|
||||
|
||||
def __lt__(self, other: Meta) -> bool:
|
||||
return True
|
||||
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return "a"
|
||||
|
||||
class A(metaclass=Meta): ...
|
||||
class B(metaclass=Meta): ...
|
||||
|
||||
reveal_type(A + B) # revealed: int
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A - B) # revealed: Unknown
|
||||
|
||||
reveal_type(A < B) # revealed: bool
|
||||
reveal_type(A > B) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A <= B) # revealed: Unknown
|
||||
|
||||
reveal_type(A[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Unsupported
|
||||
|
||||
### Dunder as instance attribute
|
||||
@@ -466,7 +433,7 @@ the unreflected dunder of the left-hand operand. For context, see
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __radd__(self, other: "Foo") -> "Foo":
|
||||
def __radd__(self, other: Foo) -> Foo:
|
||||
return self
|
||||
|
||||
# error: [unsupported-operator]
|
||||
|
||||
@@ -50,21 +50,9 @@ 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 of overloaded function)
|
||||
reveal_type(2**x) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(x**x) # revealed: @Todo(return type of overloaded function)
|
||||
```
|
||||
|
||||
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but
|
||||
the second argument is >=0, an `int` is still returned:
|
||||
|
||||
```py
|
||||
reveal_type(1**0) # revealed: Literal[1]
|
||||
reveal_type(0**1) # revealed: Literal[0]
|
||||
reveal_type(0**0) # revealed: Literal[1]
|
||||
reveal_type((-1) ** 2) # revealed: Literal[1]
|
||||
reveal_type(2 ** (-1)) # revealed: float
|
||||
reveal_type((-1) ** (-1)) # revealed: float
|
||||
reveal_type(x**2) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(2**x) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(x**x) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Division by Zero
|
||||
|
||||
@@ -49,11 +49,3 @@ def f4(x: float, y: float):
|
||||
reveal_type(x // y) # revealed: int | float
|
||||
reveal_type(x % y) # revealed: int | float
|
||||
```
|
||||
|
||||
If any of the union elements leads to a division by zero, we will report an error:
|
||||
|
||||
```py
|
||||
def f5(m: int, n: Literal[-1, 0, 1]):
|
||||
# error: [division-by-zero] "Cannot divide object of type `int` by zero"
|
||||
return m / n
|
||||
```
|
||||
|
||||
@@ -38,42 +38,3 @@ type("Foo", ())
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type("Foo", (), {}, weird_other_arg=42)
|
||||
```
|
||||
|
||||
## Calls to `str()`
|
||||
|
||||
### Valid calls
|
||||
|
||||
```py
|
||||
str()
|
||||
str("")
|
||||
str(b"")
|
||||
str(1)
|
||||
str(object=1)
|
||||
|
||||
str(b"M\xc3\xbcsli", "utf-8")
|
||||
str(b"M\xc3\xbcsli", "utf-8", "replace")
|
||||
|
||||
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16")
|
||||
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16", errors="ignore")
|
||||
|
||||
str(bytearray.fromhex("4d c3 bc 73 6c 69"), "utf-8")
|
||||
str(bytearray(), "utf-8")
|
||||
|
||||
str(encoding="utf-8", object=b"M\xc3\xbcsli")
|
||||
str(b"", errors="replace")
|
||||
str(encoding="utf-8")
|
||||
str(errors="replace")
|
||||
```
|
||||
|
||||
### Invalid calls
|
||||
|
||||
```py
|
||||
str(1, 2) # error: [no-matching-overload]
|
||||
str(o=1) # error: [no-matching-overload]
|
||||
|
||||
# First argument is not a bytes-like object:
|
||||
str("Müsli", "utf-8") # error: [no-matching-overload]
|
||||
|
||||
# Second argument is not a valid encoding:
|
||||
str(b"M\xc3\xbcsli", b"utf-8") # error: [no-matching-overload]
|
||||
```
|
||||
|
||||
@@ -204,28 +204,6 @@ def _(flag: bool):
|
||||
reveal_type(d[0]) # revealed: str | bytes
|
||||
```
|
||||
|
||||
## Calling a union of types without dunder methods
|
||||
|
||||
We add instance attributes here to make sure that we don't treat the implicit dunder calls here like
|
||||
regular method calls.
|
||||
|
||||
```py
|
||||
def external_getitem(instance, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class NotSubscriptable1:
|
||||
def __init__(self, value: int):
|
||||
self.__getitem__ = external_getitem
|
||||
|
||||
class NotSubscriptable2:
|
||||
def __init__(self, value: int):
|
||||
self.__getitem__ = external_getitem
|
||||
|
||||
def _(union: NotSubscriptable1 | NotSubscriptable2):
|
||||
# error: [non-subscriptable]
|
||||
union[0]
|
||||
```
|
||||
|
||||
## Calling a possibly-unbound dunder method
|
||||
|
||||
```py
|
||||
|
||||
@@ -43,7 +43,8 @@ def decorator(func) -> Callable[[], int]:
|
||||
def bar() -> str:
|
||||
return "bar"
|
||||
|
||||
reveal_type(bar()) # revealed: int
|
||||
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
||||
reveal_type(bar()) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Invalid callable
|
||||
|
||||
@@ -59,7 +59,7 @@ import sys
|
||||
reveal_type(inspect.getattr_static(sys, "platform")) # revealed: LiteralString
|
||||
reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static]
|
||||
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: property
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[real]
|
||||
```
|
||||
|
||||
(Implicit) instance attributes can also be accessed through `inspect.getattr_static`:
|
||||
|
||||
@@ -410,29 +410,23 @@ def does_nothing[T](f: T) -> T:
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
# TODO: no error should be emitted here (needs support for generics)
|
||||
# error: [invalid-argument-type]
|
||||
@does_nothing
|
||||
def f1(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
# TODO: no error should be emitted here (needs support for generics)
|
||||
# error: [invalid-argument-type]
|
||||
|
||||
@does_nothing
|
||||
@classmethod
|
||||
def f2(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
# TODO: All of these should be `str` (and not emit an error), once we support generics
|
||||
# TODO: We do not support decorators yet (only limited special cases). Eventually,
|
||||
# these should all return `str`:
|
||||
|
||||
# error: [call-non-callable]
|
||||
reveal_type(C.f1(1)) # revealed: Unknown
|
||||
# error: [call-non-callable]
|
||||
reveal_type(C().f1(1)) # revealed: Unknown
|
||||
reveal_type(C.f1(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C().f1(1)) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
# error: [call-non-callable]
|
||||
reveal_type(C.f2(1)) # revealed: Unknown
|
||||
# error: [call-non-callable]
|
||||
reveal_type(C().f2(1)) # revealed: Unknown
|
||||
reveal_type(C.f2(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C().f2(1)) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
||||
|
||||
@@ -382,13 +382,13 @@ arbitrary objects to a `bool`, but a comparison of tuples will fail if the resul
|
||||
pair of elements at equivalent positions cannot be converted to a `bool`:
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: None = None
|
||||
|
||||
class A:
|
||||
def __eq__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
class NotBoolable:
|
||||
__bool__: None = None
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
(A(),) == (A(),)
|
||||
```
|
||||
|
||||
@@ -44,240 +44,6 @@ def _(target: int):
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## Value match
|
||||
|
||||
A value pattern matches based on equality: the first `case` branch here will be taken if `subject`
|
||||
is equal to `2`, even if `subject` is not an instance of `int`. We can't know whether `C` here has a
|
||||
custom `__eq__` implementation that might cause it to compare equal to `2`, so we have to consider
|
||||
the possibility that the `case` branch might be taken even though the type `C` is disjoint from the
|
||||
type `Literal[2]`.
|
||||
|
||||
This leads us to infer `Literal[1, 3]` as the type of `y` after the `match` statement, rather than
|
||||
`Literal[1]`:
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class C:
|
||||
pass
|
||||
|
||||
def _(subject: C):
|
||||
y = 1
|
||||
match subject:
|
||||
case 2:
|
||||
y = 3
|
||||
reveal_type(y) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
## Class match
|
||||
|
||||
A `case` branch with a class pattern is taken if the subject is an instance of the given class, and
|
||||
all subpatterns in the class pattern match.
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
class FooSub(Foo):
|
||||
pass
|
||||
|
||||
class Bar:
|
||||
pass
|
||||
|
||||
@final
|
||||
class Baz:
|
||||
pass
|
||||
|
||||
def _(target: FooSub):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Foo():
|
||||
y = 3
|
||||
case Bar():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[3]
|
||||
|
||||
def _(target: FooSub):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Bar():
|
||||
y = 3
|
||||
case Foo():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[3, 4]
|
||||
|
||||
def _(target: FooSub | str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Foo():
|
||||
y = 3
|
||||
case Bar():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 3, 4]
|
||||
```
|
||||
|
||||
## Singleton match
|
||||
|
||||
Singleton patterns are matched based on identity, not equality comparisons or `isinstance()` checks.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(target: Literal[True, False]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: bool):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: None):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[4]
|
||||
|
||||
def _(target: None | Literal[True]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 4]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
|
||||
# bool is an int subclass
|
||||
def _(target: int):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Or match
|
||||
|
||||
A `|` pattern matches if any of the subpatterns match.
|
||||
|
||||
```py
|
||||
from typing import Literal, final
|
||||
|
||||
def _(target: Literal["foo", "baz"]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case "foo" | "bar":
|
||||
y = 2
|
||||
case "baz":
|
||||
y = 3
|
||||
|
||||
# TODO: with exhaustiveness, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: None):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case None | 3:
|
||||
y = 2
|
||||
case "foo" | 4 | True:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2]
|
||||
|
||||
@final
|
||||
class Baz:
|
||||
pass
|
||||
|
||||
def _(target: int | None | float):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case None | 3:
|
||||
y = 2
|
||||
case Baz():
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
|
||||
def _(target: None | str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz() | True | False:
|
||||
y = 2
|
||||
case int():
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
## Guard with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
# Decorators
|
||||
|
||||
Decorators are a way to modify function and class behavior. A decorator is a callable that takes the
|
||||
function or class as an argument and returns a modified version of it.
|
||||
|
||||
## Basic example
|
||||
|
||||
A decorated function definition is conceptually similar to `def f(x): ...` followed by
|
||||
`f = decorator(f)`. This means that the type of a decorated function is the same as the return type
|
||||
of the decorator (which does not necessarily need to be a callable type):
|
||||
|
||||
```py
|
||||
def custom_decorator(f) -> int:
|
||||
return 1
|
||||
|
||||
@custom_decorator
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: int
|
||||
```
|
||||
|
||||
## Type-annotated decorator
|
||||
|
||||
More commonly, a decorator returns a modified callable type:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def ensure_positive(wrapped: Callable[[int], bool]) -> Callable[[int], bool]:
|
||||
return lambda x: wrapped(x) and x > 0
|
||||
|
||||
@ensure_positive
|
||||
def even(x: int) -> bool:
|
||||
return x % 2 == 0
|
||||
|
||||
reveal_type(even) # revealed: (int, /) -> bool
|
||||
reveal_type(even(4)) # revealed: bool
|
||||
```
|
||||
|
||||
## Decorators which take arguments
|
||||
|
||||
Decorators can be arbitrary expressions. This is often useful when the decorator itself takes
|
||||
arguments:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def ensure_larger_than(lower_bound: int) -> Callable[[Callable[[int], bool]], Callable[[int], bool]]:
|
||||
def decorator(wrapped: Callable[[int], bool]) -> Callable[[int], bool]:
|
||||
return lambda x: wrapped(x) and x >= lower_bound
|
||||
return decorator
|
||||
|
||||
@ensure_larger_than(10)
|
||||
def even(x: int) -> bool:
|
||||
return x % 2 == 0
|
||||
|
||||
reveal_type(even) # revealed: (int, /) -> bool
|
||||
reveal_type(even(14)) # revealed: bool
|
||||
```
|
||||
|
||||
## Multiple decorators
|
||||
|
||||
Multiple decorators can be applied to a single function. They are applied in "bottom-up" order,
|
||||
meaning that the decorator closest to the function definition is applied first:
|
||||
|
||||
```py
|
||||
def maps_to_str(f) -> str:
|
||||
return "a"
|
||||
|
||||
def maps_to_int(f) -> int:
|
||||
return 1
|
||||
|
||||
def maps_to_bytes(f) -> bytes:
|
||||
return b"a"
|
||||
|
||||
@maps_to_str
|
||||
@maps_to_int
|
||||
@maps_to_bytes
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Decorating with a class
|
||||
|
||||
When a function is decorated with a class-based decorator, the decorated function turns into an
|
||||
instance of the class (see also: [properties](properties.md)). Attributes of the class can be
|
||||
accessed on the decorated function.
|
||||
|
||||
```py
|
||||
class accept_strings:
|
||||
custom_attribute: str = "a"
|
||||
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
|
||||
def __call__(self, x: str | int) -> bool:
|
||||
return self.f(int(x))
|
||||
|
||||
@accept_strings
|
||||
def even(x: int) -> bool:
|
||||
return x > 0
|
||||
|
||||
reveal_type(even) # revealed: accept_strings
|
||||
reveal_type(even.custom_attribute) # revealed: str
|
||||
reveal_type(even("1")) # revealed: bool
|
||||
reveal_type(even(1)) # revealed: bool
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
even(None)
|
||||
```
|
||||
|
||||
## Common decorator patterns
|
||||
|
||||
### `functools.wraps`
|
||||
|
||||
This test mainly makes sure that we do not emit any diagnostics in a case where the decorator is
|
||||
implemented using `functools.wraps`.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from functools import wraps
|
||||
|
||||
def custom_decorator(f) -> Callable[[int], str]:
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
print("Calling decorated function")
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@custom_decorator
|
||||
def f(x: int) -> str:
|
||||
return str(x)
|
||||
|
||||
reveal_type(f) # revealed: (int, /) -> str
|
||||
```
|
||||
|
||||
### `functools.cache`
|
||||
|
||||
```py
|
||||
from functools import cache
|
||||
|
||||
@cache
|
||||
def f(x: int) -> int:
|
||||
return x**2
|
||||
|
||||
# TODO: Should be `_lru_cache_wrapper[int]`
|
||||
reveal_type(f) # revealed: @Todo(generics)
|
||||
|
||||
# TODO: Should be `int`
|
||||
reveal_type(f(1)) # revealed: @Todo(generics)
|
||||
```
|
||||
|
||||
## Lambdas as decorators
|
||||
|
||||
```py
|
||||
@lambda f: f
|
||||
def g(x: int) -> str:
|
||||
return "a"
|
||||
|
||||
# TODO: This should be `Literal[g]` or `(int, /) -> str`
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Error cases
|
||||
|
||||
### Unknown decorator
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference] "Name `unknown_decorator` used when not defined"
|
||||
@unknown_decorator
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Error in the decorator expression
|
||||
|
||||
```py
|
||||
# error: [unsupported-operator]
|
||||
@(1 + "a")
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Non-callable decorator
|
||||
|
||||
```py
|
||||
non_callable = 1
|
||||
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
@non_callable
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Wrong signature
|
||||
|
||||
#### Wrong argument type
|
||||
|
||||
Here, we emit a diagnostic since `wrong_signature` takes an `int` instead of a callable type as the
|
||||
first argument:
|
||||
|
||||
```py
|
||||
def wrong_signature(f: int) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 1 (`f`) of function `wrong_signature`; expected type `int`"
|
||||
@wrong_signature
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
#### Wrong number of arguments
|
||||
|
||||
Decorators need to be callable with a single argument. If they are not, we emit a diagnostic:
|
||||
|
||||
```py
|
||||
def takes_two_arguments(f, g) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `g` of function `takes_two_arguments`"
|
||||
@takes_two_arguments
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
|
||||
def takes_no_argument() -> str:
|
||||
return "a"
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `takes_no_argument`: expected 0, got 1"
|
||||
@takes_no_argument
|
||||
def g(x): ...
|
||||
```
|
||||
@@ -506,7 +506,8 @@ class C:
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name or "Unset"
|
||||
|
||||
# TODO: No diagnostic should be emitted here
|
||||
# error: [unresolved-attribute] "Type `Literal[name]` has no attribute `setter`"
|
||||
@name.setter
|
||||
def name(self, value: str | None) -> None:
|
||||
self._value = value
|
||||
@@ -514,13 +515,22 @@ class C:
|
||||
c = C()
|
||||
|
||||
reveal_type(c._name) # revealed: str | None
|
||||
reveal_type(c.name) # revealed: str
|
||||
reveal_type(C.name) # revealed: property
|
||||
|
||||
# TODO: Should be `str`
|
||||
reveal_type(c.name) # revealed: <bound method `name` of `C`>
|
||||
|
||||
# Should be `builtins.property`
|
||||
reveal_type(C.name) # revealed: Literal[name]
|
||||
|
||||
# TODO: These should not emit errors
|
||||
# error: [invalid-assignment]
|
||||
c.name = "new"
|
||||
|
||||
# error: [invalid-assignment]
|
||||
c.name = None
|
||||
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `name` on type `C` with custom `__set__` method"
|
||||
# TODO: this should be an error, but with a proper error message
|
||||
# error: [invalid-assignment] "Implicit shadowing of function `name`; annotate to make it explicit if this is intentional"
|
||||
c.name = 42
|
||||
```
|
||||
|
||||
@@ -577,7 +587,7 @@ reveal_type(wrapper_descriptor(f, None, type(f))) # revealed: Literal[f]
|
||||
reveal_type(f.__get__.__hash__) # revealed: <bound method `__hash__` of `MethodWrapperType`>
|
||||
|
||||
# Attribute access on the wrapper-descriptor falls back to `WrapperDescriptorType`:
|
||||
reveal_type(wrapper_descriptor.__qualname__) # revealed: str
|
||||
reveal_type(wrapper_descriptor.__qualname__) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
We can also bind the free function `f` to an instance of a class `C`:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
The (inferred) type of the value and the given type do not need to have any correlation.
|
||||
|
||||
```py
|
||||
from typing import Literal, cast, Any
|
||||
from typing import Literal, cast
|
||||
|
||||
reveal_type(True) # revealed: Literal[True]
|
||||
reveal_type(cast(str, True)) # revealed: str
|
||||
@@ -25,27 +25,4 @@ reveal_type(cast(1, True)) # revealed: Unknown
|
||||
cast(str)
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `cast`: expected 2, got 3"
|
||||
cast(str, b"ar", "foo")
|
||||
|
||||
def function_returning_int() -> int:
|
||||
return 10
|
||||
|
||||
# error: [redundant-cast] "Value is already of type `int`"
|
||||
cast(int, function_returning_int())
|
||||
|
||||
def function_returning_any() -> Any:
|
||||
return "blah"
|
||||
|
||||
# error: [redundant-cast] "Value is already of type `Any`"
|
||||
cast(Any, function_returning_any())
|
||||
```
|
||||
|
||||
Complex type expressions (which may be unsupported) do not lead to spurious `[redundant-cast]`
|
||||
diagnostics.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]):
|
||||
a = cast(Callable[[list[bytes]], None], x)
|
||||
b = cast(tuple[list[bytes]], y)
|
||||
```
|
||||
|
||||
@@ -269,66 +269,3 @@ def f(cond: bool) -> int:
|
||||
if cond:
|
||||
return 2
|
||||
```
|
||||
|
||||
## NotImplemented
|
||||
|
||||
### Default Python version
|
||||
|
||||
`NotImplemented` is a special symbol in Python. It is commonly used to control the fallback behavior
|
||||
of special dunder methods. You can find more details in the
|
||||
[documentation](https://docs.python.org/3/library/numbers.html#implementing-the-arithmetic-operations).
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __add__(self, o: A) -> A:
|
||||
return NotImplemented
|
||||
```
|
||||
|
||||
However, as shown below, `NotImplemented` should not cause issues with the declared return type.
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return NotImplemented
|
||||
|
||||
def f(cond: bool) -> int:
|
||||
if cond:
|
||||
return 1
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def f(x: int) -> int | str:
|
||||
if x < 0:
|
||||
return -1
|
||||
elif x == 0:
|
||||
return NotImplemented
|
||||
else:
|
||||
return "test"
|
||||
|
||||
def f(cond: bool) -> str:
|
||||
return "hello" if cond else NotImplemented
|
||||
|
||||
def f(cond: bool) -> int:
|
||||
# error: [invalid-return-type] "Object of type `Literal["hello"]` is not assignable to return type `int`"
|
||||
return "hello" if cond else NotImplemented
|
||||
```
|
||||
|
||||
### Python 3.10+
|
||||
|
||||
Unlike Ellipsis, `_NotImplementedType` remains in `builtins.pyi` regardless of the Python version.
|
||||
Even if `builtins._NotImplementedType` is fully replaced by `types.NotImplementedType` in the
|
||||
future, it should still work as expected.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return NotImplemented
|
||||
|
||||
def f(cond: bool) -> str:
|
||||
return "hello" if cond else NotImplemented
|
||||
```
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# Narrowing for `in` conditionals
|
||||
|
||||
## `in` for tuples
|
||||
|
||||
```py
|
||||
def _(x: int):
|
||||
if x in (1, 2, 3):
|
||||
reveal_type(x) # revealed: int
|
||||
else:
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
```py
|
||||
def _(x: str):
|
||||
if x in ("a", "b", "c"):
|
||||
reveal_type(x) # revealed: str
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(x: Literal[1, 2, "a", "b", False, b"abc"]):
|
||||
if x in (1,):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
elif x in (2, "a"):
|
||||
reveal_type(x) # revealed: Literal[2, "a"]
|
||||
elif x in (b"abc",):
|
||||
reveal_type(x) # revealed: Literal[b"abc"]
|
||||
elif x not in (3,):
|
||||
reveal_type(x) # revealed: Literal["b", False]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
```
|
||||
|
||||
```py
|
||||
def _(x: Literal["a", "b", "c", 1]):
|
||||
if x in ("a", "b", "c", 2):
|
||||
reveal_type(x) # revealed: Literal["a", "b", "c"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## `in` for `str` and literal strings
|
||||
|
||||
```py
|
||||
def _(x: str):
|
||||
if x in "abc":
|
||||
reveal_type(x) # revealed: str
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(x: Literal["a", "b", "c", "d"]):
|
||||
if x in "abc":
|
||||
reveal_type(x) # revealed: Literal["a", "b", "c"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["d"]
|
||||
```
|
||||
|
||||
```py
|
||||
def _(x: Literal["a", "b", "c", "e"]):
|
||||
if x in "abcd":
|
||||
reveal_type(x) # revealed: Literal["a", "b", "c"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["e"]
|
||||
```
|
||||
|
||||
```py
|
||||
def _(x: Literal[1, "a", "b", "c", "d"]):
|
||||
# error: [unsupported-operator]
|
||||
if x in "abc":
|
||||
reveal_type(x) # revealed: Literal["a", "b", "c"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1, "d"]
|
||||
```
|
||||
@@ -64,99 +64,3 @@ match x:
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
## Value patterns
|
||||
|
||||
```py
|
||||
def get_object() -> object:
|
||||
return object()
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case "foo":
|
||||
reveal_type(x) # revealed: Literal["foo"]
|
||||
case 42:
|
||||
reveal_type(x) # revealed: Literal[42]
|
||||
case 6.0:
|
||||
reveal_type(x) # revealed: float
|
||||
case 1j:
|
||||
reveal_type(x) # revealed: complex
|
||||
case b"foo":
|
||||
reveal_type(x) # revealed: Literal[b"foo"]
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
## Value patterns with guard
|
||||
|
||||
```py
|
||||
def get_object() -> object:
|
||||
return object()
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case "foo" if reveal_type(x): # revealed: Literal["foo"]
|
||||
pass
|
||||
case 42 if reveal_type(x): # revealed: Literal[42]
|
||||
pass
|
||||
case 6.0 if reveal_type(x): # revealed: float
|
||||
pass
|
||||
case 1j if reveal_type(x): # revealed: complex
|
||||
pass
|
||||
case b"foo" if reveal_type(x): # revealed: Literal[b"foo"]
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
## Or patterns
|
||||
|
||||
```py
|
||||
def get_object() -> object:
|
||||
return object()
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case "foo" | 42 | None:
|
||||
reveal_type(x) # revealed: Literal["foo", 42] | None
|
||||
case "foo" | tuple():
|
||||
reveal_type(x) # revealed: Literal["foo"] | tuple
|
||||
case True | False:
|
||||
reveal_type(x) # revealed: bool
|
||||
case 3.14 | 2.718 | 1.414:
|
||||
reveal_type(x) # revealed: float
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
## Or patterns with guard
|
||||
|
||||
```py
|
||||
def get_object() -> object:
|
||||
return object()
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case "foo" | 42 | None if reveal_type(x): # revealed: Literal["foo", 42] | None
|
||||
pass
|
||||
case "foo" | tuple() if reveal_type(x): # revealed: Literal["foo"] | tuple
|
||||
pass
|
||||
case True | False if reveal_type(x): # revealed: bool
|
||||
pass
|
||||
case 3.14 | 2.718 | 1.414 if reveal_type(x): # revealed: float
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
@@ -28,7 +28,10 @@ def f() -> None:
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr.__value__) # revealed: Any
|
||||
# TODO: This should either fall back to the specified type from typeshed,
|
||||
# which is `Any`, or be the actual type of the runtime value expression
|
||||
# `int | str`, i.e. `types.UnionType`.
|
||||
reveal_type(IntOrStr.__value__) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
## Invalid assignment
|
||||
@@ -71,7 +74,7 @@ type ListOrSet[T] = list[T] | set[T]
|
||||
|
||||
# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`,
|
||||
# as specified in the `typeshed` stubs.
|
||||
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
## `TypeAliasType` properties
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
# Properties
|
||||
|
||||
`property` is a built-in class in Python that can be used to model class attributes with custom
|
||||
getters, setters, and deleters.
|
||||
|
||||
## Basic getter
|
||||
|
||||
`property` is typically used as a decorator on a getter method. It turns the method into a property
|
||||
object. When accessing the property on an instance, the descriptor protocol is invoked, which calls
|
||||
the getter method:
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def my_property(self) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(C().my_property) # revealed: int
|
||||
```
|
||||
|
||||
When a property is accessed on the class directly, the descriptor protocol is also invoked, but
|
||||
`property.__get__` simply returns itself in this case (when `instance` is `None`):
|
||||
|
||||
```py
|
||||
reveal_type(C.my_property) # revealed: property
|
||||
```
|
||||
|
||||
## Getter and setter
|
||||
|
||||
A property can also have a setter method, which is used to set the value of the property. The setter
|
||||
method is defined using the `@<property_name>.setter` decorator. The setter method takes the value
|
||||
to be set as an argument.
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def my_property(self) -> int:
|
||||
return 1
|
||||
|
||||
@my_property.setter
|
||||
def my_property(self, value: int) -> None:
|
||||
pass
|
||||
|
||||
c = C()
|
||||
reveal_type(c.my_property) # revealed: int
|
||||
c.my_property = 2
|
||||
|
||||
# error: [invalid-assignment]
|
||||
c.my_property = "a"
|
||||
```
|
||||
|
||||
## `property.getter`
|
||||
|
||||
`property.getter` can be used to overwrite the getter method of a property. This does not overwrite
|
||||
the existing setter:
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def my_property(self) -> int:
|
||||
return 1
|
||||
|
||||
@my_property.setter
|
||||
def my_property(self, value: int) -> None:
|
||||
pass
|
||||
|
||||
@my_property.getter
|
||||
def my_property(self) -> str:
|
||||
return "a"
|
||||
|
||||
c = C()
|
||||
reveal_type(c.my_property) # revealed: str
|
||||
c.my_property = 2
|
||||
|
||||
# error: [invalid-assignment]
|
||||
c.my_property = "b"
|
||||
```
|
||||
|
||||
## `property.deleter`
|
||||
|
||||
We do not support `property.deleter` yet, but we make sure that it does not invalidate the getter or
|
||||
setter:
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def my_property(self) -> int:
|
||||
return 1
|
||||
|
||||
@my_property.setter
|
||||
def my_property(self, value: int) -> None:
|
||||
pass
|
||||
|
||||
@my_property.deleter
|
||||
def my_property(self) -> None:
|
||||
pass
|
||||
|
||||
c = C()
|
||||
reveal_type(c.my_property) # revealed: int
|
||||
c.my_property = 2
|
||||
# error: [invalid-assignment]
|
||||
c.my_property = "a"
|
||||
```
|
||||
|
||||
## Failure cases
|
||||
|
||||
### Attempting to write to a read-only property
|
||||
|
||||
When attempting to write to a read-only property, we emit an error:
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def attr(self) -> int:
|
||||
return 1
|
||||
|
||||
c = C()
|
||||
|
||||
# error: [invalid-assignment]
|
||||
c.attr = 2
|
||||
```
|
||||
|
||||
### Attempting to read a write-only property
|
||||
|
||||
When attempting to read a write-only property, we emit an error:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def attr_setter(self, value: int) -> None:
|
||||
pass
|
||||
attr = property(fset=attr_setter)
|
||||
|
||||
c = C()
|
||||
c.attr = 1
|
||||
|
||||
# TODO: An error should be emitted here, and the type should be `Unknown`
|
||||
# or `Never`. See https://github.com/astral-sh/ruff/issues/16298 for more
|
||||
# details.
|
||||
reveal_type(c.attr) # revealed: Unknown | property
|
||||
```
|
||||
|
||||
### Wrong setter signature
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def attr(self) -> int:
|
||||
return 1
|
||||
# error: [invalid-argument-type] "Object of type `Literal[attr]` cannot be assigned to parameter 2 (`fset`) of bound method `setter`; expected type `(Any, Any, /) -> None`"
|
||||
@attr.setter
|
||||
def attr(self) -> None:
|
||||
pass
|
||||
```
|
||||
|
||||
### Wrong getter signature
|
||||
|
||||
```py
|
||||
class C:
|
||||
# error: [invalid-argument-type] "Object of type `Literal[attr]` cannot be assigned to parameter 1 (`fget`) of class `property`; expected type `((Any, /) -> Any) | None`"
|
||||
@property
|
||||
def attr(self, x: int) -> int:
|
||||
return 1
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
### Manually constructed property
|
||||
|
||||
Properties can also be constructed manually using the `property` class. We partially support this:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def attr_getter(self) -> int:
|
||||
return 1
|
||||
attr = property(attr_getter)
|
||||
|
||||
c = C()
|
||||
reveal_type(c.attr) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
But note that we return `Unknown | int` because we did not declare the `attr` attribute. This is
|
||||
consistent with how we usually treat attributes, but here, if we try to declare `attr` as
|
||||
`property`, we fail to understand the property, since the `property` declaration shadows the more
|
||||
precise type that we infer for `property(attr_getter)` (which includes the actual information about
|
||||
the getter).
|
||||
|
||||
```py
|
||||
class C:
|
||||
def attr_getter(self) -> int:
|
||||
return 1
|
||||
attr: property = property(attr_getter)
|
||||
|
||||
c = C()
|
||||
reveal_type(c.attr) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Behind the scenes
|
||||
|
||||
In this section, we trace through some of the steps that make properties work. We start with a
|
||||
simple class `C` and a property `attr`:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
self._attr: int = 0
|
||||
|
||||
@property
|
||||
def attr(self) -> int:
|
||||
return self._attr
|
||||
|
||||
@attr.setter
|
||||
def attr(self, value: str) -> None:
|
||||
self._attr = len(value)
|
||||
```
|
||||
|
||||
Next, we create an instance of `C`. As we have seen above, accessing `attr` on the instance will
|
||||
return an `int`:
|
||||
|
||||
```py
|
||||
c = C()
|
||||
|
||||
reveal_type(c.attr) # revealed: int
|
||||
```
|
||||
|
||||
Behind the scenes, when we write `c.attr`, the first thing that happens is that we statically look
|
||||
up the symbol `attr` on the meta-type of `c`, i.e. the class `C`. We can emulate this static lookup
|
||||
using `inspect.getattr_static`, to see that `attr` is actually an instance of the `property` class:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
attr_property = getattr_static(C, "attr")
|
||||
reveal_type(attr_property) # revealed: property
|
||||
```
|
||||
|
||||
The `property` class has a `__get__` method, which makes it a descriptor. It also has a `__set__`
|
||||
method, which means that it is a *data* descriptor (if there is no setter, `__set__` is still
|
||||
available but yields an `AttributeError` at runtime).
|
||||
|
||||
```py
|
||||
reveal_type(type(attr_property).__get__) # revealed: <wrapper-descriptor `__get__` of `property` objects>
|
||||
reveal_type(type(attr_property).__set__) # revealed: <wrapper-descriptor `__set__` of `property` objects>
|
||||
```
|
||||
|
||||
When we access `c.attr`, the `__get__` method of the `property` class is called, passing the
|
||||
property object itself as the first argument, and the class instance `c` as the second argument. The
|
||||
third argument is the "owner" which can be set to `None` or to `C` in this case:
|
||||
|
||||
```py
|
||||
reveal_type(type(attr_property).__get__(attr_property, c, C)) # revealed: int
|
||||
reveal_type(type(attr_property).__get__(attr_property, c, None)) # revealed: int
|
||||
```
|
||||
|
||||
Alternatively, the above can also be written as a method call:
|
||||
|
||||
```py
|
||||
reveal_type(attr_property.__get__(c, C)) # revealed: int
|
||||
```
|
||||
|
||||
When we access `attr` on the class itself, the descriptor protocol is also invoked, but the instance
|
||||
argument is set to `None`. When `instance` is `None`, the call to `property.__get__` returns the
|
||||
property instance itself. So the following expressions are all equivalent
|
||||
|
||||
```py
|
||||
reveal_type(attr_property) # revealed: property
|
||||
reveal_type(C.attr) # revealed: property
|
||||
reveal_type(attr_property.__get__(None, C)) # revealed: property
|
||||
reveal_type(type(attr_property).__get__(attr_property, None, C)) # revealed: property
|
||||
```
|
||||
|
||||
When we set the property using `c.attr = "a"`, the `__set__` method of the property class is called.
|
||||
This attribute access desugars to
|
||||
|
||||
```py
|
||||
type(attr_property).__set__(attr_property, c, "a")
|
||||
|
||||
# error: [call-non-callable] "Call of wrapper descriptor `property.__set__` failed: calling the setter failed"
|
||||
type(attr_property).__set__(attr_property, c, 1)
|
||||
```
|
||||
|
||||
which is also equivalent to the following expressions:
|
||||
|
||||
```py
|
||||
attr_property.__set__(c, "a")
|
||||
# error: [call-non-callable]
|
||||
attr_property.__set__(c, 1)
|
||||
|
||||
C.attr.__set__(c, "a")
|
||||
# error: [call-non-callable]
|
||||
C.attr.__set__(c, 1)
|
||||
```
|
||||
|
||||
Properties also have `fget` and `fset` attributes that can be used to retrieve the original getter
|
||||
and setter functions, respectively.
|
||||
|
||||
```py
|
||||
reveal_type(attr_property.fget) # revealed: Literal[attr]
|
||||
reveal_type(attr_property.fget(c)) # revealed: int
|
||||
|
||||
reveal_type(attr_property.fset) # revealed: Literal[attr]
|
||||
reveal_type(attr_property.fset(c, "a")) # revealed: None
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
attr_property.fset(c, 1)
|
||||
```
|
||||
@@ -154,10 +154,6 @@ x = 1
|
||||
[reveal_type(x) for a in range(1)]
|
||||
|
||||
x = 2
|
||||
|
||||
# error: [unresolved-reference]
|
||||
[y for a in range(1)]
|
||||
y = 1
|
||||
```
|
||||
|
||||
### Set comprehensions
|
||||
@@ -169,10 +165,6 @@ x = 1
|
||||
{reveal_type(x) for a in range(1)}
|
||||
|
||||
x = 2
|
||||
|
||||
# error: [unresolved-reference]
|
||||
{y for a in range(1)}
|
||||
y = 1
|
||||
```
|
||||
|
||||
### Dict comprehensions
|
||||
@@ -184,10 +176,6 @@ x = 1
|
||||
{a: reveal_type(x) for a in range(1)}
|
||||
|
||||
x = 2
|
||||
|
||||
# error: [unresolved-reference]
|
||||
{a: y for a in range(1)}
|
||||
y = 1
|
||||
```
|
||||
|
||||
### Generator expressions
|
||||
@@ -199,10 +187,6 @@ x = 1
|
||||
list(reveal_type(x) for a in range(1))
|
||||
|
||||
x = 2
|
||||
|
||||
# error: [unresolved-reference]
|
||||
list(y for a in range(1))
|
||||
y = 1
|
||||
```
|
||||
|
||||
`evaluated_later.py`:
|
||||
@@ -278,14 +262,6 @@ def _():
|
||||
[reveal_type(x) for a in range(1)]
|
||||
|
||||
x = 2
|
||||
|
||||
x = 1
|
||||
|
||||
def _():
|
||||
class C:
|
||||
# revealed: Unknown | Literal[1]
|
||||
[reveal_type(x) for _ in [1]]
|
||||
x = 2
|
||||
```
|
||||
|
||||
### Eager scope within a lazy scope
|
||||
|
||||
@@ -58,8 +58,9 @@ reveal_type(typing.__eq__) # revealed: <bound method `__eq__` of `ModuleType`>
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
||||
|
||||
# TODO: needs support generics; should be `dict[str, Any]`:
|
||||
reveal_type(typing.__dict__) # revealed: @Todo(generics)
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
reveal_type(typing.__dict__) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
|
||||
@@ -91,9 +92,10 @@ reveal_type(__dict__) # revealed: Literal["foo"]
|
||||
import foo
|
||||
from foo import __dict__ as foo_dict
|
||||
|
||||
# TODO: needs support generics; should be `dict[str, Any]` for both of these:
|
||||
reveal_type(foo.__dict__) # revealed: @Todo(generics)
|
||||
reveal_type(foo_dict) # revealed: @Todo(generics)
|
||||
# TODO: needs support for attribute access on instances, properties, and generics;
|
||||
# should be `dict[str, Any]` for both of these:
|
||||
reveal_type(foo.__dict__) # revealed: @Todo(@property)
|
||||
reveal_type(foo_dict) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute
|
||||
|
||||
@@ -12,12 +12,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__: None = None
|
||||
3 |
|
||||
4 | class A:
|
||||
5 | def __eq__(self, other) -> NotBoolable:
|
||||
6 | return NotBoolable()
|
||||
1 | class A:
|
||||
2 | def __eq__(self, other) -> NotBoolable:
|
||||
3 | return NotBoolable()
|
||||
4 |
|
||||
5 | class NotBoolable:
|
||||
6 | __bool__: None = None
|
||||
7 |
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | (A(),) == (A(),)
|
||||
|
||||
@@ -25,7 +25,7 @@ reveal_type(y) # revealed: Unknown
|
||||
def _(n: int):
|
||||
a = b"abcde"[n]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(a) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -44,10 +44,10 @@ b[::0] # error: [zero-stepsize-in-slice]
|
||||
def _(m: int, n: int):
|
||||
byte_slice1 = b[m:n]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
def _(s: bytes) -> bytes:
|
||||
byte_slice2 = s[0:5]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
return reveal_type(byte_slice2) # revealed: @Todo(return type of overloaded function)
|
||||
return reveal_type(byte_slice2) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
@@ -12,13 +12,13 @@ x = [1, 2, 3]
|
||||
reveal_type(x) # revealed: list
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(x[0]) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(x[0]) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
# TODO reveal list
|
||||
reveal_type(x[0:1]) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(x[0:1]) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
# TODO error
|
||||
reveal_type(x["a"]) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(x["a"]) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Assignments within list assignment
|
||||
|
||||
@@ -22,7 +22,7 @@ reveal_type(b) # revealed: Unknown
|
||||
def _(n: int):
|
||||
a = "abcde"[n]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(a) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -76,11 +76,11 @@ def _(m: int, n: int, s2: str):
|
||||
|
||||
substring1 = s[m:n]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(substring1) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
substring2 = s2[0:5]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(substring2) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(substring2) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Unsupported slice types
|
||||
|
||||
@@ -70,7 +70,7 @@ def _(m: int, n: int):
|
||||
|
||||
tuple_slice = t[m:n]
|
||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
@@ -48,8 +48,6 @@ from typing import no_type_check
|
||||
@unknown_decorator # error: [unresolved-reference]
|
||||
@no_type_check
|
||||
def test() -> int:
|
||||
# TODO: this should not be an error
|
||||
# error: [unresolved-reference]
|
||||
return a + 5
|
||||
```
|
||||
|
||||
@@ -66,8 +64,6 @@ from typing import no_type_check
|
||||
@no_type_check
|
||||
@unknown_decorator
|
||||
def test() -> int:
|
||||
# TODO: this should not be an error
|
||||
# error: [unresolved-reference]
|
||||
return a + 5
|
||||
```
|
||||
|
||||
|
||||
@@ -121,9 +121,9 @@ But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` unti
|
||||
properties on instance types:
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info.micro) # revealed: int
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(Support for `typing.TypeAlias`)
|
||||
reveal_type(sys.version_info.serial) # revealed: int
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
## Accessing fields by index/slice
|
||||
|
||||
@@ -398,16 +398,16 @@ def f(x: TypeOf) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## `CallableTypeOf`
|
||||
## `CallableTypeFromFunction`
|
||||
|
||||
The `CallableTypeOf` special form can be used to extract the `Callable` structural type inhabited by
|
||||
a given callable object. This can be used to get the externally visibly signature of the object,
|
||||
which can then be used to test various type properties.
|
||||
The `CallableTypeFromFunction` special form can be used to extract the type of a function literal as
|
||||
a callable type. This can be used to get the externally-visibly signature of the function, which can
|
||||
then be used to test various type properties.
|
||||
|
||||
It accepts a single type parameter which is expected to be a callable object.
|
||||
It accepts a single type parameter which is expected to be a function literal.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf
|
||||
from knot_extensions import CallableTypeFromFunction
|
||||
|
||||
def f1():
|
||||
return
|
||||
@@ -418,41 +418,25 @@ def f2() -> int:
|
||||
def f3(x: int, y: str) -> None:
|
||||
return
|
||||
|
||||
# error: [invalid-type-form] "Special form `knot_extensions.CallableTypeOf` expected exactly one type parameter"
|
||||
c1: CallableTypeOf[f1, f2]
|
||||
# error: [invalid-type-form] "Special form `knot_extensions.CallableTypeFromFunction` expected exactly one type parameter"
|
||||
c1: CallableTypeFromFunction[f1, f2]
|
||||
# error: [invalid-type-form] "Expected the first argument to `knot_extensions.CallableTypeFromFunction` to be a function literal, but got `Literal[int]`"
|
||||
c2: CallableTypeFromFunction[int]
|
||||
|
||||
# error: [invalid-type-form] "Expected the first argument to `knot_extensions.CallableTypeOf` to be a callable object, but got an object of type `Literal["foo"]`"
|
||||
c2: CallableTypeOf["foo"]
|
||||
|
||||
# error: [invalid-type-form] "`knot_extensions.CallableTypeOf` requires exactly one argument when used in a type expression"
|
||||
def f(x: CallableTypeOf) -> None:
|
||||
# error: [invalid-type-form] "`knot_extensions.CallableTypeFromFunction` requires exactly one argument when used in a type expression"
|
||||
def f(x: CallableTypeFromFunction) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
Using it in annotation to reveal the signature of the callable object:
|
||||
Using it in annotation to reveal the signature of the function:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __init__(self, x: int) -> None:
|
||||
pass
|
||||
|
||||
def __call__(self, x: int) -> str:
|
||||
return "foo"
|
||||
|
||||
def _(
|
||||
c1: CallableTypeOf[f1],
|
||||
c2: CallableTypeOf[f2],
|
||||
c3: CallableTypeOf[f3],
|
||||
c4: CallableTypeOf[Foo],
|
||||
c5: CallableTypeOf[Foo(42).__call__],
|
||||
c1: CallableTypeFromFunction[f1],
|
||||
c2: CallableTypeFromFunction[f2],
|
||||
c3: CallableTypeFromFunction[f3],
|
||||
) -> None:
|
||||
reveal_type(c1) # revealed: () -> Unknown
|
||||
reveal_type(c2) # revealed: () -> int
|
||||
reveal_type(c3) # revealed: (x: int, y: str) -> None
|
||||
|
||||
# TODO: should be `(x: int) -> Foo`
|
||||
reveal_type(c4) # revealed: (...) -> Foo
|
||||
|
||||
# TODO: `self` is bound here; this should probably be `(x: int) -> str`?
|
||||
reveal_type(c5) # revealed: (self, x: int) -> str
|
||||
```
|
||||
|
||||
@@ -402,7 +402,7 @@ are covered in the [subtyping tests](./is_subtype_of.md#callable).
|
||||
### Return type
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, Unknown, static_assert, is_assignable_to
|
||||
from knot_extensions import CallableTypeFromFunction, Unknown, static_assert, is_assignable_to
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_assignable_to(Callable[[], Any], Callable[[], int]))
|
||||
@@ -432,7 +432,7 @@ A `Callable` which uses the gradual form (`...`) for the parameter types is cons
|
||||
input signature.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, static_assert, is_assignable_to
|
||||
from knot_extensions import CallableTypeFromFunction, static_assert, is_assignable_to
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_assignable_to(Callable[[], None], Callable[..., None]))
|
||||
@@ -450,12 +450,12 @@ def keyword_only(*, a: int, b: int) -> None: ...
|
||||
def keyword_variadic(**kwargs: int) -> None: ...
|
||||
def mixed(a: int, /, b: int, *args: int, c: int, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(is_assignable_to(CallableTypeOf[positional_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
|
||||
```
|
||||
|
||||
And, even if the parameters are unannotated.
|
||||
@@ -468,29 +468,12 @@ def keyword_only(*, a, b) -> None: ...
|
||||
def keyword_variadic(**kwargs) -> None: ...
|
||||
def mixed(a, /, b, *args, c, **kwargs) -> None: ...
|
||||
|
||||
static_assert(is_assignable_to(CallableTypeOf[positional_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None]))
|
||||
```
|
||||
|
||||
### Function types
|
||||
|
||||
```py
|
||||
from typing import Any, Callable
|
||||
|
||||
def f(x: Any) -> str:
|
||||
return ""
|
||||
|
||||
def g(x: Any) -> int:
|
||||
return 1
|
||||
|
||||
c: Callable[[Any], str] = f
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[g]` is not assignable to `(Any, /) -> str`"
|
||||
c: Callable[[Any], str] = g
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
|
||||
```
|
||||
|
||||
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
|
||||
@@ -61,7 +61,7 @@ static_assert(is_disjoint_from(B2, FinalSubclass))
|
||||
## Tuple types
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, Never
|
||||
from typing_extensions import Literal
|
||||
from knot_extensions import TypeOf, is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(tuple[()], TypeOf[object]))
|
||||
@@ -353,41 +353,3 @@ class UsesMeta2(metaclass=Meta2): ...
|
||||
|
||||
static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2]))
|
||||
```
|
||||
|
||||
## Callables
|
||||
|
||||
No two callable types are disjoint because there exists a non-empty callable type
|
||||
`(*args: object, **kwargs: object) -> Never` that is a subtype of all fully static callable types.
|
||||
As such, for any two callable types, it is possible to conceive of a runtime callable object that
|
||||
would inhabit both types simultaneously.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_disjoint_from, static_assert
|
||||
from typing_extensions import Callable, Literal, Never
|
||||
|
||||
def mixed(a: int, /, b: str, *args: int, c: int = 2, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(not is_disjoint_from(Callable[[], Never], CallableTypeOf[mixed]))
|
||||
static_assert(not is_disjoint_from(Callable[[int, str], float], CallableTypeOf[mixed]))
|
||||
|
||||
# Using gradual form
|
||||
static_assert(not is_disjoint_from(Callable[..., None], Callable[[], None]))
|
||||
static_assert(not is_disjoint_from(Callable[..., None], Callable[..., None]))
|
||||
static_assert(not is_disjoint_from(Callable[..., None], Callable[[Literal[1]], None]))
|
||||
|
||||
# Using `Never`
|
||||
static_assert(not is_disjoint_from(Callable[[], Never], Callable[[], Never]))
|
||||
static_assert(not is_disjoint_from(Callable[[Never], str], Callable[[Never], int]))
|
||||
```
|
||||
|
||||
A callable type is disjoint from all literal types.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_disjoint_from, static_assert
|
||||
from typing_extensions import Callable, Literal, Never
|
||||
|
||||
static_assert(is_disjoint_from(Callable[[], None], Literal[""]))
|
||||
static_assert(is_disjoint_from(Callable[[], None], Literal[b""]))
|
||||
static_assert(is_disjoint_from(Callable[[], None], Literal[1]))
|
||||
static_assert(is_disjoint_from(Callable[[], None], Literal[True]))
|
||||
```
|
||||
|
||||
@@ -127,14 +127,13 @@ the parameter in one of the callable has a default value then the corresponding
|
||||
other callable should also have a default value.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_equivalent_to, static_assert
|
||||
from typing import Callable
|
||||
|
||||
def f1(a: int = 1) -> None: ...
|
||||
def f2(a: int = 2) -> None: ...
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f1] | bool | CallableTypeOf[f2], CallableTypeOf[f2] | bool | CallableTypeOf[f1]))
|
||||
static_assert(is_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
|
||||
```
|
||||
|
||||
The names of the positional-only, variadic and keyword-variadic parameters does not need to be the
|
||||
@@ -144,8 +143,7 @@ same.
|
||||
def f3(a1: int, /, *args1: int, **kwargs2: int) -> None: ...
|
||||
def f4(a2: int, /, *args2: int, **kwargs1: int) -> None: ...
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3]))
|
||||
static_assert(is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
|
||||
```
|
||||
|
||||
Putting it all together, the following two callables are equivalent:
|
||||
@@ -154,8 +152,7 @@ Putting it all together, the following two callables are equivalent:
|
||||
def f5(a1: int, /, b: float, c: bool = False, *args1: int, d: int = 1, e: str, **kwargs1: float) -> None: ...
|
||||
def f6(a2: int, /, b: float, c: bool = True, *args2: int, d: int = 2, e: str, **kwargs2: float) -> None: ...
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f5] | bool | CallableTypeOf[f6], CallableTypeOf[f6] | bool | CallableTypeOf[f5]))
|
||||
static_assert(is_equivalent_to(CallableTypeFromFunction[f5], CallableTypeFromFunction[f6]))
|
||||
```
|
||||
|
||||
### Not equivalent
|
||||
@@ -163,7 +160,7 @@ static_assert(is_equivalent_to(CallableTypeOf[f5] | bool | CallableTypeOf[f6], C
|
||||
There are multiple cases when two callable types are not equivalent which are enumerated below.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_equivalent_to, static_assert
|
||||
from typing import Callable
|
||||
```
|
||||
|
||||
@@ -173,7 +170,7 @@ When the number of parameters is different:
|
||||
def f1(a: int) -> None: ...
|
||||
def f2(a: int, b: int) -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
|
||||
```
|
||||
|
||||
When either of the callable types uses a gradual form for the parameters:
|
||||
@@ -190,9 +187,9 @@ def f3(): ...
|
||||
def f4() -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f4], CallableTypeOf[f3]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f3]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f4], CallableTypeFromFunction[f3]))
|
||||
```
|
||||
|
||||
When the parameter names are different:
|
||||
@@ -201,13 +198,13 @@ When the parameter names are different:
|
||||
def f5(a: int) -> None: ...
|
||||
def f6(b: int) -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f5], CallableTypeFromFunction[f6]))
|
||||
```
|
||||
|
||||
When only one of the callable types has parameter names:
|
||||
|
||||
```py
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f5], Callable[[int], None]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f5], Callable[[int], None]))
|
||||
```
|
||||
|
||||
When the parameter kinds are different:
|
||||
@@ -216,7 +213,7 @@ When the parameter kinds are different:
|
||||
def f7(a: int, /) -> None: ...
|
||||
def f8(a: int) -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f7], CallableTypeOf[f8]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f7], CallableTypeFromFunction[f8]))
|
||||
```
|
||||
|
||||
When the annotated types of the parameters are not equivalent or absent in one or both of the
|
||||
@@ -227,10 +224,10 @@ def f9(a: int) -> None: ...
|
||||
def f10(a: str) -> None: ...
|
||||
def f11(a) -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f9], CallableTypeOf[f10]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f10], CallableTypeOf[f11]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f10]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f9], CallableTypeFromFunction[f10]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f10], CallableTypeFromFunction[f11]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f11], CallableTypeFromFunction[f10]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f11], CallableTypeFromFunction[f11]))
|
||||
```
|
||||
|
||||
When the default value for a parameter is present only in one of the callable type:
|
||||
@@ -239,19 +236,8 @@ When the default value for a parameter is present only in one of the callable ty
|
||||
def f12(a: int) -> None: ...
|
||||
def f13(a: int = 2) -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f12], CallableTypeOf[f13]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f13], CallableTypeOf[f12]))
|
||||
```
|
||||
|
||||
### Unions containing `Callable`s containing unions
|
||||
|
||||
Differently ordered unions inside `Callable`s inside unions can still be equivalent:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import is_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str | int], None] | int))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f12], CallableTypeFromFunction[f13]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f13], CallableTypeFromFunction[f12]))
|
||||
```
|
||||
|
||||
[the equivalence relation]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent
|
||||
|
||||
@@ -80,7 +80,7 @@ Using function literals, we can check more variations of callable types as it al
|
||||
parameters without annotations and no return type.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_fully_static, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_fully_static, static_assert
|
||||
|
||||
def f00() -> None: ...
|
||||
def f01(a: int, b: str) -> None: ...
|
||||
@@ -90,12 +90,12 @@ def f13(a, b: int): ...
|
||||
def f14(a, b: int) -> None: ...
|
||||
def f15(a, b) -> None: ...
|
||||
|
||||
static_assert(is_fully_static(CallableTypeOf[f00]))
|
||||
static_assert(is_fully_static(CallableTypeOf[f01]))
|
||||
static_assert(is_fully_static(CallableTypeFromFunction[f00]))
|
||||
static_assert(is_fully_static(CallableTypeFromFunction[f01]))
|
||||
|
||||
static_assert(not is_fully_static(CallableTypeOf[f11]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f12]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f13]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f14]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f15]))
|
||||
static_assert(not is_fully_static(CallableTypeFromFunction[f11]))
|
||||
static_assert(not is_fully_static(CallableTypeFromFunction[f12]))
|
||||
static_assert(not is_fully_static(CallableTypeFromFunction[f13]))
|
||||
static_assert(not is_fully_static(CallableTypeFromFunction[f14]))
|
||||
static_assert(not is_fully_static(CallableTypeFromFunction[f15]))
|
||||
```
|
||||
|
||||
@@ -73,7 +73,7 @@ gradual types. The cases with fully static types and using different combination
|
||||
are covered in the [equivalence tests](./is_equivalent_to.md#callable).
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown, CallableTypeOf, is_gradual_equivalent_to, static_assert
|
||||
from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Callable[..., int], Callable[..., int]))
|
||||
@@ -92,7 +92,7 @@ type of `Any`.
|
||||
def f1():
|
||||
return
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[f1], Callable[[], Any]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f1], Callable[[], Any]))
|
||||
```
|
||||
|
||||
And, similarly for parameters with no annotations.
|
||||
@@ -101,7 +101,7 @@ And, similarly for parameters with no annotations.
|
||||
def f2(a, b, /) -> None:
|
||||
return
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None]))
|
||||
```
|
||||
|
||||
Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs`
|
||||
@@ -115,8 +115,8 @@ def variadic_without_annotation(*args, **kwargs):
|
||||
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
|
||||
return
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_without_annotation], Callable[..., Any]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any]))
|
||||
```
|
||||
|
||||
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
|
||||
@@ -129,8 +129,8 @@ def variadic_args(*args):
|
||||
def variadic_kwargs(**kwargs):
|
||||
return
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_args], Callable[..., Any]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any]))
|
||||
```
|
||||
|
||||
Parameter names, default values, and it's kind should also be considered when checking for gradual
|
||||
@@ -140,21 +140,18 @@ equivalence.
|
||||
def f1(a): ...
|
||||
def f2(b): ...
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
|
||||
|
||||
def f3(a=1): ...
|
||||
def f4(a=2): ...
|
||||
def f5(a): ...
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
|
||||
static_assert(
|
||||
is_gradual_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3])
|
||||
)
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f5]))
|
||||
|
||||
def f6(a, /): ...
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f6]))
|
||||
```
|
||||
|
||||
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
A type is single-valued iff it is not empty and all inhabitants of it compare equal.
|
||||
|
||||
```py
|
||||
import types
|
||||
from typing_extensions import Any, Literal, LiteralString, Never, Callable
|
||||
from knot_extensions import is_single_valued, static_assert, TypeOf
|
||||
from knot_extensions import is_single_valued, static_assert
|
||||
|
||||
static_assert(is_single_valued(None))
|
||||
static_assert(is_single_valued(Literal[True]))
|
||||
@@ -26,11 +25,4 @@ static_assert(not is_single_valued(tuple[None, int]))
|
||||
|
||||
static_assert(not is_single_valued(Callable[..., None]))
|
||||
static_assert(not is_single_valued(Callable[[int, str], None]))
|
||||
|
||||
class A:
|
||||
def method(self): ...
|
||||
|
||||
static_assert(is_single_valued(TypeOf[A().method]))
|
||||
static_assert(is_single_valued(TypeOf[types.FunctionType.__get__]))
|
||||
static_assert(is_single_valued(TypeOf[A.method.__get__]))
|
||||
```
|
||||
|
||||
@@ -72,6 +72,7 @@ python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(Ellipsis.__class__))
|
||||
@@ -94,68 +95,3 @@ from knot_extensions import static_assert, is_singleton
|
||||
|
||||
static_assert(is_singleton(types.EllipsisType))
|
||||
```
|
||||
|
||||
## `builtins.NotImplemented` / `types.NotImplementedType`
|
||||
|
||||
### All Python versions
|
||||
|
||||
Just like `Ellipsis`, the type of `NotImplemented` was not exposed on Python \<3.10. However, we
|
||||
still recognize the type as a singleton in all Python versions.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(NotImplemented.__class__))
|
||||
```
|
||||
|
||||
### Python 3.10+
|
||||
|
||||
On Python 3.10+, the standard library exposes the type of `NotImplemented` as
|
||||
`types.NotImplementedType`. We also recognize this as a singleton type when it is referenced
|
||||
directly:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
import types
|
||||
from knot_extensions import static_assert, is_singleton
|
||||
|
||||
# TODO: types.NotImplementedType is a TypeAlias of builtins._NotImplementedType
|
||||
# Once TypeAlias support is added, it should satisfy `is_singleton`
|
||||
reveal_type(types.NotImplementedType) # revealed: Unknown | Literal[_NotImplementedType]
|
||||
static_assert(not is_singleton(types.NotImplementedType))
|
||||
```
|
||||
|
||||
### Callables
|
||||
|
||||
We currently treat the type of `types.FunctionType.__get__` as a singleton type that has its own
|
||||
dedicated variant in the `Type` enum. That variant should be understood as a singleton type, but the
|
||||
similar variants `Type::BoundMethod` and `Type::MethodWrapperDunderGet` should not be; nor should
|
||||
`Type::Callable` types.
|
||||
|
||||
If we refactor `Type` in the future to get rid of some or all of these `Type` variants, the
|
||||
assertion that the type of `types.FunctionType.__get__` is a singleton type does not necessarily
|
||||
have to hold true; it's more of a unit test for our current implementation.
|
||||
|
||||
```py
|
||||
import types
|
||||
from typing import Callable
|
||||
from knot_extensions import static_assert, is_singleton, TypeOf
|
||||
|
||||
class A:
|
||||
def method(self): ...
|
||||
|
||||
static_assert(is_singleton(TypeOf[types.FunctionType.__get__]))
|
||||
|
||||
static_assert(not is_singleton(Callable[[], None]))
|
||||
static_assert(not is_singleton(TypeOf[A().method]))
|
||||
static_assert(not is_singleton(TypeOf[A.method.__get__]))
|
||||
```
|
||||
|
||||
@@ -530,13 +530,13 @@ Parameter types are contravariant.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert, TypeOf
|
||||
|
||||
def float_param(a: float, /) -> None: ...
|
||||
def int_param(a: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[float_param], CallableTypeOf[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[float_param]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[int_param], Callable[[int], None]))
|
||||
static_assert(is_subtype_of(TypeOf[float_param], Callable[[float], None]))
|
||||
@@ -550,8 +550,8 @@ Parameter name is not required to be the same for positional-only parameters at
|
||||
```py
|
||||
def int_param_different_name(b: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[int_param_different_name]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_param_different_name], CallableTypeOf[int_param]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[int_param_different_name]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_param_different_name], CallableTypeFromFunction[int_param]))
|
||||
```
|
||||
|
||||
Multiple positional-only parameters are checked in order:
|
||||
@@ -560,8 +560,8 @@ Multiple positional-only parameters are checked in order:
|
||||
def multi_param1(a: float, b: int, c: str, /) -> None: ...
|
||||
def multi_param2(b: int, c: bool, a: str, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_param1], CallableTypeOf[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[multi_param2], CallableTypeOf[multi_param1]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[multi_param1], Callable[[float, int, str], None]))
|
||||
|
||||
@@ -575,17 +575,17 @@ corresponding position in the supertype does not need to have a default value.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert, TypeOf
|
||||
|
||||
def float_with_default(a: float = 1, /) -> None: ...
|
||||
def int_with_default(a: int = 1, /) -> None: ...
|
||||
def int_without_default(a: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_without_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_without_default], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_without_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[int_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[int_with_default], Callable[[int], None]))
|
||||
static_assert(is_subtype_of(TypeOf[int_with_default], Callable[[], None]))
|
||||
@@ -600,9 +600,9 @@ As the parameter itself is optional, it can be omitted in the supertype:
|
||||
```py
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_without_default], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
|
||||
```
|
||||
|
||||
The subtype can include any number of positional-only parameters as long as they have the default
|
||||
@@ -611,8 +611,8 @@ value:
|
||||
```py
|
||||
def multi_param(a: float = 1, b: int = 2, c: str = "3", /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_param], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[multi_param]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[multi_param]))
|
||||
```
|
||||
|
||||
#### Positional-only with other kinds
|
||||
@@ -621,7 +621,7 @@ If a parameter is declared as positional-only, then the corresponding parameter
|
||||
cannot be any other parameter kind.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def positional_only(a: int, /) -> None: ...
|
||||
def standard(a: int) -> None: ...
|
||||
@@ -629,10 +629,10 @@ def keyword_only(*, a: int) -> None: ...
|
||||
def variadic(*a: int) -> None: ...
|
||||
def keyword_variadic(**a: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[standard]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[keyword_only]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[keyword_variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[standard]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[keyword_only]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
#### Standard
|
||||
@@ -642,13 +642,13 @@ A standard parameter is either a positional or a keyword parameter.
|
||||
Unlike positional-only parameters, standard parameters should have the same name in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def int_param_a(a: int) -> None: ...
|
||||
def int_param_b(b: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_param_a], CallableTypeOf[int_param_b]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_param_b], CallableTypeOf[int_param_a]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param_a], CallableTypeFromFunction[int_param_b]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param_b], CallableTypeFromFunction[int_param_a]))
|
||||
```
|
||||
|
||||
Apart from the name, it behaves the same as positional-only parameters.
|
||||
@@ -657,8 +657,8 @@ Apart from the name, it behaves the same as positional-only parameters.
|
||||
def float_param(a: float) -> None: ...
|
||||
def int_param(a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[float_param], CallableTypeOf[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[float_param]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))
|
||||
```
|
||||
|
||||
With the same rules for default values as well.
|
||||
@@ -668,14 +668,14 @@ def float_with_default(a: float = 1) -> None: ...
|
||||
def int_with_default(a: int = 1) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[int_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
|
||||
```
|
||||
|
||||
Multiple standard parameters are checked in order along with their names:
|
||||
@@ -684,8 +684,8 @@ Multiple standard parameters are checked in order along with their names:
|
||||
def multi_param1(a: float, b: int, c: str) -> None: ...
|
||||
def multi_param2(a: int, b: bool, c: str) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_param1], CallableTypeOf[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[multi_param2], CallableTypeOf[multi_param1]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
|
||||
```
|
||||
|
||||
The subtype can include as many standard parameters as long as they have the default value:
|
||||
@@ -693,8 +693,8 @@ The subtype can include as many standard parameters as long as they have the def
|
||||
```py
|
||||
def multi_param_default(a: float = 1, b: int = 2, c: str = "s") -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_param_default], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[multi_param_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[multi_param_default]))
|
||||
```
|
||||
|
||||
#### Standard with keyword-only
|
||||
@@ -704,26 +704,26 @@ parameter in the subtype with the same name. This is because a standard paramete
|
||||
than a keyword-only parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def standard_a(a: int) -> None: ...
|
||||
def keyword_b(*, b: int) -> None: ...
|
||||
|
||||
# The name of the parameters are different
|
||||
static_assert(not is_subtype_of(CallableTypeOf[standard_a], CallableTypeOf[keyword_b]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[keyword_b]))
|
||||
|
||||
def standard_float(a: float) -> None: ...
|
||||
def keyword_int(*, a: int) -> None: ...
|
||||
|
||||
# Here, the name of the parameters are the same
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_float], CallableTypeOf[keyword_int]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_float], CallableTypeFromFunction[keyword_int]))
|
||||
|
||||
def standard_with_default(a: int = 1) -> None: ...
|
||||
def keyword_with_default(*, a: int = 1) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[keyword_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[empty]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[keyword_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[empty]))
|
||||
```
|
||||
|
||||
The position of the keyword-only parameters does not matter:
|
||||
@@ -732,7 +732,7 @@ The position of the keyword-only parameters does not matter:
|
||||
def multi_standard(a: float, b: int, c: str) -> None: ...
|
||||
def multi_keyword(*, b: bool, c: str, a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_keyword]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_keyword]))
|
||||
```
|
||||
|
||||
#### Standard with positional-only
|
||||
@@ -742,25 +742,25 @@ parameter in the subtype at the same position. This is because a standard parame
|
||||
than a positional-only parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def standard_a(a: int) -> None: ...
|
||||
def positional_b(b: int, /) -> None: ...
|
||||
|
||||
# The names are not important in this context
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_a], CallableTypeOf[positional_b]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[positional_b]))
|
||||
|
||||
def standard_float(a: float) -> None: ...
|
||||
def positional_int(a: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_float], CallableTypeOf[positional_int]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_float], CallableTypeFromFunction[positional_int]))
|
||||
|
||||
def standard_with_default(a: int = 1) -> None: ...
|
||||
def positional_with_default(a: int = 1, /) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[positional_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[empty]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[positional_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[empty]))
|
||||
```
|
||||
|
||||
The position of the positional-only parameters matter:
|
||||
@@ -772,8 +772,8 @@ def multi_positional1(b: int, c: bool, a: str, /) -> None: ...
|
||||
# Here, the type of the parameter `a` makes the subtype relation invalid
|
||||
def multi_positional2(b: int, a: float, c: str, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_positional1]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_positional2]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_positional1]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_positional2]))
|
||||
```
|
||||
|
||||
#### Standard with variadic
|
||||
@@ -782,14 +782,14 @@ A variadic or keyword-variadic parameter in the supertype cannot be substituted
|
||||
parameter in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def standard(a: int) -> None: ...
|
||||
def variadic(*a: int) -> None: ...
|
||||
def keyword_variadic(**a: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keyword_variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
#### Variadic
|
||||
@@ -797,13 +797,13 @@ static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keyword
|
||||
The name of the variadic parameter does not need to be the same in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def variadic_float(*args2: float) -> None: ...
|
||||
def variadic_int(*args1: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic_float], CallableTypeOf[variadic_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic_int], CallableTypeOf[variadic_float]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic_float], CallableTypeFromFunction[variadic_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic_int], CallableTypeFromFunction[variadic_float]))
|
||||
```
|
||||
|
||||
The variadic parameter does not need to be present in the supertype:
|
||||
@@ -811,8 +811,8 @@ The variadic parameter does not need to be present in the supertype:
|
||||
```py
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic_int], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[variadic_int]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic_int], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[variadic_int]))
|
||||
```
|
||||
|
||||
#### Variadic with positional-only
|
||||
@@ -821,7 +821,7 @@ If the subtype has a variadic parameter then any unmatched positional-only param
|
||||
supertype should be checked against the variadic parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def variadic(a: int, /, *args: float) -> None: ...
|
||||
|
||||
@@ -831,8 +831,8 @@ def positional_only(a: int, b: float, c: int, /) -> None: ...
|
||||
# Here, the parameter `b` is unmatched and there's also a variadic parameter
|
||||
def positional_variadic(a: int, b: float, /, *args: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[positional_only]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[positional_variadic]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[positional_only]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[positional_variadic]))
|
||||
```
|
||||
|
||||
#### Variadic with other kinds
|
||||
@@ -841,7 +841,7 @@ Variadic parameter in a subtype can only be used to match against an unmatched p
|
||||
parameters from the supertype, not any other parameter kind.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def variadic(*args: int) -> None: ...
|
||||
|
||||
@@ -853,55 +853,9 @@ def standard(a: int, b: float, /, c: int) -> None: ...
|
||||
def keyword_only(a: int, /, *, b: int) -> None: ...
|
||||
def keyword_variadic(a: int, /, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[standard]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_only]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_variadic]))
|
||||
```
|
||||
|
||||
But, there are special cases when matching against standard parameters. This is due to the fact that
|
||||
a standard parameter can be passed as a positional or keyword parameter. This means that the
|
||||
subtyping relation needs to consider both cases.
|
||||
|
||||
```py
|
||||
def variadic_keyword(*args: int, **kwargs: int) -> None: ...
|
||||
def standard_int(a: int) -> None: ...
|
||||
def standard_float(a: float) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_float]))
|
||||
```
|
||||
|
||||
If the type of either the variadic or keyword-variadic parameter is not a supertype of the standard
|
||||
parameter, then the subtyping relation is invalid.
|
||||
|
||||
```py
|
||||
def variadic_bool(*args: bool, **kwargs: int) -> None: ...
|
||||
def keyword_variadic_bool(*args: int, **kwargs: bool) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic_bool], CallableTypeOf[standard_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keyword_variadic_bool], CallableTypeOf[standard_int]))
|
||||
```
|
||||
|
||||
The standard parameter can follow a variadic parameter in the subtype.
|
||||
|
||||
```py
|
||||
def standard_variadic_int(a: int, *args: int) -> None: ...
|
||||
def standard_variadic_float(a: int, *args: float) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_float]))
|
||||
```
|
||||
|
||||
The keyword part of the standard parameter can be matched against keyword-only parameter with the
|
||||
same name if the keyword-variadic parameter is absent.
|
||||
|
||||
```py
|
||||
def variadic_a(*args: int, a: int) -> None: ...
|
||||
def variadic_b(*args: int, b: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic_a], CallableTypeOf[standard_int]))
|
||||
# The parameter name is different
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[standard_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[standard]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[keyword_only]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
#### Keyword-only
|
||||
@@ -909,15 +863,15 @@ static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[stand
|
||||
For keyword-only parameters, the name should be the same:
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def keyword_int(*, a: int) -> None: ...
|
||||
def keyword_float(*, a: float) -> None: ...
|
||||
def keyword_b(*, b: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[keyword_float], CallableTypeOf[keyword_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keyword_int], CallableTypeOf[keyword_float]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keyword_int], CallableTypeOf[keyword_b]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[keyword_float], CallableTypeFromFunction[keyword_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_int], CallableTypeFromFunction[keyword_float]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_int], CallableTypeFromFunction[keyword_b]))
|
||||
```
|
||||
|
||||
But, the order of the keyword-only parameters is not required to be the same:
|
||||
@@ -926,28 +880,28 @@ But, the order of the keyword-only parameters is not required to be the same:
|
||||
def keyword_ab(*, a: float, b: float) -> None: ...
|
||||
def keyword_ba(*, b: int, a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[keyword_ab], CallableTypeOf[keyword_ba]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keyword_ba], CallableTypeOf[keyword_ab]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[keyword_ab], CallableTypeFromFunction[keyword_ba]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_ba], CallableTypeFromFunction[keyword_ab]))
|
||||
```
|
||||
|
||||
#### Keyword-only with default
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def float_with_default(*, a: float = 1) -> None: ...
|
||||
def int_with_default(*, a: int = 1) -> None: ...
|
||||
def int_keyword(*, a: int) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_keyword]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_keyword]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_keyword], CallableTypeFromFunction[int_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
|
||||
```
|
||||
|
||||
Keyword-only parameters with default values can be mixed with the ones without default values in any
|
||||
@@ -957,20 +911,20 @@ order:
|
||||
# A keyword-only parameter with a default value follows the one without a default value (it's valid)
|
||||
def mixed(*, b: int = 1, a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[int_keyword]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[mixed]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[int_keyword]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_keyword], CallableTypeFromFunction[mixed]))
|
||||
```
|
||||
|
||||
#### Keyword-only with standard
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def keywords1(*, a: int, b: int) -> None: ...
|
||||
def standard(b: float, a: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keywords1], CallableTypeOf[standard]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keywords1]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keywords1], CallableTypeFromFunction[standard]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keywords1]))
|
||||
```
|
||||
|
||||
The subtype can include additional standard parameters as long as it has the default value:
|
||||
@@ -979,8 +933,8 @@ The subtype can include additional standard parameters as long as it has the def
|
||||
def standard_with_default(b: float, a: float, c: float = 1) -> None: ...
|
||||
def standard_without_default(b: float, a: float, c: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[standard_without_default], CallableTypeOf[keywords1]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[keywords1]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_without_default], CallableTypeFromFunction[keywords1]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[keywords1]))
|
||||
```
|
||||
|
||||
Here, we mix keyword-only parameters with standard parameters:
|
||||
@@ -989,8 +943,8 @@ Here, we mix keyword-only parameters with standard parameters:
|
||||
def keywords2(*, a: int, c: int, b: int) -> None: ...
|
||||
def mixed(b: float, a: float, *, c: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keywords2], CallableTypeOf[mixed]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[keywords2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keywords2], CallableTypeFromFunction[mixed]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[keywords2]))
|
||||
```
|
||||
|
||||
But, we shouldn't consider any unmatched positional-only parameters:
|
||||
@@ -998,7 +952,7 @@ But, we shouldn't consider any unmatched positional-only parameters:
|
||||
```py
|
||||
def mixed_positional(b: float, /, a: float, *, c: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[mixed_positional], CallableTypeOf[keywords2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[mixed_positional], CallableTypeFromFunction[keywords2]))
|
||||
```
|
||||
|
||||
But, an unmatched variadic parameter is still valid:
|
||||
@@ -1006,7 +960,7 @@ But, an unmatched variadic parameter is still valid:
|
||||
```py
|
||||
def mixed_variadic(*args: float, a: float, b: float, c: float, **kwargs: float) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[mixed_variadic], CallableTypeOf[keywords2]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed_variadic], CallableTypeFromFunction[keywords2]))
|
||||
```
|
||||
|
||||
#### Keyword-variadic
|
||||
@@ -1014,13 +968,13 @@ static_assert(is_subtype_of(CallableTypeOf[mixed_variadic], CallableTypeOf[keywo
|
||||
The name of the keyword-variadic parameter does not need to be the same in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def kwargs_float(**kwargs2: float) -> None: ...
|
||||
def kwargs_int(**kwargs1: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[kwargs_float], CallableTypeOf[kwargs_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[kwargs_int], CallableTypeOf[kwargs_float]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs_float], CallableTypeFromFunction[kwargs_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs_int], CallableTypeFromFunction[kwargs_float]))
|
||||
```
|
||||
|
||||
A variadic parameter can be omitted in the subtype:
|
||||
@@ -1028,8 +982,8 @@ A variadic parameter can be omitted in the subtype:
|
||||
```py
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[kwargs_int], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[kwargs_int]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs_int], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[kwargs_int]))
|
||||
```
|
||||
|
||||
#### Keyword-variadic with keyword-only
|
||||
@@ -1038,14 +992,14 @@ If the subtype has a keyword-variadic parameter then any unmatched keyword-only
|
||||
supertype should be checked against the keyword-variadic parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def kwargs(**kwargs: float) -> None: ...
|
||||
def keyword_only(*, a: int, b: float, c: bool) -> None: ...
|
||||
def keyword_variadic(*, a: int, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[keyword_only]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[keyword_variadic]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[keyword_only]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
This is valid only for keyword-only parameters, not any other parameter kind:
|
||||
@@ -1056,8 +1010,8 @@ def mixed1(a: int, *, b: int) -> None: ...
|
||||
# Same as above but with the default value
|
||||
def mixed2(a: int = 1, *, b: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[mixed1]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[mixed2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed1]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed2]))
|
||||
```
|
||||
|
||||
#### Empty
|
||||
@@ -1066,86 +1020,13 @@ When the supertype has an empty list of parameters, then the subtype can have an
|
||||
as long as they contain the default values for non-variadic parameters.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def empty() -> None: ...
|
||||
def mixed(a: int = 1, /, b: int = 2, *args: int, c: int = 3, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[mixed]))
|
||||
```
|
||||
|
||||
#### Object
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf
|
||||
from typing import Callable
|
||||
|
||||
def f1(a: int, b: str, /, *c: float, d: int = 1, **e: float) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[f1], object))
|
||||
static_assert(not is_subtype_of(object, CallableTypeOf[f1]))
|
||||
|
||||
def _(
|
||||
f3: Callable[[int, str], None],
|
||||
) -> None:
|
||||
static_assert(is_subtype_of(TypeOf[f3], object))
|
||||
static_assert(not is_subtype_of(object, TypeOf[f3]))
|
||||
|
||||
class C:
|
||||
def foo(self) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[C.foo], object))
|
||||
static_assert(not is_subtype_of(object, TypeOf[C.foo]))
|
||||
```
|
||||
|
||||
### Classes with `__call__`
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to
|
||||
|
||||
class A:
|
||||
def __call__(self, a: int) -> int:
|
||||
return a
|
||||
|
||||
a = A()
|
||||
|
||||
static_assert(is_subtype_of(A, Callable[[int], int]))
|
||||
static_assert(not is_subtype_of(A, Callable[[], int]))
|
||||
static_assert(not is_subtype_of(Callable[[int], int], A))
|
||||
|
||||
def f(fn: Callable[[int], int]) -> None: ...
|
||||
|
||||
f(a)
|
||||
```
|
||||
|
||||
### Bound methods
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import TypeOf, static_assert, is_subtype_of
|
||||
|
||||
class A:
|
||||
def f(self, a: int) -> int:
|
||||
return a
|
||||
|
||||
@classmethod
|
||||
def g(cls, a: int) -> int:
|
||||
return a
|
||||
|
||||
a = A()
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[a.f], Callable[[int], int]))
|
||||
static_assert(is_subtype_of(TypeOf[a.g], Callable[[int], int]))
|
||||
static_assert(is_subtype_of(TypeOf[A.g], Callable[[int], int]))
|
||||
|
||||
static_assert(not is_subtype_of(TypeOf[a.f], Callable[[float], int]))
|
||||
static_assert(not is_subtype_of(TypeOf[A.g], Callable[[], int]))
|
||||
|
||||
# TODO: This assertion should be true
|
||||
# error: [static-assert-error] "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(is_subtype_of(TypeOf[A.f], Callable[[A, int], int]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[mixed]))
|
||||
```
|
||||
|
||||
[special case for float and complex]: https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||
|
||||
@@ -120,26 +120,3 @@ static_assert(is_subtype_of(typing.TypeAliasType, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(types.MethodWrapperType, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(types.WrapperDescriptorType, AlwaysTruthy))
|
||||
```
|
||||
|
||||
### `Callable` types always have ambiguous truthiness
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def f(x: Callable, y: Callable[[int], str]):
|
||||
reveal_type(bool(x)) # revealed: bool
|
||||
reveal_type(bool(y)) # revealed: bool
|
||||
```
|
||||
|
||||
But certain callable single-valued types are known to be always truthy:
|
||||
|
||||
```py
|
||||
from types import FunctionType
|
||||
|
||||
class A:
|
||||
def method(self): ...
|
||||
|
||||
reveal_type(bool(A().method)) # revealed: Literal[True]
|
||||
reveal_type(bool(f.__get__)) # revealed: Literal[True]
|
||||
reveal_type(bool(FunctionType.__get__)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -19,11 +19,6 @@ 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]))
|
||||
|
||||
def _(x: tuple[Never], y: tuple[int, Never], z: tuple[Never, int]):
|
||||
reveal_type(x) # revealed: Never
|
||||
reveal_type(y) # revealed: Never
|
||||
reveal_type(z) # revealed: Never
|
||||
```
|
||||
|
||||
The empty `tuple` is *not* equivalent to `Never`!
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
# Unreachable code
|
||||
|
||||
## Detecting unreachable code
|
||||
|
||||
In this section, we look at various scenarios how sections of code can become unreachable. We should
|
||||
eventually introduce a new diagnostic that would detect unreachable code.
|
||||
|
||||
### Terminal statements
|
||||
|
||||
In the following examples, the `print` statements are definitely unreachable.
|
||||
|
||||
```py
|
||||
def f1():
|
||||
return
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
|
||||
def f2():
|
||||
raise Exception()
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
|
||||
def f3():
|
||||
while True:
|
||||
break
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
|
||||
def f4():
|
||||
for _ in range(10):
|
||||
continue
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
```
|
||||
|
||||
### Infinite loops
|
||||
|
||||
```py
|
||||
def f1():
|
||||
while True:
|
||||
pass
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
```
|
||||
|
||||
### Statically known branches
|
||||
|
||||
In the following examples, the `print` statements are also unreachable, but it requires type
|
||||
inference to determine that:
|
||||
|
||||
```py
|
||||
def f1():
|
||||
if 2 + 3 > 10:
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
|
||||
def f2():
|
||||
if True:
|
||||
return
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
```
|
||||
|
||||
### `Never` / `NoReturn`
|
||||
|
||||
If a function is annotated with a return type of `Never` or `NoReturn`, we can consider all code
|
||||
after the call to that function unreachable.
|
||||
|
||||
```py
|
||||
from typing_extensions import NoReturn
|
||||
|
||||
def always_raises() -> NoReturn:
|
||||
raise Exception()
|
||||
|
||||
def f():
|
||||
always_raises()
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
```
|
||||
|
||||
## Python version and platform checks
|
||||
|
||||
It is common to have code that is specific to a certain Python version or platform. This case is
|
||||
special because whether or not the code is reachable depends on externally configured constants. And
|
||||
if we are checking for a set of parameters that makes one of these branches unreachable, that is
|
||||
likely not something that the user wants to be warned about, because there are probably other sets
|
||||
of parameters that make the branch reachable.
|
||||
|
||||
### `sys.version_info` branches
|
||||
|
||||
Consider the following example. If we check with a Python version lower than 3.11, the import
|
||||
statement is unreachable. If we check with a Python version equal to or greater than 3.11, the
|
||||
import statement is definitely reachable. We should not emit any diagnostics in either case.
|
||||
|
||||
#### Checking with Python version 3.10
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
# TODO: we should not emit an error here
|
||||
# error: [unresolved-import]
|
||||
from typing import Self
|
||||
```
|
||||
|
||||
#### Checking with Python version 3.12
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self
|
||||
```
|
||||
|
||||
### `sys.platform` branches
|
||||
|
||||
The problem is even more pronounced with `sys.platform` branches, since we don't necessarily have
|
||||
the platform information available.
|
||||
|
||||
#### Checking with platform `win32`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-platform = "win32"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.platform == "win32":
|
||||
sys.getwindowsversion()
|
||||
```
|
||||
|
||||
#### Checking with platform `linux`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-platform = "linux"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.platform == "win32":
|
||||
# TODO: we should not emit an error here
|
||||
# error: [unresolved-attribute]
|
||||
sys.getwindowsversion()
|
||||
```
|
||||
|
||||
#### Checking without a specified platform
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
# python-platform not specified
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.platform == "win32":
|
||||
# TODO: we should not emit an error here
|
||||
# error: [possibly-unbound-attribute]
|
||||
sys.getwindowsversion()
|
||||
```
|
||||
|
||||
#### Checking with platform set to `all`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-platform = "all"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.platform == "win32":
|
||||
# TODO: we should not emit an error here
|
||||
# error: [possibly-unbound-attribute]
|
||||
sys.getwindowsversion()
|
||||
```
|
||||
|
||||
## No false positive diagnostics in unreachable code
|
||||
|
||||
In this section, we make sure that we do not emit false positive diagnostics in unreachable code.
|
||||
|
||||
### Use of variables in unreachable code
|
||||
|
||||
We should not emit any diagnostics for uses of symbols in unreachable code:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
return
|
||||
|
||||
print("unreachable")
|
||||
|
||||
# TODO: we should not emit an error here; we currently do, since there is no control flow path from this
|
||||
# use of 'x' to any definition of 'x'.
|
||||
# error: [unresolved-reference]
|
||||
print(x)
|
||||
```
|
||||
|
||||
### Use of variable in nested function
|
||||
|
||||
In the example below, since we use `x` in the `inner` function, we use the "public" type of `x`,
|
||||
which currently refers to the end-of-scope type of `x`. Since the end of the `outer` scope is
|
||||
unreachable, we treat `x` as if it was not defined. This behavior can certainly be improved.
|
||||
|
||||
```py
|
||||
def outer():
|
||||
x = 1
|
||||
|
||||
def inner():
|
||||
# TODO: we should not emit an error here
|
||||
# error: [unresolved-reference]
|
||||
return x # Name `x` used when not defined
|
||||
while True:
|
||||
pass
|
||||
```
|
||||
|
||||
## No diagnostics in unreachable code
|
||||
|
||||
In general, no diagnostics should be emitted in unreachable code. The reasoning is that any issues
|
||||
inside the unreachable section would not cause problems at runtime. And type checking the
|
||||
unreachable code under the assumption that it *is* reachable might lead to false positives:
|
||||
|
||||
```py
|
||||
FEATURE_X_ACTIVATED = False
|
||||
|
||||
if FEATURE_X_ACTIVATED:
|
||||
def feature_x():
|
||||
print("Performing 'X'")
|
||||
|
||||
def f():
|
||||
if FEATURE_X_ACTIVATED:
|
||||
# Type checking this particular section as if it were reachable would
|
||||
# lead to a false positive, so we should not emit diagnostics here.
|
||||
|
||||
# TODO: no error should be emitted here
|
||||
# error: [unresolved-reference]
|
||||
feature_x()
|
||||
```
|
||||
@@ -104,7 +104,6 @@ impl ModuleKind {
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum KnownModule {
|
||||
Builtins,
|
||||
Enum,
|
||||
Types,
|
||||
#[strum(serialize = "_typeshed")]
|
||||
Typeshed,
|
||||
@@ -122,7 +121,6 @@ impl KnownModule {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Builtins => "builtins",
|
||||
Self::Enum => "enum",
|
||||
Self::Types => "types",
|
||||
Self::Typing => "typing",
|
||||
Self::Typeshed => "_typeshed",
|
||||
@@ -166,10 +164,6 @@ impl KnownModule {
|
||||
pub const fn is_inspect(self) -> bool {
|
||||
matches!(self, Self::Inspect)
|
||||
}
|
||||
|
||||
pub const fn is_enum(self) -> bool {
|
||||
matches!(self, Self::Enum)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KnownModule {
|
||||
|
||||
@@ -11,7 +11,7 @@ use ruff_python_ast::PythonVersion;
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions};
|
||||
use crate::site_packages::{SitePackagesDiscoveryError, SysPrefixPathOrigin, VirtualEnvironment};
|
||||
use crate::site_packages::VirtualEnvironment;
|
||||
use crate::{Program, PythonPath, SearchPathSettings};
|
||||
|
||||
use super::module::{Module, ModuleKind};
|
||||
@@ -133,15 +133,6 @@ pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
|
||||
Program::get(db).search_paths(db).iter(db)
|
||||
}
|
||||
|
||||
/// Searches for a `.venv` directory in `project_root` that contains a `pyvenv.cfg` file.
|
||||
fn discover_venv_in(system: &dyn System, project_root: &SystemPath) -> Option<SystemPathBuf> {
|
||||
let virtual_env_directory = project_root.join(".venv");
|
||||
|
||||
system
|
||||
.is_file(&virtual_env_directory.join("pyvenv.cfg"))
|
||||
.then_some(virtual_env_directory)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SearchPaths {
|
||||
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
|
||||
@@ -244,37 +235,6 @@ impl SearchPaths {
|
||||
.and_then(|venv| venv.site_packages_directories(system))?
|
||||
}
|
||||
|
||||
PythonPath::Discover(root) => {
|
||||
tracing::debug!("Discovering virtual environment in `{root}`");
|
||||
let virtual_env_path = discover_venv_in(db.system(), root);
|
||||
if let Some(virtual_env_path) = virtual_env_path {
|
||||
tracing::debug!("Found `.venv` folder at `{}`", virtual_env_path);
|
||||
|
||||
let handle_invalid_virtual_env = |error: SitePackagesDiscoveryError| {
|
||||
tracing::debug!(
|
||||
"Ignoring automatically detected virtual environment at `{}`: {}",
|
||||
virtual_env_path,
|
||||
error
|
||||
);
|
||||
vec![]
|
||||
};
|
||||
|
||||
match VirtualEnvironment::new(
|
||||
virtual_env_path.clone(),
|
||||
SysPrefixPathOrigin::LocalVenv,
|
||||
system,
|
||||
) {
|
||||
Ok(venv) => venv
|
||||
.site_packages_directories(system)
|
||||
.unwrap_or_else(handle_invalid_virtual_env),
|
||||
Err(error) => handle_invalid_virtual_env(error),
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("No virtual environment found");
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
PythonPath::KnownSitePackages(paths) => paths
|
||||
.iter()
|
||||
.map(|path| canonicalize(path, system))
|
||||
|
||||
@@ -145,9 +145,6 @@ pub enum PythonPath {
|
||||
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
|
||||
SysPrefix(SystemPathBuf, SysPrefixPathOrigin),
|
||||
|
||||
/// Tries to discover a virtual environment in the given path.
|
||||
Discover(SystemPathBuf),
|
||||
|
||||
/// Resolved site packages paths.
|
||||
///
|
||||
/// This variant is mainly intended for testing where we want to skip resolving `site-packages`
|
||||
|
||||
@@ -124,12 +124,6 @@ pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
|
||||
FileScopeId::global().to_scope_id(db, file)
|
||||
}
|
||||
|
||||
pub(crate) enum EagerBindingsResult<'map, 'db> {
|
||||
Found(BindingWithConstraintsIterator<'map, 'db>),
|
||||
NotFound,
|
||||
NoLongerInEagerContext,
|
||||
}
|
||||
|
||||
/// The symbol tables and use-def maps for all scopes in a file.
|
||||
#[derive(Debug, Update)]
|
||||
pub(crate) struct SemanticIndex<'db> {
|
||||
@@ -325,40 +319,21 @@ impl<'db> SemanticIndex<'db> {
|
||||
self.has_future_annotations
|
||||
}
|
||||
|
||||
/// Returns
|
||||
/// * `NoLongerInEagerContext` if the nested scope is no longer in an eager context
|
||||
/// (that is, not every scope that will be traversed is eager).
|
||||
/// * an iterator of bindings for a particular nested eager scope reference if the bindings exist.
|
||||
/// * `NotFound` if the bindings do not exist in the nested eager scope.
|
||||
/// Returns an iterator of bindings for a particular nested eager scope reference.
|
||||
pub(crate) fn eager_bindings(
|
||||
&self,
|
||||
enclosing_scope: FileScopeId,
|
||||
symbol: &str,
|
||||
nested_scope: FileScopeId,
|
||||
) -> EagerBindingsResult<'_, 'db> {
|
||||
for (ancestor_scope_id, ancestor_scope) in self.ancestor_scopes(nested_scope) {
|
||||
if ancestor_scope_id == enclosing_scope {
|
||||
break;
|
||||
}
|
||||
if !ancestor_scope.is_eager() {
|
||||
return EagerBindingsResult::NoLongerInEagerContext;
|
||||
}
|
||||
}
|
||||
let Some(symbol_id) = self.symbol_tables[enclosing_scope].symbol_id_by_name(symbol) else {
|
||||
return EagerBindingsResult::NotFound;
|
||||
};
|
||||
) -> Option<BindingWithConstraintsIterator<'_, 'db>> {
|
||||
let symbol_id = self.symbol_tables[enclosing_scope].symbol_id_by_name(symbol)?;
|
||||
let key = EagerBindingsKey {
|
||||
enclosing_scope,
|
||||
enclosing_symbol: symbol_id,
|
||||
nested_scope,
|
||||
};
|
||||
let Some(id) = self.eager_bindings.get(&key) else {
|
||||
return EagerBindingsResult::NotFound;
|
||||
};
|
||||
match self.use_def_maps[enclosing_scope].eager_bindings(*id) {
|
||||
Some(bindings) => EagerBindingsResult::Found(bindings),
|
||||
None => EagerBindingsResult::NotFound,
|
||||
}
|
||||
let id = self.eager_bindings.get(&key)?;
|
||||
self.use_def_maps[enclosing_scope].eager_bindings(*id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ use crate::semantic_index::visibility_constraints::{
|
||||
ScopedVisibilityConstraintId, VisibilityConstraintsBuilder,
|
||||
};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue};
|
||||
use crate::unpack::{Unpack, UnpackValue};
|
||||
use crate::Db;
|
||||
|
||||
mod except_handlers;
|
||||
@@ -256,11 +256,11 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
}
|
||||
|
||||
for nested_symbol in self.symbol_tables[popped_scope_id].symbols() {
|
||||
// Skip this symbol if this enclosing scope doesn't contain any bindings for it.
|
||||
// Note that even if this symbol is bound in the popped scope,
|
||||
// it may refer to the enclosing scope bindings
|
||||
// so we also need to snapshot the bindings of the enclosing scope.
|
||||
|
||||
// Skip this symbol if this enclosing scope doesn't contain any bindings for
|
||||
// it, or if the nested scope _does_.
|
||||
if nested_symbol.is_bound() {
|
||||
continue;
|
||||
}
|
||||
let Some(enclosing_symbol_id) =
|
||||
enclosing_symbol_table.symbol_id_by_name(nested_symbol.name())
|
||||
else {
|
||||
@@ -589,31 +589,6 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn predicate_kind(&mut self, pattern: &ast::Pattern) -> PatternPredicateKind<'db> {
|
||||
match pattern {
|
||||
ast::Pattern::MatchValue(pattern) => {
|
||||
let value = self.add_standalone_expression(&pattern.value);
|
||||
PatternPredicateKind::Value(value)
|
||||
}
|
||||
ast::Pattern::MatchSingleton(singleton) => {
|
||||
PatternPredicateKind::Singleton(singleton.value)
|
||||
}
|
||||
ast::Pattern::MatchClass(pattern) => {
|
||||
let cls = self.add_standalone_expression(&pattern.cls);
|
||||
PatternPredicateKind::Class(cls)
|
||||
}
|
||||
ast::Pattern::MatchOr(pattern) => {
|
||||
let predicates = pattern
|
||||
.patterns
|
||||
.iter()
|
||||
.map(|pattern| self.predicate_kind(pattern))
|
||||
.collect();
|
||||
PatternPredicateKind::Or(predicates)
|
||||
}
|
||||
_ => PatternPredicateKind::Unsupported,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_pattern_narrowing_constraint(
|
||||
&mut self,
|
||||
subject: Expression<'db>,
|
||||
@@ -631,16 +606,29 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
//
|
||||
// See the comment in TypeInferenceBuilder::infer_match_pattern for more details.
|
||||
|
||||
let kind = self.predicate_kind(pattern);
|
||||
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);
|
||||
PatternPredicateKind::Value(value, guard)
|
||||
}
|
||||
ast::Pattern::MatchSingleton(singleton) => {
|
||||
PatternPredicateKind::Singleton(singleton.value, guard)
|
||||
}
|
||||
ast::Pattern::MatchClass(pattern) => {
|
||||
let cls = self.add_standalone_expression(&pattern.cls);
|
||||
PatternPredicateKind::Class(cls, guard)
|
||||
}
|
||||
_ => PatternPredicateKind::Unsupported,
|
||||
};
|
||||
|
||||
let pattern_predicate = PatternPredicate::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
subject,
|
||||
kind,
|
||||
guard,
|
||||
countme::Count::default(),
|
||||
);
|
||||
let predicate = Predicate {
|
||||
@@ -831,64 +819,6 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
debug_assert_eq!(existing_definition, None);
|
||||
}
|
||||
|
||||
/// Add an unpackable assignment for the given [`Unpackable`].
|
||||
///
|
||||
/// This method handles assignments that can contain unpacking like assignment statements,
|
||||
/// for statements, etc.
|
||||
fn add_unpackable_assignment(
|
||||
&mut self,
|
||||
unpackable: &Unpackable<'db>,
|
||||
target: &'db ast::Expr,
|
||||
value: Expression<'db>,
|
||||
) {
|
||||
// We only handle assignments to names and unpackings here, other targets like
|
||||
// attribute and subscript are handled separately as they don't create a new
|
||||
// definition.
|
||||
|
||||
let current_assignment = match target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
|
||||
let unpack = Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
// SAFETY: `target` belongs to the `self.module` tree
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
},
|
||||
UnpackValue::new(unpackable.kind(), value),
|
||||
countme::Count::default(),
|
||||
));
|
||||
Some(unpackable.as_current_assignment(unpack))
|
||||
}
|
||||
ast::Expr::Name(_) => Some(unpackable.as_current_assignment(None)),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
unpackable.as_attribute_assignment(value),
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
|
||||
self.visit_expr(target);
|
||||
|
||||
if current_assignment.is_some() {
|
||||
// Only need to pop in the case where we pushed something
|
||||
self.pop_assignment();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn build(mut self) -> SemanticIndex<'db> {
|
||||
let module = self.module;
|
||||
self.visit_body(module.suite());
|
||||
@@ -1188,7 +1118,59 @@ where
|
||||
let value = self.add_standalone_expression(&node.value);
|
||||
|
||||
for target in &node.targets {
|
||||
self.add_unpackable_assignment(&Unpackable::Assign(node), target, value);
|
||||
// We only handle assignments to names and unpackings here, other targets like
|
||||
// attribute and subscript are handled separately as they don't create a new
|
||||
// definition.
|
||||
let current_assignment = match target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
|
||||
Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
first: true,
|
||||
unpack: Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
// SAFETY: `target` belongs to the `self.module` tree
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
},
|
||||
UnpackValue::Assign(value),
|
||||
countme::Count::default(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
ast::Expr::Name(_) => Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
unpack: None,
|
||||
first: false,
|
||||
}),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::Unannotated { value },
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
|
||||
self.visit_expr(target);
|
||||
|
||||
if current_assignment.is_some() {
|
||||
// Only need to pop in the case where we pushed something
|
||||
self.pop_assignment();
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::Stmt::AnnAssign(node) => {
|
||||
@@ -1379,7 +1361,7 @@ where
|
||||
is_async,
|
||||
..
|
||||
}) => {
|
||||
for item @ ast::WithItem {
|
||||
for item @ ruff_python_ast::WithItem {
|
||||
range: _,
|
||||
context_expr,
|
||||
optional_vars,
|
||||
@@ -1388,14 +1370,55 @@ where
|
||||
self.visit_expr(context_expr);
|
||||
if let Some(optional_vars) = optional_vars.as_deref() {
|
||||
let context_manager = self.add_standalone_expression(context_expr);
|
||||
self.add_unpackable_assignment(
|
||||
&Unpackable::WithItem {
|
||||
let current_assignment = match optional_vars {
|
||||
ast::Expr::Tuple(_) | ast::Expr::List(_) => {
|
||||
Some(CurrentAssignment::WithItem {
|
||||
item,
|
||||
first: true,
|
||||
is_async: *is_async,
|
||||
unpack: Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
// SAFETY: the node `optional_vars` belongs to the `self.module` tree
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), optional_vars)
|
||||
},
|
||||
UnpackValue::ContextManager(context_manager),
|
||||
countme::Count::default(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
ast::Expr::Name(_) => Some(CurrentAssignment::WithItem {
|
||||
item,
|
||||
is_async: *is_async,
|
||||
},
|
||||
optional_vars,
|
||||
context_manager,
|
||||
);
|
||||
unpack: None,
|
||||
// `false` is arbitrary here---we don't actually use it other than in the actual unpacks
|
||||
first: false,
|
||||
}),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::ContextManager { context_manager },
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
self.visit_expr(optional_vars);
|
||||
if current_assignment.is_some() {
|
||||
self.pop_assignment();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.visit_body(body);
|
||||
@@ -1420,7 +1443,52 @@ where
|
||||
|
||||
let pre_loop = self.flow_snapshot();
|
||||
|
||||
self.add_unpackable_assignment(&Unpackable::For(for_stmt), target, iter_expr);
|
||||
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(),
|
||||
// SAFETY: the node `target` belongs to the `self.module` tree
|
||||
#[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,
|
||||
}),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::Iterable {
|
||||
iterable: iter_expr,
|
||||
},
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
self.visit_expr(target);
|
||||
if current_assignment.is_some() {
|
||||
self.pop_assignment();
|
||||
}
|
||||
|
||||
let outer_loop = self.push_loop();
|
||||
self.visit_body(body);
|
||||
@@ -1657,13 +1725,18 @@ where
|
||||
|
||||
if is_definition {
|
||||
match self.current_assignment() {
|
||||
Some(CurrentAssignment::Assign { node, unpack }) => {
|
||||
Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
first,
|
||||
unpack,
|
||||
}) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
AssignmentDefinitionNodeRef {
|
||||
unpack,
|
||||
value: &node.value,
|
||||
name: name_node,
|
||||
first,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1673,11 +1746,16 @@ where
|
||||
Some(CurrentAssignment::AugAssign(aug_assign)) => {
|
||||
self.add_definition(symbol, aug_assign);
|
||||
}
|
||||
Some(CurrentAssignment::For { node, unpack }) => {
|
||||
Some(CurrentAssignment::For {
|
||||
node,
|
||||
first,
|
||||
unpack,
|
||||
}) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
ForStmtDefinitionNodeRef {
|
||||
unpack,
|
||||
first,
|
||||
iterable: &node.iter,
|
||||
name: name_node,
|
||||
is_async: node.is_async,
|
||||
@@ -1703,6 +1781,7 @@ where
|
||||
}
|
||||
Some(CurrentAssignment::WithItem {
|
||||
item,
|
||||
first,
|
||||
is_async,
|
||||
unpack,
|
||||
}) => {
|
||||
@@ -1712,6 +1791,7 @@ where
|
||||
unpack,
|
||||
context_expr: &item.context_expr,
|
||||
name: name_node,
|
||||
first,
|
||||
is_async,
|
||||
},
|
||||
);
|
||||
@@ -1720,11 +1800,13 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(unpack_position) = self
|
||||
.current_assignment_mut()
|
||||
.and_then(CurrentAssignment::unpack_position_mut)
|
||||
if let Some(
|
||||
CurrentAssignment::Assign { first, .. }
|
||||
| CurrentAssignment::For { first, .. }
|
||||
| CurrentAssignment::WithItem { first, .. },
|
||||
) = self.current_assignment_mut()
|
||||
{
|
||||
*unpack_position = UnpackPosition::Other;
|
||||
*first = false;
|
||||
}
|
||||
|
||||
walk_expr(self, expr);
|
||||
@@ -1893,10 +1975,20 @@ where
|
||||
ctx: ExprContext::Store,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(unpack) = self
|
||||
.current_assignment()
|
||||
.as_ref()
|
||||
.and_then(CurrentAssignment::unpack)
|
||||
if let Some(
|
||||
CurrentAssignment::Assign {
|
||||
unpack: Some(unpack),
|
||||
..
|
||||
}
|
||||
| CurrentAssignment::For {
|
||||
unpack: Some(unpack),
|
||||
..
|
||||
}
|
||||
| CurrentAssignment::WithItem {
|
||||
unpack: Some(unpack),
|
||||
..
|
||||
},
|
||||
) = self.current_assignment()
|
||||
{
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
@@ -1971,13 +2063,15 @@ where
|
||||
enum CurrentAssignment<'a> {
|
||||
Assign {
|
||||
node: &'a ast::StmtAssign,
|
||||
unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
first: bool,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
AnnAssign(&'a ast::StmtAnnAssign),
|
||||
AugAssign(&'a ast::StmtAugAssign),
|
||||
For {
|
||||
node: &'a ast::StmtFor,
|
||||
unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
first: bool,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
Named(&'a ast::ExprNamed),
|
||||
Comprehension {
|
||||
@@ -1986,37 +2080,12 @@ enum CurrentAssignment<'a> {
|
||||
},
|
||||
WithItem {
|
||||
item: &'a ast::WithItem,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> CurrentAssignment<'a> {
|
||||
fn unpack(&self) -> Option<Unpack<'a>> {
|
||||
match self {
|
||||
Self::Assign { unpack, .. }
|
||||
| Self::For { unpack, .. }
|
||||
| Self::WithItem { unpack, .. } => unpack.map(|(_, unpack)| unpack),
|
||||
Self::AnnAssign(_)
|
||||
| Self::AugAssign(_)
|
||||
| Self::Named(_)
|
||||
| Self::Comprehension { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unpack_position_mut(&mut self) -> Option<&mut UnpackPosition> {
|
||||
match self {
|
||||
Self::Assign { unpack, .. }
|
||||
| Self::For { unpack, .. }
|
||||
| Self::WithItem { unpack, .. } => unpack.as_mut().map(|(position, _)| position),
|
||||
Self::AnnAssign(_)
|
||||
| Self::AugAssign(_)
|
||||
| Self::Named(_)
|
||||
| Self::Comprehension { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtAnnAssign> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::StmtAnnAssign) -> Self {
|
||||
Self::AnnAssign(value)
|
||||
@@ -2059,47 +2128,3 @@ impl<'a> CurrentMatchCase<'a> {
|
||||
Self { pattern, index: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
enum Unpackable<'a> {
|
||||
Assign(&'a ast::StmtAssign),
|
||||
For(&'a ast::StmtFor),
|
||||
WithItem {
|
||||
item: &'a ast::WithItem,
|
||||
is_async: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> Unpackable<'a> {
|
||||
const fn kind(&self) -> UnpackKind {
|
||||
match self {
|
||||
Unpackable::Assign(_) => UnpackKind::Assign,
|
||||
Unpackable::For(_) => UnpackKind::Iterable,
|
||||
Unpackable::WithItem { .. } => UnpackKind::ContextManager,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_current_assignment(&self, unpack: Option<Unpack<'a>>) -> CurrentAssignment<'a> {
|
||||
let unpack = unpack.map(|unpack| (UnpackPosition::First, unpack));
|
||||
match self {
|
||||
Unpackable::Assign(stmt) => CurrentAssignment::Assign { node: stmt, unpack },
|
||||
Unpackable::For(stmt) => CurrentAssignment::For { node: stmt, unpack },
|
||||
Unpackable::WithItem { item, is_async } => CurrentAssignment::WithItem {
|
||||
item,
|
||||
is_async: *is_async,
|
||||
unpack,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn as_attribute_assignment(&self, expression: Expression<'a>) -> AttributeAssignment<'a> {
|
||||
match self {
|
||||
Unpackable::Assign(_) => AttributeAssignment::Unannotated { value: expression },
|
||||
Unpackable::For(_) => AttributeAssignment::Iterable {
|
||||
iterable: expression,
|
||||
},
|
||||
Unpackable::WithItem { .. } => AttributeAssignment::ContextManager {
|
||||
context_manager: expression,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
@@ -8,7 +8,7 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId};
|
||||
use crate::unpack::{Unpack, UnpackPosition};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::Db;
|
||||
|
||||
/// A definition of a symbol.
|
||||
@@ -52,14 +52,6 @@ impl<'db> Definition<'db> {
|
||||
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
|
||||
self.file_scope(db).to_scope_id(db, self.file(db))
|
||||
}
|
||||
|
||||
pub fn full_range(self, db: &'db dyn Db) -> FileRange {
|
||||
FileRange::new(self.file(db), self.kind(db).full_range())
|
||||
}
|
||||
|
||||
pub fn focus_range(self, db: &'db dyn Db) -> FileRange {
|
||||
FileRange::new(self.file(db), self.kind(db).target_range())
|
||||
}
|
||||
}
|
||||
|
||||
/// One or more [`Definition`]s.
|
||||
@@ -247,24 +239,27 @@ pub(crate) struct ImportFromDefinitionNodeRef<'a> {
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct AssignmentDefinitionNodeRef<'a> {
|
||||
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) value: &'a ast::Expr,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct WithItemDefinitionNodeRef<'a> {
|
||||
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) context_expr: &'a ast::Expr,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
|
||||
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) iterable: &'a ast::Expr,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
@@ -337,10 +332,12 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
unpack,
|
||||
value,
|
||||
name,
|
||||
first,
|
||||
}) => DefinitionKind::Assignment(AssignmentDefinitionKind {
|
||||
target: TargetKind::from(unpack),
|
||||
value: AstNodeRef::new(parsed.clone(), value),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
first,
|
||||
}),
|
||||
DefinitionNodeRef::AnnotatedAssignment(assign) => {
|
||||
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
|
||||
@@ -352,11 +349,13 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
unpack,
|
||||
iterable,
|
||||
name,
|
||||
first,
|
||||
is_async,
|
||||
}) => DefinitionKind::For(ForStmtDefinitionKind {
|
||||
target: TargetKind::from(unpack),
|
||||
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
first,
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
|
||||
@@ -383,11 +382,13 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
unpack,
|
||||
context_expr,
|
||||
name,
|
||||
first,
|
||||
is_async,
|
||||
}) => DefinitionKind::WithItem(WithItemDefinitionKind {
|
||||
target: TargetKind::from(unpack),
|
||||
context_expr: AstNodeRef::new(parsed.clone(), context_expr),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
first,
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
|
||||
@@ -450,6 +451,7 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
value: _,
|
||||
unpack: _,
|
||||
name,
|
||||
first: _,
|
||||
}) => name.into(),
|
||||
Self::AnnotatedAssignment(node) => node.into(),
|
||||
Self::AugmentedAssignment(node) => node.into(),
|
||||
@@ -457,6 +459,7 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
unpack: _,
|
||||
iterable: _,
|
||||
name,
|
||||
first: _,
|
||||
is_async: _,
|
||||
}) => name.into(),
|
||||
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
|
||||
@@ -466,6 +469,7 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
Self::WithItem(WithItemDefinitionNodeRef {
|
||||
unpack: _,
|
||||
context_expr: _,
|
||||
first: _,
|
||||
is_async: _,
|
||||
name,
|
||||
}) => name.into(),
|
||||
@@ -567,6 +571,8 @@ impl DefinitionKind<'_> {
|
||||
///
|
||||
/// 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(import) => import.alias().range(),
|
||||
@@ -593,33 +599,6 @@ impl DefinitionKind<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`TextRange`] of the entire definition.
|
||||
pub(crate) fn full_range(&self) -> TextRange {
|
||||
match self {
|
||||
DefinitionKind::Import(import) => import.alias().range(),
|
||||
DefinitionKind::ImportFrom(import) => import.alias().range(),
|
||||
DefinitionKind::StarImport(import) => import.import().range(),
|
||||
DefinitionKind::Function(function) => function.range(),
|
||||
DefinitionKind::Class(class) => class.range(),
|
||||
DefinitionKind::TypeAlias(type_alias) => type_alias.range(),
|
||||
DefinitionKind::NamedExpression(named) => named.range(),
|
||||
DefinitionKind::Assignment(assignment) => assignment.name().range(),
|
||||
DefinitionKind::AnnotatedAssignment(assign) => assign.range(),
|
||||
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.range(),
|
||||
DefinitionKind::For(for_stmt) => for_stmt.name().range(),
|
||||
DefinitionKind::Comprehension(comp) => comp.target().range(),
|
||||
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.range(),
|
||||
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.range(),
|
||||
DefinitionKind::Parameter(parameter) => parameter.parameter.range(),
|
||||
DefinitionKind::WithItem(with_item) => with_item.name().range(),
|
||||
DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(),
|
||||
DefinitionKind::ExceptHandler(handler) => handler.node().range(),
|
||||
DefinitionKind::TypeVar(type_var) => type_var.range(),
|
||||
DefinitionKind::ParamSpec(param_spec) => param_spec.range(),
|
||||
DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.range(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn category(&self, in_stub: bool) -> DefinitionCategory {
|
||||
match self {
|
||||
// functions, classes, and imports always bind, and we consider them declarations
|
||||
@@ -673,14 +652,14 @@ impl DefinitionKind<'_> {
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Hash)]
|
||||
pub(crate) enum TargetKind<'db> {
|
||||
Sequence(UnpackPosition, Unpack<'db>),
|
||||
Sequence(Unpack<'db>),
|
||||
Name,
|
||||
}
|
||||
|
||||
impl<'db> From<Option<(UnpackPosition, Unpack<'db>)>> for TargetKind<'db> {
|
||||
fn from(value: Option<(UnpackPosition, Unpack<'db>)>) -> Self {
|
||||
impl<'db> From<Option<Unpack<'db>>> for TargetKind<'db> {
|
||||
fn from(value: Option<Unpack<'db>>) -> Self {
|
||||
match value {
|
||||
Some((unpack_position, unpack)) => TargetKind::Sequence(unpack_position, unpack),
|
||||
Some(unpack) => TargetKind::Sequence(unpack),
|
||||
None => TargetKind::Name,
|
||||
}
|
||||
}
|
||||
@@ -801,6 +780,7 @@ pub struct AssignmentDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
value: AstNodeRef<ast::Expr>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
}
|
||||
|
||||
impl<'db> AssignmentDefinitionKind<'db> {
|
||||
@@ -815,6 +795,10 @@ impl<'db> AssignmentDefinitionKind<'db> {
|
||||
pub(crate) fn name(&self) -> &ast::ExprName {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -822,6 +806,7 @@ pub struct WithItemDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
context_expr: AstNodeRef<ast::Expr>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
@@ -838,6 +823,10 @@ impl<'db> WithItemDefinitionKind<'db> {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
|
||||
pub(crate) const fn is_async(&self) -> bool {
|
||||
self.is_async
|
||||
}
|
||||
@@ -848,6 +837,7 @@ pub struct ForStmtDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
iterable: AstNodeRef<ast::Expr>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
@@ -864,6 +854,10 @@ impl<'db> ForStmtDefinitionKind<'db> {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
|
||||
pub(crate) const fn is_async(&self) -> bool {
|
||||
self.is_async
|
||||
}
|
||||
|
||||
@@ -57,10 +57,9 @@ pub(crate) enum PredicateNode<'db> {
|
||||
/// Pattern kinds for which we support type narrowing and/or static visibility analysis.
|
||||
#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)]
|
||||
pub(crate) enum PatternPredicateKind<'db> {
|
||||
Singleton(Singleton),
|
||||
Value(Expression<'db>),
|
||||
Or(Vec<PatternPredicateKind<'db>>),
|
||||
Class(Expression<'db>),
|
||||
Singleton(Singleton, Option<Expression<'db>>),
|
||||
Value(Expression<'db>, Option<Expression<'db>>),
|
||||
Class(Expression<'db>, Option<Expression<'db>>),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
@@ -75,8 +74,6 @@ pub(crate) struct PatternPredicate<'db> {
|
||||
#[return_ref]
|
||||
pub(crate) kind: PatternPredicateKind<'db>,
|
||||
|
||||
pub(crate) guard: Option<Expression<'db>>,
|
||||
|
||||
count: countme::Count<PatternPredicate<'static>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -114,10 +114,6 @@ impl<'db> ScopeId<'db> {
|
||||
self.node(db).scope_kind().is_function_like()
|
||||
}
|
||||
|
||||
pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool {
|
||||
self.node(db).scope_kind().is_type_parameter()
|
||||
}
|
||||
|
||||
pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind {
|
||||
self.scope(db).node()
|
||||
}
|
||||
@@ -230,8 +226,9 @@ pub enum ScopeKind {
|
||||
impl ScopeKind {
|
||||
pub(crate) fn is_eager(self) -> bool {
|
||||
match self {
|
||||
ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => true,
|
||||
ScopeKind::Annotation
|
||||
ScopeKind::Class | ScopeKind::Comprehension => true,
|
||||
ScopeKind::Module
|
||||
| ScopeKind::Annotation
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::Lambda
|
||||
| ScopeKind::TypeAlias => false,
|
||||
@@ -254,14 +251,10 @@ impl ScopeKind {
|
||||
pub(crate) fn is_class(self) -> bool {
|
||||
matches!(self, ScopeKind::Class)
|
||||
}
|
||||
|
||||
pub(crate) fn is_type_parameter(self) -> bool {
|
||||
matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias)
|
||||
}
|
||||
}
|
||||
|
||||
/// Symbol table for a specific [`Scope`].
|
||||
#[derive(Default, salsa::Update)]
|
||||
#[derive(Debug, Default, salsa::Update)]
|
||||
pub struct SymbolTable {
|
||||
/// The symbols in this scope.
|
||||
symbols: IndexVec<ScopedSymbolId, Symbol>,
|
||||
@@ -322,16 +315,6 @@ impl PartialEq for SymbolTable {
|
||||
|
||||
impl Eq for SymbolTable {}
|
||||
|
||||
impl std::fmt::Debug for SymbolTable {
|
||||
/// Exclude the `symbols_by_name` field from the debug output.
|
||||
/// It's very noisy and not useful for debugging.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("SymbolTable")
|
||||
.field(&self.symbols)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct SymbolTableBuilder {
|
||||
table: SymbolTable,
|
||||
|
||||
@@ -178,11 +178,10 @@ use std::cmp::Ordering;
|
||||
use ruff_index::{Idx, IndexVec};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::predicate::{
|
||||
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId,
|
||||
PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId,
|
||||
};
|
||||
use crate::types::{infer_expression_type, Truthiness, Type};
|
||||
use crate::types::{infer_expression_type, Truthiness};
|
||||
use crate::Db;
|
||||
|
||||
/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula
|
||||
@@ -554,102 +553,36 @@ impl VisibilityConstraints {
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_single_pattern_predicate_kind<'db>(
|
||||
db: &'db dyn Db,
|
||||
predicate_kind: &PatternPredicateKind<'db>,
|
||||
subject: Expression<'db>,
|
||||
) -> Truthiness {
|
||||
match predicate_kind {
|
||||
PatternPredicateKind::Value(value) => {
|
||||
let subject_ty = infer_expression_type(db, subject);
|
||||
let value_ty = infer_expression_type(db, *value);
|
||||
|
||||
if subject_ty.is_single_valued(db) {
|
||||
Truthiness::from(subject_ty.is_equivalent_to(db, value_ty))
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
PatternPredicateKind::Singleton(singleton) => {
|
||||
let subject_ty = infer_expression_type(db, subject);
|
||||
|
||||
let singleton_ty = match singleton {
|
||||
ruff_python_ast::Singleton::None => Type::none(db),
|
||||
ruff_python_ast::Singleton::True => Type::BooleanLiteral(true),
|
||||
ruff_python_ast::Singleton::False => Type::BooleanLiteral(false),
|
||||
};
|
||||
|
||||
debug_assert!(singleton_ty.is_singleton(db));
|
||||
|
||||
if subject_ty.is_equivalent_to(db, singleton_ty) {
|
||||
Truthiness::AlwaysTrue
|
||||
} else if subject_ty.is_disjoint_from(db, singleton_ty) {
|
||||
Truthiness::AlwaysFalse
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
PatternPredicateKind::Or(predicates) => {
|
||||
use std::ops::ControlFlow;
|
||||
let (ControlFlow::Break(truthiness) | ControlFlow::Continue(truthiness)) =
|
||||
predicates
|
||||
.iter()
|
||||
.map(|p| Self::analyze_single_pattern_predicate_kind(db, p, subject))
|
||||
// this is just a "max", but with a slight optimization: `AlwaysTrue` is the "greatest" possible element, so we short-circuit if we get there
|
||||
.try_fold(Truthiness::AlwaysFalse, |acc, next| match (acc, next) {
|
||||
(Truthiness::AlwaysTrue, _) | (_, Truthiness::AlwaysTrue) => {
|
||||
ControlFlow::Break(Truthiness::AlwaysTrue)
|
||||
}
|
||||
(Truthiness::Ambiguous, _) | (_, Truthiness::Ambiguous) => {
|
||||
ControlFlow::Continue(Truthiness::Ambiguous)
|
||||
}
|
||||
(Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => {
|
||||
ControlFlow::Continue(Truthiness::AlwaysFalse)
|
||||
}
|
||||
});
|
||||
truthiness
|
||||
}
|
||||
PatternPredicateKind::Class(class_expr) => {
|
||||
let subject_ty = infer_expression_type(db, subject);
|
||||
let class_ty = infer_expression_type(db, *class_expr).to_instance(db);
|
||||
|
||||
class_ty.map_or(Truthiness::Ambiguous, |class_ty| {
|
||||
if subject_ty.is_subtype_of(db, class_ty) {
|
||||
Truthiness::AlwaysTrue
|
||||
} else if subject_ty.is_disjoint_from(db, class_ty) {
|
||||
Truthiness::AlwaysFalse
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
})
|
||||
}
|
||||
PatternPredicateKind::Unsupported => Truthiness::Ambiguous,
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_single_pattern_predicate(db: &dyn Db, predicate: PatternPredicate) -> Truthiness {
|
||||
let truthiness = Self::analyze_single_pattern_predicate_kind(
|
||||
db,
|
||||
predicate.kind(db),
|
||||
predicate.subject(db),
|
||||
);
|
||||
|
||||
if truthiness == Truthiness::AlwaysTrue && predicate.guard(db).is_some() {
|
||||
// Fall back to ambiguous, the guard might change the result.
|
||||
// TODO: actually analyze guard truthiness
|
||||
Truthiness::Ambiguous
|
||||
} else {
|
||||
truthiness
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness {
|
||||
match predicate.node {
|
||||
PredicateNode::Expression(test_expr) => {
|
||||
let ty = infer_expression_type(db, test_expr);
|
||||
ty.bool(db).negate_if(!predicate.is_positive)
|
||||
}
|
||||
PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner),
|
||||
PredicateNode::Pattern(inner) => match inner.kind(db) {
|
||||
PatternPredicateKind::Value(value, guard) => {
|
||||
let subject_expression = inner.subject(db);
|
||||
let subject_ty = infer_expression_type(db, subject_expression);
|
||||
let value_ty = infer_expression_type(db, *value);
|
||||
|
||||
if subject_ty.is_single_valued(db) {
|
||||
let truthiness =
|
||||
Truthiness::from(subject_ty.is_equivalent_to(db, value_ty));
|
||||
|
||||
if truthiness.is_always_true() && guard.is_some() {
|
||||
// Fall back to ambiguous, the guard might change the result.
|
||||
Truthiness::Ambiguous
|
||||
} else {
|
||||
truthiness
|
||||
}
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
PatternPredicateKind::Singleton(..)
|
||||
| PatternPredicateKind::Class(..)
|
||||
| PatternPredicateKind::Unsupported => Truthiness::Ambiguous,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,6 @@ impl_binding_has_ty!(ast::StmtFunctionDef);
|
||||
impl_binding_has_ty!(ast::StmtClassDef);
|
||||
impl_binding_has_ty!(ast::Parameter);
|
||||
impl_binding_has_ty!(ast::ParameterWithDefault);
|
||||
impl_binding_has_ty!(ast::ExceptHandlerExceptHandler);
|
||||
|
||||
impl HasType for ast::Alias {
|
||||
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
|
||||
@@ -459,7 +459,6 @@ pub enum SysPrefixPathOrigin {
|
||||
PythonCliFlag,
|
||||
VirtualEnvVar,
|
||||
Derived,
|
||||
LocalVenv,
|
||||
}
|
||||
|
||||
impl Display for SysPrefixPathOrigin {
|
||||
@@ -468,7 +467,6 @@ impl Display for SysPrefixPathOrigin {
|
||||
Self::PythonCliFlag => f.write_str("`--python` argument"),
|
||||
Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"),
|
||||
Self::Derived => f.write_str("derived `sys.prefix` path"),
|
||||
Self::LocalVenv => f.write_str("local virtual environment"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::lint::{GetLintError, Level, LintMetadata, LintRegistry, LintStatus};
|
||||
use crate::types::TypeCheckDiagnostics;
|
||||
use crate::types::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
use crate::{declare_lint, lint::LintId, Db};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Span};
|
||||
use ruff_db::diagnostic::DiagnosticId;
|
||||
use ruff_db::{files::File, parsed::parsed_module, source::source_text};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_python_trivia::Cursor;
|
||||
@@ -319,11 +319,14 @@ impl<'a> CheckSuppressionsContext<'a> {
|
||||
return;
|
||||
};
|
||||
|
||||
let id = DiagnosticId::Lint(lint.name());
|
||||
let mut diag = Diagnostic::new(id, severity, "");
|
||||
let span = Span::from(self.file).with_range(range);
|
||||
diag.annotate(Annotation::primary(span).message(message));
|
||||
self.diagnostics.push(diag);
|
||||
self.diagnostics.push(TypeCheckDiagnostic {
|
||||
id: DiagnosticId::Lint(lint.name()),
|
||||
message: message.to_string(),
|
||||
range,
|
||||
severity,
|
||||
file: self.file,
|
||||
secondary_messages: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,8 @@ use crate::types::diagnostic::{
|
||||
};
|
||||
use crate::types::signatures::{Parameter, ParameterForm};
|
||||
use crate::types::{
|
||||
todo_type, BoundMethodType, ClassLiteralType, FunctionDecorators, KnownClass, KnownFunction,
|
||||
KnownInstanceType, MethodWrapperKind, PropertyInstanceType, UnionType, WrapperDescriptorKind,
|
||||
todo_type, BoundMethodType, CallableType, ClassLiteralType, KnownClass, KnownFunction,
|
||||
KnownInstanceType, UnionType,
|
||||
};
|
||||
use ruff_db::diagnostic::{OldSecondaryDiagnosticMessage, Span};
|
||||
use ruff_python_ast as ast;
|
||||
@@ -210,20 +210,26 @@ impl<'db> Bindings<'db> {
|
||||
};
|
||||
|
||||
match binding_type {
|
||||
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
|
||||
if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) {
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
|
||||
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||
&& function.decorators(db).len() == 1
|
||||
{
|
||||
match overload.parameter_types() {
|
||||
[_, Some(owner)] => {
|
||||
overload.set_return_type(Type::BoundMethod(BoundMethodType::new(
|
||||
db, function, *owner,
|
||||
)));
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db, function, *owner,
|
||||
)),
|
||||
));
|
||||
}
|
||||
[Some(instance), None] => {
|
||||
overload.set_return_type(Type::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
function,
|
||||
instance.to_meta_type(db),
|
||||
)));
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
function,
|
||||
instance.to_meta_type(db),
|
||||
)),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -231,32 +237,36 @@ impl<'db> Bindings<'db> {
|
||||
if first.is_none(db) {
|
||||
overload.set_return_type(Type::FunctionLiteral(function));
|
||||
} else {
|
||||
overload.set_return_type(Type::BoundMethod(BoundMethodType::new(
|
||||
db, function, *first,
|
||||
overload.set_return_type(Type::Callable(CallableType::BoundMethod(
|
||||
BoundMethodType::new(db, function, *first),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::WrapperDescriptor(WrapperDescriptorKind::FunctionTypeDunderGet) => {
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
|
||||
if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) {
|
||||
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||
&& function.decorators(db).len() == 1
|
||||
{
|
||||
match overload.parameter_types() {
|
||||
[_, _, Some(owner)] => {
|
||||
overload.set_return_type(Type::BoundMethod(
|
||||
BoundMethodType::new(db, *function, *owner),
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db, *function, *owner,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
[_, Some(instance), None] => {
|
||||
overload.set_return_type(Type::BoundMethod(
|
||||
BoundMethodType::new(
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
*function,
|
||||
instance.to_meta_type(db),
|
||||
),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -267,9 +277,41 @@ impl<'db> Bindings<'db> {
|
||||
[_, Some(instance), _] if instance.is_none(db) => {
|
||||
overload.set_return_type(*function_ty);
|
||||
}
|
||||
|
||||
[_, Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(
|
||||
type_alias,
|
||||
))), Some(Type::ClassLiteral(ClassLiteralType { class }))]
|
||||
if class.is_known(db, KnownClass::TypeAliasType)
|
||||
&& function.name(db) == "__name__" =>
|
||||
{
|
||||
overload.set_return_type(Type::string_literal(
|
||||
db,
|
||||
type_alias.name(db),
|
||||
));
|
||||
}
|
||||
|
||||
[_, Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), Some(Type::ClassLiteral(ClassLiteralType { class }))]
|
||||
if class.is_known(db, KnownClass::TypeVar)
|
||||
&& function.name(db) == "__name__" =>
|
||||
{
|
||||
overload.set_return_type(Type::string_literal(
|
||||
db,
|
||||
typevar.name(db),
|
||||
));
|
||||
}
|
||||
|
||||
[_, Some(_), _]
|
||||
if function
|
||||
.has_known_class_decorator(db, KnownClass::Property) =>
|
||||
{
|
||||
overload.set_return_type(todo_type!("@property"));
|
||||
}
|
||||
|
||||
[_, Some(instance), _] => {
|
||||
overload.set_return_type(Type::BoundMethod(
|
||||
BoundMethodType::new(db, *function, *instance),
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db, *function, *instance,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -279,165 +321,6 @@ impl<'db> Bindings<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderGet) => {
|
||||
match overload.parameter_types() {
|
||||
[Some(property @ Type::PropertyInstance(_)), Some(instance), ..]
|
||||
if instance.is_none(db) =>
|
||||
{
|
||||
overload.set_return_type(*property);
|
||||
}
|
||||
[Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias))), ..]
|
||||
if property.getter(db).is_some_and(|getter| {
|
||||
getter
|
||||
.into_function_literal()
|
||||
.is_some_and(|f| f.name(db) == "__name__")
|
||||
}) =>
|
||||
{
|
||||
overload.set_return_type(Type::string_literal(db, type_alias.name(db)));
|
||||
}
|
||||
[Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeVar(type_var))), ..]
|
||||
if property.getter(db).is_some_and(|getter| {
|
||||
getter
|
||||
.into_function_literal()
|
||||
.is_some_and(|f| f.name(db) == "__name__")
|
||||
}) =>
|
||||
{
|
||||
overload.set_return_type(Type::string_literal(db, type_var.name(db)));
|
||||
}
|
||||
[Some(Type::PropertyInstance(property)), Some(instance), ..] => {
|
||||
if let Some(getter) = property.getter(db) {
|
||||
if let Ok(return_ty) = getter
|
||||
.try_call(db, CallArgumentTypes::positional([*instance]))
|
||||
.map(|binding| binding.return_type(db))
|
||||
{
|
||||
overload.set_return_type(return_ty);
|
||||
} else {
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"calling the getter failed",
|
||||
));
|
||||
overload.set_return_type(Type::unknown());
|
||||
}
|
||||
} else {
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"property has no getter",
|
||||
));
|
||||
overload.set_return_type(Type::Never);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => {
|
||||
match overload.parameter_types() {
|
||||
[Some(instance), ..] if instance.is_none(db) => {
|
||||
overload.set_return_type(Type::PropertyInstance(property));
|
||||
}
|
||||
[Some(instance), ..] => {
|
||||
if let Some(getter) = property.getter(db) {
|
||||
if let Ok(return_ty) = getter
|
||||
.try_call(db, CallArgumentTypes::positional([*instance]))
|
||||
.map(|binding| binding.return_type(db))
|
||||
{
|
||||
overload.set_return_type(return_ty);
|
||||
} else {
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"calling the getter failed",
|
||||
));
|
||||
overload.set_return_type(Type::unknown());
|
||||
}
|
||||
} else {
|
||||
overload.set_return_type(Type::Never);
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"property has no getter",
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderSet) => {
|
||||
if let [Some(Type::PropertyInstance(property)), Some(instance), Some(value), ..] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
if let Some(setter) = property.setter(db) {
|
||||
if let Err(_call_error) = setter
|
||||
.try_call(db, CallArgumentTypes::positional([*instance, *value]))
|
||||
{
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"calling the setter failed",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
overload
|
||||
.errors
|
||||
.push(BindingError::InternalCallError("property has no setter"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => {
|
||||
if let [Some(instance), Some(value), ..] = overload.parameter_types() {
|
||||
if let Some(setter) = property.setter(db) {
|
||||
if let Err(_call_error) = setter
|
||||
.try_call(db, CallArgumentTypes::positional([*instance, *value]))
|
||||
{
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"calling the setter failed",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
overload
|
||||
.errors
|
||||
.push(BindingError::InternalCallError("property has no setter"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::BoundMethod(bound_method)
|
||||
if bound_method.self_instance(db).is_property_instance() =>
|
||||
{
|
||||
match bound_method.function(db).name(db).as_str() {
|
||||
"setter" => {
|
||||
if let [Some(_), Some(setter)] = overload.parameter_types() {
|
||||
let mut ty_property = bound_method.self_instance(db);
|
||||
if let Type::PropertyInstance(property) = ty_property {
|
||||
ty_property =
|
||||
Type::PropertyInstance(PropertyInstanceType::new(
|
||||
db,
|
||||
property.getter(db),
|
||||
Some(*setter),
|
||||
));
|
||||
}
|
||||
overload.set_return_type(ty_property);
|
||||
}
|
||||
}
|
||||
"getter" => {
|
||||
if let [Some(_), Some(getter)] = overload.parameter_types() {
|
||||
let mut ty_property = bound_method.self_instance(db);
|
||||
if let Type::PropertyInstance(property) = ty_property {
|
||||
ty_property =
|
||||
Type::PropertyInstance(PropertyInstanceType::new(
|
||||
db,
|
||||
Some(*getter),
|
||||
property.setter(db),
|
||||
));
|
||||
}
|
||||
overload.set_return_type(ty_property);
|
||||
}
|
||||
}
|
||||
"deleter" => {
|
||||
// TODO: we do not store deleters yet
|
||||
let ty_property = bound_method.self_instance(db);
|
||||
overload.set_return_type(ty_property);
|
||||
}
|
||||
_ => {
|
||||
// Fall back to typeshed stubs for all other methods
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::FunctionLiteral(function_type) => match function_type.known(db) {
|
||||
Some(KnownFunction::IsEquivalentTo) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
@@ -587,14 +470,6 @@ impl<'db> Bindings<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownClass::Property) => {
|
||||
if let [getter, setter, ..] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::PropertyInstance(
|
||||
PropertyInstanceType::new(db, *getter, *setter),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
|
||||
@@ -1060,29 +935,19 @@ impl<'db> CallableDescription<'db> {
|
||||
kind: "class",
|
||||
name: class_type.class().name(db),
|
||||
}),
|
||||
Type::BoundMethod(bound_method) => Some(CallableDescription {
|
||||
Type::Callable(CallableType::BoundMethod(bound_method)) => Some(CallableDescription {
|
||||
kind: "bound method",
|
||||
name: bound_method.function(db).name(db),
|
||||
}),
|
||||
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
|
||||
Some(CallableDescription {
|
||||
kind: "method wrapper `__get__` of function",
|
||||
name: function.name(db),
|
||||
})
|
||||
}
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => {
|
||||
Some(CallableDescription {
|
||||
kind: "method wrapper",
|
||||
name: "`__get__` of property",
|
||||
})
|
||||
}
|
||||
Type::WrapperDescriptor(kind) => Some(CallableDescription {
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => Some(CallableDescription {
|
||||
kind: "wrapper descriptor",
|
||||
name: match kind {
|
||||
WrapperDescriptorKind::FunctionTypeDunderGet => "FunctionType.__get__",
|
||||
WrapperDescriptorKind::PropertyDunderGet => "property.__get__",
|
||||
WrapperDescriptorKind::PropertyDunderSet => "property.__set__",
|
||||
},
|
||||
name: "FunctionType.__get__",
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
@@ -1170,10 +1035,6 @@ pub(crate) enum BindingError<'db> {
|
||||
argument_index: Option<usize>,
|
||||
parameter: ParameterContext,
|
||||
},
|
||||
/// The call itself might be well constructed, but an error occurred while evaluating the call.
|
||||
/// We use this variant to report errors in `property.__get__` and `property.__set__`, which
|
||||
/// can occur when the call to the underlying getter/setter fails.
|
||||
InternalCallError(&'static str),
|
||||
}
|
||||
|
||||
impl<'db> BindingError<'db> {
|
||||
@@ -1200,11 +1061,13 @@ impl<'db> BindingError<'db> {
|
||||
None
|
||||
}
|
||||
}
|
||||
Type::BoundMethod(bound_method) => Self::parameter_span_from_index(
|
||||
db,
|
||||
Type::FunctionLiteral(bound_method.function(db)),
|
||||
parameter_index,
|
||||
),
|
||||
Type::Callable(CallableType::BoundMethod(bound_method)) => {
|
||||
Self::parameter_span_from_index(
|
||||
db,
|
||||
Type::FunctionLiteral(bound_method.function(db)),
|
||||
parameter_index,
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1247,7 +1110,7 @@ impl<'db> BindingError<'db> {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
&messages,
|
||||
messages,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1322,21 +1185,6 @@ impl<'db> BindingError<'db> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Self::InternalCallError(reason) => {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
Self::get_node(node, None),
|
||||
format_args!(
|
||||
"Call{} failed: {reason}",
|
||||
if let Some(CallableDescription { kind, name }) = callable_description {
|
||||
format!(" of {kind} `{name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{
|
||||
};
|
||||
use indexmap::IndexSet;
|
||||
use itertools::Itertools as _;
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, PythonVersion};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
@@ -153,15 +153,6 @@ impl<'db> Class<'db> {
|
||||
self.body_scope(db).node(db).expect_class()
|
||||
}
|
||||
|
||||
/// Returns the file range of the class's name.
|
||||
pub fn focus_range(self, db: &dyn Db) -> FileRange {
|
||||
FileRange::new(self.file(db), self.node(db).name.range)
|
||||
}
|
||||
|
||||
pub fn full_range(self, db: &dyn Db) -> FileRange {
|
||||
FileRange::new(self.file(db), self.node(db).range)
|
||||
}
|
||||
|
||||
/// Return the types of the decorators on this class
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||
@@ -763,7 +754,7 @@ pub struct ClassLiteralType<'db> {
|
||||
}
|
||||
|
||||
impl<'db> ClassLiteralType<'db> {
|
||||
pub fn class(self) -> Class<'db> {
|
||||
pub(crate) fn class(self) -> Class<'db> {
|
||||
self.class
|
||||
}
|
||||
|
||||
@@ -789,7 +780,7 @@ pub struct InstanceType<'db> {
|
||||
}
|
||||
|
||||
impl<'db> InstanceType<'db> {
|
||||
pub fn class(self) -> Class<'db> {
|
||||
pub(super) fn class(self) -> Class<'db> {
|
||||
self.class
|
||||
}
|
||||
|
||||
@@ -820,7 +811,6 @@ pub enum KnownClass {
|
||||
Bool,
|
||||
Object,
|
||||
Bytes,
|
||||
Bytearray,
|
||||
Type,
|
||||
Int,
|
||||
Float,
|
||||
@@ -837,9 +827,6 @@ pub enum KnownClass {
|
||||
BaseException,
|
||||
BaseExceptionGroup,
|
||||
Classmethod,
|
||||
Super,
|
||||
// enum
|
||||
Enum,
|
||||
// Types
|
||||
GenericAlias,
|
||||
ModuleType,
|
||||
@@ -850,13 +837,10 @@ pub enum KnownClass {
|
||||
// Typeshed
|
||||
NoneType, // Part of `types` for Python >= 3.10
|
||||
// Typing
|
||||
Any,
|
||||
StdlibAlias,
|
||||
SpecialForm,
|
||||
TypeVar,
|
||||
ParamSpec,
|
||||
ParamSpecArgs,
|
||||
ParamSpecKwargs,
|
||||
TypeVarTuple,
|
||||
TypeAliasType,
|
||||
NoDefaultType,
|
||||
@@ -875,7 +859,6 @@ pub enum KnownClass {
|
||||
// Exposed as `types.EllipsisType` on Python >=3.10;
|
||||
// backported as `builtins.ellipsis` by typeshed on Python <=3.9
|
||||
EllipsisType,
|
||||
NotImplementedType,
|
||||
}
|
||||
|
||||
impl<'db> KnownClass {
|
||||
@@ -906,16 +889,13 @@ impl<'db> KnownClass {
|
||||
| Self::TypeAliasType
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::TypeVarTuple
|
||||
| Self::WrapperDescriptorType
|
||||
| Self::MethodWrapperType => Truthiness::AlwaysTrue,
|
||||
|
||||
Self::NoneType => Truthiness::AlwaysFalse,
|
||||
|
||||
Self::Any
|
||||
| Self::BaseException
|
||||
Self::BaseException
|
||||
| Self::Object
|
||||
| Self::OrderedDict
|
||||
| Self::BaseExceptionGroup
|
||||
@@ -931,7 +911,6 @@ impl<'db> KnownClass {
|
||||
| Self::Int
|
||||
| Self::Type
|
||||
| Self::Bytes
|
||||
| Self::Bytearray
|
||||
| Self::FrozenSet
|
||||
| Self::Range
|
||||
| Self::Property
|
||||
@@ -945,23 +924,15 @@ impl<'db> KnownClass {
|
||||
| Self::Deque
|
||||
| Self::Float
|
||||
| Self::Sized
|
||||
| Self::Enum
|
||||
| Self::Super
|
||||
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
|
||||
// and raises a `TypeError` in Python >=3.14
|
||||
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
|
||||
| Self::NotImplementedType
|
||||
| Self::Classmethod => Truthiness::Ambiguous,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn name(self, db: &'db dyn Db) -> &'static str {
|
||||
match self {
|
||||
Self::Any => "Any",
|
||||
Self::Bool => "bool",
|
||||
Self::Object => "object",
|
||||
Self::Bytes => "bytes",
|
||||
Self::Bytearray => "bytearray",
|
||||
Self::Tuple => "tuple",
|
||||
Self::Int => "int",
|
||||
Self::Float => "float",
|
||||
@@ -988,8 +959,6 @@ impl<'db> KnownClass {
|
||||
Self::SpecialForm => "_SpecialForm",
|
||||
Self::TypeVar => "TypeVar",
|
||||
Self::ParamSpec => "ParamSpec",
|
||||
Self::ParamSpecArgs => "ParamSpecArgs",
|
||||
Self::ParamSpecKwargs => "ParamSpecKwargs",
|
||||
Self::TypeVarTuple => "TypeVarTuple",
|
||||
Self::TypeAliasType => "TypeAliasType",
|
||||
Self::NoDefaultType => "_NoDefaultType",
|
||||
@@ -1001,8 +970,6 @@ impl<'db> KnownClass {
|
||||
Self::Deque => "deque",
|
||||
Self::Sized => "Sized",
|
||||
Self::OrderedDict => "OrderedDict",
|
||||
Self::Enum => "Enum",
|
||||
Self::Super => "super",
|
||||
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
|
||||
Self::StdlibAlias => "_Alias",
|
||||
// This is the name the type of `sys.version_info` has in typeshed,
|
||||
@@ -1020,7 +987,6 @@ impl<'db> KnownClass {
|
||||
"ellipsis"
|
||||
}
|
||||
}
|
||||
Self::NotImplementedType => "_NotImplementedType",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1139,7 +1105,6 @@ impl<'db> KnownClass {
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Bytearray
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
@@ -1155,10 +1120,8 @@ impl<'db> KnownClass {
|
||||
| Self::Classmethod
|
||||
| Self::Slice
|
||||
| Self::Range
|
||||
| Self::Super
|
||||
| Self::Property => KnownModule::Builtins,
|
||||
Self::VersionInfo => KnownModule::Sys,
|
||||
Self::Enum => KnownModule::Enum,
|
||||
Self::GenericAlias
|
||||
| Self::ModuleType
|
||||
| Self::FunctionType
|
||||
@@ -1166,18 +1129,14 @@ impl<'db> KnownClass {
|
||||
| Self::MethodWrapperType
|
||||
| Self::WrapperDescriptorType => KnownModule::Types,
|
||||
Self::NoneType => KnownModule::Typeshed,
|
||||
Self::Any
|
||||
| Self::SpecialForm
|
||||
Self::SpecialForm
|
||||
| Self::TypeVar
|
||||
| Self::StdlibAlias
|
||||
| Self::SupportsIndex
|
||||
| Self::Sized => KnownModule::Typing,
|
||||
Self::TypeAliasType
|
||||
| Self::TypeVarTuple
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::NewType => KnownModule::TypingExtensions,
|
||||
Self::TypeAliasType | Self::TypeVarTuple | Self::ParamSpec | Self::NewType => {
|
||||
KnownModule::TypingExtensions
|
||||
}
|
||||
Self::NoDefaultType => {
|
||||
let python_version = Program::get(db).python_version(db);
|
||||
|
||||
@@ -1199,7 +1158,6 @@ impl<'db> KnownClass {
|
||||
KnownModule::Builtins
|
||||
}
|
||||
}
|
||||
Self::NotImplementedType => KnownModule::Builtins,
|
||||
Self::ChainMap
|
||||
| Self::Counter
|
||||
| Self::DefaultDict
|
||||
@@ -1215,14 +1173,11 @@ impl<'db> KnownClass {
|
||||
| Self::NoDefaultType
|
||||
| Self::VersionInfo
|
||||
| Self::EllipsisType
|
||||
| Self::TypeAliasType
|
||||
| Self::NotImplementedType => true,
|
||||
| Self::TypeAliasType => true,
|
||||
|
||||
Self::Any
|
||||
| Self::Bool
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Bytearray
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
@@ -1255,12 +1210,8 @@ impl<'db> KnownClass {
|
||||
| Self::StdlibAlias
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::TypeVarTuple
|
||||
| Self::Sized
|
||||
| Self::Enum
|
||||
| Self::Super
|
||||
| Self::NewType => false,
|
||||
}
|
||||
}
|
||||
@@ -1269,19 +1220,17 @@ impl<'db> KnownClass {
|
||||
///
|
||||
/// A singleton class is a class where it is known that only one instance can ever exist at runtime.
|
||||
pub(super) const fn is_singleton(self) -> bool {
|
||||
// TODO there are other singleton types (NotImplementedType -- any others?)
|
||||
match self {
|
||||
Self::NoneType
|
||||
| Self::EllipsisType
|
||||
| Self::NoDefaultType
|
||||
| Self::VersionInfo
|
||||
| Self::TypeAliasType
|
||||
| Self::NotImplementedType => true,
|
||||
| Self::TypeAliasType => true,
|
||||
|
||||
Self::Any
|
||||
| Self::Bool
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Bytearray
|
||||
| Self::Tuple
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
@@ -1314,12 +1263,8 @@ impl<'db> KnownClass {
|
||||
| Self::Classmethod
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::TypeVarTuple
|
||||
| Self::Sized
|
||||
| Self::Enum
|
||||
| Self::Super
|
||||
| Self::NewType => false,
|
||||
}
|
||||
}
|
||||
@@ -1332,11 +1277,9 @@ impl<'db> KnownClass {
|
||||
// We assert that this match is exhaustive over the right-hand side in the unit test
|
||||
// `known_class_roundtrip_from_str()`
|
||||
let candidate = match class_name {
|
||||
"Any" => Self::Any,
|
||||
"bool" => Self::Bool,
|
||||
"object" => Self::Object,
|
||||
"bytes" => Self::Bytes,
|
||||
"bytearray" => Self::Bytearray,
|
||||
"tuple" => Self::Tuple,
|
||||
"type" => Self::Type,
|
||||
"int" => Self::Int,
|
||||
@@ -1364,8 +1307,6 @@ impl<'db> KnownClass {
|
||||
"TypeAliasType" => Self::TypeAliasType,
|
||||
"TypeVar" => Self::TypeVar,
|
||||
"ParamSpec" => Self::ParamSpec,
|
||||
"ParamSpecArgs" => Self::ParamSpecArgs,
|
||||
"ParamSpecKwargs" => Self::ParamSpecKwargs,
|
||||
"TypeVarTuple" => Self::TypeVarTuple,
|
||||
"ChainMap" => Self::ChainMap,
|
||||
"Counter" => Self::Counter,
|
||||
@@ -1377,8 +1318,6 @@ impl<'db> KnownClass {
|
||||
"_NoDefaultType" => Self::NoDefaultType,
|
||||
"SupportsIndex" => Self::SupportsIndex,
|
||||
"Sized" => Self::Sized,
|
||||
"Enum" => Self::Enum,
|
||||
"super" => Self::Super,
|
||||
"_version_info" => Self::VersionInfo,
|
||||
"ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => {
|
||||
Self::EllipsisType
|
||||
@@ -1386,7 +1325,6 @@ impl<'db> KnownClass {
|
||||
"EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => {
|
||||
Self::EllipsisType
|
||||
}
|
||||
"_NotImplementedType" => Self::NotImplementedType,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
@@ -1398,11 +1336,9 @@ impl<'db> KnownClass {
|
||||
/// Return `true` if the module of `self` matches `module`
|
||||
fn check_module(self, db: &'db dyn Db, module: KnownModule) -> bool {
|
||||
match self {
|
||||
Self::Any
|
||||
| Self::Bool
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Bytearray
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
@@ -1432,9 +1368,6 @@ impl<'db> KnownClass {
|
||||
| Self::FunctionType
|
||||
| Self::MethodType
|
||||
| Self::MethodWrapperType
|
||||
| Self::Enum
|
||||
| Self::Super
|
||||
| Self::NotImplementedType
|
||||
| Self::WrapperDescriptorType => module == self.canonical_module(db),
|
||||
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
|
||||
Self::SpecialForm
|
||||
@@ -1443,8 +1376,6 @@ impl<'db> KnownClass {
|
||||
| Self::NoDefaultType
|
||||
| Self::SupportsIndex
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::TypeVarTuple
|
||||
| Self::Sized
|
||||
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
|
||||
@@ -1526,9 +1457,6 @@ pub enum KnownInstanceType<'db> {
|
||||
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
|
||||
Never,
|
||||
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
|
||||
/// This is not used since typeshed switched to representing `Any` as a class; now we use
|
||||
/// `KnownClass::Any` instead. But we still support the old `Any = object()` representation, at
|
||||
/// least for now. TODO maybe remove?
|
||||
Any,
|
||||
/// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`)
|
||||
Tuple,
|
||||
@@ -1570,8 +1498,8 @@ pub enum KnownInstanceType<'db> {
|
||||
Intersection,
|
||||
/// The symbol `knot_extensions.TypeOf`
|
||||
TypeOf,
|
||||
/// The symbol `knot_extensions.CallableTypeOf`
|
||||
CallableTypeOf,
|
||||
/// The symbol `knot_extensions.CallableTypeFromFunction`
|
||||
CallableTypeFromFunction,
|
||||
|
||||
// Various special forms, special aliases and type qualifiers that we don't yet understand
|
||||
// (all currently inferred as TODO in most contexts):
|
||||
@@ -1634,7 +1562,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
| Self::Not
|
||||
| Self::Intersection
|
||||
| Self::TypeOf
|
||||
| Self::CallableTypeOf => Truthiness::AlwaysTrue,
|
||||
| Self::CallableTypeFromFunction => Truthiness::AlwaysTrue,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1681,7 +1609,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::Not => "knot_extensions.Not",
|
||||
Self::Intersection => "knot_extensions.Intersection",
|
||||
Self::TypeOf => "knot_extensions.TypeOf",
|
||||
Self::CallableTypeOf => "knot_extensions.CallableTypeOf",
|
||||
Self::CallableTypeFromFunction => "knot_extensions.CallableTypeFromFunction",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1725,7 +1653,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::TypeOf => KnownClass::SpecialForm,
|
||||
Self::Not => KnownClass::SpecialForm,
|
||||
Self::Intersection => KnownClass::SpecialForm,
|
||||
Self::CallableTypeOf => KnownClass::SpecialForm,
|
||||
Self::CallableTypeFromFunction => KnownClass::SpecialForm,
|
||||
Self::Unknown => KnownClass::Object,
|
||||
Self::AlwaysTruthy => KnownClass::Object,
|
||||
Self::AlwaysFalsy => KnownClass::Object,
|
||||
@@ -1790,7 +1718,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
"Not" => Self::Not,
|
||||
"Intersection" => Self::Intersection,
|
||||
"TypeOf" => Self::TypeOf,
|
||||
"CallableTypeOf" => Self::CallableTypeOf,
|
||||
"CallableTypeFromFunction" => Self::CallableTypeFromFunction,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
@@ -1847,7 +1775,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
| Self::Not
|
||||
| Self::Intersection
|
||||
| Self::TypeOf
|
||||
| Self::CallableTypeOf => module.is_knot_extensions(),
|
||||
| Self::CallableTypeFromFunction => module.is_knot_extensions(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use itertools::Either;
|
||||
/// all types that would be invalid to have as a class base are
|
||||
/// transformed into [`ClassBase::unknown`]
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
|
||||
pub enum ClassBase<'db> {
|
||||
pub(crate) enum ClassBase<'db> {
|
||||
Dynamic(DynamicType),
|
||||
Class(Class<'db>),
|
||||
}
|
||||
@@ -18,7 +18,7 @@ impl<'db> ClassBase<'db> {
|
||||
Self::Dynamic(DynamicType::Any)
|
||||
}
|
||||
|
||||
pub const fn unknown() -> Self {
|
||||
pub(crate) const fn unknown() -> Self {
|
||||
Self::Dynamic(DynamicType::Unknown)
|
||||
}
|
||||
|
||||
@@ -61,22 +61,14 @@ impl<'db> ClassBase<'db> {
|
||||
pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
|
||||
match ty {
|
||||
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
|
||||
Type::ClassLiteral(literal) => Some(if literal.class().is_known(db, KnownClass::Any) {
|
||||
Self::Dynamic(DynamicType::Any)
|
||||
} else {
|
||||
Self::Class(literal.class())
|
||||
}),
|
||||
Type::ClassLiteral(literal) => Some(Self::Class(literal.class())),
|
||||
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
|
||||
Type::Intersection(_) => None, // TODO -- probably incorrect?
|
||||
Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?
|
||||
Type::PropertyInstance(_) => None,
|
||||
Type::Never
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::Callable(..)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
@@ -111,7 +103,7 @@ impl<'db> ClassBase<'db> {
|
||||
| KnownInstanceType::Not
|
||||
| KnownInstanceType::Intersection
|
||||
| KnownInstanceType::TypeOf
|
||||
| KnownInstanceType::CallableTypeOf
|
||||
| KnownInstanceType::CallableTypeFromFunction
|
||||
| KnownInstanceType::AlwaysTruthy
|
||||
| KnownInstanceType::AlwaysFalsy => None,
|
||||
KnownInstanceType::Unknown => Some(Self::unknown()),
|
||||
|
||||
@@ -2,22 +2,20 @@ use std::fmt;
|
||||
|
||||
use drop_bomb::DebugDropBomb;
|
||||
use ruff_db::{
|
||||
diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, OldSecondaryDiagnosticMessage, Severity, Span,
|
||||
},
|
||||
diagnostic::{DiagnosticId, OldSecondaryDiagnosticMessage, Severity},
|
||||
files::File,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use super::{binding_type, Type, TypeCheckDiagnostics};
|
||||
use super::{binding_type, KnownFunction, Type, TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::{
|
||||
lint::{LintId, LintMetadata},
|
||||
suppression::suppressions,
|
||||
Db,
|
||||
};
|
||||
use crate::{semantic_index::semantic_index, types::FunctionDecorators};
|
||||
|
||||
/// Context for inferring the types of a single file.
|
||||
///
|
||||
@@ -61,8 +59,11 @@ impl<'db> InferContext<'db> {
|
||||
self.db
|
||||
}
|
||||
|
||||
pub(crate) fn extend(&mut self, other: &TypeCheckDiagnostics) {
|
||||
self.diagnostics.get_mut().extend(other);
|
||||
pub(crate) fn extend<T>(&mut self, other: &T)
|
||||
where
|
||||
T: WithDiagnostics,
|
||||
{
|
||||
self.diagnostics.get_mut().extend(other.diagnostics());
|
||||
}
|
||||
|
||||
/// Reports a lint located at `ranged`.
|
||||
@@ -74,7 +75,7 @@ impl<'db> InferContext<'db> {
|
||||
) where
|
||||
T: Ranged,
|
||||
{
|
||||
self.report_lint_with_secondary_messages(lint, ranged, message, &[]);
|
||||
self.report_lint_with_secondary_messages(lint, ranged, message, vec![]);
|
||||
}
|
||||
|
||||
/// Reports a lint located at `ranged`.
|
||||
@@ -83,7 +84,7 @@ impl<'db> InferContext<'db> {
|
||||
lint: &'static LintMetadata,
|
||||
ranged: T,
|
||||
message: fmt::Arguments,
|
||||
secondary_messages: &[OldSecondaryDiagnosticMessage],
|
||||
secondary_messages: Vec<OldSecondaryDiagnosticMessage>,
|
||||
) where
|
||||
T: Ranged,
|
||||
{
|
||||
@@ -135,7 +136,7 @@ impl<'db> InferContext<'db> {
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
message: fmt::Arguments,
|
||||
secondary_messages: &[OldSecondaryDiagnosticMessage],
|
||||
secondary_messages: Vec<OldSecondaryDiagnosticMessage>,
|
||||
) where
|
||||
T: Ranged,
|
||||
{
|
||||
@@ -149,13 +150,14 @@ impl<'db> InferContext<'db> {
|
||||
// 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.
|
||||
|
||||
let mut diag = Diagnostic::new(id, severity, "");
|
||||
for secondary_msg in secondary_messages {
|
||||
diag.sub(secondary_msg.to_sub_diagnostic());
|
||||
}
|
||||
let span = Span::from(self.file).with_range(ranged.range());
|
||||
diag.annotate(Annotation::primary(span).message(message));
|
||||
self.diagnostics.borrow_mut().push(diag);
|
||||
self.diagnostics.borrow_mut().push(TypeCheckDiagnostic {
|
||||
file: self.file,
|
||||
id,
|
||||
message: message.to_string(),
|
||||
range: ranged.range(),
|
||||
severity,
|
||||
secondary_messages,
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn set_in_no_type_check(&mut self, no_type_check: InNoTypeCheck) {
|
||||
@@ -180,7 +182,13 @@ impl<'db> InferContext<'db> {
|
||||
|
||||
// Iterate over all functions and test if any is decorated with `@no_type_check`.
|
||||
function_scope_tys.any(|function_ty| {
|
||||
function_ty.has_known_decorator(self.db, FunctionDecorators::NO_TYPE_CHECK)
|
||||
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,
|
||||
@@ -221,3 +229,7 @@ pub(crate) enum InNoTypeCheck {
|
||||
/// The inference is known to be in an `@no_type_check` decorated function.
|
||||
Yes,
|
||||
}
|
||||
|
||||
pub(crate) trait WithDiagnostics {
|
||||
fn diagnostics(&self) -> &TypeCheckDiagnostics;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,17 @@ use crate::types::string_annotation::{
|
||||
RAW_STRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use crate::types::{ClassLiteralType, KnownInstanceType, Type};
|
||||
use ruff_db::diagnostic::{Diagnostic, OldSecondaryDiagnosticMessage, Span};
|
||||
use ruff_db::diagnostic::{
|
||||
DiagnosticId, OldDiagnosticTrait, OldSecondaryDiagnosticMessage, Severity, Span,
|
||||
};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::Ranged;
|
||||
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;
|
||||
|
||||
/// Registers all known type check lints.
|
||||
pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
@@ -61,7 +67,6 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
|
||||
registry.register_lint(&STATIC_ASSERT_ERROR);
|
||||
registry.register_lint(&INVALID_ATTRIBUTE_ACCESS);
|
||||
registry.register_lint(&REDUNDANT_CAST);
|
||||
|
||||
// String annotations
|
||||
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
||||
@@ -873,37 +878,68 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Detects redundant `cast` calls where the value already has the target type.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// These casts have no effect and can be removed.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def f() -> int:
|
||||
/// return 10
|
||||
///
|
||||
/// cast(int, f()) # Redundant
|
||||
/// ```
|
||||
pub(crate) static REDUNDANT_CAST = {
|
||||
summary: "detects redundant `cast` calls",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Warn,
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct TypeCheckDiagnostic {
|
||||
pub(crate) id: DiagnosticId,
|
||||
pub(crate) message: String,
|
||||
pub(crate) range: TextRange,
|
||||
pub(crate) severity: Severity,
|
||||
pub(crate) file: File,
|
||||
pub(crate) secondary_messages: Vec<OldSecondaryDiagnosticMessage>,
|
||||
}
|
||||
|
||||
impl TypeCheckDiagnostic {
|
||||
pub fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
|
||||
pub fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl OldDiagnosticTrait for TypeCheckDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
TypeCheckDiagnostic::message(self).into()
|
||||
}
|
||||
|
||||
fn span(&self) -> Option<Span> {
|
||||
Some(Span::from(self.file).with_range(self.range))
|
||||
}
|
||||
|
||||
fn secondary_messages(&self) -> &[OldSecondaryDiagnosticMessage] {
|
||||
&self.secondary_messages
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
self.severity
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of type check diagnostics.
|
||||
///
|
||||
/// The diagnostics are wrapped in an `Arc` because they need to be cloned multiple times
|
||||
/// when going from `infer_expression` to `check_file`. We could consider
|
||||
/// making [`TypeCheckDiagnostic`] a Salsa struct to have them Arena-allocated (once the Tables refactor is done).
|
||||
/// Using Salsa struct does have the downside that it leaks the Salsa dependency into diagnostics and
|
||||
/// each Salsa-struct comes with an overhead.
|
||||
#[derive(Default, Eq, PartialEq)]
|
||||
pub struct TypeCheckDiagnostics {
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
diagnostics: Vec<Arc<TypeCheckDiagnostic>>,
|
||||
used_suppressions: FxHashSet<FileSuppressionId>,
|
||||
}
|
||||
|
||||
impl TypeCheckDiagnostics {
|
||||
pub(crate) fn push(&mut self, diagnostic: Diagnostic) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
pub(crate) fn push(&mut self, diagnostic: TypeCheckDiagnostic) {
|
||||
self.diagnostics.push(Arc::new(diagnostic));
|
||||
}
|
||||
|
||||
pub(super) fn extend(&mut self, other: &TypeCheckDiagnostics) {
|
||||
@@ -927,10 +963,6 @@ impl TypeCheckDiagnostics {
|
||||
self.used_suppressions.shrink_to_fit();
|
||||
self.diagnostics.shrink_to_fit();
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic> {
|
||||
self.diagnostics.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TypeCheckDiagnostics {
|
||||
@@ -939,9 +971,17 @@ impl std::fmt::Debug for TypeCheckDiagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TypeCheckDiagnostics {
|
||||
type Target = [std::sync::Arc<TypeCheckDiagnostic>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for TypeCheckDiagnostics {
|
||||
type Item = Diagnostic;
|
||||
type IntoIter = std::vec::IntoIter<Diagnostic>;
|
||||
type Item = Arc<TypeCheckDiagnostic>;
|
||||
type IntoIter = std::vec::IntoIter<std::sync::Arc<TypeCheckDiagnostic>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.diagnostics.into_iter()
|
||||
@@ -949,8 +989,8 @@ impl IntoIterator for TypeCheckDiagnostics {
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a TypeCheckDiagnostics {
|
||||
type Item = &'a Diagnostic;
|
||||
type IntoIter = std::slice::Iter<'a, Diagnostic>;
|
||||
type Item = &'a Arc<TypeCheckDiagnostic>;
|
||||
type IntoIter = std::slice::Iter<'a, std::sync::Arc<TypeCheckDiagnostic>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.diagnostics.iter()
|
||||
@@ -1094,7 +1134,7 @@ pub(super) fn report_invalid_return_type(
|
||||
actual_ty.display(context.db()),
|
||||
expected_ty.display(context.db())
|
||||
),
|
||||
&[OldSecondaryDiagnosticMessage::new(
|
||||
vec![OldSecondaryDiagnosticMessage::new(
|
||||
return_type_span,
|
||||
format!(
|
||||
"Return type is declared here as `{}`",
|
||||
|
||||
@@ -9,8 +9,8 @@ use ruff_python_literal::escape::AsciiEscape;
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::signatures::{Parameter, Parameters, Signature};
|
||||
use crate::types::{
|
||||
ClassLiteralType, InstanceType, IntersectionType, KnownClass, MethodWrapperKind,
|
||||
StringLiteralType, Type, UnionType, WrapperDescriptorKind,
|
||||
CallableType, ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
|
||||
Type, UnionType,
|
||||
};
|
||||
use crate::Db;
|
||||
use rustc_hash::FxHashMap;
|
||||
@@ -33,19 +33,18 @@ pub struct DisplayType<'db> {
|
||||
impl Display for DisplayType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let representation = self.ty.representation(self.db);
|
||||
match self.ty {
|
||||
Type::ClassLiteral(literal) if literal.class().is_known(self.db, KnownClass::Any) => {
|
||||
write!(f, "typing.Any")
|
||||
}
|
||||
if matches!(
|
||||
self.ty,
|
||||
Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::FunctionLiteral(_) => {
|
||||
write!(f, "Literal[{representation}]")
|
||||
}
|
||||
_ => representation.fmt(f),
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
) {
|
||||
write!(f, "Literal[{representation}]")
|
||||
} else {
|
||||
representation.fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +76,6 @@ impl Display for DisplayRepresentation<'_> {
|
||||
};
|
||||
f.write_str(representation)
|
||||
}
|
||||
Type::PropertyInstance(_) => f.write_str("property"),
|
||||
Type::ModuleLiteral(module) => {
|
||||
write!(f, "<module '{}'>", module.module(self.db).name())
|
||||
}
|
||||
@@ -91,8 +89,10 @@ impl Display for DisplayRepresentation<'_> {
|
||||
},
|
||||
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
|
||||
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
|
||||
Type::Callable(callable) => callable.signature(self.db).display(self.db).fmt(f),
|
||||
Type::BoundMethod(bound_method) => {
|
||||
Type::Callable(CallableType::General(callable)) => {
|
||||
callable.signature(self.db).display(self.db).fmt(f)
|
||||
}
|
||||
Type::Callable(CallableType::BoundMethod(bound_method)) => {
|
||||
write!(
|
||||
f,
|
||||
"<bound method `{method}` of `{instance}`>",
|
||||
@@ -100,26 +100,18 @@ impl Display for DisplayRepresentation<'_> {
|
||||
instance = bound_method.self_instance(self.db).display(self.db)
|
||||
)
|
||||
}
|
||||
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
|
||||
write!(
|
||||
f,
|
||||
"<method-wrapper `__get__` of `{function}`>",
|
||||
function = function.name(self.db)
|
||||
)
|
||||
}
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => {
|
||||
write!(f, "<method-wrapper `__get__` of `property` object>",)
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
|
||||
f.write_str("<wrapper-descriptor `__get__` of `function` objects>")
|
||||
}
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => {
|
||||
write!(f, "<method-wrapper `__set__` of `property` object>",)
|
||||
}
|
||||
Type::WrapperDescriptor(kind) => {
|
||||
let (method, object) = match kind {
|
||||
WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"),
|
||||
WrapperDescriptorKind::PropertyDunderGet => ("__get__", "property"),
|
||||
WrapperDescriptorKind::PropertyDunderSet => ("__set__", "property"),
|
||||
};
|
||||
write!(f, "<wrapper-descriptor `{method}` of `{object}` objects>")
|
||||
Type::Callable(CallableType::SpecializedGetitem) => {
|
||||
f.write_str("<specialized `__getitem__`>")
|
||||
}
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
|
||||
@@ -433,7 +425,9 @@ struct DisplayMaybeParenthesizedType<'db> {
|
||||
|
||||
impl Display for DisplayMaybeParenthesizedType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Type::Callable(_) | Type::MethodWrapper(_) = self.ty {
|
||||
if let Type::Callable(CallableType::General(_) | CallableType::MethodWrapperDunderGet(_)) =
|
||||
self.ty
|
||||
{
|
||||
write!(f, "({})", self.ty.display(self.db))
|
||||
} else {
|
||||
self.ty.display(self.db).fmt(f)
|
||||
|
||||
@@ -51,10 +51,11 @@ use crate::semantic_index::definition::{
|
||||
ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind, WithItemDefinitionKind,
|
||||
};
|
||||
use crate::semantic_index::expression::{Expression, ExpressionKind};
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::{
|
||||
FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind,
|
||||
};
|
||||
use crate::semantic_index::{semantic_index, EagerBindingsResult, SemanticIndex};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::symbol::{
|
||||
builtins_module_scope, builtins_symbol, explicit_global_symbol,
|
||||
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||
@@ -76,25 +77,26 @@ use crate::types::diagnostic::{
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, IntersectionBuilder,
|
||||
IntersectionType, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, Parameter,
|
||||
ParameterForm, Parameters, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers,
|
||||
Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay,
|
||||
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
|
||||
class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, InstanceType,
|
||||
IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType,
|
||||
MetaclassCandidate, Parameter, ParameterForm, Parameters, SliceLiteralType, SubclassOfType,
|
||||
Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
|
||||
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
|
||||
UnionType,
|
||||
};
|
||||
use crate::types::{CallableType, FunctionDecorators, Signature};
|
||||
use crate::unpack::{Unpack, UnpackPosition};
|
||||
use crate::types::{CallableType, GeneralCallableType, Signature};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
use crate::Db;
|
||||
|
||||
use super::class_base::ClassBase;
|
||||
use super::context::{InNoTypeCheck, InferContext};
|
||||
use super::context::{InNoTypeCheck, InferContext, WithDiagnostics};
|
||||
use super::diagnostic::{
|
||||
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
|
||||
report_invalid_exception_raised, report_invalid_type_checking_constant,
|
||||
report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero,
|
||||
report_unresolved_reference, INVALID_METACLASS, REDUNDANT_CAST, STATIC_ASSERT_ERROR,
|
||||
SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
|
||||
report_unresolved_reference, INVALID_METACLASS, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS,
|
||||
TYPE_ASSERTION_FAILURE,
|
||||
};
|
||||
use super::slots::check_class_slots;
|
||||
use super::string_annotation::{
|
||||
@@ -439,6 +441,12 @@ impl<'db> TypeInference<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
impl WithDiagnostics for TypeInference<'_> {
|
||||
fn diagnostics(&self) -> &TypeCheckDiagnostics {
|
||||
&self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the intersection type is on the left or right side of the comparison.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum IntersectionOn {
|
||||
@@ -561,7 +569,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.extend(inference.declarations.iter());
|
||||
self.types.expressions.extend(inference.expressions.iter());
|
||||
self.types.deferred.extend(inference.deferred.iter());
|
||||
self.context.extend(inference.diagnostics());
|
||||
self.context.extend(inference);
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
@@ -971,12 +979,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
/// Raise a diagnostic if the given type cannot be divided by zero.
|
||||
///
|
||||
/// Expects the resolved type of the left side of the binary expression.
|
||||
fn check_division_by_zero(
|
||||
&mut self,
|
||||
node: AnyNodeRef<'_>,
|
||||
op: ast::Operator,
|
||||
left: Type<'db>,
|
||||
) -> bool {
|
||||
fn check_division_by_zero(&mut self, expr: &ast::ExprBinOp, left: Type<'db>) {
|
||||
match left {
|
||||
Type::BooleanLiteral(_) | Type::IntLiteral(_) => {}
|
||||
Type::Instance(instance)
|
||||
@@ -984,26 +987,24 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
instance.class().known(self.db()),
|
||||
Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool)
|
||||
) => {}
|
||||
_ => return false,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let (op, by_zero) = match op {
|
||||
let (op, by_zero) = match expr.op {
|
||||
ast::Operator::Div => ("divide", "by zero"),
|
||||
ast::Operator::FloorDiv => ("floor divide", "by zero"),
|
||||
ast::Operator::Mod => ("reduce", "modulo zero"),
|
||||
_ => return false,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
self.context.report_lint(
|
||||
&DIVISION_BY_ZERO,
|
||||
node,
|
||||
expr,
|
||||
format_args!(
|
||||
"Cannot {op} object of type `{}` {by_zero}",
|
||||
left.display(self.db())
|
||||
),
|
||||
);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn add_binding(&mut self, node: AnyNodeRef, binding: Definition<'db>, ty: Type<'db>) {
|
||||
@@ -1265,21 +1266,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for invalid in self
|
||||
.return_types_and_ranges
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|ty_range| match ty_range.ty {
|
||||
// We skip `is_assignable_to` checks for `NotImplemented`,
|
||||
// so we remove it beforehand.
|
||||
Type::Union(union) => Some(TypeAndRange {
|
||||
ty: union.filter(self.db(), |ty| !ty.is_notimplemented(self.db())),
|
||||
range: ty_range.range,
|
||||
}),
|
||||
ty if ty.is_notimplemented(self.db()) => None,
|
||||
_ => Some(ty_range),
|
||||
})
|
||||
.filter(|ty_range| !ty_range.ty.is_assignable_to(self.db(), declared_ty))
|
||||
{
|
||||
report_invalid_return_type(
|
||||
@@ -1374,31 +1363,19 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
decorator_list,
|
||||
} = function;
|
||||
|
||||
let mut decorator_types_and_nodes = Vec::with_capacity(decorator_list.len());
|
||||
let mut function_decorators = FunctionDecorators::empty();
|
||||
// Check if the function is decorated with the `no_type_check` decorator
|
||||
// and, if so, suppress any errors that come after the decorators.
|
||||
let mut decorator_tys = Vec::with_capacity(decorator_list.len());
|
||||
|
||||
for decorator in decorator_list {
|
||||
let decorator_ty = self.infer_decorator(decorator);
|
||||
let ty = self.infer_decorator(decorator);
|
||||
decorator_tys.push(ty);
|
||||
|
||||
if let Type::FunctionLiteral(function) = decorator_ty {
|
||||
if let Type::FunctionLiteral(function) = ty {
|
||||
if function.is_known(self.db(), KnownFunction::NoTypeCheck) {
|
||||
// If the function is decorated with the `no_type_check` decorator,
|
||||
// we need to suppress any errors that come after the decorators.
|
||||
self.context.set_in_no_type_check(InNoTypeCheck::Yes);
|
||||
function_decorators |= FunctionDecorators::NO_TYPE_CHECK;
|
||||
continue;
|
||||
} else if function.is_known(self.db(), KnownFunction::Overload) {
|
||||
function_decorators |= FunctionDecorators::OVERLOAD;
|
||||
continue;
|
||||
}
|
||||
} else if let Type::ClassLiteral(class) = decorator_ty {
|
||||
if class.class.is_known(self.db(), KnownClass::Classmethod) {
|
||||
function_decorators |= FunctionDecorators::CLASSMETHOD;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
decorator_types_and_nodes.push((decorator_ty, decorator));
|
||||
}
|
||||
|
||||
for default in parameters
|
||||
@@ -1430,31 +1407,18 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.node_scope(NodeWithScopeRef::Function(function))
|
||||
.to_scope_id(self.db(), self.file());
|
||||
|
||||
let mut inferred_ty = Type::FunctionLiteral(FunctionType::new(
|
||||
let function_ty = Type::FunctionLiteral(FunctionType::new(
|
||||
self.db(),
|
||||
&name.id,
|
||||
function_kind,
|
||||
body_scope,
|
||||
function_decorators,
|
||||
decorator_tys.into_boxed_slice(),
|
||||
));
|
||||
|
||||
for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() {
|
||||
inferred_ty = match decorator_ty
|
||||
.try_call(self.db(), CallArgumentTypes::positional([inferred_ty]))
|
||||
.map(|bindings| bindings.return_type(self.db()))
|
||||
{
|
||||
Ok(return_ty) => return_ty,
|
||||
Err(CallError(_, bindings)) => {
|
||||
bindings.report_diagnostics(&self.context, (*decorator_node).into());
|
||||
bindings.return_type(self.db())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
self.add_declaration_with_binding(
|
||||
function.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(inferred_ty),
|
||||
&DeclaredAndInferredType::AreTheSame(function_ty),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1860,11 +1824,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
todo_type!("async `with` statement")
|
||||
} else {
|
||||
match with_item.target() {
|
||||
TargetKind::Sequence(unpack_position, unpack) => {
|
||||
TargetKind::Sequence(unpack) => {
|
||||
let unpacked = infer_unpack_types(self.db(), unpack);
|
||||
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
|
||||
if unpack_position == UnpackPosition::First {
|
||||
self.context.extend(unpacked.diagnostics());
|
||||
if with_item.is_first() {
|
||||
self.context.extend(unpacked);
|
||||
}
|
||||
unpacked.expression_type(name_ast_id)
|
||||
}
|
||||
@@ -2017,7 +1981,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new(
|
||||
self.db(),
|
||||
name.id.clone(),
|
||||
definition,
|
||||
bound_or_constraint,
|
||||
default_ty,
|
||||
)));
|
||||
@@ -2156,11 +2119,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
self.infer_standalone_expression(cls);
|
||||
}
|
||||
ast::Pattern::MatchOr(match_or) => {
|
||||
for pattern in &match_or.patterns {
|
||||
self.infer_match_pattern(pattern);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.infer_nested_match_pattern(pattern);
|
||||
}
|
||||
@@ -2338,12 +2296,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
| Type::SliceLiteral(..)
|
||||
| Type::Tuple(..)
|
||||
| Type::KnownInstance(..)
|
||||
| Type::PropertyInstance(..)
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::Callable(..)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy => match object_ty.class_member(db, attribute.into()) {
|
||||
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
|
||||
@@ -2669,12 +2623,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let value_ty = self.infer_standalone_expression(value);
|
||||
|
||||
let mut target_ty = match assignment.target() {
|
||||
TargetKind::Sequence(unpack_position, unpack) => {
|
||||
TargetKind::Sequence(unpack) => {
|
||||
let unpacked = infer_unpack_types(self.db(), unpack);
|
||||
// Only copy the diagnostics if this is the first assignment to avoid duplicating the
|
||||
// unpack assignments.
|
||||
if unpack_position == UnpackPosition::First {
|
||||
self.context.extend(unpacked.diagnostics());
|
||||
if assignment.is_first() {
|
||||
self.context.extend(unpacked);
|
||||
}
|
||||
|
||||
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
|
||||
@@ -2865,7 +2819,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
// Fall back to non-augmented binary operator inference.
|
||||
let mut binary_return_ty = || {
|
||||
self.infer_binary_expression_type(assignment.into(), false, target_type, value_type, op)
|
||||
self.infer_binary_expression_type(target_type, value_type, op)
|
||||
.unwrap_or_else(|| {
|
||||
report_unsupported_augmented_op(&mut self.context);
|
||||
Type::unknown()
|
||||
@@ -2971,10 +2925,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
todo_type!("async iterables/iterators")
|
||||
} else {
|
||||
match for_stmt.target() {
|
||||
TargetKind::Sequence(unpack_position, unpack) => {
|
||||
TargetKind::Sequence(unpack) => {
|
||||
let unpacked = infer_unpack_types(self.db(), unpack);
|
||||
if unpack_position == UnpackPosition::First {
|
||||
self.context.extend(unpacked.diagnostics());
|
||||
if for_stmt.is_first() {
|
||||
self.context.extend(unpacked);
|
||||
}
|
||||
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
|
||||
unpacked.expression_type(name_ast_id)
|
||||
@@ -3626,7 +3580,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
self.infer_first_comprehension_iter(generators);
|
||||
|
||||
todo_type!("generator type")
|
||||
// TODO generator type
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_list_comprehension_expression(&mut self, listcomp: &ast::ExprListComp) -> Type<'db> {
|
||||
@@ -3638,7 +3593,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
self.infer_first_comprehension_iter(generators);
|
||||
|
||||
todo_type!("list comprehension type")
|
||||
// TODO list type
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_dict_comprehension_expression(&mut self, dictcomp: &ast::ExprDictComp) -> Type<'db> {
|
||||
@@ -3651,7 +3607,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
self.infer_first_comprehension_iter(generators);
|
||||
|
||||
todo_type!("dict comprehension type")
|
||||
// TODO dict type
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_set_comprehension_expression(&mut self, setcomp: &ast::ExprSetComp) -> Type<'db> {
|
||||
@@ -3663,7 +3620,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
self.infer_first_comprehension_iter(generators);
|
||||
|
||||
todo_type!("set comprehension type")
|
||||
// TODO set type
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_generator_expression_scope(&mut self, generator: &ast::ExprGenerator) {
|
||||
@@ -3918,10 +3876,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// TODO: Useful inference of a lambda's return type will require a different approach,
|
||||
// which does the inference of the body expression based on arguments at each call site,
|
||||
// rather than eagerly computing a return type without knowing the argument types.
|
||||
Type::Callable(CallableType::new(
|
||||
Type::Callable(CallableType::General(GeneralCallableType::new(
|
||||
self.db(),
|
||||
Signature::new(parameters, Some(Type::unknown())),
|
||||
))
|
||||
)))
|
||||
}
|
||||
|
||||
fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> {
|
||||
@@ -3967,7 +3925,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
"Revealed type is `{}`",
|
||||
revealed_type.display(self.db())
|
||||
),
|
||||
&[],
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4048,22 +4006,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
KnownFunction::Cast => {
|
||||
if let [Some(casted_ty), Some(source_ty)] = overload.parameter_types() {
|
||||
if source_ty.is_gradual_equivalent_to(self.context.db(), *casted_ty)
|
||||
&& !source_ty.contains_todo(self.context.db())
|
||||
{
|
||||
self.context.report_lint(
|
||||
&REDUNDANT_CAST,
|
||||
call_expression,
|
||||
format_args!(
|
||||
"Value is already of type `{}`",
|
||||
casted_ty.display(self.context.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -4184,15 +4126,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// Class scopes are not visible to nested scopes, and we need to handle global
|
||||
// scope differently (because an unbound name there falls back to builtins), so
|
||||
// check only function-like scopes.
|
||||
// There is one exception to this rule: type parameter scopes can see
|
||||
// names defined in an immediately-enclosing class scope.
|
||||
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file);
|
||||
let is_immediately_enclosing_scope = scope.is_type_parameter(db)
|
||||
&& scope
|
||||
.scope(db)
|
||||
.parent()
|
||||
.is_some_and(|parent| parent == enclosing_scope_file_id);
|
||||
if !enclosing_scope_id.is_function_like(db) && !is_immediately_enclosing_scope {
|
||||
if !enclosing_scope_id.is_function_like(db) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4203,20 +4138,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// enclosing scopes that actually contain bindings that we should use when
|
||||
// resolving the reference.)
|
||||
if !self.is_deferred() {
|
||||
match self.index.eager_bindings(
|
||||
if let Some(bindings) = self.index.eager_bindings(
|
||||
enclosing_scope_file_id,
|
||||
symbol_name,
|
||||
file_scope_id,
|
||||
) {
|
||||
EagerBindingsResult::Found(bindings) => {
|
||||
return symbol_from_bindings(db, bindings).into();
|
||||
}
|
||||
// There are no visible bindings here.
|
||||
// Don't fall back to non-eager symbol resolution.
|
||||
EagerBindingsResult::NotFound => {
|
||||
continue;
|
||||
}
|
||||
EagerBindingsResult::NoLongerInEagerContext => {}
|
||||
return symbol_from_bindings(db, bindings).into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4244,19 +4171,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
|
||||
if !self.is_deferred() {
|
||||
match self.index.eager_bindings(
|
||||
if let Some(bindings) = self.index.eager_bindings(
|
||||
FileScopeId::global(),
|
||||
symbol_name,
|
||||
file_scope_id,
|
||||
) {
|
||||
EagerBindingsResult::Found(bindings) => {
|
||||
return symbol_from_bindings(db, bindings).into();
|
||||
}
|
||||
// There are no visible bindings here.
|
||||
EagerBindingsResult::NotFound => {
|
||||
return Symbol::Unbound.into();
|
||||
}
|
||||
EagerBindingsResult::NoLongerInEagerContext => {}
|
||||
return symbol_from_bindings(db, bindings).into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4441,15 +4361,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert),
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::Callable(..)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::SubclassOf(_)
|
||||
| Type::Instance(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::Union(_)
|
||||
| Type::Intersection(_)
|
||||
| Type::AlwaysTruthy
|
||||
@@ -4502,7 +4418,19 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let left_ty = self.infer_expression(left);
|
||||
let right_ty = self.infer_expression(right);
|
||||
|
||||
self.infer_binary_expression_type(binary.into(), false, left_ty, right_ty, *op)
|
||||
// Check for division by zero; this doesn't change the inferred type for the expression, but
|
||||
// may emit a diagnostic
|
||||
if matches!(
|
||||
(op, right_ty),
|
||||
(
|
||||
ast::Operator::Div | ast::Operator::FloorDiv | ast::Operator::Mod,
|
||||
Type::IntLiteral(0) | Type::BooleanLiteral(false)
|
||||
)
|
||||
) {
|
||||
self.check_division_by_zero(binary, left_ty);
|
||||
}
|
||||
|
||||
self.infer_binary_expression_type(left_ty, right_ty, *op)
|
||||
.unwrap_or_else(|| {
|
||||
self.context.report_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
@@ -4519,37 +4447,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
fn infer_binary_expression_type(
|
||||
&mut self,
|
||||
node: AnyNodeRef<'_>,
|
||||
mut emitted_division_by_zero_diagnostic: bool,
|
||||
left_ty: Type<'db>,
|
||||
right_ty: Type<'db>,
|
||||
op: ast::Operator,
|
||||
) -> Option<Type<'db>> {
|
||||
// Check for division by zero; this doesn't change the inferred type for the expression, but
|
||||
// may emit a diagnostic
|
||||
if !emitted_division_by_zero_diagnostic
|
||||
&& matches!(
|
||||
(op, right_ty),
|
||||
(
|
||||
ast::Operator::Div | ast::Operator::FloorDiv | ast::Operator::Mod,
|
||||
Type::IntLiteral(0) | Type::BooleanLiteral(false)
|
||||
)
|
||||
)
|
||||
{
|
||||
emitted_division_by_zero_diagnostic = self.check_division_by_zero(node, op, left_ty);
|
||||
}
|
||||
|
||||
match (left_ty, right_ty, op) {
|
||||
(Type::Union(lhs_union), rhs, _) => {
|
||||
let mut union = UnionBuilder::new(self.db());
|
||||
for lhs in lhs_union.elements(self.db()) {
|
||||
let result = self.infer_binary_expression_type(
|
||||
node,
|
||||
emitted_division_by_zero_diagnostic,
|
||||
*lhs,
|
||||
rhs,
|
||||
op,
|
||||
)?;
|
||||
let result = self.infer_binary_expression_type(*lhs, rhs, op)?;
|
||||
union = union.add(result);
|
||||
}
|
||||
Some(union.build())
|
||||
@@ -4557,13 +4463,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
(lhs, Type::Union(rhs_union), _) => {
|
||||
let mut union = UnionBuilder::new(self.db());
|
||||
for rhs in rhs_union.elements(self.db()) {
|
||||
let result = self.infer_binary_expression_type(
|
||||
node,
|
||||
emitted_division_by_zero_diagnostic,
|
||||
lhs,
|
||||
*rhs,
|
||||
op,
|
||||
)?;
|
||||
let result = self.infer_binary_expression_type(lhs, *rhs, op)?;
|
||||
union = union.add(result);
|
||||
}
|
||||
Some(union.build())
|
||||
@@ -4615,17 +4515,16 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.unwrap_or_else(|| KnownClass::Int.to_instance(self.db())),
|
||||
),
|
||||
|
||||
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Pow) => Some({
|
||||
if m < 0 {
|
||||
KnownClass::Float.to_instance(self.db())
|
||||
} else {
|
||||
u32::try_from(m)
|
||||
.ok()
|
||||
.and_then(|m| n.checked_pow(m))
|
||||
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Pow) => {
|
||||
let m = u32::try_from(m);
|
||||
Some(match m {
|
||||
Ok(m) => n
|
||||
.checked_pow(m)
|
||||
.map(Type::IntLiteral)
|
||||
.unwrap_or_else(|| KnownClass::Int.to_instance(self.db()))
|
||||
}
|
||||
}),
|
||||
.unwrap_or_else(|| KnownClass::Int.to_instance(self.db())),
|
||||
Err(_) => KnownClass::Int.to_instance(self.db()),
|
||||
})
|
||||
}
|
||||
|
||||
(Type::BytesLiteral(lhs), Type::BytesLiteral(rhs), ast::Operator::Add) => {
|
||||
let bytes = [&**lhs.value(self.db()), &**rhs.value(self.db())].concat();
|
||||
@@ -4683,19 +4582,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
|
||||
(Type::BooleanLiteral(bool_value), right, op) => self.infer_binary_expression_type(
|
||||
node,
|
||||
emitted_division_by_zero_diagnostic,
|
||||
Type::IntLiteral(i64::from(bool_value)),
|
||||
right,
|
||||
op,
|
||||
),
|
||||
(left, Type::BooleanLiteral(bool_value), op) => self.infer_binary_expression_type(
|
||||
node,
|
||||
emitted_division_by_zero_diagnostic,
|
||||
left,
|
||||
Type::IntLiteral(i64::from(bool_value)),
|
||||
op,
|
||||
),
|
||||
(left, Type::BooleanLiteral(bool_value), op) => {
|
||||
self.infer_binary_expression_type(left, Type::IntLiteral(i64::from(bool_value)), op)
|
||||
}
|
||||
|
||||
(Type::Tuple(lhs), Type::Tuple(rhs), ast::Operator::Add) => {
|
||||
// Note: this only works on heterogeneous tuples.
|
||||
@@ -4716,15 +4609,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
(
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::Callable(..)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::SubclassOf(_)
|
||||
| Type::Instance(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::Intersection(_)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy
|
||||
@@ -4736,15 +4625,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
| Type::Tuple(_),
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::Callable(..)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::SubclassOf(_)
|
||||
| Type::Instance(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::Intersection(_)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy
|
||||
@@ -5399,11 +5284,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup the rich comparison `__dunder__` methods
|
||||
_ => {
|
||||
let rich_comparison = |op| self.infer_rich_comparison(left, right, op);
|
||||
// Lookup the rich comparison `__dunder__` methods on instances
|
||||
(Type::Instance(left_instance), Type::Instance(right_instance)) => {
|
||||
let rich_comparison =
|
||||
|op| self.infer_rich_comparison(left_instance, right_instance, op);
|
||||
let membership_test_comparison = |op, range: TextRange| {
|
||||
self.infer_membership_test_comparison(left, right, op, range)
|
||||
self.infer_membership_test_comparison(left_instance, right_instance, op, range)
|
||||
};
|
||||
match op {
|
||||
ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq),
|
||||
@@ -5442,27 +5328,37 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => match op {
|
||||
ast::CmpOp::Is | ast::CmpOp::IsNot => Ok(KnownClass::Bool.to_instance(self.db())),
|
||||
_ => Ok(todo_type!("Binary comparisons between more types")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Rich comparison in Python are the operators `==`, `!=`, `<`, `<=`, `>`, and `>=`. Their
|
||||
/// behaviour can be edited for classes by implementing corresponding dunder methods.
|
||||
/// This function performs rich comparison between two types and returns the resulting type.
|
||||
/// This function performs rich comparison between two instances and returns the resulting type.
|
||||
/// see `<https://docs.python.org/3/reference/datamodel.html#object.__lt__>`
|
||||
fn infer_rich_comparison(
|
||||
&self,
|
||||
left: Type<'db>,
|
||||
right: Type<'db>,
|
||||
left: InstanceType<'db>,
|
||||
right: InstanceType<'db>,
|
||||
op: RichCompareOperator,
|
||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||
let db = self.db();
|
||||
// The following resource has details about the rich comparison algorithm:
|
||||
// https://snarky.ca/unravelling-rich-comparison-operators/
|
||||
let call_dunder = |op: RichCompareOperator, left: Type<'db>, right: Type<'db>| {
|
||||
left.try_call_dunder(db, op.dunder(), CallArgumentTypes::positional([right]))
|
||||
.map(|outcome| outcome.return_type(db))
|
||||
.ok()
|
||||
};
|
||||
let call_dunder =
|
||||
|op: RichCompareOperator, left: InstanceType<'db>, right: InstanceType<'db>| {
|
||||
Type::Instance(left)
|
||||
.try_call_dunder(
|
||||
db,
|
||||
op.dunder(),
|
||||
CallArgumentTypes::positional([Type::Instance(right)]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(db))
|
||||
.ok()
|
||||
};
|
||||
|
||||
// The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side.
|
||||
if left != right && right.is_subtype_of(db, left) {
|
||||
@@ -5482,8 +5378,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
})
|
||||
.ok_or_else(|| CompareUnsupportedError {
|
||||
op: op.into(),
|
||||
left_ty: left,
|
||||
right_ty: right,
|
||||
left_ty: left.into(),
|
||||
right_ty: right.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5493,25 +5389,31 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
/// and `<https://docs.python.org/3/reference/expressions.html#membership-test-details>`
|
||||
fn infer_membership_test_comparison(
|
||||
&self,
|
||||
left: Type<'db>,
|
||||
right: Type<'db>,
|
||||
left: InstanceType<'db>,
|
||||
right: InstanceType<'db>,
|
||||
op: MembershipTestCompareOperator,
|
||||
range: TextRange,
|
||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||
let db = self.db();
|
||||
|
||||
let contains_dunder = right.class_member(db, "__contains__".into()).symbol;
|
||||
let contains_dunder = right.class().class_member(db, "__contains__").symbol;
|
||||
let compare_result_opt = match contains_dunder {
|
||||
Symbol::Type(contains_dunder, Boundness::Bound) => {
|
||||
// If `__contains__` is available, it is used directly for the membership test.
|
||||
contains_dunder
|
||||
.try_call(db, CallArgumentTypes::positional([right, left]))
|
||||
.try_call(
|
||||
db,
|
||||
CallArgumentTypes::positional([
|
||||
Type::Instance(right),
|
||||
Type::Instance(left),
|
||||
]),
|
||||
)
|
||||
.map(|bindings| bindings.return_type(db))
|
||||
.ok()
|
||||
}
|
||||
_ => {
|
||||
// iteration-based membership test
|
||||
right
|
||||
Type::Instance(right)
|
||||
.try_iterate(db)
|
||||
.map(|_| KnownClass::Bool.to_instance(db))
|
||||
.ok()
|
||||
@@ -5536,8 +5438,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
})
|
||||
.ok_or_else(|| CompareUnsupportedError {
|
||||
op: op.into(),
|
||||
left_ty: left,
|
||||
right_ty: right,
|
||||
left_ty: left.into(),
|
||||
right_ty: right.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5641,20 +5543,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
slice_ty: Type<'db>,
|
||||
) -> Type<'db> {
|
||||
match (value_ty, slice_ty) {
|
||||
(
|
||||
Type::Instance(instance),
|
||||
Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::SliceLiteral(_),
|
||||
) if instance
|
||||
.class()
|
||||
.is_known(self.db(), KnownClass::VersionInfo) =>
|
||||
{
|
||||
self.infer_subscript_expression_types(
|
||||
value_node,
|
||||
Type::version_info_tuple(self.db()),
|
||||
slice_ty,
|
||||
)
|
||||
}
|
||||
|
||||
// Ex) Given `("a", "b", "c", "d")[1]`, return `"b"`
|
||||
(Type::Tuple(tuple_ty), Type::IntLiteral(int)) if i32::try_from(int).is_ok() => {
|
||||
let elements = tuple_ty.elements(self.db());
|
||||
@@ -5796,6 +5684,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
return err.fallback_return_type(self.db());
|
||||
}
|
||||
Err(CallDunderError::CallError(_, bindings)) => {
|
||||
bindings.report_diagnostics(&self.context, value_node.into());
|
||||
self.context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node,
|
||||
@@ -6039,9 +5928,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
ast::ExprContext::Invalid => TypeAndQualifiers::unknown(),
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => {
|
||||
todo_type!("Name expression annotation in Store/Del context").into()
|
||||
}
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!().into(),
|
||||
},
|
||||
|
||||
ast::Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => {
|
||||
@@ -6198,9 +6085,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.in_type_expression(self.db())
|
||||
.unwrap_or_else(|error| error.into_fallback_type(&self.context, expression)),
|
||||
ast::ExprContext::Invalid => Type::unknown(),
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => {
|
||||
todo_type!("Name expression annotation in Store/Del context")
|
||||
}
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(),
|
||||
},
|
||||
|
||||
ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
|
||||
@@ -6209,9 +6094,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.in_type_expression(self.db())
|
||||
.unwrap_or_else(|error| error.into_fallback_type(&self.context, expression)),
|
||||
ast::ExprContext::Invalid => Type::unknown(),
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => {
|
||||
todo_type!("Attribute expression annotation in Store/Del context")
|
||||
}
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(),
|
||||
},
|
||||
|
||||
ast::Expr::NoneLiteral(_literal) => Type::none(self.db()),
|
||||
@@ -6517,25 +6400,14 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
/// homogeneous tuple and a partly homogeneous tuple (respectively) due to the `...`
|
||||
/// and the starred expression (respectively), Neither is supported by us right now,
|
||||
/// so we should infer `Todo` for the *entire* tuple if we encounter one of those elements.
|
||||
fn element_could_alter_type_of_whole_tuple(
|
||||
element: &ast::Expr,
|
||||
element_ty: Type,
|
||||
builder: &TypeInferenceBuilder,
|
||||
) -> bool {
|
||||
if !element_ty.is_todo() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match element {
|
||||
ast::Expr::EllipsisLiteral(_) | ast::Expr::Starred(_) => true,
|
||||
ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => {
|
||||
matches!(
|
||||
builder.expression_type(value),
|
||||
Type::KnownInstance(KnownInstanceType::Unpack)
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
/// Even a subscript subelement could alter the type of the entire tuple
|
||||
/// if the subscript is `Unpack[]` (which again, we don't yet support).
|
||||
fn element_could_alter_type_of_whole_tuple(element: &ast::Expr, element_ty: Type) -> bool {
|
||||
element_ty.is_todo()
|
||||
&& matches!(
|
||||
element,
|
||||
ast::Expr::EllipsisLiteral(_) | ast::Expr::Starred(_) | ast::Expr::Subscript(_)
|
||||
)
|
||||
}
|
||||
|
||||
// TODO:
|
||||
@@ -6551,8 +6423,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
for element in elements {
|
||||
let element_ty = self.infer_type_expression(element);
|
||||
return_todo |=
|
||||
element_could_alter_type_of_whole_tuple(element, element_ty, self);
|
||||
return_todo |= element_could_alter_type_of_whole_tuple(element, element_ty);
|
||||
element_types.push(element_ty);
|
||||
}
|
||||
|
||||
@@ -6571,8 +6442,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
single_element => {
|
||||
let single_element_ty = self.infer_type_expression(single_element);
|
||||
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty, self)
|
||||
{
|
||||
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty) {
|
||||
todo_type!("full tuple[...] support")
|
||||
} else {
|
||||
TupleType::from_elements(self.db(), std::iter::once(single_element_ty))
|
||||
@@ -6588,14 +6458,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let name_ty = self.infer_expression(slice);
|
||||
match name_ty {
|
||||
Type::ClassLiteral(class_literal_ty) => {
|
||||
if class_literal_ty
|
||||
.class()
|
||||
.is_known(self.db(), KnownClass::Any)
|
||||
{
|
||||
SubclassOfType::subclass_of_any()
|
||||
} else {
|
||||
SubclassOfType::from(self.db(), class_literal_ty.class())
|
||||
}
|
||||
SubclassOfType::from(self.db(), class_literal_ty.class())
|
||||
}
|
||||
Type::KnownInstance(KnownInstanceType::Any) => {
|
||||
SubclassOfType::subclass_of_any()
|
||||
@@ -6675,14 +6538,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
} = subscript;
|
||||
|
||||
match value_ty {
|
||||
Type::ClassLiteral(literal) if literal.class().is_known(self.db(), KnownClass::Any) => {
|
||||
self.context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
subscript,
|
||||
format_args!("Type `typing.Any` expected no type parameter",),
|
||||
);
|
||||
Type::unknown()
|
||||
}
|
||||
Type::KnownInstance(known_instance) => {
|
||||
self.infer_parameterized_known_instance_type_expression(subscript, known_instance)
|
||||
}
|
||||
@@ -6810,12 +6665,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let callable_type = if let (Some(parameters), Some(return_type), true) =
|
||||
(parameters, return_type, correct_argument_number)
|
||||
{
|
||||
CallableType::new(db, Signature::new(parameters, Some(return_type)))
|
||||
GeneralCallableType::new(db, Signature::new(parameters, Some(return_type)))
|
||||
} else {
|
||||
CallableType::unknown(db)
|
||||
GeneralCallableType::unknown(db)
|
||||
};
|
||||
|
||||
let callable_type = Type::Callable(callable_type);
|
||||
let callable_type = Type::Callable(CallableType::General(callable_type));
|
||||
|
||||
// `Signature` / `Parameters` are not a `Type` variant, so we're storing
|
||||
// the outer callable type on the these expressions instead.
|
||||
@@ -6875,7 +6730,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
argument_type
|
||||
}
|
||||
},
|
||||
KnownInstanceType::CallableTypeOf => match arguments_slice {
|
||||
KnownInstanceType::CallableTypeFromFunction => match arguments_slice {
|
||||
ast::Expr::Tuple(_) => {
|
||||
self.context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
@@ -6889,23 +6744,19 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
_ => {
|
||||
let argument_type = self.infer_expression(arguments_slice);
|
||||
let signatures = argument_type.signatures(db);
|
||||
|
||||
// TODO overloads
|
||||
let Some(signature) = signatures.iter().flatten().next() else {
|
||||
let Some(function_type) = argument_type.into_function_literal() else {
|
||||
self.context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
arguments_slice,
|
||||
format_args!(
|
||||
"Expected the first argument to `{}` to be a callable object, but got an object of type `{}`",
|
||||
"Expected the first argument to `{}` to be a function literal, but got `{}`",
|
||||
known_instance.repr(db),
|
||||
argument_type.display(db)
|
||||
),
|
||||
);
|
||||
return Type::unknown();
|
||||
};
|
||||
|
||||
Type::Callable(CallableType::new(db, signature.clone()))
|
||||
function_type.into_callable_type(db)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7373,7 +7224,6 @@ mod tests {
|
||||
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
|
||||
use crate::symbol::global_symbol;
|
||||
use crate::types::check_types;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::DbWithWritableSystem as _;
|
||||
use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run};
|
||||
@@ -7408,7 +7258,7 @@ mod tests {
|
||||
fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) {
|
||||
let messages: Vec<&str> = diagnostics
|
||||
.iter()
|
||||
.map(Diagnostic::primary_message)
|
||||
.map(|diagnostic| diagnostic.message())
|
||||
.collect();
|
||||
assert_eq!(&messages, expected);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ use rustc_hash::FxHashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::UnionType;
|
||||
|
||||
/// Return the type constraint that `test` (if true) would place on `definition`, if any.
|
||||
///
|
||||
/// For example, if we have this code:
|
||||
@@ -113,15 +111,7 @@ impl KnownConstraintFunction {
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
Type::ClassLiteral(class_literal) => {
|
||||
// At runtime (on Python 3.11+), this will return `True` for classes that actually
|
||||
// do inherit `typing.Any` and `False` otherwise. We could accurately model that?
|
||||
if class_literal.class().is_known(db, KnownClass::Any) {
|
||||
None
|
||||
} else {
|
||||
Some(constraint_fn(class_literal.class()))
|
||||
}
|
||||
}
|
||||
Type::ClassLiteral(class_literal) => Some(constraint_fn(class_literal.class())),
|
||||
Type::SubclassOf(subclass_of_ty) => {
|
||||
subclass_of_ty.subclass_of().into_class().map(constraint_fn)
|
||||
}
|
||||
@@ -235,31 +225,22 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_pattern_predicate_kind(
|
||||
&mut self,
|
||||
pattern_predicate_kind: &PatternPredicateKind<'db>,
|
||||
subject: Expression<'db>,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
match pattern_predicate_kind {
|
||||
PatternPredicateKind::Singleton(singleton) => {
|
||||
self.evaluate_match_pattern_singleton(subject, *singleton)
|
||||
}
|
||||
PatternPredicateKind::Class(cls) => self.evaluate_match_pattern_class(subject, *cls),
|
||||
PatternPredicateKind::Value(expr) => self.evaluate_match_pattern_value(subject, *expr),
|
||||
PatternPredicateKind::Or(predicates) => {
|
||||
self.evaluate_match_pattern_or(subject, predicates)
|
||||
}
|
||||
PatternPredicateKind::Unsupported => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_pattern_predicate(
|
||||
&mut self,
|
||||
pattern: PatternPredicate<'db>,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let subject = pattern.subject(self.db);
|
||||
|
||||
self.evaluate_pattern_predicate_kind(pattern.kind(self.db), subject)
|
||||
match pattern.kind(self.db) {
|
||||
PatternPredicateKind::Singleton(singleton, _guard) => {
|
||||
self.evaluate_match_pattern_singleton(subject, *singleton)
|
||||
}
|
||||
PatternPredicateKind::Class(cls, _guard) => {
|
||||
self.evaluate_match_pattern_class(subject, *cls)
|
||||
}
|
||||
// TODO: support more pattern kinds
|
||||
PatternPredicateKind::Value(..) | PatternPredicateKind::Unsupported => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn symbols(&self) -> Arc<SymbolTable> {
|
||||
@@ -273,13 +254,6 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn expect_expr_name_symbol(&self, symbol: &str) -> ScopedSymbolId {
|
||||
self.symbols()
|
||||
.symbol_id_by_name(symbol)
|
||||
.expect("We should always have a symbol for every `Name` node")
|
||||
}
|
||||
|
||||
fn evaluate_expr_name(
|
||||
&mut self,
|
||||
expr_name: &ast::ExprName,
|
||||
@@ -287,37 +261,22 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
) -> NarrowingConstraints<'db> {
|
||||
let ast::ExprName { id, .. } = expr_name;
|
||||
|
||||
let symbol = self.expect_expr_name_symbol(id);
|
||||
let symbol = self
|
||||
.symbols()
|
||||
.symbol_id_by_name(id)
|
||||
.expect("Should always have a symbol for every Name node");
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
|
||||
let ty = if is_positive {
|
||||
Type::AlwaysFalsy.negate(self.db)
|
||||
} else {
|
||||
Type::AlwaysTruthy.negate(self.db)
|
||||
};
|
||||
constraints.insert(
|
||||
symbol,
|
||||
if is_positive {
|
||||
Type::AlwaysFalsy.negate(self.db)
|
||||
} else {
|
||||
Type::AlwaysTruthy.negate(self.db)
|
||||
},
|
||||
);
|
||||
|
||||
NarrowingConstraints::from_iter([(symbol, ty)])
|
||||
}
|
||||
|
||||
fn evaluate_expr_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option<Type<'db>> {
|
||||
if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) {
|
||||
match rhs_ty {
|
||||
Type::Tuple(rhs_tuple) => Some(UnionType::from_elements(
|
||||
self.db,
|
||||
rhs_tuple.elements(self.db),
|
||||
)),
|
||||
|
||||
Type::StringLiteral(string_literal) => Some(UnionType::from_elements(
|
||||
self.db,
|
||||
string_literal
|
||||
.iter_each_char(self.db)
|
||||
.map(Type::StringLiteral),
|
||||
)),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
constraints
|
||||
}
|
||||
|
||||
fn evaluate_expr_compare(
|
||||
@@ -376,7 +335,10 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
id,
|
||||
ctx: _,
|
||||
}) => {
|
||||
let symbol = self.expect_expr_name_symbol(id);
|
||||
let symbol = self
|
||||
.symbols()
|
||||
.symbol_id_by_name(id)
|
||||
.expect("Should always have a symbol for every Name node");
|
||||
|
||||
match if is_positive { *op } else { op.negate() } {
|
||||
ast::CmpOp::IsNot => {
|
||||
@@ -403,16 +365,6 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
ast::CmpOp::Eq if lhs_ty.is_literal_string() => {
|
||||
constraints.insert(symbol, rhs_ty);
|
||||
}
|
||||
ast::CmpOp::In => {
|
||||
if let Some(ty) = self.evaluate_expr_in(lhs_ty, rhs_ty) {
|
||||
constraints.insert(symbol, ty);
|
||||
}
|
||||
}
|
||||
ast::CmpOp::NotIn => {
|
||||
if let Some(ty) = self.evaluate_expr_in(lhs_ty, rhs_ty) {
|
||||
constraints.insert(symbol, ty.negate(self.db));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// TODO other comparison types
|
||||
}
|
||||
@@ -453,7 +405,10 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
.into_class_literal()
|
||||
.is_some_and(|c| c.class().is_known(self.db, KnownClass::Type))
|
||||
{
|
||||
let symbol = self.expect_expr_name_symbol(id);
|
||||
let symbol = self
|
||||
.symbols()
|
||||
.symbol_id_by_name(id)
|
||||
.expect("Should always have a symbol for every Name node");
|
||||
constraints.insert(symbol, Type::instance(rhs_class));
|
||||
}
|
||||
}
|
||||
@@ -487,7 +442,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
return None;
|
||||
};
|
||||
|
||||
let symbol = self.expect_expr_name_symbol(id);
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
|
||||
let class_info_ty =
|
||||
inference.expression_type(class_info.scoped_expression_id(self.db, scope));
|
||||
@@ -495,10 +450,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
function
|
||||
.generate_constraint(self.db, class_info_ty)
|
||||
.map(|constraint| {
|
||||
NarrowingConstraints::from_iter([(
|
||||
symbol,
|
||||
constraint.negate_if(self.db, !is_positive),
|
||||
)])
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
constraints.insert(symbol, constraint.negate_if(self.db, !is_positive));
|
||||
constraints
|
||||
})
|
||||
}
|
||||
// for the expression `bool(E)`, we further narrow the type based on `E`
|
||||
@@ -522,14 +476,21 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
subject: Expression<'db>,
|
||||
singleton: ast::Singleton,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let symbol = self.expect_expr_name_symbol(&subject.node_ref(self.db).as_name_expr()?.id);
|
||||
if let Some(ast::ExprName { id, .. }) = subject.node_ref(self.db).as_name_expr() {
|
||||
// SAFETY: we should always have a symbol for every Name node.
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
|
||||
let ty = match singleton {
|
||||
ast::Singleton::None => Type::none(self.db),
|
||||
ast::Singleton::True => Type::BooleanLiteral(true),
|
||||
ast::Singleton::False => Type::BooleanLiteral(false),
|
||||
};
|
||||
Some(NarrowingConstraints::from_iter([(symbol, ty)]))
|
||||
let ty = match singleton {
|
||||
ast::Singleton::None => Type::none(self.db),
|
||||
ast::Singleton::True => Type::BooleanLiteral(true),
|
||||
ast::Singleton::False => Type::BooleanLiteral(false),
|
||||
};
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
constraints.insert(symbol, ty);
|
||||
Some(constraints)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_match_pattern_class(
|
||||
@@ -537,36 +498,16 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
subject: Expression<'db>,
|
||||
cls: Expression<'db>,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let symbol = self.expect_expr_name_symbol(&subject.node_ref(self.db).as_name_expr()?.id);
|
||||
let ast::ExprName { id, .. } = subject.node_ref(self.db).as_name_expr()?;
|
||||
let symbol = self
|
||||
.symbols()
|
||||
.symbol_id_by_name(id)
|
||||
.expect("We should always have a symbol for every `Name` node");
|
||||
let ty = infer_same_file_expression_type(self.db, cls).to_instance(self.db)?;
|
||||
|
||||
Some(NarrowingConstraints::from_iter([(symbol, ty)]))
|
||||
}
|
||||
|
||||
fn evaluate_match_pattern_value(
|
||||
&mut self,
|
||||
subject: Expression<'db>,
|
||||
value: Expression<'db>,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let symbol = self.expect_expr_name_symbol(&subject.node_ref(self.db).as_name_expr()?.id);
|
||||
let ty = infer_same_file_expression_type(self.db, value);
|
||||
Some(NarrowingConstraints::from_iter([(symbol, ty)]))
|
||||
}
|
||||
|
||||
fn evaluate_match_pattern_or(
|
||||
&mut self,
|
||||
subject: Expression<'db>,
|
||||
predicates: &Vec<PatternPredicateKind<'db>>,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let db = self.db;
|
||||
|
||||
predicates
|
||||
.iter()
|
||||
.filter_map(|predicate| self.evaluate_pattern_predicate_kind(predicate, subject))
|
||||
.reduce(|mut constraints, constraints_| {
|
||||
merge_constraints_or(&mut constraints, &constraints_, db);
|
||||
constraints
|
||||
})
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
constraints.insert(symbol, ty);
|
||||
Some(constraints)
|
||||
}
|
||||
|
||||
fn evaluate_bool_op(
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::symbol::{builtins_symbol, known_module_symbol};
|
||||
use crate::types::{
|
||||
BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType, Parameter,
|
||||
Parameters, Signature, SubclassOfType, TupleType, Type, UnionType,
|
||||
BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType,
|
||||
SubclassOfType, TupleType, Type, UnionType,
|
||||
};
|
||||
use crate::{Db, KnownModule};
|
||||
use hashbrown::HashSet;
|
||||
use quickcheck::{Arbitrary, Gen};
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
/// A test representation of a type that can be transformed unambiguously into a real Type,
|
||||
/// given a db.
|
||||
@@ -47,59 +45,6 @@ pub(crate) enum Ty {
|
||||
class: &'static str,
|
||||
method: &'static str,
|
||||
},
|
||||
Callable {
|
||||
params: CallableParams,
|
||||
returns: Option<Box<Ty>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum CallableParams {
|
||||
GradualForm,
|
||||
List(Vec<Param>),
|
||||
}
|
||||
|
||||
impl CallableParams {
|
||||
pub(crate) fn into_parameters(self, db: &TestDb) -> Parameters<'_> {
|
||||
match self {
|
||||
CallableParams::GradualForm => Parameters::gradual_form(),
|
||||
CallableParams::List(params) => Parameters::new(params.into_iter().map(|param| {
|
||||
let mut parameter = match param.kind {
|
||||
ParamKind::PositionalOnly => Parameter::positional_only(param.name),
|
||||
ParamKind::PositionalOrKeyword => {
|
||||
Parameter::positional_or_keyword(param.name.unwrap())
|
||||
}
|
||||
ParamKind::Variadic => Parameter::variadic(param.name.unwrap()),
|
||||
ParamKind::KeywordOnly => Parameter::keyword_only(param.name.unwrap()),
|
||||
ParamKind::KeywordVariadic => Parameter::keyword_variadic(param.name.unwrap()),
|
||||
};
|
||||
if let Some(annotated_ty) = param.annotated_ty {
|
||||
parameter = parameter.with_annotated_type(annotated_ty.into_type(db));
|
||||
}
|
||||
if let Some(default_ty) = param.default_ty {
|
||||
parameter = parameter.with_default_type(default_ty.into_type(db));
|
||||
}
|
||||
parameter
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Param {
|
||||
kind: ParamKind,
|
||||
name: Option<Name>,
|
||||
annotated_ty: Option<Ty>,
|
||||
default_ty: Option<Ty>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ParamKind {
|
||||
PositionalOnly,
|
||||
PositionalOrKeyword,
|
||||
Variadic,
|
||||
KeywordOnly,
|
||||
KeywordVariadic,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
@@ -108,11 +53,11 @@ fn create_bound_method<'db>(
|
||||
function: Type<'db>,
|
||||
builtins_class: Type<'db>,
|
||||
) -> Type<'db> {
|
||||
Type::BoundMethod(BoundMethodType::new(
|
||||
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
function.expect_function_literal(),
|
||||
builtins_class.to_instance(db).unwrap(),
|
||||
))
|
||||
)))
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
@@ -186,13 +131,6 @@ impl Ty {
|
||||
|
||||
create_bound_method(db, function, builtins_class)
|
||||
}
|
||||
Ty::Callable { params, returns } => Type::Callable(CallableType::new(
|
||||
db,
|
||||
Signature::new(
|
||||
params.into_parameters(db),
|
||||
returns.map(|ty| ty.into_type(db)),
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,7 +205,7 @@ fn arbitrary_type(g: &mut Gen, size: u32) -> Ty {
|
||||
if size == 0 {
|
||||
arbitrary_core_type(g)
|
||||
} else {
|
||||
match u32::arbitrary(g) % 5 {
|
||||
match u32::arbitrary(g) % 4 {
|
||||
0 => arbitrary_core_type(g),
|
||||
1 => Ty::Union(
|
||||
(0..*g.choose(&[2, 3]).unwrap())
|
||||
@@ -287,103 +225,11 @@ fn arbitrary_type(g: &mut Gen, size: u32) -> Ty {
|
||||
.map(|_| arbitrary_type(g, size - 1))
|
||||
.collect(),
|
||||
},
|
||||
4 => Ty::Callable {
|
||||
params: match u32::arbitrary(g) % 2 {
|
||||
0 => CallableParams::GradualForm,
|
||||
1 => CallableParams::List(arbitrary_parameter_list(g, size)),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
returns: arbitrary_optional_type(g, size - 1).map(Box::new),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec<Param> {
|
||||
let mut params: Vec<Param> = vec![];
|
||||
let mut used_names = HashSet::new();
|
||||
|
||||
// First, choose the number of parameters to generate.
|
||||
for _ in 0..*g.choose(&[0, 1, 2, 3, 4, 5]).unwrap() {
|
||||
// Next, choose the kind of parameters that can be generated based on the last parameter.
|
||||
let next_kind = match params.last().map(|p| p.kind) {
|
||||
None | Some(ParamKind::PositionalOnly) => *g
|
||||
.choose(&[
|
||||
ParamKind::PositionalOnly,
|
||||
ParamKind::PositionalOrKeyword,
|
||||
ParamKind::Variadic,
|
||||
ParamKind::KeywordOnly,
|
||||
ParamKind::KeywordVariadic,
|
||||
])
|
||||
.unwrap(),
|
||||
Some(ParamKind::PositionalOrKeyword) => *g
|
||||
.choose(&[
|
||||
ParamKind::PositionalOrKeyword,
|
||||
ParamKind::Variadic,
|
||||
ParamKind::KeywordOnly,
|
||||
ParamKind::KeywordVariadic,
|
||||
])
|
||||
.unwrap(),
|
||||
Some(ParamKind::Variadic | ParamKind::KeywordOnly) => *g
|
||||
.choose(&[ParamKind::KeywordOnly, ParamKind::KeywordVariadic])
|
||||
.unwrap(),
|
||||
Some(ParamKind::KeywordVariadic) => {
|
||||
// There can't be any other parameter kind after a keyword variadic parameter.
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let name = loop {
|
||||
let name = if matches!(next_kind, ParamKind::PositionalOnly) {
|
||||
arbitrary_optional_name(g)
|
||||
} else {
|
||||
Some(arbitrary_name(g))
|
||||
};
|
||||
if let Some(name) = name {
|
||||
if used_names.insert(name.clone()) {
|
||||
break Some(name);
|
||||
}
|
||||
} else {
|
||||
break None;
|
||||
}
|
||||
};
|
||||
|
||||
params.push(Param {
|
||||
kind: next_kind,
|
||||
name,
|
||||
annotated_ty: arbitrary_optional_type(g, size),
|
||||
default_ty: if matches!(next_kind, ParamKind::Variadic | ParamKind::KeywordVariadic) {
|
||||
None
|
||||
} else {
|
||||
arbitrary_optional_type(g, size)
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
params
|
||||
}
|
||||
|
||||
fn arbitrary_optional_type(g: &mut Gen, size: u32) -> Option<Ty> {
|
||||
match u32::arbitrary(g) % 2 {
|
||||
0 => None,
|
||||
1 => Some(arbitrary_type(g, size)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn arbitrary_name(g: &mut Gen) -> Name {
|
||||
Name::new(format!("n{}", u32::arbitrary(g) % 10))
|
||||
}
|
||||
|
||||
fn arbitrary_optional_name(g: &mut Gen) -> Option<Name> {
|
||||
match u32::arbitrary(g) % 2 {
|
||||
0 => None,
|
||||
1 => Some(arbitrary_name(g)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for Ty {
|
||||
fn arbitrary(g: &mut Gen) -> Ty {
|
||||
const MAX_SIZE: u32 = 2;
|
||||
|
||||
@@ -265,13 +265,6 @@ impl<'db> Signature<'db> {
|
||||
pub(crate) fn parameters(&self) -> &Parameters<'db> {
|
||||
&self.parameters
|
||||
}
|
||||
|
||||
pub(crate) fn bind_self(&self) -> Self {
|
||||
Self {
|
||||
parameters: Parameters::new(self.parameters().iter().skip(1).cloned()),
|
||||
return_ty: self.return_ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
@@ -511,12 +504,6 @@ impl<'db, 'a> IntoIterator for &'a Parameters<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> FromIterator<Parameter<'db>> for Parameters<'db> {
|
||||
fn from_iter<T: IntoIterator<Item = Parameter<'db>>>(iter: T) -> Self {
|
||||
Self::new(iter)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> std::ops::Index<usize> for Parameters<'db> {
|
||||
type Output = Parameter<'db>;
|
||||
|
||||
@@ -606,56 +593,6 @@ impl<'db> Parameter<'db> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Strip information from the parameter so that two equivalent parameters compare equal.
|
||||
/// Normalize nested unions and intersections in the annotated type, if any.
|
||||
///
|
||||
/// See [`Type::normalized`] for more details.
|
||||
pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self {
|
||||
let Parameter {
|
||||
annotated_type,
|
||||
kind,
|
||||
form,
|
||||
} = self;
|
||||
|
||||
// Ensure unions and intersections are ordered in the annotated type (if there is one)
|
||||
let annotated_type = annotated_type.map(|ty| ty.normalized(db));
|
||||
|
||||
// Ensure that parameter names are stripped from positional-only, variadic and keyword-variadic parameters.
|
||||
// Ensure that we only record whether a parameter *has* a default
|
||||
// (strip the precise *type* of the default from the parameter, replacing it with `Never`).
|
||||
let kind = match kind {
|
||||
ParameterKind::PositionalOnly {
|
||||
name: _,
|
||||
default_type,
|
||||
} => ParameterKind::PositionalOnly {
|
||||
name: None,
|
||||
default_type: default_type.map(|_| Type::Never),
|
||||
},
|
||||
ParameterKind::PositionalOrKeyword { name, default_type } => {
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
name: name.clone(),
|
||||
default_type: default_type.map(|_| Type::Never),
|
||||
}
|
||||
}
|
||||
ParameterKind::KeywordOnly { name, default_type } => ParameterKind::KeywordOnly {
|
||||
name: name.clone(),
|
||||
default_type: default_type.map(|_| Type::Never),
|
||||
},
|
||||
ParameterKind::Variadic { name: _ } => ParameterKind::Variadic {
|
||||
name: Name::new_static("args"),
|
||||
},
|
||||
ParameterKind::KeywordVariadic { name: _ } => ParameterKind::KeywordVariadic {
|
||||
name: Name::new_static("kwargs"),
|
||||
},
|
||||
};
|
||||
|
||||
Self {
|
||||
annotated_type,
|
||||
kind,
|
||||
form: *form,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_node_and_kind(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
@@ -1053,4 +990,25 @@ mod tests {
|
||||
// With no decorators, internal and external signature are the same
|
||||
assert_eq!(func.signature(&db), &expected_sig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_signature_decorated() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/a.py",
|
||||
"
|
||||
def deco(func): ...
|
||||
|
||||
@deco
|
||||
def f(a: int) -> int: ...
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
let func = get_function_f(&db, "/src/a.py");
|
||||
|
||||
let expected_sig = Signature::todo("return type of decorated function");
|
||||
|
||||
// With no decorators, internal and external signature are the same
|
||||
assert_eq!(func.signature(&db), &expected_sig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ impl<'db> SubclassOfType<'db> {
|
||||
}
|
||||
|
||||
/// Return the inner [`ClassBase`] value wrapped by this `SubclassOfType`.
|
||||
pub const fn subclass_of(self) -> ClassBase<'db> {
|
||||
pub(crate) const fn subclass_of(self) -> ClassBase<'db> {
|
||||
self.subclass_of
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::types::CallableType;
|
||||
|
||||
use super::{
|
||||
class_base::ClassBase, ClassLiteralType, DynamicType, InstanceType, KnownInstanceType,
|
||||
@@ -61,29 +62,43 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||
(Type::FunctionLiteral(_), _) => Ordering::Less,
|
||||
(_, Type::FunctionLiteral(_)) => Ordering::Greater,
|
||||
|
||||
(Type::BoundMethod(left), Type::BoundMethod(right)) => left.cmp(right),
|
||||
(Type::BoundMethod(_), _) => Ordering::Less,
|
||||
(_, Type::BoundMethod(_)) => Ordering::Greater,
|
||||
(
|
||||
Type::Callable(CallableType::BoundMethod(left)),
|
||||
Type::Callable(CallableType::BoundMethod(right)),
|
||||
) => left.cmp(right),
|
||||
(Type::Callable(CallableType::BoundMethod(_)), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::BoundMethod(_))) => Ordering::Greater,
|
||||
|
||||
(Type::MethodWrapper(left), Type::MethodWrapper(right)) => left.cmp(right),
|
||||
(Type::MethodWrapper(_), _) => Ordering::Less,
|
||||
(_, Type::MethodWrapper(_)) => Ordering::Greater,
|
||||
(
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(left)),
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(right)),
|
||||
) => left.cmp(right),
|
||||
(Type::Callable(CallableType::MethodWrapperDunderGet(_)), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::MethodWrapperDunderGet(_))) => Ordering::Greater,
|
||||
|
||||
(Type::WrapperDescriptor(left), Type::WrapperDescriptor(right)) => left.cmp(right),
|
||||
(Type::WrapperDescriptor(_), _) => Ordering::Less,
|
||||
(_, Type::WrapperDescriptor(_)) => Ordering::Greater,
|
||||
(
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet),
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet),
|
||||
) => Ordering::Equal,
|
||||
(Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater,
|
||||
|
||||
(Type::Callable(left), Type::Callable(right)) => {
|
||||
debug_assert_eq!(*left, left.normalized(db));
|
||||
debug_assert_eq!(*right, right.normalized(db));
|
||||
left.cmp(right)
|
||||
(
|
||||
Type::Callable(CallableType::SpecializedGetitem),
|
||||
Type::Callable(CallableType::SpecializedGetitem),
|
||||
) => Ordering::Equal,
|
||||
(Type::Callable(CallableType::SpecializedGetitem), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::SpecializedGetitem)) => Ordering::Greater,
|
||||
|
||||
(Type::Callable(CallableType::General(_)), Type::Callable(CallableType::General(_))) => {
|
||||
Ordering::Equal
|
||||
}
|
||||
(Type::Callable(_), _) => Ordering::Less,
|
||||
(_, Type::Callable(_)) => Ordering::Greater,
|
||||
(Type::Callable(CallableType::General(_)), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::General(_))) => Ordering::Greater,
|
||||
|
||||
(Type::Tuple(left), Type::Tuple(right)) => {
|
||||
debug_assert_eq!(*left, left.normalized(db));
|
||||
debug_assert_eq!(*right, right.normalized(db));
|
||||
debug_assert_eq!(*left, left.with_sorted_unions_and_intersections(db));
|
||||
debug_assert_eq!(*right, right.with_sorted_unions_and_intersections(db));
|
||||
left.cmp(right)
|
||||
}
|
||||
(Type::Tuple(_), _) => Ordering::Less,
|
||||
@@ -222,8 +237,8 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||
(KnownInstanceType::TypeOf, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeOf) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::CallableTypeOf, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::CallableTypeOf) => Ordering::Greater,
|
||||
(KnownInstanceType::CallableTypeFromFunction, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::CallableTypeFromFunction) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Unpack, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Unpack) => Ordering::Greater,
|
||||
@@ -262,10 +277,6 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||
(Type::KnownInstance(_), _) => Ordering::Less,
|
||||
(_, Type::KnownInstance(_)) => Ordering::Greater,
|
||||
|
||||
(Type::PropertyInstance(left), Type::PropertyInstance(right)) => left.cmp(right),
|
||||
(Type::PropertyInstance(_), _) => Ordering::Less,
|
||||
(_, Type::PropertyInstance(_)) => Ordering::Greater,
|
||||
|
||||
(Type::Dynamic(left), Type::Dynamic(right)) => dynamic_elements_ordering(*left, *right),
|
||||
(Type::Dynamic(_), _) => Ordering::Less,
|
||||
(_, Type::Dynamic(_)) => Ordering::Greater,
|
||||
@@ -275,8 +286,8 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||
}
|
||||
|
||||
(Type::Intersection(left), Type::Intersection(right)) => {
|
||||
debug_assert_eq!(*left, left.normalized(db));
|
||||
debug_assert_eq!(*right, right.normalized(db));
|
||||
debug_assert_eq!(*left, left.to_sorted_intersection(db));
|
||||
debug_assert_eq!(*right, right.to_sorted_intersection(db));
|
||||
|
||||
if left == right {
|
||||
return Ordering::Equal;
|
||||
@@ -321,7 +332,18 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
|
||||
(_, DynamicType::Unknown) => Ordering::Greater,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
(DynamicType::Todo(TodoType(left)), DynamicType::Todo(TodoType(right))) => left.cmp(right),
|
||||
(DynamicType::Todo(left), DynamicType::Todo(right)) => match (left, right) {
|
||||
(
|
||||
TodoType::FileAndLine(left_file, left_line),
|
||||
TodoType::FileAndLine(right_file, right_line),
|
||||
) => left_file
|
||||
.cmp(right_file)
|
||||
.then_with(|| left_line.cmp(&right_line)),
|
||||
(TodoType::FileAndLine(..), _) => Ordering::Less,
|
||||
(_, TodoType::FileAndLine(..)) => Ordering::Greater,
|
||||
|
||||
(TodoType::Message(left), TodoType::Message(right)) => left.cmp(right),
|
||||
},
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
(DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal,
|
||||
|
||||
@@ -8,10 +8,10 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::types::{infer_expression_types, todo_type, Type, TypeCheckDiagnostics};
|
||||
use crate::unpack::{UnpackKind, UnpackValue};
|
||||
use crate::unpack::UnpackValue;
|
||||
use crate::Db;
|
||||
|
||||
use super::context::InferContext;
|
||||
use super::context::{InferContext, WithDiagnostics};
|
||||
use super::diagnostic::INVALID_ASSIGNMENT;
|
||||
use super::{TupleType, UnionType};
|
||||
|
||||
@@ -45,27 +45,30 @@ impl<'db> Unpacker<'db> {
|
||||
let value_type = infer_expression_types(self.db(), value.expression())
|
||||
.expression_type(value.scoped_expression_id(self.db(), self.scope));
|
||||
|
||||
let value_type = match value.kind() {
|
||||
UnpackKind::Assign => {
|
||||
let value_type = match value {
|
||||
UnpackValue::Assign(expression) => {
|
||||
if self.context.in_stub()
|
||||
&& value
|
||||
.expression()
|
||||
.node_ref(self.db())
|
||||
.is_ellipsis_literal_expr()
|
||||
&& expression.node_ref(self.db()).is_ellipsis_literal_expr()
|
||||
{
|
||||
Type::unknown()
|
||||
} else {
|
||||
value_type
|
||||
}
|
||||
}
|
||||
UnpackKind::Iterable => value_type.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||
UnpackValue::Iterable(_) => value_type.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||
err.report_diagnostic(&self.context, value_type, value.as_any_node_ref(self.db()));
|
||||
err.fallback_element_type(self.db())
|
||||
}),
|
||||
UnpackKind::ContextManager => value_type.try_enter(self.db()).unwrap_or_else(|err| {
|
||||
err.report_diagnostic(&self.context, value_type, value.as_any_node_ref(self.db()));
|
||||
err.fallback_enter_type(self.db())
|
||||
}),
|
||||
UnpackValue::ContextManager(_) => {
|
||||
value_type.try_enter(self.db()).unwrap_or_else(|err| {
|
||||
err.report_diagnostic(
|
||||
&self.context,
|
||||
value_type,
|
||||
value.as_any_node_ref(self.db()),
|
||||
);
|
||||
err.fallback_enter_type(self.db())
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
self.unpack_inner(target, value.as_any_node_ref(self.db()), value_type);
|
||||
@@ -283,9 +286,10 @@ impl<'db> UnpackResult<'db> {
|
||||
pub(crate) fn expression_type(&self, expr_id: ScopedExpressionId) -> Type<'db> {
|
||||
self.targets[&expr_id]
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the diagnostics in this unpacking assignment.
|
||||
pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics {
|
||||
impl WithDiagnostics for UnpackResult<'_> {
|
||||
fn diagnostics(&self) -> &TypeCheckDiagnostics {
|
||||
&self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,21 +60,23 @@ impl<'db> Unpack<'db> {
|
||||
|
||||
/// The expression that is being unpacked.
|
||||
#[derive(Clone, Copy, Debug, Hash, salsa::Update)]
|
||||
pub(crate) struct UnpackValue<'db> {
|
||||
/// The kind of unpack expression
|
||||
kind: UnpackKind,
|
||||
/// The expression we are unpacking
|
||||
expression: Expression<'db>,
|
||||
pub(crate) enum UnpackValue<'db> {
|
||||
/// An iterable expression like the one in a `for` loop or a comprehension.
|
||||
Iterable(Expression<'db>),
|
||||
/// An context manager expression like the one in a `with` statement.
|
||||
ContextManager(Expression<'db>),
|
||||
/// An expression that is being assigned to a target.
|
||||
Assign(Expression<'db>),
|
||||
}
|
||||
|
||||
impl<'db> UnpackValue<'db> {
|
||||
pub(crate) fn new(kind: UnpackKind, expression: Expression<'db>) -> Self {
|
||||
Self { kind, expression }
|
||||
}
|
||||
|
||||
/// Returns the underlying [`Expression`] that is being unpacked.
|
||||
pub(crate) const fn expression(self) -> Expression<'db> {
|
||||
self.expression
|
||||
match self {
|
||||
UnpackValue::Assign(expr)
|
||||
| UnpackValue::Iterable(expr)
|
||||
| UnpackValue::ContextManager(expr) => expr,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`ScopedExpressionId`] of the underlying expression.
|
||||
@@ -92,27 +94,4 @@ impl<'db> UnpackValue<'db> {
|
||||
pub(crate) fn as_any_node_ref(self, db: &'db dyn Db) -> AnyNodeRef<'db> {
|
||||
self.expression().node_ref(db).node().into()
|
||||
}
|
||||
|
||||
pub(crate) const fn kind(self) -> UnpackKind {
|
||||
self.kind
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, salsa::Update)]
|
||||
pub(crate) enum UnpackKind {
|
||||
/// An iterable expression like the one in a `for` loop or a comprehension.
|
||||
Iterable,
|
||||
/// An context manager expression like the one in a `with` statement.
|
||||
ContextManager,
|
||||
/// An expression that is being assigned to a target.
|
||||
Assign,
|
||||
}
|
||||
|
||||
/// The position of the target element in an unpacking.
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, salsa::Update)]
|
||||
pub(crate) enum UnpackPosition {
|
||||
/// The target element is in the first position of the unpacking.
|
||||
First,
|
||||
/// The target element is in the position other than the first position of the unpacking.
|
||||
Other,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use camino::Utf8Path;
|
||||
use dir_test::{dir_test, Fixture};
|
||||
use red_knot_test::OutputFormat;
|
||||
|
||||
/// See `crates/red_knot_test/README.md` for documentation on these tests.
|
||||
#[dir_test(
|
||||
@@ -19,19 +18,12 @@ fn mdtest(fixture: Fixture<&str>) {
|
||||
|
||||
let test_name = test_name("mdtest", absolute_fixture_path);
|
||||
|
||||
let output_format = if std::env::var("MDTEST_GITHUB_ANNOTATIONS_FORMAT").is_ok() {
|
||||
OutputFormat::GitHub
|
||||
} else {
|
||||
OutputFormat::Cli
|
||||
};
|
||||
|
||||
red_knot_test::run(
|
||||
absolute_fixture_path,
|
||||
relative_fixture_path,
|
||||
&snapshot_path,
|
||||
short_title,
|
||||
&test_name,
|
||||
output_format,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user