Compare commits

..

1 Commits

Author SHA1 Message Date
Douglas Creager
9eff2734bb wip: subscript always via __getitem__ 2025-03-27 14:19:48 -04:00
489 changed files with 12694 additions and 24274 deletions

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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: |

View File

@@ -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: |

View File

@@ -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-v3"
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 ]

View File

@@ -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: |

View File

@@ -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 }}

View File

@@ -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-*

View File

@@ -1,6 +1,6 @@
# This file was autogenerated by dist: https://github.com/astral-sh/cargo-dist
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
#
# Copyright 2025 Astral Software Inc.
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
@@ -66,7 +66,7 @@ jobs:
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.3/cargo-dist-installer.sh | sh"
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.25.2-prerelease.3/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:

View File

@@ -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: |

View File

@@ -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

View File

@@ -1,53 +1,5 @@
# Changelog
## 0.11.4
### Preview features
- \[`ruff`\] Implement `invalid-rule-code` as `RUF102` ([#17138](https://github.com/astral-sh/ruff/pull/17138))
- [syntax-errors] Detect duplicate keys in `match` mapping patterns ([#17129](https://github.com/astral-sh/ruff/pull/17129))
- [syntax-errors] Detect duplicate attributes in `match` class patterns ([#17186](https://github.com/astral-sh/ruff/pull/17186))
- [syntax-errors] Detect invalid syntax in annotations ([#17101](https://github.com/astral-sh/ruff/pull/17101))
### Bug fixes
- [syntax-errors] Fix multiple assignment error for class fields in `match` patterns ([#17184](https://github.com/astral-sh/ruff/pull/17184))
- Don't skip visiting non-tuple slice in `typing.Annotated` subscripts ([#17201](https://github.com/astral-sh/ruff/pull/17201))
## 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

69
Cargo.lock generated
View File

@@ -207,9 +207,9 @@ dependencies = [
[[package]]
name = "boxcar"
version = "0.2.11"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6740c6e2fc6360fa57c35214c7493826aee95993926092606f27c983b40837be"
checksum = "225450ee9328e1e828319b48a89726cffc1b0ad26fd9211ad435de9fa376acae"
dependencies = [
"loom",
]
@@ -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,23 +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",
"rustc-hash 2.1.1",
"salsa",
"smallvec",
"tracing",
]
[[package]]
name = "red_knot_project"
version = "0.0.0"
@@ -2531,7 +2514,6 @@ dependencies = [
"notify",
"pep440_rs",
"rayon",
"red_knot_ide",
"red_knot_python_semantic",
"red_knot_vendored",
"ruff_cache",
@@ -2603,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",
@@ -2666,7 +2646,6 @@ dependencies = [
"getrandom 0.3.2",
"js-sys",
"log",
"red_knot_ide",
"red_knot_project",
"red_knot_python_semantic",
"ruff_db",
@@ -2755,7 +2734,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.4"
version = "0.11.2"
dependencies = [
"anyhow",
"argfile",
@@ -2990,7 +2969,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.4"
version = "0.11.2"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3019,6 +2998,7 @@ dependencies = [
"ruff_annotate_snippets",
"ruff_cache",
"ruff_diagnostics",
"ruff_index",
"ruff_macros",
"ruff_notebook",
"ruff_python_ast",
@@ -3213,7 +3193,6 @@ name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.9.0",
"insta",
"is-macro",
"ruff_cache",
"ruff_index",
@@ -3226,7 +3205,6 @@ dependencies = [
"schemars",
"serde",
"smallvec",
"test-case",
]
[[package]]
@@ -3314,7 +3292,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.4"
version = "0.11.2"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3408,7 +3386,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3421,7 +3399,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.3",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3439,7 +3417,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc#296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
dependencies = [
"boxcar",
"compact_str 0.8.1",
@@ -3455,19 +3433,18 @@ dependencies = [
"salsa-macro-rules",
"salsa-macros",
"smallvec",
"thin-vec",
"tracing",
]
[[package]]
name = "salsa-macro-rules"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc#296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
[[package]]
name = "salsa-macros"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc#296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
dependencies = [
"heck",
"proc-macro2",
@@ -3807,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]]
@@ -3880,12 +3857,6 @@ dependencies = [
"test-case-core",
]
[[package]]
name = "thin-vec"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
[[package]]
name = "thiserror"
version = "1.0.69"

View File

@@ -4,7 +4,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
rust-version = "1.84"
rust-version = "1.83"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
@@ -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" }
@@ -124,7 +123,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d758691ba17ee1a60c5356ea90888d529e1782ad" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -209,7 +208,6 @@ must_use_candidate = "allow"
similar_names = "allow"
single_match_else = "allow"
too_many_lines = "allow"
needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
# Without the hashes we run into a `rustfmt` bug in some snapshot tests, see #13250
needless_raw_string_hashes = "allow"
# Disallowed restriction lints
@@ -272,7 +270,7 @@ inherits = "release"
# Config for 'dist'
[workspace.metadata.dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.28.3"
cargo-dist-version = "0.25.2-prerelease.3"
# CI backends to support
ci = "github"
# The installers to generate for each app

View File

@@ -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.4/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.4/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.4
rev: v0.11.2
hooks:
# Run the linter.
- id: ruff

View File

@@ -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>),

View File

@@ -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`"
);

View File

@@ -1,31 +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 }
rustc-hash = { 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

View File

@@ -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);
}
}
}

View File

@@ -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()
}
}

View File

@@ -1,851 +0,0 @@
use crate::find_node::covering_node;
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
use red_knot_python_semantic::types::Type;
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 = goto_target.inferred_type(&model)?;
tracing::debug!(
"Inferred type of covering node is {}",
ty.display(db.upcast())
);
let navigation_targets = ty.navigation_targets(db);
Some(RangedValue {
range: FileRange::new(file, goto_target.range()),
value: navigation_targets,
})
}
#[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<'db> GotoTarget<'db> {
pub(crate) fn inferred_type(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
let ty = match self {
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,
};
Some(ty)
}
}
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)
.max_by_key(|token| match token.kind() {
TokenKind::Name
| TokenKind::String
| TokenKind::Complex
| TokenKind::Float
| TokenKind::Int => 1,
_ => 0,
})?;
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 crate::tests::{cursor_test, CursorTest, IntoDiagnostic};
use crate::{goto_type_definition, NavigationTarget};
use insta::assert_snapshot;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
};
use ruff_db::files::FileRange;
use ruff_text_size::Ranged;
#[test]
fn goto_type_of_expression_with_class_type() {
let test = cursor_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 = cursor_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 = cursor_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 = cursor_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 = cursor_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 = cursor_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 = cursor_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 = cursor_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 = cursor_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 = cursor_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 = cursor_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 = cursor_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 = cursor_test(
r#"
def foo(a: str):
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):
3 | a
| ^
|
"###);
}
#[test]
fn goto_type_definition_cursor_between_object_and_attribute() {
let test = cursor_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 = cursor_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 = cursor_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 = cursor_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/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
| ^
|
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
| ^
|
");
}
impl CursorTest {
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 source = targets.range;
self.render_diagnostics(
targets
.into_iter()
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
)
}
}
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()),
}
}
}
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
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
}
}
}

View File

@@ -1,545 +0,0 @@
use crate::goto::find_goto_target;
use crate::{Db, MarkupKind, RangedValue};
use red_knot_python_semantic::types::Type;
use red_knot_python_semantic::SemanticModel;
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize};
use std::fmt;
use std::fmt::Formatter;
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover>> {
let parsed = parsed_module(db.upcast(), file);
let goto_target = find_goto_target(parsed, offset)?;
let model = SemanticModel::new(db.upcast(), file);
let ty = goto_target.inferred_type(&model)?;
tracing::debug!(
"Inferred type of covering node is {}",
ty.display(db.upcast())
);
// TODO: Add documentation of the symbol (not the type's definition).
// TODO: Render the symbol's signature instead of just its type.
let contents = vec![HoverContent::Type(ty)];
Some(RangedValue {
range: FileRange::new(file, goto_target.range()),
value: Hover { contents },
})
}
pub struct Hover<'db> {
contents: Vec<HoverContent<'db>>,
}
impl<'db> Hover<'db> {
/// Renders the hover to a string using the specified markup kind.
pub const fn display<'a>(&'a self, db: &'a dyn Db, kind: MarkupKind) -> DisplayHover<'a> {
DisplayHover {
db,
hover: self,
kind,
}
}
fn iter(&self) -> std::slice::Iter<'_, HoverContent<'db>> {
self.contents.iter()
}
}
impl<'db> IntoIterator for Hover<'db> {
type Item = HoverContent<'db>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.contents.into_iter()
}
}
impl<'a, 'db> IntoIterator for &'a Hover<'db> {
type Item = &'a HoverContent<'db>;
type IntoIter = std::slice::Iter<'a, HoverContent<'db>>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
pub struct DisplayHover<'a> {
db: &'a dyn Db,
hover: &'a Hover<'a>,
kind: MarkupKind,
}
impl fmt::Display for DisplayHover<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut first = true;
for content in &self.hover.contents {
if !first {
self.kind.horizontal_line().fmt(f)?;
}
content.display(self.db, self.kind).fmt(f)?;
first = false;
}
Ok(())
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum HoverContent<'db> {
Type(Type<'db>),
}
impl<'db> HoverContent<'db> {
fn display(&self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHoverContent<'_, 'db> {
DisplayHoverContent {
db,
content: self,
kind,
}
}
}
pub(crate) struct DisplayHoverContent<'a, 'db> {
db: &'db dyn Db,
content: &'a HoverContent<'db>,
kind: MarkupKind,
}
impl fmt::Display for DisplayHoverContent<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.content {
HoverContent::Type(ty) => self
.kind
.fenced_code_block(ty.display(self.db.upcast()), "text")
.fmt(f),
}
}
}
#[cfg(test)]
mod tests {
use crate::tests::{cursor_test, CursorTest};
use crate::{hover, MarkupKind};
use insta::assert_snapshot;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
Severity, Span,
};
use ruff_text_size::{Ranged, TextRange};
#[test]
fn hover_basic() {
let test = cursor_test(
r#"
a = 10
a<CURSOR>
"#,
);
assert_snapshot!(test.hover(), @r"
Literal[10]
---------------------------------------------
```text
Literal[10]
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:4:9
|
2 | a = 10
3 |
4 | a
| ^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_member() {
let test = cursor_test(
r#"
class Foo:
a: int = 10
def __init__(a: int, b: str):
self.a = a
self.b: str = b
foo = Foo()
foo.<CURSOR>a
"#,
);
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
```text
int
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:10:9
|
9 | foo = Foo()
10 | foo.a
| ^^^^-
| | |
| | Cursor offset
| source
|
");
}
#[test]
fn hover_function_typed_variable() {
let test = cursor_test(
r#"
def foo(a, b): ...
foo<CURSOR>
"#,
);
assert_snapshot!(test.hover(), @r"
Literal[foo]
---------------------------------------------
```text
Literal[foo]
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:4:13
|
2 | def foo(a, b): ...
3 |
4 | foo
| ^^^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_binary_expression() {
let test = cursor_test(
r#"
def foo(a: int, b: int, c: int):
a + b ==<CURSOR> c
"#,
);
assert_snapshot!(test.hover(), @r"
bool
---------------------------------------------
```text
bool
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:3:17
|
2 | def foo(a: int, b: int, c: int):
3 | a + b == c
| ^^^^^^^^-^
| | |
| | Cursor offset
| source
|
");
}
#[test]
fn hover_keyword_parameter() {
let test = cursor_test(
r#"
def test(a: int): ...
test(a<CURSOR>= 123)
"#,
);
// TODO: This should reveal `int` because the user hovers over the parameter and not the value.
assert_snapshot!(test.hover(), @r"
Literal[123]
---------------------------------------------
```text
Literal[123]
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:4:18
|
2 | def test(a: int): ...
3 |
4 | test(a= 123)
| ^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_union() {
let test = cursor_test(
r#"
def foo(a, b): ...
def bar(a, b): ...
if random.choice([True, False]):
a = foo
else:
a = bar
a<CURSOR>
"#,
);
assert_snapshot!(test.hover(), @r"
Literal[foo, bar]
---------------------------------------------
```text
Literal[foo, bar]
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:12:13
|
10 | a = bar
11 |
12 | a
| ^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_module() {
let mut test = cursor_test(
r#"
import lib
li<CURSOR>b
"#,
);
test.write_file("lib.py", "a = 10").unwrap();
assert_snapshot!(test.hover(), @r"
<module 'lib'>
---------------------------------------------
```text
<module 'lib'>
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:4:13
|
2 | import lib
3 |
4 | lib
| ^^-
| | |
| | Cursor offset
| source
|
");
}
#[test]
fn hover_type_of_expression_with_type_var_type() {
let test = cursor_test(
r#"
type Alias[T: int = bool] = list[T<CURSOR>]
"#,
);
assert_snapshot!(test.hover(), @r"
T
---------------------------------------------
```text
T
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:2:46
|
2 | type Alias[T: int = bool] = list[T]
| ^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_type_of_expression_with_type_param_spec() {
let test = cursor_test(
r#"
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
"#,
);
assert_snapshot!(test.hover(), @r"
@Todo
---------------------------------------------
```text
@Todo
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:2:53
|
2 | type Alias[**P = [int, str]] = Callable[P, int]
| ^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_type_of_expression_with_type_var_tuple() {
let test = cursor_test(
r#"
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
"#,
);
assert_snapshot!(test.hover(), @r"
@Todo
---------------------------------------------
```text
@Todo
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:2:43
|
2 | type Alias[*Ts = ()] = tuple[*Ts]
| ^^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_class_member_declaration() {
let test = cursor_test(
r#"
class Foo:
a<CURSOR>: int
"#,
);
// TODO: This should be int and not `Never`, https://github.com/astral-sh/ruff/issues/17122
assert_snapshot!(test.hover(), @r"
Never
---------------------------------------------
```text
Never
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:3:13
|
2 | class Foo:
3 | a: int
| ^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_type_narrowing() {
let test = cursor_test(
r#"
def foo(a: str | None, b):
if a is not None:
print(a<CURSOR>)
"#,
);
assert_snapshot!(test.hover(), @r"
str
---------------------------------------------
```text
str
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:4:27
|
2 | def foo(a: str | None, b):
3 | if a is not None:
4 | print(a)
| ^- Cursor offset
| |
| source
|
");
}
impl CursorTest {
fn hover(&self) -> String {
use std::fmt::Write;
let Some(hover) = hover(&self.db, self.file, self.cursor_offset) else {
return "Hover provided no content".to_string();
};
let source = hover.range;
let mut buf = String::new();
write!(
&mut buf,
"{plaintext}{line}{markdown}{line}",
plaintext = hover.display(&self.db, MarkupKind::PlainText),
line = MarkupKind::PlainText.horizontal_line(),
markdown = hover.display(&self.db, MarkupKind::Markdown),
)
.unwrap();
let config = DisplayDiagnosticConfig::default()
.color(false)
.format(DiagnosticFormat::Full);
let mut diagnostic = Diagnostic::new(
DiagnosticId::Lint(LintName::of("hover")),
Severity::Info,
"Hovered content is",
);
diagnostic.annotate(
Annotation::primary(Span::from(source.file()).with_range(source.range()))
.message("source"),
);
diagnostic.annotate(
Annotation::secondary(
Span::from(source.file()).with_range(TextRange::empty(self.cursor_offset)),
)
.message("Cursor offset"),
);
write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap();
buf
}
}
}

View File

@@ -1,294 +0,0 @@
mod db;
mod find_node;
mod goto;
mod hover;
mod markup;
pub use db::Db;
pub use goto::goto_type_definition;
pub use hover::hover;
pub use markup::MarkupKind;
use rustc_hash::FxHashSet;
use std::ops::{Deref, DerefMut};
use red_knot_python_semantic::types::{Type, TypeDefinition};
use ruff_db::files::{File, FileRange};
use ruff_text_size::{Ranged, 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, PartialEq, Eq, Hash)]
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 unique(targets: impl IntoIterator<Item = NavigationTarget>) -> Self {
let unique: FxHashSet<_> = targets.into_iter().collect();
if unique.is_empty() {
Self::empty()
} else {
let mut targets = unique.into_iter().collect::<Vec<_>>();
targets.sort_by_key(|target| (target.file, target.focus_range.start()));
Self(targets.into())
}
}
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::unique(iter)
}
}
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::Union(union) => union
.iter(db.upcast())
.flat_map(|target| target.navigation_targets(db))
.collect(),
Type::Intersection(intersection) => {
// Only consider the positive elements because the negative elements are mainly from narrowing constraints.
let mut targets = intersection
.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),
}
}
ty => ty
.definition(db.upcast())
.map(|definition| definition.navigation_targets(db))
.unwrap_or_else(NavigationTargets::empty),
}
}
}
impl HasNavigationTargets for TypeDefinition<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
let full_range = self.full_range(db.upcast());
NavigationTargets::single(NavigationTarget {
file: full_range.file(),
focus_range: self.focus_range(db.upcast()).unwrap_or(full_range).range(),
full_range: full_range.range(),
})
}
}
#[cfg(test)]
mod tests {
use crate::db::tests::TestDb;
use insta::internals::SettingsBindDropGuard;
use red_knot_python_semantic::{
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
};
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
use ruff_python_ast::PythonVersion;
use ruff_text_size::TextSize;
pub(super) fn cursor_test(source: &str) -> CursorTest {
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");
// Filter out TODO types because they are different between debug and release builds.
insta_settings.add_filter(r"@Todo\(.+\)", "@Todo");
let insta_settings_guard = insta_settings.bind_to_scope();
CursorTest {
db,
cursor_offset: TextSize::try_from(cursor_offset)
.expect("source to be smaller than 4GB"),
file,
_insta_settings_guard: insta_settings_guard,
}
}
pub(super) struct CursorTest {
pub(super) db: TestDb,
pub(super) cursor_offset: TextSize,
pub(super) file: File,
_insta_settings_guard: SettingsBindDropGuard,
}
impl CursorTest {
pub(super) fn write_file(
&mut self,
path: impl AsRef<SystemPath>,
content: &str,
) -> std::io::Result<()> {
self.db.write_file(path, content)
}
pub(super) fn render_diagnostics<I, D>(&self, diagnostics: I) -> String
where
I: IntoIterator<Item = D>,
D: IntoDiagnostic,
{
use std::fmt::Write;
let mut buf = String::new();
let config = DisplayDiagnosticConfig::default()
.color(false)
.format(DiagnosticFormat::Full);
for diagnostic in diagnostics {
let diag = diagnostic.into_diagnostic();
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
}
buf
}
}
pub(super) trait IntoDiagnostic {
fn into_diagnostic(self) -> Diagnostic;
}
}

View File

@@ -1,66 +0,0 @@
use std::fmt;
use std::fmt::Formatter;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum MarkupKind {
PlainText,
Markdown,
}
impl MarkupKind {
pub(crate) const fn fenced_code_block<T>(self, code: T, language: &str) -> FencedCodeBlock<T>
where
T: fmt::Display,
{
FencedCodeBlock {
language,
code,
kind: self,
}
}
pub(crate) const fn horizontal_line(self) -> HorizontalLine {
HorizontalLine { kind: self }
}
}
pub(crate) struct FencedCodeBlock<'a, T> {
language: &'a str,
code: T,
kind: MarkupKind,
}
impl<T> fmt::Display for FencedCodeBlock<'_, T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
MarkupKind::PlainText => self.code.fmt(f),
MarkupKind::Markdown => write!(
f,
"```{language}\n{code}\n```",
language = self.language,
code = self.code
),
}
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct HorizontalLine {
kind: MarkupKind,
}
impl fmt::Display for HorizontalLine {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.kind {
MarkupKind::PlainText => {
f.write_str("\n---------------------------------------------\n")
}
MarkupKind::Markdown => {
write!(f, "\n---\n")
}
}
}
}

View File

@@ -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 }

View File

@@ -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

View File

@@ -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 {
@@ -159,7 +145,7 @@ impl salsa::Database for ProjectDatabase {
}
let event = event();
if matches!(event.kind, salsa::EventKind::WillCheckCancellation) {
if matches!(event.kind, salsa::EventKind::WillCheckCancellation { .. }) {
return;
}

View File

@@ -187,7 +187,7 @@ impl ProjectDatabase {
let program = Program::get(self);
if let Err(error) = program.update_from_settings(self, program_settings) {
tracing::error!("Failed to update the program settings, keeping the old program settings: {error}");
}
};
if metadata.root() == project.root(self) {
tracing::debug!("Reloading project after structural change");

View File

@@ -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>
);

View File

@@ -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
}
}

View File

@@ -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):

View File

@@ -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
```

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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
```

View File

@@ -26,11 +26,7 @@ reveal_type(type(1)) # revealed: Literal[int]
But a three-argument call to type creates a dynamic instance of the `type` class:
```py
class Base: ...
reveal_type(type("Foo", (), {})) # revealed: type
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type
```
Other numbers of arguments are invalid
@@ -42,60 +38,3 @@ type("Foo", ())
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", (), {}, weird_other_arg=42)
```
The following calls are also invalid, due to incorrect argument types:
```py
class Base: ...
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type(b"Foo", (), {})
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", Base, {})
# TODO: this should be an error
type("Foo", (1, 2), {})
# TODO: this should be an error
type("Foo", (Base,), {b"attr": 1})
```
## 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]
```

View File

@@ -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

View File

@@ -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

View File

@@ -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`:

View File

@@ -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

View File

@@ -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(),)
```

View File

@@ -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

View File

@@ -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): ...
```

View File

@@ -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`:

View File

@@ -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,46 +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)
```
A cast from `Todo` or `Unknown` to `Any` is not considered a "redundant cast": even if these are
understood as gradually equivalent types by red-knot, they are understood as different types by
human readers of red-knot's output. For `Unknown` in particular, we may consider it differently in
the context of some opt-in diagnostics, as it indicates that the gradual type has come about due to
an invalid annotation, missing annotation or missing type argument somewhere.
```py
from knot_extensions import Unknown
def f(x: Any, y: Unknown, z: Any | str | int):
a = cast(dict[str, Any], x)
reveal_type(a) # revealed: @Todo(generics)
b = cast(Any, y)
reveal_type(b) # revealed: Any
c = cast(str | int | Any, z) # error: [redundant-cast]
```

View File

@@ -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
```

View File

@@ -107,7 +107,8 @@ def good_return[T: int](x: T) -> T:
return x
def bad_return[T: int](x: T) -> T:
# error: [invalid-return-type] "Object of type `int` is not assignable to return type `T`"
# TODO: error: int is not assignable to T
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `Literal[1]`"
return x + 1
```

View File

@@ -48,523 +48,4 @@ class C[T]:
reveal_type(x) # revealed: T
```
## Fully static typevars
We consider a typevar to be fully static unless it has a non-fully-static bound or constraint. This
is true even though a fully static typevar might be specialized to a gradual form like `Any`. (This
is similar to how you can assign an expression whose type is not fully static to a target whose type
is.)
```py
from knot_extensions import is_fully_static, static_assert
from typing import Any
def unbounded_unconstrained[T](t: list[T]) -> None:
static_assert(is_fully_static(T))
def bounded[T: int](t: list[T]) -> None:
static_assert(is_fully_static(T))
def bounded_by_gradual[T: Any](t: list[T]) -> None:
static_assert(not is_fully_static(T))
def constrained[T: (int, str)](t: list[T]) -> None:
static_assert(is_fully_static(T))
def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None:
static_assert(not is_fully_static(T))
```
## Subtyping and assignability
(Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static
typevars. Unless otherwise noted, all of the claims also apply to _assignability_ involving gradual
typevars.)
We can make no assumption about what type an unbounded, unconstrained, fully static typevar will be
specialized to. Properties are true of the typevar only if they are true for every valid
specialization. Thus, the typevar is a subtype of itself and of `object`, but not of any other type
(including other typevars).
```py
from knot_extensions import is_assignable_to, is_subtype_of, static_assert
class Super: ...
class Base(Super): ...
class Sub(Base): ...
class Unrelated: ...
def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None:
static_assert(is_assignable_to(T, T))
static_assert(is_assignable_to(T, object))
static_assert(not is_assignable_to(T, Super))
static_assert(is_assignable_to(U, U))
static_assert(is_assignable_to(U, object))
static_assert(not is_assignable_to(U, Super))
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(is_subtype_of(T, T))
static_assert(is_subtype_of(T, object))
static_assert(not is_subtype_of(T, Super))
static_assert(is_subtype_of(U, U))
static_assert(is_subtype_of(U, object))
static_assert(not is_subtype_of(U, Super))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
```
A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of
its bound. (A typevar with a non-fully-static bound is itself non-fully-static, and therefore does
not participate in subtyping.) A fully static bound is not assignable to, nor a subtype of, the
typevar, since the typevar might be specialized to a smaller type. (This is true even if the bound
is a final class, since the typevar can still be specialized to `Never`.)
```py
from typing import Any
from typing_extensions import final
def bounded[T: Super](t: list[T]) -> None:
static_assert(is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Sub))
static_assert(not is_assignable_to(Super, T))
static_assert(not is_assignable_to(Sub, T))
static_assert(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Sub, T))
def bounded_by_gradual[T: Any](t: list[T]) -> None:
static_assert(is_assignable_to(T, Any))
static_assert(is_assignable_to(Any, T))
static_assert(is_assignable_to(T, Super))
static_assert(not is_assignable_to(Super, T))
static_assert(is_assignable_to(T, Sub))
static_assert(not is_assignable_to(Sub, T))
static_assert(not is_subtype_of(T, Any))
static_assert(not is_subtype_of(Any, T))
static_assert(not is_subtype_of(T, Super))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(Sub, T))
@final
class FinalClass: ...
def bounded_final[T: FinalClass](t: list[T]) -> None:
static_assert(is_assignable_to(T, FinalClass))
static_assert(not is_assignable_to(FinalClass, T))
static_assert(is_subtype_of(T, FinalClass))
static_assert(not is_subtype_of(FinalClass, T))
```
Two distinct fully static typevars are not subtypes of each other, even if they have the same
bounds, since there is (still) no guarantee that they will be specialized to the same type. This is
true even if both typevars are bounded by the same final class, since you can specialize the
typevars to `Never` in addition to that final class.
```py
def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
```
A constrained fully static typevar is assignable to the union of its constraints, but not to any of
the constraints individually. None of the constraints are subtypes of the typevar, though the
intersection of all of its constraints is a subtype of the typevar.
```py
from knot_extensions import Intersection
def constrained[T: (Base, Unrelated)](t: list[T]) -> None:
static_assert(not is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Base))
static_assert(not is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Unrelated))
static_assert(is_assignable_to(T, Super | Unrelated))
static_assert(is_assignable_to(T, Base | Unrelated))
static_assert(not is_assignable_to(T, Sub | Unrelated))
static_assert(not is_assignable_to(Super, T))
static_assert(not is_assignable_to(Unrelated, T))
static_assert(not is_assignable_to(Super | Unrelated, T))
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
static_assert(not is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Base))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Unrelated))
static_assert(is_subtype_of(T, Super | Unrelated))
static_assert(is_subtype_of(T, Base | Unrelated))
static_assert(not is_subtype_of(T, Sub | Unrelated))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Unrelated, T))
static_assert(not is_subtype_of(Super | Unrelated, T))
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None:
static_assert(is_assignable_to(T, Super))
static_assert(is_assignable_to(T, Base))
static_assert(not is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Unrelated))
static_assert(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Super | Any))
static_assert(is_assignable_to(T, Super | Unrelated))
static_assert(not is_assignable_to(Super, T))
static_assert(is_assignable_to(Base, T))
static_assert(not is_assignable_to(Unrelated, T))
static_assert(is_assignable_to(Any, T))
static_assert(not is_assignable_to(Super | Any, T))
static_assert(is_assignable_to(Base | Any, T))
static_assert(not is_assignable_to(Super | Unrelated, T))
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
static_assert(is_assignable_to(Intersection[Base, Any], T))
static_assert(not is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Base))
static_assert(not is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Unrelated))
static_assert(not is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Super | Any))
static_assert(not is_subtype_of(T, Super | Unrelated))
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Base, T))
static_assert(not is_subtype_of(Unrelated, T))
static_assert(not is_subtype_of(Any, T))
static_assert(not is_subtype_of(Super | Any, T))
static_assert(not is_subtype_of(Base | Any, T))
static_assert(not is_subtype_of(Super | Unrelated, T))
static_assert(not is_subtype_of(Intersection[Base, Unrelated], T))
static_assert(not is_subtype_of(Intersection[Base, Any], T))
```
Two distinct fully static typevars are not subtypes of each other, even if they have the same
constraints, and even if any of the constraints are final. There must always be at least two
distinct constraints, meaning that there is (still) no guarantee that they will be specialized to
the same type.
```py
def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
@final
class AnotherFinalClass: ...
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
```
## Singletons and single-valued types
(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the
claims also apply to _single-valued_ types.)
An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a
non-singleton type.
```py
from knot_extensions import is_singleton, is_single_valued, static_assert
def unbounded_unconstrained[T](t: list[T]) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
```
A bounded typevar is not a singleton, even if its bound is a singleton, since it can still be
specialized to `Never`.
```py
def bounded[T: None](t: list[T]) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
```
A constrained typevar is a singleton if all of its constraints are singletons. (Note that you cannot
specialize a constrained typevar to a subtype of a constraint.)
```py
from typing_extensions import Literal
def constrained_non_singletons[T: (int, str)](t: list[T]) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None:
static_assert(is_singleton(T))
def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None:
static_assert(is_single_valued(T))
```
## Unions involving typevars
The union of an unbounded unconstrained typevar with any other type cannot be simplified, since
there is no guarantee what type the typevar will be specialized to.
```py
from typing import Any
class Super: ...
class Base(Super): ...
class Sub(Base): ...
class Unrelated: ...
def unbounded_unconstrained[T](t: T) -> None:
def _(x: T | Super) -> None:
reveal_type(x) # revealed: T | Super
def _(x: T | Base) -> None:
reveal_type(x) # revealed: T | Base
def _(x: T | Sub) -> None:
reveal_type(x) # revealed: T | Sub
def _(x: T | Unrelated) -> None:
reveal_type(x) # revealed: T | Unrelated
def _(x: T | Any) -> None:
reveal_type(x) # revealed: T | Any
```
The union of a bounded typevar with its bound is that bound. (The typevar is guaranteed to be
specialized to a subtype of the bound.) The union of a bounded typevar with a subtype of its bound
cannot be simplified. (The typevar might be specialized to a different subtype of the bound.)
```py
def bounded[T: Base](t: T) -> None:
def _(x: T | Super) -> None:
reveal_type(x) # revealed: Super
def _(x: T | Base) -> None:
reveal_type(x) # revealed: Base
def _(x: T | Sub) -> None:
reveal_type(x) # revealed: T | Sub
def _(x: T | Unrelated) -> None:
reveal_type(x) # revealed: T | Unrelated
def _(x: T | Any) -> None:
reveal_type(x) # revealed: T | Any
```
The union of a constrained typevar with a type depends on how that type relates to the constraints.
If all of the constraints are a subtype of that type, the union simplifies to that type. Inversely,
if the type is a subtype of every constraint, the union simplifies to the typevar. Otherwise, the
union cannot be simplified.
```py
def constrained[T: (Base, Sub)](t: T) -> None:
def _(x: T | Super) -> None:
reveal_type(x) # revealed: Super
def _(x: T | Base) -> None:
reveal_type(x) # revealed: Base
def _(x: T | Sub) -> None:
reveal_type(x) # revealed: T
def _(x: T | Unrelated) -> None:
reveal_type(x) # revealed: T | Unrelated
def _(x: T | Any) -> None:
reveal_type(x) # revealed: T | Any
```
## Intersections involving typevars
The intersection of an unbounded unconstrained typevar with any other type cannot be simplified,
since there is no guarantee what type the typevar will be specialized to.
```py
from knot_extensions import Intersection
from typing import Any
class Super: ...
class Base(Super): ...
class Sub(Base): ...
class Unrelated: ...
def unbounded_unconstrained[T](t: T) -> None:
def _(x: Intersection[T, Super]) -> None:
reveal_type(x) # revealed: T & Super
def _(x: Intersection[T, Base]) -> None:
reveal_type(x) # revealed: T & Base
def _(x: Intersection[T, Sub]) -> None:
reveal_type(x) # revealed: T & Sub
def _(x: Intersection[T, Unrelated]) -> None:
reveal_type(x) # revealed: T & Unrelated
def _(x: Intersection[T, Any]) -> None:
reveal_type(x) # revealed: T & Any
```
The intersection of a bounded typevar with its bound or a supertype of its bound is the typevar
itself. (The typevar might be specialized to a subtype of the bound.) The intersection of a bounded
typevar with a subtype of its bound cannot be simplified. (The typevar might be specialized to a
different subtype of the bound.) The intersection of a bounded typevar with a type that is disjoint
from its bound is `Never`.
```py
def bounded[T: Base](t: T) -> None:
def _(x: Intersection[T, Super]) -> None:
reveal_type(x) # revealed: T
def _(x: Intersection[T, Base]) -> None:
reveal_type(x) # revealed: T
def _(x: Intersection[T, Sub]) -> None:
reveal_type(x) # revealed: T & Sub
def _(x: Intersection[T, None]) -> None:
reveal_type(x) # revealed: Never
def _(x: Intersection[T, Any]) -> None:
reveal_type(x) # revealed: T & Any
```
Constrained typevars can be modeled using a hypothetical `OneOf` connector, where the typevar must
be specialized to _one_ of its constraints. The typevar is not the _union_ of those constraints,
since that would allow the typevar to take on values from _multiple_ constraints simultaneously. The
`OneOf` connector would not be a “type” according to a strict reading of the typing spec, since it
would not represent a single set of runtime objects; it would instead represent a _set of_ sets of
runtime objects. This is one reason we have not actually added this connector to our data model yet.
Nevertheless, describing constrained typevars this way helps explain how we simplify intersections
involving them.
This means that when intersecting a constrained typevar with a type `T`, constraints that are
supertypes of `T` can be simplified to `T`, since intersection distributes over `OneOf`. Moreover,
constraints that are disjoint from `T` are no longer valid specializations of the typevar, since
`Never` is an identity for `OneOf`. After these simplifications, if only one constraint remains, we
can simplify the intersection as a whole to that constraint.
```py
def constrained[T: (Base, Sub, Unrelated)](t: T) -> None:
def _(x: Intersection[T, Base]) -> None:
# With OneOf this would be OneOf[Base, Sub]
reveal_type(x) # revealed: T & Base
def _(x: Intersection[T, Unrelated]) -> None:
reveal_type(x) # revealed: Unrelated
def _(x: Intersection[T, Sub]) -> None:
reveal_type(x) # revealed: Sub
def _(x: Intersection[T, None]) -> None:
reveal_type(x) # revealed: Never
def _(x: Intersection[T, Any]) -> None:
reveal_type(x) # revealed: T & Any
```
We can simplify the intersection similarly when removing a type from a constrained typevar, since
this is modeled internally as an intersection with a negation.
```py
from knot_extensions import Not
def remove_constraint[T: (int, str, bool)](t: T) -> None:
def _(x: Intersection[T, Not[int]]) -> None:
reveal_type(x) # revealed: str & ~int
def _(x: Intersection[T, Not[str]]) -> None:
# With OneOf this would be OneOf[int, bool]
reveal_type(x) # revealed: T & ~str
def _(x: Intersection[T, Not[bool]]) -> None:
reveal_type(x) # revealed: T & ~bool
def _(x: Intersection[T, Not[int], Not[str]]) -> None:
reveal_type(x) # revealed: Never
def _(x: Intersection[T, Not[None]]) -> None:
reveal_type(x) # revealed: T
def _(x: Intersection[T, Not[Any]]) -> None:
reveal_type(x) # revealed: T & Any
```
## Narrowing
We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar:
```py
class P: ...
class Q: ...
class R: ...
def f[T: (P, Q)](t: T) -> None:
if isinstance(t, P):
reveal_type(t) # revealed: P
p: P = t
else:
reveal_type(t) # revealed: Q & ~P
q: Q = t
if isinstance(t, Q):
reveal_type(t) # revealed: Q
q: Q = t
else:
reveal_type(t) # revealed: P & ~Q
p: P = t
def g[T: (P, Q, R)](t: T) -> None:
if isinstance(t, P):
reveal_type(t) # revealed: P
p: P = t
elif isinstance(t, Q):
reveal_type(t) # revealed: Q & ~P
q: Q = t
else:
reveal_type(t) # revealed: R & ~P & ~Q
r: R = t
if isinstance(t, P):
reveal_type(t) # revealed: P
p: P = t
elif isinstance(t, Q):
reveal_type(t) # revealed: Q & ~P
q: Q = t
elif isinstance(t, R):
reveal_type(t) # revealed: R & ~P & ~Q
r: R = t
else:
reveal_type(t) # revealed: Never
```
If the constraints are disjoint, simplification does eliminate the redundant negative:
```py
def h[T: (P, None)](t: T) -> None:
if t is None:
reveal_type(t) # revealed: None
p: None = t
else:
reveal_type(t) # revealed: P
p: P = t
```
[pep 695]: https://peps.python.org/pep-0695/

View File

@@ -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"]
```

View File

@@ -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
```

View File

@@ -76,9 +76,6 @@ No narrowing should occur if `type` is used to dynamically create a class:
```py
def _(x: str | int):
# The following diagnostic is valid, since the three-argument form of `type`
# can only be called with `str` as the first argument.
# error: [no-matching-overload] "No overload of class `type` matches arguments"
if type(x, (), {}) is str:
reveal_type(x) # revealed: str | int
else:

View File

@@ -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

View File

@@ -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)
```

View File

@@ -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

View File

@@ -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

View File

@@ -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(),)

View File

@@ -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)
```

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
```

View File

@@ -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

View File

@@ -654,7 +654,9 @@ def f(cond: bool) -> str:
reveal_type(x) # revealed: Literal["before"]
return "a"
x = "after-return"
reveal_type(x) # revealed: Never
# TODO: no unresolved-reference error
# error: [unresolved-reference]
reveal_type(x) # revealed: Unknown
else:
x = "else"
return reveal_type(x) # revealed: Literal["else"]

View File

@@ -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
```

View File

@@ -194,9 +194,6 @@ static_assert(is_assignable_to(tuple[int, str], tuple[int, str]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, int]))
static_assert(is_assignable_to(tuple[Any, Literal[2]], tuple[int, int]))
static_assert(is_assignable_to(tuple[Literal[1], Any], tuple[int, int]))
static_assert(is_assignable_to(tuple[()], tuple))
static_assert(is_assignable_to(tuple[int, str], tuple))
static_assert(is_assignable_to(tuple[Any], tuple))
static_assert(not is_assignable_to(tuple[()], tuple[int]))
static_assert(not is_assignable_to(tuple[int], tuple[str]))
@@ -405,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]))
@@ -435,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]))
@@ -453,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.
@@ -471,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

View File

@@ -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]))
```

View File

@@ -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

View File

@@ -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]))
```

View File

@@ -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

View File

@@ -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__]))
```

View File

@@ -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__]))
```

View File

@@ -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

View File

@@ -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]
```

View File

@@ -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`!

View File

@@ -1,254 +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")
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():
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()
```

View File

@@ -19,14 +19,14 @@ pub(crate) mod tests {
use std::sync::Arc;
use crate::program::{Program, SearchPathSettings};
use crate::{default_lint_registry, ProgramSettings, PythonPath, PythonPlatform};
use crate::{default_lint_registry, ProgramSettings, PythonPlatform};
use super::Db;
use crate::lint::{LintRegistry, RuleSelection};
use anyhow::Context;
use ruff_db::files::{File, Files};
use ruff_db::system::{
DbWithTestSystem, DbWithWritableSystem as _, System, SystemPath, SystemPathBuf, TestSystem,
DbWithTestSystem, DbWithWritableSystem as _, System, SystemPathBuf, TestSystem,
};
use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast};
@@ -139,8 +139,8 @@ pub(crate) mod tests {
python_version: PythonVersion,
/// Target Python platform
python_platform: PythonPlatform,
/// Paths to the directory to use for `site-packages`
site_packages: Vec<SystemPathBuf>,
/// Path to a custom typeshed directory
custom_typeshed: Option<SystemPathBuf>,
/// Path and content pairs for files that should be present
files: Vec<(&'a str, &'a str)>,
}
@@ -150,7 +150,7 @@ pub(crate) mod tests {
Self {
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
site_packages: vec![],
custom_typeshed: None,
files: vec![],
}
}
@@ -160,20 +160,8 @@ pub(crate) mod tests {
self
}
pub(crate) fn with_file(
mut self,
path: &'a (impl AsRef<SystemPath> + ?Sized),
content: &'a str,
) -> Self {
self.files.push((path.as_ref().as_str(), content));
self
}
pub(crate) fn with_site_packages_search_path(
mut self,
path: &(impl AsRef<SystemPath> + ?Sized),
) -> Self {
self.site_packages.push(path.as_ref().to_path_buf());
pub(crate) fn with_file(mut self, path: &'a str, content: &'a str) -> Self {
self.files.push((path, content));
self
}
@@ -187,7 +175,7 @@ pub(crate) mod tests {
.context("Failed to write test files")?;
let mut search_paths = SearchPathSettings::new(vec![src_root]);
search_paths.python_path = PythonPath::KnownSitePackages(self.site_packages);
search_paths.custom_typeshed = self.custom_typeshed;
Program::from_settings(
&db,

View File

@@ -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 {

View File

@@ -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};
@@ -96,13 +96,18 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
FilePath::SystemVirtual(_) => return None,
};
let module_name = search_paths(db).find_map(|candidate| {
let mut search_paths = search_paths(db);
let module_name = loop {
let candidate = search_paths.next()?;
let relative_path = match path {
SystemOrVendoredPathRef::System(path) => candidate.relativize_system_path(path),
SystemOrVendoredPathRef::Vendored(path) => candidate.relativize_vendored_path(path),
}?;
relative_path.to_module_name()
})?;
};
if let Some(relative_path) = relative_path {
break relative_path.to_module_name()?;
}
};
// Resolve the module name to see if Python would resolve the name to the same path.
// If it doesn't, then that means that multiple modules have the same name in different
@@ -110,7 +115,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
// in which case we ignore it.
let module = resolve_module(db, &module_name)?;
if file.path(db) == module.file().path(db) {
if file == module.file() {
Some(module)
} else {
// This path is for a module with the same name but with a different precedence. For example:
@@ -128,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.
@@ -239,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))
@@ -1964,33 +1929,4 @@ not_a_directory
Ok(())
}
#[test]
fn file_to_module_where_one_search_path_is_subdirectory_of_other() {
let project_directory = SystemPathBuf::from("/project");
let site_packages = project_directory.join(".venv/lib/python3.13/site-packages");
let installed_foo_module = site_packages.join("foo/__init__.py");
let mut db = TestDb::new();
db.write_file(&installed_foo_module, "").unwrap();
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
src_roots: vec![project_directory],
custom_typeshed: None,
python_path: PythonPath::KnownSitePackages(vec![site_packages.clone()]),
},
},
)
.unwrap();
let foo_module_file = File::new(&db, FilePath::System(installed_foo_module));
let module = file_to_module(&db, foo_module_file).unwrap();
assert_eq!(module.search_path(), &site_packages);
}
}

View File

@@ -322,7 +322,6 @@ fn python_version_from_versions_file_string(
#[cfg(test)]
mod tests {
use std::fmt::Write as _;
use std::num::{IntErrorKind, NonZeroU16};
use std::path::Path;
@@ -334,7 +333,8 @@ mod tests {
const TYPESHED_STDLIB_DIR: &str = "stdlib";
const ONE: Option<NonZeroU16> = Some(NonZeroU16::new(1).unwrap());
#[allow(unsafe_code)]
const ONE: Option<NonZeroU16> = Some(unsafe { NonZeroU16::new_unchecked(1) });
impl TypeshedVersions {
#[must_use]
@@ -571,7 +571,7 @@ foo: 3.8- # trailing comment
let mut massive_versions_file = String::new();
for i in 0..too_many {
let _ = writeln!(&mut massive_versions_file, "x{i}: 3.8-");
massive_versions_file.push_str(&format!("x{i}: 3.8-\n"));
}
assert_eq!(

View File

@@ -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`

View File

@@ -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)
}
}

View File

@@ -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);
@@ -1448,7 +1516,7 @@ where
self.visit_expr(subject);
if cases.is_empty() {
return;
}
};
let after_subject = self.flow_snapshot();
let mut vis_constraints = vec![];
@@ -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,
},
}
}
}

View File

@@ -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,18 +8,22 @@ 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.
///
/// ## ID stability
/// The `Definition`'s ID is stable when the only field that change is its `kind` (AST node).
/// ## Module-local type
/// This type should not be used as part of any cross-module API because
/// it holds a reference to the AST node. Range-offset changes
/// then propagate through all usages, and deserialization requires
/// reparsing the entire module.
///
/// The `Definition` changes when the `file`, `scope`, or `symbol` change. This can be
/// because a new scope gets inserted before the `Definition` or a new symbol is inserted
/// before this `Definition`. However, the ID can be considered stable and it is okay to use
/// `Definition` in cross-module` salsa queries or as a field on other salsa tracked structs.
/// E.g. don't use this type in:
///
/// * a return type of a cross-module query
/// * a field of a type that is a return type of a cross-module query
/// * an argument of a cross-module query
#[salsa::tracked(debug)]
pub struct Definition<'db> {
/// The file in which the definition occurs.
@@ -48,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.
@@ -243,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,
}
@@ -333,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))
@@ -348,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 {
@@ -379,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 {
@@ -446,6 +451,7 @@ impl<'db> DefinitionNodeRef<'db> {
value: _,
unpack: _,
name,
first: _,
}) => name.into(),
Self::AnnotatedAssignment(node) => node.into(),
Self::AugmentedAssignment(node) => node.into(),
@@ -453,6 +459,7 @@ impl<'db> DefinitionNodeRef<'db> {
unpack: _,
iterable: _,
name,
first: _,
is_async: _,
}) => name.into(),
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
@@ -462,6 +469,7 @@ impl<'db> DefinitionNodeRef<'db> {
Self::WithItem(WithItemDefinitionNodeRef {
unpack: _,
context_expr: _,
first: _,
is_async: _,
name,
}) => name.into(),
@@ -563,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(),
@@ -589,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
@@ -669,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,
}
}
@@ -797,6 +780,7 @@ pub struct AssignmentDefinitionKind<'db> {
target: TargetKind<'db>,
value: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
first: bool,
}
impl<'db> AssignmentDefinitionKind<'db> {
@@ -811,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)]
@@ -818,6 +806,7 @@ pub struct WithItemDefinitionKind<'db> {
target: TargetKind<'db>,
context_expr: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
first: bool,
is_async: bool,
}
@@ -834,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
}
@@ -844,6 +837,7 @@ pub struct ForStmtDefinitionKind<'db> {
target: TargetKind<'db>,
iterable: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
first: bool,
is_async: bool,
}
@@ -860,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
}

View File

@@ -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>>,
}

View File

@@ -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,

View File

@@ -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,
},
}
}
}

View File

@@ -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> {

View File

@@ -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"),
}
}
}

View File

@@ -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;
@@ -145,7 +145,7 @@ pub(crate) fn check_suppressions(db: &dyn Db, file: File, diagnostics: &mut Type
fn check_unknown_rule(context: &mut CheckSuppressionsContext) {
if context.is_lint_disabled(&UNKNOWN_RULE) {
return;
}
};
for unknown in &context.suppressions.unknown {
match &unknown.reason {
@@ -174,7 +174,7 @@ fn check_unknown_rule(context: &mut CheckSuppressionsContext) {
format_args!("Unknown rule `{prefixed}`. Did you mean `{suggestion}`?"),
);
}
}
};
}
}
@@ -267,7 +267,7 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) {
suppression.range,
format_args!("Unused `{kind}` without a code", kind = suppression.kind),
),
}
};
}
}
@@ -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![],
});
}
}

View File

@@ -464,11 +464,11 @@ impl<'db> SymbolAndQualifiers<'db> {
///
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
/// 2. Else, evaluate `fallback_fn()`:
/// 1. If `self` is definitely unbound, return the result of `fallback_fn()`.
/// 2. Else, if `fallback` is definitely unbound, return `self`.
/// 3. Else, if `self` is possibly unbound and `fallback` is definitely bound,
/// a. If `self` is definitely unbound, return the result of `fallback_fn()`.
/// b. Else, if `fallback` is definitely unbound, return `self`.
/// c. Else, if `self` is possibly unbound and `fallback` is definitely bound,
/// return `Symbol(<union of self-type and fallback-type>, Boundness::Bound)`
/// 4. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
/// d. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
/// return `Symbol(<union of self-type and fallback-type>, Boundness::PossiblyUnbound)`
#[must_use]
pub(crate) fn or_fall_back_to(
@@ -657,7 +657,7 @@ fn symbol_from_bindings_impl<'db>(
binding,
visibility_constraint,
narrowing_constraint: _,
}) if binding.is_none_or(is_non_exported) => {
}) if binding.map_or(true, is_non_exported) => {
visibility_constraints.evaluate(db, predicates, *visibility_constraint)
}
_ => Truthiness::AlwaysFalse,
@@ -679,53 +679,6 @@ fn symbol_from_bindings_impl<'db>(
visibility_constraints.evaluate(db, predicates, visibility_constraint);
if static_visibility.is_always_false() {
// We found a binding that we have statically determined to not be visible from
// the use of the symbol that we are investigating. There are three interesting
// cases to consider:
//
// ```py
// def f1():
// if False:
// x = 1
// use(x)
//
// def f2():
// y = 1
// return
// use(y)
//
// def f3(flag: bool):
// z = 1
// if flag:
// z = 2
// return
// use(z)
// ```
//
// In the first case, there is a single binding for `x`, and due to the statically
// known `False` condition, it is not visible at the use of `x`. However, we *can*
// see/reach the start of the scope from `use(x)`. This means that `x` is unbound
// and we should return `None`.
//
// In the second case, `y` is also not visible at the use of `y`, but here, we can
// not see/reach the start of the scope. There is only one path of control flow,
// and it passes through that binding of `y` (which we can not see). This implies
// that we are in an unreachable section of code. We return `Never` in order to
// silence the `unresolve-reference` diagnostic that would otherwise be emitted at
// the use of `y`.
//
// In the third case, we have two bindings for `z`. The first one is visible, so we
// consider the case that we now encounter the second binding `z = 2`, which is not
// visible due to the early return. We *also* can not see the start of the scope
// from `use(z)` because both paths of control flow pass through a binding of `z`.
// The `z = 1` binding is visible, and so we are *not* in an unreachable section of
// code. However, it is still okay to return `Never` in this case, because we will
// union the types of all bindings, and `Never` will be eliminated automatically.
if unbound_visibility.is_always_false() {
// The scope-start is not visible
return Some(Type::Never);
}
return None;
}
@@ -794,7 +747,7 @@ fn symbol_from_declarations_impl<'db>(
Some(DeclarationWithConstraint {
declaration,
visibility_constraint,
}) if declaration.is_none_or(is_non_exported) => {
}) if declaration.map_or(true, is_non_exported) => {
visibility_constraints.evaluate(db, predicates, *visibility_constraint)
}
_ => Truthiness::AlwaysFalse,

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@
//! eliminate the supertype from the intersection).
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
use crate::types::{IntersectionType, KnownClass, Type, TypeVarBoundOrConstraints, UnionType};
use crate::types::{IntersectionType, KnownClass, Type, UnionType};
use crate::{Db, FxOrderSet};
use smallvec::SmallVec;
@@ -485,101 +485,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
}
}
/// Tries to simplify any constrained typevars in the intersection:
///
/// - If the intersection contains a positive entry for exactly one of the constraints, we can
/// remove the typevar (effectively replacing it with that one positive constraint).
///
/// - If the intersection contains negative entries for all but one of the constraints, we can
/// remove the negative constraints and replace the typevar with the remaining positive
/// constraint.
///
/// - If the intersection contains negative entries for all of the constraints, the overall
/// intersection is `Never`.
fn simplify_constrained_typevars(&mut self, db: &'db dyn Db) {
let mut to_add = SmallVec::<[Type<'db>; 1]>::new();
let mut positive_to_remove = SmallVec::<[usize; 1]>::new();
for (typevar_index, ty) in self.positive.iter().enumerate() {
let Type::TypeVar(typevar) = ty else {
continue;
};
let Some(TypeVarBoundOrConstraints::Constraints(constraints)) =
typevar.bound_or_constraints(db)
else {
continue;
};
// Determine which constraints appear as positive entries in the intersection. Note
// that we shouldn't have duplicate entries in the positive or negative lists, so we
// don't need to worry about finding any particular constraint more than once.
let constraints = constraints.elements(db);
let mut positive_constraint_count = 0;
for positive in &self.positive {
// This linear search should be fine as long as we don't encounter typevars with
// thousands of constraints.
positive_constraint_count += constraints
.iter()
.filter(|c| c.is_subtype_of(db, *positive))
.count();
}
// If precisely one constraint appears as a positive element, we can replace the
// typevar with that positive constraint.
if positive_constraint_count == 1 {
positive_to_remove.push(typevar_index);
continue;
}
// Determine which constraints appear as negative entries in the intersection.
let mut to_remove = Vec::with_capacity(constraints.len());
let mut remaining_constraints: Vec<_> = constraints.iter().copied().map(Some).collect();
for (negative_index, negative) in self.negative.iter().enumerate() {
// This linear search should be fine as long as we don't encounter typevars with
// thousands of constraints.
let matching_constraints = constraints
.iter()
.enumerate()
.filter(|(_, c)| c.is_subtype_of(db, *negative));
for (constraint_index, _) in matching_constraints {
to_remove.push(negative_index);
remaining_constraints[constraint_index] = None;
}
}
let mut iter = remaining_constraints.into_iter().flatten();
let Some(remaining_constraint) = iter.next() else {
// All of the typevar constraints have been removed, so the entire intersection is
// `Never`.
*self = Self::default();
self.positive.insert(Type::Never);
return;
};
let more_than_one_remaining_constraint = iter.next().is_some();
if more_than_one_remaining_constraint {
// This typevar cannot be simplified.
continue;
}
// Only one typevar constraint remains. Remove all of the negative constraints, and
// replace the typevar itself with the remaining positive constraint.
to_add.push(remaining_constraint);
positive_to_remove.push(typevar_index);
}
// We don't need to sort the positive list, since we only append to it in increasing order.
for index in positive_to_remove.into_iter().rev() {
self.positive.swap_remove_index(index);
}
for remaining_constraint in to_add {
self.add_positive(db, remaining_constraint);
}
}
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
self.simplify_constrained_typevars(db);
match (self.positive.len(), self.negative.len()) {
(0, 0) => Type::object(db),
(1, 0) => self.positive[0],

View File

@@ -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() {
@@ -502,13 +385,13 @@ impl<'db> Bindings<'db> {
if let Some(len_ty) = first_arg.len(db) {
overload.set_return_type(len_ty);
}
}
};
}
Some(KnownFunction::Repr) => {
if let [Some(first_arg)] = overload.parameter_types() {
overload.set_return_type(first_arg.repr(db));
}
};
}
Some(KnownFunction::Cast) => {
@@ -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()
}
),
);
}
}
}

View File

@@ -1,11 +1,5 @@
use std::sync::{LazyLock, Mutex};
use super::{
class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder,
KnownFunction, Mro, MroError, MroIterator, SubclassOfType, Truthiness, Type, TypeAliasType,
TypeQualifiers, TypeVarInstance,
};
use crate::semantic_index::definition::Definition;
use crate::{
module_resolver::file_to_module,
semantic_index::{
@@ -24,10 +18,16 @@ 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;
use super::{
class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder,
KnownFunction, Mro, MroError, MroIterator, SubclassOfType, Truthiness, Type, TypeAliasType,
TypeQualifiers, TypeVarInstance,
};
/// Representation of a runtime class object.
///
/// Does not in itself represent a type,
@@ -43,14 +43,52 @@ pub struct Class<'db> {
pub(crate) known: Option<KnownClass>,
}
fn explicit_bases_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &[Type<'db>],
_count: u32,
_self: Class<'db>,
) -> salsa::CycleRecoveryAction<Box<[Type<'db>]>> {
salsa::CycleRecoveryAction::Iterate
}
fn explicit_bases_cycle_initial<'db>(_db: &'db dyn Db, _self: Class<'db>) -> Box<[Type<'db>]> {
Box::default()
}
fn try_mro_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &Result<Mro<'db>, MroError<'db>>,
_count: u32,
_self: Class<'db>,
) -> salsa::CycleRecoveryAction<Result<Mro<'db>, MroError<'db>>> {
salsa::CycleRecoveryAction::Iterate
}
#[allow(clippy::unnecessary_wraps)]
fn try_mro_cycle_initial<'db>(
db: &'db dyn Db,
self_: Class<'db>,
) -> Result<Mro<'db>, MroError<'db>> {
Ok(Mro::from_error(db, self_))
}
#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)]
fn inheritance_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &Option<InheritanceCycle>,
_count: u32,
_self: Class<'db>,
) -> salsa::CycleRecoveryAction<Option<InheritanceCycle>> {
salsa::CycleRecoveryAction::Iterate
}
fn inheritance_cycle_initial<'db>(_db: &'db dyn Db, _self: Class<'db>) -> Option<InheritanceCycle> {
None
}
#[salsa::tracked]
impl<'db> Class<'db> {
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
let scope = self.body_scope(db);
let index = semantic_index(db, scope.file(db));
index.expect_single_definition(scope.node(db).expect_class())
}
/// Return `true` if this class represents `known_class`
pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool {
self.known(db) == Some(known_class)
@@ -115,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>]> {
@@ -201,7 +230,8 @@ impl<'db> Class<'db> {
.find_keyword("metaclass")?
.value;
let class_definition = self.definition(db);
let class_definition =
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);
Some(definition_expression_type(
db,
@@ -701,50 +731,6 @@ impl<'db> Class<'db> {
}
}
fn explicit_bases_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &[Type<'db>],
_count: u32,
_self: Class<'db>,
) -> salsa::CycleRecoveryAction<Box<[Type<'db>]>> {
salsa::CycleRecoveryAction::Iterate
}
fn explicit_bases_cycle_initial<'db>(_db: &'db dyn Db, _self: Class<'db>) -> Box<[Type<'db>]> {
Box::default()
}
fn try_mro_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &Result<Mro<'db>, MroError<'db>>,
_count: u32,
_self: Class<'db>,
) -> salsa::CycleRecoveryAction<Result<Mro<'db>, MroError<'db>>> {
salsa::CycleRecoveryAction::Iterate
}
#[allow(clippy::unnecessary_wraps)]
fn try_mro_cycle_initial<'db>(
db: &'db dyn Db,
self_: Class<'db>,
) -> Result<Mro<'db>, MroError<'db>> {
Ok(Mro::from_error(db, self_))
}
#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)]
fn inheritance_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &Option<InheritanceCycle>,
_count: u32,
_self: Class<'db>,
) -> salsa::CycleRecoveryAction<Option<InheritanceCycle>> {
salsa::CycleRecoveryAction::Iterate
}
fn inheritance_cycle_initial<'db>(_db: &'db dyn Db, _self: Class<'db>) -> Option<InheritanceCycle> {
None
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub(super) enum InheritanceCycle {
/// The class is cyclically defined and is a participant in the cycle.
@@ -768,7 +754,7 @@ pub struct ClassLiteralType<'db> {
}
impl<'db> ClassLiteralType<'db> {
pub(super) fn class(self) -> Class<'db> {
pub(crate) fn class(self) -> Class<'db> {
self.class
}
@@ -825,7 +811,6 @@ pub enum KnownClass {
Bool,
Object,
Bytes,
Bytearray,
Type,
Int,
Float,
@@ -842,9 +827,6 @@ pub enum KnownClass {
BaseException,
BaseExceptionGroup,
Classmethod,
Super,
// enum
Enum,
// Types
GenericAlias,
ModuleType,
@@ -855,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,
@@ -880,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 {
@@ -911,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
@@ -936,7 +911,6 @@ impl<'db> KnownClass {
| Self::Int
| Self::Type
| Self::Bytes
| Self::Bytearray
| Self::FrozenSet
| Self::Range
| Self::Property
@@ -950,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",
@@ -993,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",
@@ -1006,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,
@@ -1025,7 +987,6 @@ impl<'db> KnownClass {
"ellipsis"
}
}
Self::NotImplementedType => "_NotImplementedType",
}
}
@@ -1144,7 +1105,6 @@ impl<'db> KnownClass {
Self::Bool
| Self::Object
| Self::Bytes
| Self::Bytearray
| Self::Type
| Self::Int
| Self::Float
@@ -1160,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
@@ -1171,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);
@@ -1204,7 +1158,6 @@ impl<'db> KnownClass {
KnownModule::Builtins
}
}
Self::NotImplementedType => KnownModule::Builtins,
Self::ChainMap
| Self::Counter
| Self::DefaultDict
@@ -1220,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
@@ -1260,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,
}
}
@@ -1274,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
@@ -1319,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,
}
}
@@ -1337,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,
@@ -1369,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,
@@ -1382,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
@@ -1391,7 +1325,6 @@ impl<'db> KnownClass {
"EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => {
Self::EllipsisType
}
"_NotImplementedType" => Self::NotImplementedType,
_ => return None,
};
@@ -1403,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
@@ -1437,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
@@ -1448,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),
@@ -1531,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,
@@ -1575,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):
@@ -1639,7 +1562,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Not
| Self::Intersection
| Self::TypeOf
| Self::CallableTypeOf => Truthiness::AlwaysTrue,
| Self::CallableTypeFromFunction => Truthiness::AlwaysTrue,
}
}
@@ -1686,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",
}
}
@@ -1730,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,
@@ -1795,7 +1718,7 @@ impl<'db> KnownInstanceType<'db> {
"Not" => Self::Not,
"Intersection" => Self::Intersection,
"TypeOf" => Self::TypeOf,
"CallableTypeOf" => Self::CallableTypeOf,
"CallableTypeFromFunction" => Self::CallableTypeFromFunction,
_ => return None,
};
@@ -1852,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(),
}
}
}

View File

@@ -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(_)
@@ -85,7 +77,6 @@ impl<'db> ClassBase<'db> {
| Type::SliceLiteral(_)
| Type::ModuleLiteral(_)
| Type::SubclassOf(_)
| Type::TypeVar(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy => None,
Type::KnownInstance(known_instance) => match known_instance {
@@ -112,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()),

Some files were not shown because too many files have changed in this diff Show More