Compare commits

..

1 Commits

Author SHA1 Message Date
Douglas Creager
9eff2734bb wip: subscript always via __getitem__ 2025-03-27 14:19:48 -04:00
316 changed files with 11995 additions and 15132 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-v2"
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
- name: Run mypy_primer
shell: bash
@@ -68,7 +68,7 @@ jobs:
--type-checker knot \
--old base_commit \
--new "$GITHUB_SHA" \
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument|typeshed-stats|scrapy)$' \
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument)$' \
--output concise \
--debug > mypy_primer.diff || [ $? -eq 1 ]

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

@@ -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,39 +1,5 @@
# Changelog
## 0.11.3
### Preview features
- \[`airflow`\] Add more autofixes for `AIR302` ([#16876](https://github.com/astral-sh/ruff/pull/16876), [#16977](https://github.com/astral-sh/ruff/pull/16977), [#16976](https://github.com/astral-sh/ruff/pull/16976), [#16965](https://github.com/astral-sh/ruff/pull/16965))
- \[`airflow`\] Move `AIR301` to `AIR002` ([#16978](https://github.com/astral-sh/ruff/pull/16978))
- \[`airflow`\] Move `AIR302` to `AIR301` and `AIR303` to `AIR302` ([#17151](https://github.com/astral-sh/ruff/pull/17151))
- \[`flake8-bandit`\] Mark `str` and `list[str]` literals as trusted input (`S603`) ([#17136](https://github.com/astral-sh/ruff/pull/17136))
- \[`ruff`\] Support slices in `RUF005` ([#17078](https://github.com/astral-sh/ruff/pull/17078))
- [syntax-errors] Start detecting compile-time syntax errors ([#16106](https://github.com/astral-sh/ruff/pull/16106))
- [syntax-errors] Duplicate type parameter names ([#16858](https://github.com/astral-sh/ruff/pull/16858))
- [syntax-errors] Irrefutable `case` pattern before final case ([#16905](https://github.com/astral-sh/ruff/pull/16905))
- [syntax-errors] Multiple assignments in `case` pattern ([#16957](https://github.com/astral-sh/ruff/pull/16957))
- [syntax-errors] Single starred assignment target ([#17024](https://github.com/astral-sh/ruff/pull/17024))
- [syntax-errors] Starred expressions in `return`, `yield`, and `for` ([#17134](https://github.com/astral-sh/ruff/pull/17134))
- [syntax-errors] Store to or delete `__debug__` ([#16984](https://github.com/astral-sh/ruff/pull/16984))
### Bug fixes
- Error instead of `panic!` when running Ruff from a deleted directory (#16903) ([#17054](https://github.com/astral-sh/ruff/pull/17054))
- [syntax-errors] Fix false positive for parenthesized tuple index ([#16948](https://github.com/astral-sh/ruff/pull/16948))
### CLI
- Check `pyproject.toml` correctly when it is passed via stdin ([#16971](https://github.com/astral-sh/ruff/pull/16971))
### Configuration
- \[`flake8-import-conventions`\] Add import `numpy.typing as npt` to default `flake8-import-conventions.aliases` ([#17133](https://github.com/astral-sh/ruff/pull/17133))
### Documentation
- \[`refurb`\] Document why `UserDict`, `UserList`, and `UserString` are preferred over `dict`, `list`, and `str` (`FURB189`) ([#16927](https://github.com/astral-sh/ruff/pull/16927))
## 0.11.2
### Preview features

51
Cargo.lock generated
View File

@@ -334,9 +334,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.34"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
dependencies = [
"clap_builder",
"clap_derive",
@@ -344,9 +344,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.34"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
dependencies = [
"anstream",
"anstyle",
@@ -918,7 +918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1499,7 +1499,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.0",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1726,9 +1726,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.27"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "loom"
@@ -2503,22 +2503,6 @@ dependencies = [
"wild",
]
[[package]]
name = "red_knot_ide"
version = "0.0.0"
dependencies = [
"insta",
"red_knot_python_semantic",
"red_knot_vendored",
"ruff_db",
"ruff_python_ast",
"ruff_python_parser",
"ruff_text_size",
"salsa",
"smallvec",
"tracing",
]
[[package]]
name = "red_knot_project"
version = "0.0.0"
@@ -2530,7 +2514,6 @@ dependencies = [
"notify",
"pep440_rs",
"rayon",
"red_knot_ide",
"red_knot_python_semantic",
"red_knot_vendored",
"ruff_cache",
@@ -2602,9 +2585,7 @@ dependencies = [
"libc",
"lsp-server",
"lsp-types",
"red_knot_ide",
"red_knot_project",
"red_knot_python_semantic",
"ruff_db",
"ruff_notebook",
"ruff_python_ast",
@@ -2665,7 +2646,6 @@ dependencies = [
"getrandom 0.3.2",
"js-sys",
"log",
"red_knot_ide",
"red_knot_project",
"red_knot_python_semantic",
"ruff_db",
@@ -2754,7 +2734,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.3"
version = "0.11.2"
dependencies = [
"anyhow",
"argfile",
@@ -2989,7 +2969,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.3"
version = "0.11.2"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3018,6 +2998,7 @@ dependencies = [
"ruff_annotate_snippets",
"ruff_cache",
"ruff_diagnostics",
"ruff_index",
"ruff_macros",
"ruff_notebook",
"ruff_python_ast",
@@ -3212,7 +3193,6 @@ name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.9.0",
"insta",
"is-macro",
"ruff_cache",
"ruff_index",
@@ -3225,7 +3205,6 @@ dependencies = [
"schemars",
"serde",
"smallvec",
"test-case",
]
[[package]]
@@ -3313,7 +3292,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.3"
version = "0.11.2"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3407,7 +3386,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3420,7 +3399,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.3",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3805,7 +3784,7 @@ dependencies = [
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.2",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]

View File

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

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.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.3/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.11.2/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.2/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.3
rev: v0.11.2
hooks:
# Run the linter.
- id: ruff

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,30 +0,0 @@
[package]
name = "red_knot_ide"
version = "0.0.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
[dependencies]
ruff_db = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_text_size = { workspace = true }
red_knot_python_semantic = { workspace = true }
salsa = { workspace = true }
smallvec = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
red_knot_vendored = { workspace = true }
insta = { workspace = true, features = ["filters"] }
[lints]
workspace = true

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,912 +0,0 @@
use crate::find_node::covering_node;
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
use red_knot_python_semantic::{HasType, SemanticModel};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::{parsed_module, ParsedModule};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
pub fn goto_type_definition(
db: &dyn Db,
file: File,
offset: TextSize,
) -> Option<RangedValue<NavigationTargets>> {
let parsed = parsed_module(db.upcast(), file);
let goto_target = find_goto_target(parsed, offset)?;
let model = SemanticModel::new(db.upcast(), file);
let ty = match goto_target {
GotoTarget::Expression(expression) => expression.inferred_type(&model),
GotoTarget::FunctionDef(function) => function.inferred_type(&model),
GotoTarget::ClassDef(class) => class.inferred_type(&model),
GotoTarget::Parameter(parameter) => parameter.inferred_type(&model),
GotoTarget::Alias(alias) => alias.inferred_type(&model),
GotoTarget::ExceptVariable(except) => except.inferred_type(&model),
GotoTarget::KeywordArgument(argument) => {
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
// than using the inferred value.
argument.value.inferred_type(&model)
}
// TODO: Support identifier targets
GotoTarget::PatternMatchRest(_)
| GotoTarget::PatternKeywordArgument(_)
| GotoTarget::PatternMatchStarName(_)
| GotoTarget::PatternMatchAsName(_)
| GotoTarget::ImportedModule(_)
| GotoTarget::TypeParamTypeVarName(_)
| GotoTarget::TypeParamParamSpecName(_)
| GotoTarget::TypeParamTypeVarTupleName(_)
| GotoTarget::NonLocal { .. }
| GotoTarget::Globals { .. } => return None,
};
tracing::debug!(
"Inferred type of covering node is {}",
ty.display(db.upcast())
);
Some(RangedValue {
range: FileRange::new(file, goto_target.range()),
value: ty.navigation_targets(db),
})
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum GotoTarget<'a> {
Expression(ast::ExprRef<'a>),
FunctionDef(&'a ast::StmtFunctionDef),
ClassDef(&'a ast::StmtClassDef),
Parameter(&'a ast::Parameter),
Alias(&'a ast::Alias),
/// Go to on the module name of an import from
/// ```py
/// from foo import bar
/// ^^^
/// ```
ImportedModule(&'a ast::StmtImportFrom),
/// Go to on the exception handler variable
/// ```py
/// try: ...
/// except Exception as e: ...
/// ^
/// ```
ExceptVariable(&'a ast::ExceptHandlerExceptHandler),
/// Go to on a keyword argument
/// ```py
/// test(a = 1)
/// ^
/// ```
KeywordArgument(&'a ast::Keyword),
/// Go to on the rest parameter of a pattern match
///
/// ```py
/// match x:
/// case {"a": a, "b": b, **rest}: ...
/// ^^^^
/// ```
PatternMatchRest(&'a ast::PatternMatchMapping),
/// Go to on a keyword argument of a class pattern
///
/// ```py
/// match Point3D(0, 0, 0):
/// case Point3D(x=0, y=0, z=0): ...
/// ^ ^ ^
/// ```
PatternKeywordArgument(&'a ast::PatternKeyword),
/// Go to on a pattern star argument
///
/// ```py
/// match array:
/// case [*args]: ...
/// ^^^^
PatternMatchStarName(&'a ast::PatternMatchStar),
/// Go to on the name of a pattern match as pattern
///
/// ```py
/// match x:
/// case [x] as y: ...
/// ^
PatternMatchAsName(&'a ast::PatternMatchAs),
/// Go to on the name of a type variable
///
/// ```py
/// type Alias[T: int = bool] = list[T]
/// ^
/// ```
TypeParamTypeVarName(&'a ast::TypeParamTypeVar),
/// Go to on the name of a type param spec
///
/// ```py
/// type Alias[**P = [int, str]] = Callable[P, int]
/// ^
/// ```
TypeParamParamSpecName(&'a ast::TypeParamParamSpec),
/// Go to on the name of a type var tuple
///
/// ```py
/// type Alias[*Ts = ()] = tuple[*Ts]
/// ^^
/// ```
TypeParamTypeVarTupleName(&'a ast::TypeParamTypeVarTuple),
NonLocal {
identifier: &'a ast::Identifier,
},
Globals {
identifier: &'a ast::Identifier,
},
}
impl Ranged for GotoTarget<'_> {
fn range(&self) -> TextRange {
match self {
GotoTarget::Expression(expression) => expression.range(),
GotoTarget::FunctionDef(function) => function.name.range,
GotoTarget::ClassDef(class) => class.name.range,
GotoTarget::Parameter(parameter) => parameter.name.range,
GotoTarget::Alias(alias) => alias.name.range,
GotoTarget::ImportedModule(module) => module.module.as_ref().unwrap().range,
GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range,
GotoTarget::KeywordArgument(keyword) => keyword.arg.as_ref().unwrap().range,
GotoTarget::PatternMatchRest(rest) => rest.rest.as_ref().unwrap().range,
GotoTarget::PatternKeywordArgument(keyword) => keyword.attr.range,
GotoTarget::PatternMatchStarName(star) => star.name.as_ref().unwrap().range,
GotoTarget::PatternMatchAsName(as_name) => as_name.name.as_ref().unwrap().range,
GotoTarget::TypeParamTypeVarName(type_var) => type_var.name.range,
GotoTarget::TypeParamParamSpecName(spec) => spec.name.range,
GotoTarget::TypeParamTypeVarTupleName(tuple) => tuple.name.range,
GotoTarget::NonLocal { identifier, .. } => identifier.range,
GotoTarget::Globals { identifier, .. } => identifier.range,
}
}
}
pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option<GotoTarget> {
let token = parsed.tokens().at_offset(offset).find(|token| {
matches!(
token.kind(),
TokenKind::Name
| TokenKind::String
| TokenKind::Complex
| TokenKind::Float
| TokenKind::Int
)
})?;
let covering_node = covering_node(parsed.syntax().into(), token.range())
.find(|node| node.is_identifier() || node.is_expression())
.ok()?;
tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind());
match covering_node.node() {
AnyNodeRef::Identifier(identifier) => match covering_node.parent() {
Some(AnyNodeRef::StmtFunctionDef(function)) => Some(GotoTarget::FunctionDef(function)),
Some(AnyNodeRef::StmtClassDef(class)) => Some(GotoTarget::ClassDef(class)),
Some(AnyNodeRef::Parameter(parameter)) => Some(GotoTarget::Parameter(parameter)),
Some(AnyNodeRef::Alias(alias)) => Some(GotoTarget::Alias(alias)),
Some(AnyNodeRef::StmtImportFrom(from)) => Some(GotoTarget::ImportedModule(from)),
Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => {
Some(GotoTarget::ExceptVariable(handler))
}
Some(AnyNodeRef::Keyword(keyword)) => Some(GotoTarget::KeywordArgument(keyword)),
Some(AnyNodeRef::PatternMatchMapping(mapping)) => {
Some(GotoTarget::PatternMatchRest(mapping))
}
Some(AnyNodeRef::PatternKeyword(keyword)) => {
Some(GotoTarget::PatternKeywordArgument(keyword))
}
Some(AnyNodeRef::PatternMatchStar(star)) => {
Some(GotoTarget::PatternMatchStarName(star))
}
Some(AnyNodeRef::PatternMatchAs(as_pattern)) => {
Some(GotoTarget::PatternMatchAsName(as_pattern))
}
Some(AnyNodeRef::TypeParamTypeVar(var)) => Some(GotoTarget::TypeParamTypeVarName(var)),
Some(AnyNodeRef::TypeParamParamSpec(bound)) => {
Some(GotoTarget::TypeParamParamSpecName(bound))
}
Some(AnyNodeRef::TypeParamTypeVarTuple(var_tuple)) => {
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
}
Some(AnyNodeRef::ExprAttribute(attribute)) => {
Some(GotoTarget::Expression(attribute.into()))
}
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
None => None,
Some(parent) => {
tracing::debug!(
"Missing `GoToTarget` for identifier with parent {:?}",
parent.kind()
);
None
}
},
node => node.as_expr_ref().map(GotoTarget::Expression),
}
}
#[cfg(test)]
mod tests {
use std::fmt::Write;
use crate::db::tests::TestDb;
use crate::{goto_type_definition, NavigationTarget};
use insta::assert_snapshot;
use insta::internals::SettingsBindDropGuard;
use red_knot_python_semantic::{
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
};
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
Severity, Span, SubDiagnostic,
};
use ruff_db::files::{system_path_to_file, File, FileRange};
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
use ruff_python_ast::PythonVersion;
use ruff_text_size::{Ranged, TextSize};
#[test]
fn goto_type_of_expression_with_class_type() {
let test = goto_test(
r#"
class Test: ...
a<CURSOR>b = Test()
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> /main.py:2:19
|
2 | class Test: ...
| ^^^^
3 |
4 | ab = Test()
|
info: Source
--> /main.py:4:13
|
2 | class Test: ...
3 |
4 | ab = Test()
| ^^
|
"###);
}
#[test]
fn goto_type_of_expression_with_function_type() {
let test = goto_test(
r#"
def foo(a, b): ...
ab = foo
a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> /main.py:2:17
|
2 | def foo(a, b): ...
| ^^^
3 |
4 | ab = foo
|
info: Source
--> /main.py:6:13
|
4 | ab = foo
5 |
6 | ab
| ^^
|
"###);
}
#[test]
fn goto_type_of_expression_with_union_type() {
let test = goto_test(
r#"
def foo(a, b): ...
def bar(a, b): ...
if random.choice():
a = foo
else:
a = bar
a<CURSOR>
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> /main.py:3:17
|
3 | def foo(a, b): ...
| ^^^
4 |
5 | def bar(a, b): ...
|
info: Source
--> /main.py:12:13
|
10 | a = bar
11 |
12 | a
| ^
|
info: lint:goto-type-definition: Type definition
--> /main.py:5:17
|
3 | def foo(a, b): ...
4 |
5 | def bar(a, b): ...
| ^^^
6 |
7 | if random.choice():
|
info: Source
--> /main.py:12:13
|
10 | a = bar
11 |
12 | a
| ^
|
");
}
#[test]
fn goto_type_of_expression_with_module() {
let mut test = goto_test(
r#"
import lib
lib<CURSOR>
"#,
);
test.write_file("lib.py", "a = 10").unwrap();
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> /lib.py:1:1
|
1 | a = 10
| ^
|
info: Source
--> /main.py:4:13
|
2 | import lib
3 |
4 | lib
| ^^^
|
"###);
}
#[test]
fn goto_type_of_expression_with_literal_type() {
let test = goto_test(
r#"
a: str = "test"
a<CURSOR>
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:4:13
|
2 | a: str = "test"
3 |
4 | a
| ^
|
"###);
}
#[test]
fn goto_type_of_expression_with_literal_node() {
let test = goto_test(
r#"
a: str = "te<CURSOR>st"
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:2:22
|
2 | a: str = "test"
| ^^^^^^
|
"###);
}
#[test]
fn goto_type_of_expression_with_type_var_type() {
let test = goto_test(
r#"
type Alias[T: int = bool] = list[T<CURSOR>]
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> /main.py:2:24
|
2 | type Alias[T: int = bool] = list[T]
| ^
|
info: Source
--> /main.py:2:46
|
2 | type Alias[T: int = bool] = list[T]
| ^
|
"###);
}
#[test]
fn goto_type_of_expression_with_type_param_spec() {
let test = goto_test(
r#"
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
"#,
);
// TODO: Goto type definition currently doesn't work for type param specs
// because the inference doesn't support them yet.
// This snapshot should show a single target pointing to `T`
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]
fn goto_type_of_expression_with_type_var_tuple() {
let test = goto_test(
r#"
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
"#,
);
// TODO: Goto type definition currently doesn't work for type var tuples
// because the inference doesn't support them yet.
// This snapshot should show a single target pointing to `T`
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]
fn goto_type_on_keyword_argument() {
let test = goto_test(
r#"
def test(a: str): ...
test(a<CURSOR>= "123")
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:4:18
|
2 | def test(a: str): ...
3 |
4 | test(a= "123")
| ^
|
"###);
}
#[test]
fn goto_type_on_incorrectly_typed_keyword_argument() {
let test = goto_test(
r#"
def test(a: str): ...
test(a<CURSOR>= 123)
"#,
);
// TODO: This should jump to `str` and not `int` because
// the keyword is typed as a string. It's only the passed argument that
// is an int. Navigating to `str` would match pyright's behavior.
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:234:7
|
232 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
233 |
234 | class int:
| ^^^
235 | @overload
236 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ...
|
info: Source
--> /main.py:4:18
|
2 | def test(a: str): ...
3 |
4 | test(a= 123)
| ^
|
"###);
}
#[test]
fn goto_type_on_kwargs() {
let test = goto_test(
r#"
def f(name: str): ...
kwargs = { "name": "test"}
f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:1098:7
|
1096 | def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
1097 |
1098 | class dict(MutableMapping[_KT, _VT]):
| ^^^^
1099 | # __init__ should be kept roughly in line with `collections.UserDict.__init__`, which has similar semantics
1100 | # Also multiprocessing.managers.SyncManager.dict()
|
info: Source
--> /main.py:6:5
|
4 | kwargs = { "name": "test"}
5 |
6 | f(**kwargs)
| ^^^^^^
|
"###);
}
#[test]
fn goto_type_of_expression_with_builtin() {
let test = goto_test(
r#"
def foo(a: str):
a<CURSOR>
"#,
);
// FIXME: This should go to `str`
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:3:17
|
2 | def foo(a: str):
3 | a
| ^
|
"###);
}
#[test]
fn goto_type_definition_cursor_between_object_and_attribute() {
let test = goto_test(
r#"
class X:
def foo(a, b): ...
x = X()
x<CURSOR>.foo()
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> /main.py:2:19
|
2 | class X:
| ^
3 | def foo(a, b): ...
|
info: Source
--> /main.py:7:13
|
5 | x = X()
6 |
7 | x.foo()
| ^
|
"###);
}
#[test]
fn goto_between_call_arguments() {
let test = goto_test(
r#"
def foo(a, b): ...
foo<CURSOR>()
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> /main.py:2:17
|
2 | def foo(a, b): ...
| ^^^
3 |
4 | foo()
|
info: Source
--> /main.py:4:13
|
2 | def foo(a, b): ...
3 |
4 | foo()
| ^^^
|
"###);
}
#[test]
fn goto_type_narrowing() {
let test = goto_test(
r#"
def foo(a: str | None, b):
if a is not None:
print(a<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:4:27
|
2 | def foo(a: str | None, b):
3 | if a is not None:
4 | print(a)
| ^
|
"###);
}
#[test]
fn goto_type_none() {
let test = goto_test(
r#"
def foo(a: str | None, b):
a<CURSOR>
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:3:17
|
2 | def foo(a: str | None, b):
3 | a
| ^
|
info: lint:goto-type-definition: Type definition
--> stdlib/types.pyi:677:11
|
675 | if sys.version_info >= (3, 10):
676 | @final
677 | class NoneType:
| ^^^^^^^^
678 | def __bool__(self) -> Literal[False]: ...
|
info: Source
--> /main.py:3:17
|
2 | def foo(a: str | None, b):
3 | a
| ^
|
");
}
fn goto_test(source: &str) -> GotoTest {
let mut db = TestDb::new();
let cursor_offset = source.find("<CURSOR>").expect(
"`source`` should contain a `<CURSOR>` marker, indicating the position of the cursor.",
);
let mut content = source[..cursor_offset].to_string();
content.push_str(&source[cursor_offset + "<CURSOR>".len()..]);
db.write_file("main.py", &content)
.expect("write to memory file system to be successful");
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::latest(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
src_roots: vec![SystemPathBuf::from("/")],
custom_typeshed: None,
python_path: PythonPath::KnownSitePackages(vec![]),
},
},
)
.expect("Default settings to be valid");
let mut insta_settings = insta::Settings::clone_current();
insta_settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
let insta_settings_guard = insta_settings.bind_to_scope();
GotoTest {
db,
cursor_offset: TextSize::try_from(cursor_offset)
.expect("source to be smaller than 4GB"),
file,
_insta_settings_guard: insta_settings_guard,
}
}
struct GotoTest {
db: TestDb,
cursor_offset: TextSize,
file: File,
_insta_settings_guard: SettingsBindDropGuard,
}
impl GotoTest {
fn write_file(
&mut self,
path: impl AsRef<SystemPath>,
content: &str,
) -> std::io::Result<()> {
self.db.write_file(path, content)
}
fn goto_type_definition(&self) -> String {
let Some(targets) = goto_type_definition(&self.db, self.file, self.cursor_offset)
else {
return "No goto target found".to_string();
};
if targets.is_empty() {
return "No type definitions found".to_string();
}
let mut buf = String::new();
let source = targets.range;
let config = DisplayDiagnosticConfig::default()
.color(false)
.format(DiagnosticFormat::Full);
for target in &*targets {
let diag = GotoTypeDefinitionDiagnostic::new(source, target).into_diagnostic();
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
}
buf
}
}
struct GotoTypeDefinitionDiagnostic {
source: FileRange,
target: FileRange,
}
impl GotoTypeDefinitionDiagnostic {
fn new(source: FileRange, target: &NavigationTarget) -> Self {
Self {
source,
target: FileRange::new(target.file(), target.focus_range()),
}
}
fn into_diagnostic(self) -> Diagnostic {
let mut source = SubDiagnostic::new(Severity::Info, "Source");
source.annotate(Annotation::primary(
Span::from(self.source.file()).with_range(self.source.range()),
));
let mut main = Diagnostic::new(
DiagnosticId::Lint(LintName::of("goto-type-definition")),
Severity::Info,
"Type definition".to_string(),
);
main.annotate(Annotation::primary(
Span::from(self.target.file()).with_range(self.target.range()),
));
main.sub(source);
main
}
}
}

View File

@@ -1,263 +0,0 @@
mod db;
mod find_node;
mod goto;
use std::ops::{Deref, DerefMut};
pub use db::Db;
pub use goto::goto_type_definition;
use red_knot_python_semantic::types::{
Class, ClassBase, ClassLiteralType, FunctionType, InstanceType, IntersectionType,
KnownInstanceType, ModuleLiteralType, Type,
};
use ruff_db::files::{File, FileRange};
use ruff_db::source::source_text;
use ruff_text_size::{Ranged, TextLen, TextRange};
/// Information associated with a text range.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct RangedValue<T> {
pub range: FileRange,
pub value: T,
}
impl<T> RangedValue<T> {
pub fn file_range(&self) -> FileRange {
self.range
}
}
impl<T> Deref for RangedValue<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> DerefMut for RangedValue<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
impl<T> IntoIterator for RangedValue<T>
where
T: IntoIterator,
{
type Item = T::Item;
type IntoIter = T::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.value.into_iter()
}
}
/// Target to which the editor can navigate to.
#[derive(Debug, Clone)]
pub struct NavigationTarget {
file: File,
/// The range that should be focused when navigating to the target.
///
/// This is typically not the full range of the node. For example, it's the range of the class's name in a class definition.
///
/// The `focus_range` must be fully covered by `full_range`.
focus_range: TextRange,
/// The range covering the entire target.
full_range: TextRange,
}
impl NavigationTarget {
pub fn file(&self) -> File {
self.file
}
pub fn focus_range(&self) -> TextRange {
self.focus_range
}
pub fn full_range(&self) -> TextRange {
self.full_range
}
}
#[derive(Debug, Clone)]
pub struct NavigationTargets(smallvec::SmallVec<[NavigationTarget; 1]>);
impl NavigationTargets {
fn single(target: NavigationTarget) -> Self {
Self(smallvec::smallvec![target])
}
fn empty() -> Self {
Self(smallvec::SmallVec::new())
}
fn iter(&self) -> std::slice::Iter<'_, NavigationTarget> {
self.0.iter()
}
#[cfg(test)]
fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl IntoIterator for NavigationTargets {
type Item = NavigationTarget;
type IntoIter = smallvec::IntoIter<[NavigationTarget; 1]>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a NavigationTargets {
type Item = &'a NavigationTarget;
type IntoIter = std::slice::Iter<'a, NavigationTarget>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl FromIterator<NavigationTarget> for NavigationTargets {
fn from_iter<T: IntoIterator<Item = NavigationTarget>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
pub trait HasNavigationTargets {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets;
}
impl HasNavigationTargets for Type<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
match self {
Type::BoundMethod(method) => method.function(db).navigation_targets(db),
Type::FunctionLiteral(function) => function.navigation_targets(db),
Type::ModuleLiteral(module) => module.navigation_targets(db),
Type::Union(union) => union
.iter(db.upcast())
.flat_map(|target| target.navigation_targets(db))
.collect(),
Type::ClassLiteral(class) => class.navigation_targets(db),
Type::Instance(instance) => instance.navigation_targets(db),
Type::KnownInstance(instance) => instance.navigation_targets(db),
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
ClassBase::Class(class) => class.navigation_targets(db),
ClassBase::Dynamic(_) => NavigationTargets::empty(),
},
Type::StringLiteral(_)
| Type::BooleanLiteral(_)
| Type::LiteralString
| Type::IntLiteral(_)
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::MethodWrapper(_)
| Type::WrapperDescriptor(_)
| Type::PropertyInstance(_)
| Type::Tuple(_) => self.to_meta_type(db.upcast()).navigation_targets(db),
Type::Intersection(intersection) => intersection.navigation_targets(db),
Type::Dynamic(_)
| Type::Never
| Type::Callable(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy => NavigationTargets::empty(),
}
}
}
impl HasNavigationTargets for FunctionType<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
let function_range = self.focus_range(db.upcast());
NavigationTargets::single(NavigationTarget {
file: function_range.file(),
focus_range: function_range.range(),
full_range: self.full_range(db.upcast()).range(),
})
}
}
impl HasNavigationTargets for Class<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
let class_range = self.focus_range(db.upcast());
NavigationTargets::single(NavigationTarget {
file: class_range.file(),
focus_range: class_range.range(),
full_range: self.full_range(db.upcast()).range(),
})
}
}
impl HasNavigationTargets for ClassLiteralType<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
self.class().navigation_targets(db)
}
}
impl HasNavigationTargets for InstanceType<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
self.class().navigation_targets(db)
}
}
impl HasNavigationTargets for ModuleLiteralType<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
let file = self.module(db).file();
let source = source_text(db.upcast(), file);
NavigationTargets::single(NavigationTarget {
file,
focus_range: TextRange::default(),
full_range: TextRange::up_to(source.text_len()),
})
}
}
impl HasNavigationTargets for KnownInstanceType<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
match self {
KnownInstanceType::TypeVar(var) => {
let definition = var.definition(db);
let full_range = definition.full_range(db.upcast());
NavigationTargets::single(NavigationTarget {
file: full_range.file(),
focus_range: definition.focus_range(db.upcast()).range(),
full_range: full_range.range(),
})
}
// TODO: Track the definition of `KnownInstance` and navigate to their definition.
_ => NavigationTargets::empty(),
}
}
}
impl HasNavigationTargets for IntersectionType<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
// Only consider the positive elements because the negative elements are mainly from narrowing constraints.
let mut targets = self
.iter_positive(db.upcast())
.filter(|ty| !ty.is_unknown());
let Some(first) = targets.next() else {
return NavigationTargets::empty();
};
match targets.next() {
Some(_) => {
// If there are multiple types in the intersection, we can't navigate to a single one
// because the type is the intersection of all those types.
NavigationTargets::empty()
}
None => first.navigation_targets(db),
}
}
}

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 {

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

@@ -38,42 +38,3 @@ type("Foo", ())
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", (), {}, weird_other_arg=42)
```
## Calls to `str()`
### Valid calls
```py
str()
str("")
str(b"")
str(1)
str(object=1)
str(b"M\xc3\xbcsli", "utf-8")
str(b"M\xc3\xbcsli", "utf-8", "replace")
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16")
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16", errors="ignore")
str(bytearray.fromhex("4d c3 bc 73 6c 69"), "utf-8")
str(bytearray(), "utf-8")
str(encoding="utf-8", object=b"M\xc3\xbcsli")
str(b"", errors="replace")
str(encoding="utf-8")
str(errors="replace")
```
### Invalid calls
```py
str(1, 2) # error: [no-matching-overload]
str(o=1) # error: [no-matching-overload]
# First argument is not a bytes-like object:
str("Müsli", "utf-8") # error: [no-matching-overload]
# Second argument is not a valid encoding:
str(b"M\xc3\xbcsli", b"utf-8") # error: [no-matching-overload]
```

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,27 +25,4 @@ reveal_type(cast(1, True)) # revealed: Unknown
cast(str)
# error: [too-many-positional-arguments] "Too many positional arguments to function `cast`: expected 2, got 3"
cast(str, b"ar", "foo")
def function_returning_int() -> int:
return 10
# error: [redundant-cast] "Value is already of type `int`"
cast(int, function_returning_int())
def function_returning_any() -> Any:
return "blah"
# error: [redundant-cast] "Value is already of type `Any`"
cast(Any, function_returning_any())
```
Complex type expressions (which may be unsupported) do not lead to spurious `[redundant-cast]`
diagnostics.
```py
from typing import Callable
def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]):
a = cast(Callable[[list[bytes]], None], x)
b = cast(tuple[list[bytes]], y)
```

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

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

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

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

@@ -402,7 +402,7 @@ are covered in the [subtyping tests](./is_subtype_of.md#callable).
### Return type
```py
from knot_extensions import CallableTypeOf, Unknown, static_assert, is_assignable_to
from knot_extensions import CallableTypeFromFunction, Unknown, static_assert, is_assignable_to
from typing import Any, Callable
static_assert(is_assignable_to(Callable[[], Any], Callable[[], int]))
@@ -432,7 +432,7 @@ A `Callable` which uses the gradual form (`...`) for the parameter types is cons
input signature.
```py
from knot_extensions import CallableTypeOf, static_assert, is_assignable_to
from knot_extensions import CallableTypeFromFunction, static_assert, is_assignable_to
from typing import Any, Callable
static_assert(is_assignable_to(Callable[[], None], Callable[..., None]))
@@ -450,12 +450,12 @@ def keyword_only(*, a: int, b: int) -> None: ...
def keyword_variadic(**kwargs: int) -> None: ...
def mixed(a: int, /, b: int, *args: int, c: int, **kwargs: int) -> None: ...
static_assert(is_assignable_to(CallableTypeOf[positional_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
```
And, even if the parameters are unannotated.
@@ -468,29 +468,12 @@ def keyword_only(*, a, b) -> None: ...
def keyword_variadic(**kwargs) -> None: ...
def mixed(a, /, b, *args, c, **kwargs) -> None: ...
static_assert(is_assignable_to(CallableTypeOf[positional_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None]))
```
### Function types
```py
from typing import Any, Callable
def f(x: Any) -> str:
return ""
def g(x: Any) -> int:
return 1
c: Callable[[Any], str] = f
# error: [invalid-assignment] "Object of type `Literal[g]` is not assignable to `(Any, /) -> str`"
c: Callable[[Any], str] = g
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
```
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation

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,259 +0,0 @@
# Unreachable code
## Detecting unreachable code
In this section, we look at various scenarios how sections of code can become unreachable. We should
eventually introduce a new diagnostic that would detect unreachable code.
### Terminal statements
In the following examples, the `print` statements are definitely unreachable.
```py
def f1():
return
# TODO: we should mark this as unreachable
print("unreachable")
def f2():
raise Exception()
# TODO: we should mark this as unreachable
print("unreachable")
def f3():
while True:
break
# TODO: we should mark this as unreachable
print("unreachable")
def f4():
for _ in range(10):
continue
# TODO: we should mark this as unreachable
print("unreachable")
```
### Infinite loops
```py
def f1():
while True:
pass
# TODO: we should mark this as unreachable
print("unreachable")
```
### Statically known branches
In the following examples, the `print` statements are also unreachable, but it requires type
inference to determine that:
```py
def f1():
if 2 + 3 > 10:
# TODO: we should mark this as unreachable
print("unreachable")
def f2():
if True:
return
# TODO: we should mark this as unreachable
print("unreachable")
```
### `Never` / `NoReturn`
If a function is annotated with a return type of `Never` or `NoReturn`, we can consider all code
after the call to that function unreachable.
```py
from typing_extensions import NoReturn
def always_raises() -> NoReturn:
raise Exception()
def f():
always_raises()
# TODO: we should mark this as unreachable
print("unreachable")
```
## Python version and platform checks
It is common to have code that is specific to a certain Python version or platform. This case is
special because whether or not the code is reachable depends on externally configured constants. And
if we are checking for a set of parameters that makes one of these branches unreachable, that is
likely not something that the user wants to be warned about, because there are probably other sets
of parameters that make the branch reachable.
### `sys.version_info` branches
Consider the following example. If we check with a Python version lower than 3.11, the import
statement is unreachable. If we check with a Python version equal to or greater than 3.11, the
import statement is definitely reachable. We should not emit any diagnostics in either case.
#### Checking with Python version 3.10
```toml
[environment]
python-version = "3.10"
```
```py
import sys
if sys.version_info >= (3, 11):
# TODO: we should not emit an error here
# error: [unresolved-import]
from typing import Self
```
#### Checking with Python version 3.12
```toml
[environment]
python-version = "3.12"
```
```py
import sys
if sys.version_info >= (3, 11):
from typing import Self
```
### `sys.platform` branches
The problem is even more pronounced with `sys.platform` branches, since we don't necessarily have
the platform information available.
#### Checking with platform `win32`
```toml
[environment]
python-platform = "win32"
```
```py
import sys
if sys.platform == "win32":
sys.getwindowsversion()
```
#### Checking with platform `linux`
```toml
[environment]
python-platform = "linux"
```
```py
import sys
if sys.platform == "win32":
# TODO: we should not emit an error here
# error: [unresolved-attribute]
sys.getwindowsversion()
```
#### Checking without a specified platform
```toml
[environment]
# python-platform not specified
```
```py
import sys
if sys.platform == "win32":
# TODO: we should not emit an error here
# error: [possibly-unbound-attribute]
sys.getwindowsversion()
```
#### Checking with platform set to `all`
```toml
[environment]
python-platform = "all"
```
```py
import sys
if sys.platform == "win32":
# TODO: we should not emit an error here
# error: [possibly-unbound-attribute]
sys.getwindowsversion()
```
## No false positive diagnostics in unreachable code
In this section, we make sure that we do not emit false positive diagnostics in unreachable code.
### Use of variables in unreachable code
We should not emit any diagnostics for uses of symbols in unreachable code:
```py
def f():
x = 1
return
print("unreachable")
# TODO: we should not emit an error here; we currently do, since there is no control flow path from this
# use of 'x' to any definition of 'x'.
# error: [unresolved-reference]
print(x)
```
### Use of variable in nested function
In the example below, since we use `x` in the `inner` function, we use the "public" type of `x`,
which currently refers to the end-of-scope type of `x`. Since the end of the `outer` scope is
unreachable, we treat `x` as if it was not defined. This behavior can certainly be improved.
```py
def outer():
x = 1
def inner():
# TODO: we should not emit an error here
# error: [unresolved-reference]
return x # Name `x` used when not defined
while True:
pass
```
## No diagnostics in unreachable code
In general, no diagnostics should be emitted in unreachable code. The reasoning is that any issues
inside the unreachable section would not cause problems at runtime. And type checking the
unreachable code under the assumption that it *is* reachable might lead to false positives:
```py
FEATURE_X_ACTIVATED = False
if FEATURE_X_ACTIVATED:
def feature_x():
print("Performing 'X'")
def f():
if FEATURE_X_ACTIVATED:
# Type checking this particular section as if it were reachable would
# lead to a false positive, so we should not emit diagnostics here.
# TODO: no error should be emitted here
# error: [unresolved-reference]
feature_x()
```

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};
@@ -133,15 +133,6 @@ pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
Program::get(db).search_paths(db).iter(db)
}
/// Searches for a `.venv` directory in `project_root` that contains a `pyvenv.cfg` file.
fn discover_venv_in(system: &dyn System, project_root: &SystemPath) -> Option<SystemPathBuf> {
let virtual_env_directory = project_root.join(".venv");
system
.is_file(&virtual_env_directory.join("pyvenv.cfg"))
.then_some(virtual_env_directory)
}
#[derive(Debug, PartialEq, Eq)]
pub struct SearchPaths {
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
@@ -244,37 +235,6 @@ impl SearchPaths {
.and_then(|venv| venv.site_packages_directories(system))?
}
PythonPath::Discover(root) => {
tracing::debug!("Discovering virtual environment in `{root}`");
let virtual_env_path = discover_venv_in(db.system(), root);
if let Some(virtual_env_path) = virtual_env_path {
tracing::debug!("Found `.venv` folder at `{}`", virtual_env_path);
let handle_invalid_virtual_env = |error: SitePackagesDiscoveryError| {
tracing::debug!(
"Ignoring automatically detected virtual environment at `{}`: {}",
virtual_env_path,
error
);
vec![]
};
match VirtualEnvironment::new(
virtual_env_path.clone(),
SysPrefixPathOrigin::LocalVenv,
system,
) {
Ok(venv) => venv
.site_packages_directories(system)
.unwrap_or_else(handle_invalid_virtual_env),
Err(error) => handle_invalid_virtual_env(error),
}
} else {
tracing::debug!("No virtual environment found");
vec![]
}
}
PythonPath::KnownSitePackages(paths) => paths
.iter()
.map(|path| canonicalize(path, system))

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);
@@ -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,7 +8,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::ast_node_ref::AstNodeRef;
use crate::node_key::NodeKey;
use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId};
use crate::unpack::{Unpack, UnpackPosition};
use crate::unpack::Unpack;
use crate::Db;
/// A definition of a symbol.
@@ -52,14 +52,6 @@ impl<'db> Definition<'db> {
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
self.file_scope(db).to_scope_id(db, self.file(db))
}
pub fn full_range(self, db: &'db dyn Db) -> FileRange {
FileRange::new(self.file(db), self.kind(db).full_range())
}
pub fn focus_range(self, db: &'db dyn Db) -> FileRange {
FileRange::new(self.file(db), self.kind(db).target_range())
}
}
/// One or more [`Definition`]s.
@@ -247,24 +239,27 @@ pub(crate) struct ImportFromDefinitionNodeRef<'a> {
#[derive(Copy, Clone, Debug)]
pub(crate) struct AssignmentDefinitionNodeRef<'a> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
pub(crate) unpack: Option<Unpack<'a>>,
pub(crate) value: &'a ast::Expr,
pub(crate) name: &'a ast::ExprName,
pub(crate) first: bool,
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct WithItemDefinitionNodeRef<'a> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
pub(crate) unpack: Option<Unpack<'a>>,
pub(crate) context_expr: &'a ast::Expr,
pub(crate) name: &'a ast::ExprName,
pub(crate) first: bool,
pub(crate) is_async: bool,
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
pub(crate) unpack: Option<Unpack<'a>>,
pub(crate) iterable: &'a ast::Expr,
pub(crate) name: &'a ast::ExprName,
pub(crate) first: bool,
pub(crate) is_async: bool,
}
@@ -337,10 +332,12 @@ impl<'db> DefinitionNodeRef<'db> {
unpack,
value,
name,
first,
}) => DefinitionKind::Assignment(AssignmentDefinitionKind {
target: TargetKind::from(unpack),
value: AstNodeRef::new(parsed.clone(), value),
name: AstNodeRef::new(parsed, name),
first,
}),
DefinitionNodeRef::AnnotatedAssignment(assign) => {
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
@@ -352,11 +349,13 @@ impl<'db> DefinitionNodeRef<'db> {
unpack,
iterable,
name,
first,
is_async,
}) => DefinitionKind::For(ForStmtDefinitionKind {
target: TargetKind::from(unpack),
iterable: AstNodeRef::new(parsed.clone(), iterable),
name: AstNodeRef::new(parsed, name),
first,
is_async,
}),
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
@@ -383,11 +382,13 @@ impl<'db> DefinitionNodeRef<'db> {
unpack,
context_expr,
name,
first,
is_async,
}) => DefinitionKind::WithItem(WithItemDefinitionKind {
target: TargetKind::from(unpack),
context_expr: AstNodeRef::new(parsed.clone(), context_expr),
name: AstNodeRef::new(parsed, name),
first,
is_async,
}),
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
@@ -450,6 +451,7 @@ impl<'db> DefinitionNodeRef<'db> {
value: _,
unpack: _,
name,
first: _,
}) => name.into(),
Self::AnnotatedAssignment(node) => node.into(),
Self::AugmentedAssignment(node) => node.into(),
@@ -457,6 +459,7 @@ impl<'db> DefinitionNodeRef<'db> {
unpack: _,
iterable: _,
name,
first: _,
is_async: _,
}) => name.into(),
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
@@ -466,6 +469,7 @@ impl<'db> DefinitionNodeRef<'db> {
Self::WithItem(WithItemDefinitionNodeRef {
unpack: _,
context_expr: _,
first: _,
is_async: _,
name,
}) => name.into(),
@@ -567,6 +571,8 @@ impl DefinitionKind<'_> {
///
/// A definition target would mainly be the node representing the symbol being defined i.e.,
/// [`ast::ExprName`] or [`ast::Identifier`] but could also be other nodes.
///
/// This is mainly used for logging and debugging purposes.
pub(crate) fn target_range(&self) -> TextRange {
match self {
DefinitionKind::Import(import) => import.alias().range(),
@@ -593,33 +599,6 @@ impl DefinitionKind<'_> {
}
}
/// Returns the [`TextRange`] of the entire definition.
pub(crate) fn full_range(&self) -> TextRange {
match self {
DefinitionKind::Import(import) => import.alias().range(),
DefinitionKind::ImportFrom(import) => import.alias().range(),
DefinitionKind::StarImport(import) => import.import().range(),
DefinitionKind::Function(function) => function.range(),
DefinitionKind::Class(class) => class.range(),
DefinitionKind::TypeAlias(type_alias) => type_alias.range(),
DefinitionKind::NamedExpression(named) => named.range(),
DefinitionKind::Assignment(assignment) => assignment.name().range(),
DefinitionKind::AnnotatedAssignment(assign) => assign.range(),
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.range(),
DefinitionKind::For(for_stmt) => for_stmt.name().range(),
DefinitionKind::Comprehension(comp) => comp.target().range(),
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.range(),
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.range(),
DefinitionKind::Parameter(parameter) => parameter.parameter.range(),
DefinitionKind::WithItem(with_item) => with_item.name().range(),
DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(),
DefinitionKind::ExceptHandler(handler) => handler.node().range(),
DefinitionKind::TypeVar(type_var) => type_var.range(),
DefinitionKind::ParamSpec(param_spec) => param_spec.range(),
DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.range(),
}
}
pub(crate) fn category(&self, in_stub: bool) -> DefinitionCategory {
match self {
// functions, classes, and imports always bind, and we consider them declarations
@@ -673,14 +652,14 @@ impl DefinitionKind<'_> {
#[derive(Copy, Clone, Debug, PartialEq, Hash)]
pub(crate) enum TargetKind<'db> {
Sequence(UnpackPosition, Unpack<'db>),
Sequence(Unpack<'db>),
Name,
}
impl<'db> From<Option<(UnpackPosition, Unpack<'db>)>> for TargetKind<'db> {
fn from(value: Option<(UnpackPosition, Unpack<'db>)>) -> Self {
impl<'db> From<Option<Unpack<'db>>> for TargetKind<'db> {
fn from(value: Option<Unpack<'db>>) -> Self {
match value {
Some((unpack_position, unpack)) => TargetKind::Sequence(unpack_position, unpack),
Some(unpack) => TargetKind::Sequence(unpack),
None => TargetKind::Name,
}
}
@@ -801,6 +780,7 @@ pub struct AssignmentDefinitionKind<'db> {
target: TargetKind<'db>,
value: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
first: bool,
}
impl<'db> AssignmentDefinitionKind<'db> {
@@ -815,6 +795,10 @@ impl<'db> AssignmentDefinitionKind<'db> {
pub(crate) fn name(&self) -> &ast::ExprName {
self.name.node()
}
pub(crate) fn is_first(&self) -> bool {
self.first
}
}
#[derive(Clone, Debug)]
@@ -822,6 +806,7 @@ pub struct WithItemDefinitionKind<'db> {
target: TargetKind<'db>,
context_expr: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
first: bool,
is_async: bool,
}
@@ -838,6 +823,10 @@ impl<'db> WithItemDefinitionKind<'db> {
self.name.node()
}
pub(crate) const fn is_first(&self) -> bool {
self.first
}
pub(crate) const fn is_async(&self) -> bool {
self.is_async
}
@@ -848,6 +837,7 @@ pub struct ForStmtDefinitionKind<'db> {
target: TargetKind<'db>,
iterable: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
first: bool,
is_async: bool,
}
@@ -864,6 +854,10 @@ impl<'db> ForStmtDefinitionKind<'db> {
self.name.node()
}
pub(crate) const fn is_first(&self) -> bool {
self.first
}
pub(crate) const fn is_async(&self) -> bool {
self.is_async
}

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;
@@ -319,11 +319,14 @@ impl<'a> CheckSuppressionsContext<'a> {
return;
};
let id = DiagnosticId::Lint(lint.name());
let mut diag = Diagnostic::new(id, severity, "");
let span = Span::from(self.file).with_range(range);
diag.annotate(Annotation::primary(span).message(message));
self.diagnostics.push(diag);
self.diagnostics.push(TypeCheckDiagnostic {
id: DiagnosticId::Lint(lint.name()),
message: message.to_string(),
range,
severity,
file: self.file,
secondary_messages: vec![],
});
}
}

File diff suppressed because it is too large Load Diff

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

@@ -18,7 +18,7 @@ use crate::{
};
use indexmap::IndexSet;
use itertools::Itertools as _;
use ruff_db::files::{File, FileRange};
use ruff_db::files::File;
use ruff_python_ast::{self as ast, PythonVersion};
use rustc_hash::FxHashSet;
@@ -153,15 +153,6 @@ impl<'db> Class<'db> {
self.body_scope(db).node(db).expect_class()
}
/// Returns the file range of the class's name.
pub fn focus_range(self, db: &dyn Db) -> FileRange {
FileRange::new(self.file(db), self.node(db).name.range)
}
pub fn full_range(self, db: &dyn Db) -> FileRange {
FileRange::new(self.file(db), self.node(db).range)
}
/// Return the types of the decorators on this class
#[salsa::tracked(return_ref)]
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
@@ -763,7 +754,7 @@ pub struct ClassLiteralType<'db> {
}
impl<'db> ClassLiteralType<'db> {
pub fn class(self) -> Class<'db> {
pub(crate) fn class(self) -> Class<'db> {
self.class
}
@@ -789,7 +780,7 @@ pub struct InstanceType<'db> {
}
impl<'db> InstanceType<'db> {
pub fn class(self) -> Class<'db> {
pub(super) fn class(self) -> Class<'db> {
self.class
}
@@ -820,7 +811,6 @@ pub enum KnownClass {
Bool,
Object,
Bytes,
Bytearray,
Type,
Int,
Float,
@@ -837,9 +827,6 @@ pub enum KnownClass {
BaseException,
BaseExceptionGroup,
Classmethod,
Super,
// enum
Enum,
// Types
GenericAlias,
ModuleType,
@@ -850,13 +837,10 @@ pub enum KnownClass {
// Typeshed
NoneType, // Part of `types` for Python >= 3.10
// Typing
Any,
StdlibAlias,
SpecialForm,
TypeVar,
ParamSpec,
ParamSpecArgs,
ParamSpecKwargs,
TypeVarTuple,
TypeAliasType,
NoDefaultType,
@@ -875,7 +859,6 @@ pub enum KnownClass {
// Exposed as `types.EllipsisType` on Python >=3.10;
// backported as `builtins.ellipsis` by typeshed on Python <=3.9
EllipsisType,
NotImplementedType,
}
impl<'db> KnownClass {
@@ -906,16 +889,13 @@ impl<'db> KnownClass {
| Self::TypeAliasType
| Self::TypeVar
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::TypeVarTuple
| Self::WrapperDescriptorType
| Self::MethodWrapperType => Truthiness::AlwaysTrue,
Self::NoneType => Truthiness::AlwaysFalse,
Self::Any
| Self::BaseException
Self::BaseException
| Self::Object
| Self::OrderedDict
| Self::BaseExceptionGroup
@@ -931,7 +911,6 @@ impl<'db> KnownClass {
| Self::Int
| Self::Type
| Self::Bytes
| Self::Bytearray
| Self::FrozenSet
| Self::Range
| Self::Property
@@ -945,23 +924,15 @@ impl<'db> KnownClass {
| Self::Deque
| Self::Float
| Self::Sized
| Self::Enum
| Self::Super
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
// and raises a `TypeError` in Python >=3.14
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
| Self::NotImplementedType
| Self::Classmethod => Truthiness::Ambiguous,
}
}
pub(crate) fn name(self, db: &'db dyn Db) -> &'static str {
match self {
Self::Any => "Any",
Self::Bool => "bool",
Self::Object => "object",
Self::Bytes => "bytes",
Self::Bytearray => "bytearray",
Self::Tuple => "tuple",
Self::Int => "int",
Self::Float => "float",
@@ -988,8 +959,6 @@ impl<'db> KnownClass {
Self::SpecialForm => "_SpecialForm",
Self::TypeVar => "TypeVar",
Self::ParamSpec => "ParamSpec",
Self::ParamSpecArgs => "ParamSpecArgs",
Self::ParamSpecKwargs => "ParamSpecKwargs",
Self::TypeVarTuple => "TypeVarTuple",
Self::TypeAliasType => "TypeAliasType",
Self::NoDefaultType => "_NoDefaultType",
@@ -1001,8 +970,6 @@ impl<'db> KnownClass {
Self::Deque => "deque",
Self::Sized => "Sized",
Self::OrderedDict => "OrderedDict",
Self::Enum => "Enum",
Self::Super => "super",
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
Self::StdlibAlias => "_Alias",
// This is the name the type of `sys.version_info` has in typeshed,
@@ -1020,7 +987,6 @@ impl<'db> KnownClass {
"ellipsis"
}
}
Self::NotImplementedType => "_NotImplementedType",
}
}
@@ -1139,7 +1105,6 @@ impl<'db> KnownClass {
Self::Bool
| Self::Object
| Self::Bytes
| Self::Bytearray
| Self::Type
| Self::Int
| Self::Float
@@ -1155,10 +1120,8 @@ impl<'db> KnownClass {
| Self::Classmethod
| Self::Slice
| Self::Range
| Self::Super
| Self::Property => KnownModule::Builtins,
Self::VersionInfo => KnownModule::Sys,
Self::Enum => KnownModule::Enum,
Self::GenericAlias
| Self::ModuleType
| Self::FunctionType
@@ -1166,18 +1129,14 @@ impl<'db> KnownClass {
| Self::MethodWrapperType
| Self::WrapperDescriptorType => KnownModule::Types,
Self::NoneType => KnownModule::Typeshed,
Self::Any
| Self::SpecialForm
Self::SpecialForm
| Self::TypeVar
| Self::StdlibAlias
| Self::SupportsIndex
| Self::Sized => KnownModule::Typing,
Self::TypeAliasType
| Self::TypeVarTuple
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::NewType => KnownModule::TypingExtensions,
Self::TypeAliasType | Self::TypeVarTuple | Self::ParamSpec | Self::NewType => {
KnownModule::TypingExtensions
}
Self::NoDefaultType => {
let python_version = Program::get(db).python_version(db);
@@ -1199,7 +1158,6 @@ impl<'db> KnownClass {
KnownModule::Builtins
}
}
Self::NotImplementedType => KnownModule::Builtins,
Self::ChainMap
| Self::Counter
| Self::DefaultDict
@@ -1215,14 +1173,11 @@ impl<'db> KnownClass {
| Self::NoDefaultType
| Self::VersionInfo
| Self::EllipsisType
| Self::TypeAliasType
| Self::NotImplementedType => true,
| Self::TypeAliasType => true,
Self::Any
| Self::Bool
Self::Bool
| Self::Object
| Self::Bytes
| Self::Bytearray
| Self::Type
| Self::Int
| Self::Float
@@ -1255,12 +1210,8 @@ impl<'db> KnownClass {
| Self::StdlibAlias
| Self::TypeVar
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::TypeVarTuple
| Self::Sized
| Self::Enum
| Self::Super
| Self::NewType => false,
}
}
@@ -1269,19 +1220,17 @@ impl<'db> KnownClass {
///
/// A singleton class is a class where it is known that only one instance can ever exist at runtime.
pub(super) const fn is_singleton(self) -> bool {
// TODO there are other singleton types (NotImplementedType -- any others?)
match self {
Self::NoneType
| Self::EllipsisType
| Self::NoDefaultType
| Self::VersionInfo
| Self::TypeAliasType
| Self::NotImplementedType => true,
| Self::TypeAliasType => true,
Self::Any
| Self::Bool
Self::Bool
| Self::Object
| Self::Bytes
| Self::Bytearray
| Self::Tuple
| Self::Int
| Self::Float
@@ -1314,12 +1263,8 @@ impl<'db> KnownClass {
| Self::Classmethod
| Self::TypeVar
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::TypeVarTuple
| Self::Sized
| Self::Enum
| Self::Super
| Self::NewType => false,
}
}
@@ -1332,11 +1277,9 @@ impl<'db> KnownClass {
// We assert that this match is exhaustive over the right-hand side in the unit test
// `known_class_roundtrip_from_str()`
let candidate = match class_name {
"Any" => Self::Any,
"bool" => Self::Bool,
"object" => Self::Object,
"bytes" => Self::Bytes,
"bytearray" => Self::Bytearray,
"tuple" => Self::Tuple,
"type" => Self::Type,
"int" => Self::Int,
@@ -1364,8 +1307,6 @@ impl<'db> KnownClass {
"TypeAliasType" => Self::TypeAliasType,
"TypeVar" => Self::TypeVar,
"ParamSpec" => Self::ParamSpec,
"ParamSpecArgs" => Self::ParamSpecArgs,
"ParamSpecKwargs" => Self::ParamSpecKwargs,
"TypeVarTuple" => Self::TypeVarTuple,
"ChainMap" => Self::ChainMap,
"Counter" => Self::Counter,
@@ -1377,8 +1318,6 @@ impl<'db> KnownClass {
"_NoDefaultType" => Self::NoDefaultType,
"SupportsIndex" => Self::SupportsIndex,
"Sized" => Self::Sized,
"Enum" => Self::Enum,
"super" => Self::Super,
"_version_info" => Self::VersionInfo,
"ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => {
Self::EllipsisType
@@ -1386,7 +1325,6 @@ impl<'db> KnownClass {
"EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => {
Self::EllipsisType
}
"_NotImplementedType" => Self::NotImplementedType,
_ => return None,
};
@@ -1398,11 +1336,9 @@ impl<'db> KnownClass {
/// Return `true` if the module of `self` matches `module`
fn check_module(self, db: &'db dyn Db, module: KnownModule) -> bool {
match self {
Self::Any
| Self::Bool
Self::Bool
| Self::Object
| Self::Bytes
| Self::Bytearray
| Self::Type
| Self::Int
| Self::Float
@@ -1432,9 +1368,6 @@ impl<'db> KnownClass {
| Self::FunctionType
| Self::MethodType
| Self::MethodWrapperType
| Self::Enum
| Self::Super
| Self::NotImplementedType
| Self::WrapperDescriptorType => module == self.canonical_module(db),
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
Self::SpecialForm
@@ -1443,8 +1376,6 @@ impl<'db> KnownClass {
| Self::NoDefaultType
| Self::SupportsIndex
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::TypeVarTuple
| Self::Sized
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
@@ -1526,9 +1457,6 @@ pub enum KnownInstanceType<'db> {
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
Never,
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
/// This is not used since typeshed switched to representing `Any` as a class; now we use
/// `KnownClass::Any` instead. But we still support the old `Any = object()` representation, at
/// least for now. TODO maybe remove?
Any,
/// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`)
Tuple,
@@ -1570,8 +1498,8 @@ pub enum KnownInstanceType<'db> {
Intersection,
/// The symbol `knot_extensions.TypeOf`
TypeOf,
/// The symbol `knot_extensions.CallableTypeOf`
CallableTypeOf,
/// The symbol `knot_extensions.CallableTypeFromFunction`
CallableTypeFromFunction,
// Various special forms, special aliases and type qualifiers that we don't yet understand
// (all currently inferred as TODO in most contexts):
@@ -1634,7 +1562,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Not
| Self::Intersection
| Self::TypeOf
| Self::CallableTypeOf => Truthiness::AlwaysTrue,
| Self::CallableTypeFromFunction => Truthiness::AlwaysTrue,
}
}
@@ -1681,7 +1609,7 @@ impl<'db> KnownInstanceType<'db> {
Self::Not => "knot_extensions.Not",
Self::Intersection => "knot_extensions.Intersection",
Self::TypeOf => "knot_extensions.TypeOf",
Self::CallableTypeOf => "knot_extensions.CallableTypeOf",
Self::CallableTypeFromFunction => "knot_extensions.CallableTypeFromFunction",
}
}
@@ -1725,7 +1653,7 @@ impl<'db> KnownInstanceType<'db> {
Self::TypeOf => KnownClass::SpecialForm,
Self::Not => KnownClass::SpecialForm,
Self::Intersection => KnownClass::SpecialForm,
Self::CallableTypeOf => KnownClass::SpecialForm,
Self::CallableTypeFromFunction => KnownClass::SpecialForm,
Self::Unknown => KnownClass::Object,
Self::AlwaysTruthy => KnownClass::Object,
Self::AlwaysFalsy => KnownClass::Object,
@@ -1790,7 +1718,7 @@ impl<'db> KnownInstanceType<'db> {
"Not" => Self::Not,
"Intersection" => Self::Intersection,
"TypeOf" => Self::TypeOf,
"CallableTypeOf" => Self::CallableTypeOf,
"CallableTypeFromFunction" => Self::CallableTypeFromFunction,
_ => return None,
};
@@ -1847,7 +1775,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Not
| Self::Intersection
| Self::TypeOf
| Self::CallableTypeOf => module.is_knot_extensions(),
| Self::CallableTypeFromFunction => module.is_knot_extensions(),
}
}
}

View File

@@ -8,7 +8,7 @@ use itertools::Either;
/// all types that would be invalid to have as a class base are
/// transformed into [`ClassBase::unknown`]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
pub enum ClassBase<'db> {
pub(crate) enum ClassBase<'db> {
Dynamic(DynamicType),
Class(Class<'db>),
}
@@ -18,7 +18,7 @@ impl<'db> ClassBase<'db> {
Self::Dynamic(DynamicType::Any)
}
pub const fn unknown() -> Self {
pub(crate) const fn unknown() -> Self {
Self::Dynamic(DynamicType::Unknown)
}
@@ -61,22 +61,14 @@ impl<'db> ClassBase<'db> {
pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
match ty {
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
Type::ClassLiteral(literal) => Some(if literal.class().is_known(db, KnownClass::Any) {
Self::Dynamic(DynamicType::Any)
} else {
Self::Class(literal.class())
}),
Type::ClassLiteral(literal) => Some(Self::Class(literal.class())),
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
Type::Intersection(_) => None, // TODO -- probably incorrect?
Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?
Type::PropertyInstance(_) => None,
Type::Never
| Type::BooleanLiteral(_)
| Type::FunctionLiteral(_)
| Type::Callable(..)
| Type::BoundMethod(_)
| Type::MethodWrapper(_)
| Type::WrapperDescriptor(_)
| Type::BytesLiteral(_)
| Type::IntLiteral(_)
| Type::StringLiteral(_)
@@ -111,7 +103,7 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::Not
| KnownInstanceType::Intersection
| KnownInstanceType::TypeOf
| KnownInstanceType::CallableTypeOf
| KnownInstanceType::CallableTypeFromFunction
| KnownInstanceType::AlwaysTruthy
| KnownInstanceType::AlwaysFalsy => None,
KnownInstanceType::Unknown => Some(Self::unknown()),

View File

@@ -2,22 +2,20 @@ use std::fmt;
use drop_bomb::DebugDropBomb;
use ruff_db::{
diagnostic::{
Annotation, Diagnostic, DiagnosticId, OldSecondaryDiagnosticMessage, Severity, Span,
},
diagnostic::{DiagnosticId, OldSecondaryDiagnosticMessage, Severity},
files::File,
};
use ruff_text_size::{Ranged, TextRange};
use super::{binding_type, Type, TypeCheckDiagnostics};
use super::{binding_type, KnownFunction, Type, TypeCheckDiagnostic, TypeCheckDiagnostics};
use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::ScopeId;
use crate::{
lint::{LintId, LintMetadata},
suppression::suppressions,
Db,
};
use crate::{semantic_index::semantic_index, types::FunctionDecorators};
/// Context for inferring the types of a single file.
///
@@ -61,8 +59,11 @@ impl<'db> InferContext<'db> {
self.db
}
pub(crate) fn extend(&mut self, other: &TypeCheckDiagnostics) {
self.diagnostics.get_mut().extend(other);
pub(crate) fn extend<T>(&mut self, other: &T)
where
T: WithDiagnostics,
{
self.diagnostics.get_mut().extend(other.diagnostics());
}
/// Reports a lint located at `ranged`.
@@ -74,7 +75,7 @@ impl<'db> InferContext<'db> {
) where
T: Ranged,
{
self.report_lint_with_secondary_messages(lint, ranged, message, &[]);
self.report_lint_with_secondary_messages(lint, ranged, message, vec![]);
}
/// Reports a lint located at `ranged`.
@@ -83,7 +84,7 @@ impl<'db> InferContext<'db> {
lint: &'static LintMetadata,
ranged: T,
message: fmt::Arguments,
secondary_messages: &[OldSecondaryDiagnosticMessage],
secondary_messages: Vec<OldSecondaryDiagnosticMessage>,
) where
T: Ranged,
{
@@ -135,7 +136,7 @@ impl<'db> InferContext<'db> {
id: DiagnosticId,
severity: Severity,
message: fmt::Arguments,
secondary_messages: &[OldSecondaryDiagnosticMessage],
secondary_messages: Vec<OldSecondaryDiagnosticMessage>,
) where
T: Ranged,
{
@@ -149,13 +150,14 @@ impl<'db> InferContext<'db> {
// returns a rule selector for a given file that respects the package's settings,
// any global pragma comments in the file, and any per-file-ignores.
let mut diag = Diagnostic::new(id, severity, "");
for secondary_msg in secondary_messages {
diag.sub(secondary_msg.to_sub_diagnostic());
}
let span = Span::from(self.file).with_range(ranged.range());
diag.annotate(Annotation::primary(span).message(message));
self.diagnostics.borrow_mut().push(diag);
self.diagnostics.borrow_mut().push(TypeCheckDiagnostic {
file: self.file,
id,
message: message.to_string(),
range: ranged.range(),
severity,
secondary_messages,
});
}
pub(super) fn set_in_no_type_check(&mut self, no_type_check: InNoTypeCheck) {
@@ -180,7 +182,13 @@ impl<'db> InferContext<'db> {
// Iterate over all functions and test if any is decorated with `@no_type_check`.
function_scope_tys.any(|function_ty| {
function_ty.has_known_decorator(self.db, FunctionDecorators::NO_TYPE_CHECK)
function_ty
.decorators(self.db)
.iter()
.filter_map(|decorator| decorator.into_function_literal())
.any(|decorator_ty| {
decorator_ty.is_known(self.db, KnownFunction::NoTypeCheck)
})
})
}
InNoTypeCheck::Yes => true,
@@ -221,3 +229,7 @@ pub(crate) enum InNoTypeCheck {
/// The inference is known to be in an `@no_type_check` decorated function.
Yes,
}
pub(crate) trait WithDiagnostics {
fn diagnostics(&self) -> &TypeCheckDiagnostics;
}

View File

@@ -8,11 +8,17 @@ use crate::types::string_annotation::{
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::{ClassLiteralType, KnownInstanceType, Type};
use ruff_db::diagnostic::{Diagnostic, OldSecondaryDiagnosticMessage, Span};
use ruff_db::diagnostic::{
DiagnosticId, OldDiagnosticTrait, OldSecondaryDiagnosticMessage, Severity, Span,
};
use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
use std::borrow::Cow;
use std::fmt::Formatter;
use std::ops::Deref;
use std::sync::Arc;
/// Registers all known type check lints.
pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
@@ -61,7 +67,6 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
registry.register_lint(&STATIC_ASSERT_ERROR);
registry.register_lint(&INVALID_ATTRIBUTE_ACCESS);
registry.register_lint(&REDUNDANT_CAST);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -873,37 +878,68 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Detects redundant `cast` calls where the value already has the target type.
///
/// ## Why is this bad?
/// These casts have no effect and can be removed.
///
/// ## Example
/// ```python
/// def f() -> int:
/// return 10
///
/// cast(int, f()) # Redundant
/// ```
pub(crate) static REDUNDANT_CAST = {
summary: "detects redundant `cast` calls",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct TypeCheckDiagnostic {
pub(crate) id: DiagnosticId,
pub(crate) message: String,
pub(crate) range: TextRange,
pub(crate) severity: Severity,
pub(crate) file: File,
pub(crate) secondary_messages: Vec<OldSecondaryDiagnosticMessage>,
}
impl TypeCheckDiagnostic {
pub fn id(&self) -> DiagnosticId {
self.id
}
pub fn message(&self) -> &str {
&self.message
}
pub fn file(&self) -> File {
self.file
}
}
impl OldDiagnosticTrait for TypeCheckDiagnostic {
fn id(&self) -> DiagnosticId {
self.id
}
fn message(&self) -> Cow<str> {
TypeCheckDiagnostic::message(self).into()
}
fn span(&self) -> Option<Span> {
Some(Span::from(self.file).with_range(self.range))
}
fn secondary_messages(&self) -> &[OldSecondaryDiagnosticMessage] {
&self.secondary_messages
}
fn severity(&self) -> Severity {
self.severity
}
}
/// A collection of type check diagnostics.
///
/// The diagnostics are wrapped in an `Arc` because they need to be cloned multiple times
/// when going from `infer_expression` to `check_file`. We could consider
/// making [`TypeCheckDiagnostic`] a Salsa struct to have them Arena-allocated (once the Tables refactor is done).
/// Using Salsa struct does have the downside that it leaks the Salsa dependency into diagnostics and
/// each Salsa-struct comes with an overhead.
#[derive(Default, Eq, PartialEq)]
pub struct TypeCheckDiagnostics {
diagnostics: Vec<Diagnostic>,
diagnostics: Vec<Arc<TypeCheckDiagnostic>>,
used_suppressions: FxHashSet<FileSuppressionId>,
}
impl TypeCheckDiagnostics {
pub(crate) fn push(&mut self, diagnostic: Diagnostic) {
self.diagnostics.push(diagnostic);
pub(crate) fn push(&mut self, diagnostic: TypeCheckDiagnostic) {
self.diagnostics.push(Arc::new(diagnostic));
}
pub(super) fn extend(&mut self, other: &TypeCheckDiagnostics) {
@@ -927,10 +963,6 @@ impl TypeCheckDiagnostics {
self.used_suppressions.shrink_to_fit();
self.diagnostics.shrink_to_fit();
}
pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic> {
self.diagnostics.iter()
}
}
impl std::fmt::Debug for TypeCheckDiagnostics {
@@ -939,9 +971,17 @@ impl std::fmt::Debug for TypeCheckDiagnostics {
}
}
impl Deref for TypeCheckDiagnostics {
type Target = [std::sync::Arc<TypeCheckDiagnostic>];
fn deref(&self) -> &Self::Target {
&self.diagnostics
}
}
impl IntoIterator for TypeCheckDiagnostics {
type Item = Diagnostic;
type IntoIter = std::vec::IntoIter<Diagnostic>;
type Item = Arc<TypeCheckDiagnostic>;
type IntoIter = std::vec::IntoIter<std::sync::Arc<TypeCheckDiagnostic>>;
fn into_iter(self) -> Self::IntoIter {
self.diagnostics.into_iter()
@@ -949,8 +989,8 @@ impl IntoIterator for TypeCheckDiagnostics {
}
impl<'a> IntoIterator for &'a TypeCheckDiagnostics {
type Item = &'a Diagnostic;
type IntoIter = std::slice::Iter<'a, Diagnostic>;
type Item = &'a Arc<TypeCheckDiagnostic>;
type IntoIter = std::slice::Iter<'a, std::sync::Arc<TypeCheckDiagnostic>>;
fn into_iter(self) -> Self::IntoIter {
self.diagnostics.iter()
@@ -1094,7 +1134,7 @@ pub(super) fn report_invalid_return_type(
actual_ty.display(context.db()),
expected_ty.display(context.db())
),
&[OldSecondaryDiagnosticMessage::new(
vec![OldSecondaryDiagnosticMessage::new(
return_type_span,
format!(
"Return type is declared here as `{}`",

View File

@@ -9,8 +9,8 @@ use ruff_python_literal::escape::AsciiEscape;
use crate::types::class_base::ClassBase;
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::{
ClassLiteralType, InstanceType, IntersectionType, KnownClass, MethodWrapperKind,
StringLiteralType, Type, UnionType, WrapperDescriptorKind,
CallableType, ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
Type, UnionType,
};
use crate::Db;
use rustc_hash::FxHashMap;
@@ -33,19 +33,18 @@ pub struct DisplayType<'db> {
impl Display for DisplayType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let representation = self.ty.representation(self.db);
match self.ty {
Type::ClassLiteral(literal) if literal.class().is_known(self.db, KnownClass::Any) => {
write!(f, "typing.Any")
}
if matches!(
self.ty,
Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::ClassLiteral(_)
| Type::FunctionLiteral(_) => {
write!(f, "Literal[{representation}]")
}
_ => representation.fmt(f),
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::ClassLiteral(_)
| Type::FunctionLiteral(_)
) {
write!(f, "Literal[{representation}]")
} else {
representation.fmt(f)
}
}
}
@@ -77,7 +76,6 @@ impl Display for DisplayRepresentation<'_> {
};
f.write_str(representation)
}
Type::PropertyInstance(_) => f.write_str("property"),
Type::ModuleLiteral(module) => {
write!(f, "<module '{}'>", module.module(self.db).name())
}
@@ -91,8 +89,10 @@ impl Display for DisplayRepresentation<'_> {
},
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
Type::Callable(callable) => callable.signature(self.db).display(self.db).fmt(f),
Type::BoundMethod(bound_method) => {
Type::Callable(CallableType::General(callable)) => {
callable.signature(self.db).display(self.db).fmt(f)
}
Type::Callable(CallableType::BoundMethod(bound_method)) => {
write!(
f,
"<bound method `{method}` of `{instance}`>",
@@ -100,26 +100,18 @@ impl Display for DisplayRepresentation<'_> {
instance = bound_method.self_instance(self.db).display(self.db)
)
}
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
write!(
f,
"<method-wrapper `__get__` of `{function}`>",
function = function.name(self.db)
)
}
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => {
write!(f, "<method-wrapper `__get__` of `property` object>",)
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
f.write_str("<wrapper-descriptor `__get__` of `function` objects>")
}
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => {
write!(f, "<method-wrapper `__set__` of `property` object>",)
}
Type::WrapperDescriptor(kind) => {
let (method, object) = match kind {
WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"),
WrapperDescriptorKind::PropertyDunderGet => ("__get__", "property"),
WrapperDescriptorKind::PropertyDunderSet => ("__set__", "property"),
};
write!(f, "<wrapper-descriptor `{method}` of `{object}` objects>")
Type::Callable(CallableType::SpecializedGetitem) => {
f.write_str("<specialized `__getitem__`>")
}
Type::Union(union) => union.display(self.db).fmt(f),
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
@@ -433,7 +425,9 @@ struct DisplayMaybeParenthesizedType<'db> {
impl Display for DisplayMaybeParenthesizedType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Type::Callable(_) | Type::MethodWrapper(_) = self.ty {
if let Type::Callable(CallableType::General(_) | CallableType::MethodWrapperDunderGet(_)) =
self.ty
{
write!(f, "({})", self.ty.display(self.db))
} else {
self.ty.display(self.db).fmt(f)

View File

@@ -51,10 +51,11 @@ use crate::semantic_index::definition::{
ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind, WithItemDefinitionKind,
};
use crate::semantic_index::expression::{Expression, ExpressionKind};
use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind,
};
use crate::semantic_index::{semantic_index, EagerBindingsResult, SemanticIndex};
use crate::semantic_index::SemanticIndex;
use crate::symbol::{
builtins_module_scope, builtins_symbol, explicit_global_symbol,
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
@@ -76,25 +77,26 @@ use crate::types::diagnostic::{
use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, IntersectionBuilder,
IntersectionType, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, Parameter,
ParameterForm, Parameters, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers,
Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay,
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, InstanceType,
IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType,
MetaclassCandidate, Parameter, ParameterForm, Parameters, SliceLiteralType, SubclassOfType,
Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
UnionType,
};
use crate::types::{CallableType, FunctionDecorators, Signature};
use crate::unpack::{Unpack, UnpackPosition};
use crate::types::{CallableType, GeneralCallableType, Signature};
use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
use crate::Db;
use super::class_base::ClassBase;
use super::context::{InNoTypeCheck, InferContext};
use super::context::{InNoTypeCheck, InferContext, WithDiagnostics};
use super::diagnostic::{
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_invalid_type_checking_constant,
report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero,
report_unresolved_reference, INVALID_METACLASS, REDUNDANT_CAST, STATIC_ASSERT_ERROR,
SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
report_unresolved_reference, INVALID_METACLASS, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS,
TYPE_ASSERTION_FAILURE,
};
use super::slots::check_class_slots;
use super::string_annotation::{
@@ -439,6 +441,12 @@ impl<'db> TypeInference<'db> {
}
}
impl WithDiagnostics for TypeInference<'_> {
fn diagnostics(&self) -> &TypeCheckDiagnostics {
&self.diagnostics
}
}
/// Whether the intersection type is on the left or right side of the comparison.
#[derive(Debug, Clone, Copy)]
enum IntersectionOn {
@@ -561,7 +569,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.extend(inference.declarations.iter());
self.types.expressions.extend(inference.expressions.iter());
self.types.deferred.extend(inference.deferred.iter());
self.context.extend(inference.diagnostics());
self.context.extend(inference);
}
fn file(&self) -> File {
@@ -971,12 +979,7 @@ impl<'db> TypeInferenceBuilder<'db> {
/// Raise a diagnostic if the given type cannot be divided by zero.
///
/// Expects the resolved type of the left side of the binary expression.
fn check_division_by_zero(
&mut self,
node: AnyNodeRef<'_>,
op: ast::Operator,
left: Type<'db>,
) -> bool {
fn check_division_by_zero(&mut self, expr: &ast::ExprBinOp, left: Type<'db>) {
match left {
Type::BooleanLiteral(_) | Type::IntLiteral(_) => {}
Type::Instance(instance)
@@ -984,26 +987,24 @@ impl<'db> TypeInferenceBuilder<'db> {
instance.class().known(self.db()),
Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool)
) => {}
_ => return false,
_ => return,
};
let (op, by_zero) = match op {
let (op, by_zero) = match expr.op {
ast::Operator::Div => ("divide", "by zero"),
ast::Operator::FloorDiv => ("floor divide", "by zero"),
ast::Operator::Mod => ("reduce", "modulo zero"),
_ => return false,
_ => return,
};
self.context.report_lint(
&DIVISION_BY_ZERO,
node,
expr,
format_args!(
"Cannot {op} object of type `{}` {by_zero}",
left.display(self.db())
),
);
true
}
fn add_binding(&mut self, node: AnyNodeRef, binding: Definition<'db>, ty: Type<'db>) {
@@ -1265,21 +1266,9 @@ impl<'db> TypeInferenceBuilder<'db> {
{
return;
}
for invalid in self
.return_types_and_ranges
.iter()
.copied()
.filter_map(|ty_range| match ty_range.ty {
// We skip `is_assignable_to` checks for `NotImplemented`,
// so we remove it beforehand.
Type::Union(union) => Some(TypeAndRange {
ty: union.filter(self.db(), |ty| !ty.is_notimplemented(self.db())),
range: ty_range.range,
}),
ty if ty.is_notimplemented(self.db()) => None,
_ => Some(ty_range),
})
.filter(|ty_range| !ty_range.ty.is_assignable_to(self.db(), declared_ty))
{
report_invalid_return_type(
@@ -1374,31 +1363,19 @@ impl<'db> TypeInferenceBuilder<'db> {
decorator_list,
} = function;
let mut decorator_types_and_nodes = Vec::with_capacity(decorator_list.len());
let mut function_decorators = FunctionDecorators::empty();
// Check if the function is decorated with the `no_type_check` decorator
// and, if so, suppress any errors that come after the decorators.
let mut decorator_tys = Vec::with_capacity(decorator_list.len());
for decorator in decorator_list {
let decorator_ty = self.infer_decorator(decorator);
let ty = self.infer_decorator(decorator);
decorator_tys.push(ty);
if let Type::FunctionLiteral(function) = decorator_ty {
if let Type::FunctionLiteral(function) = ty {
if function.is_known(self.db(), KnownFunction::NoTypeCheck) {
// If the function is decorated with the `no_type_check` decorator,
// we need to suppress any errors that come after the decorators.
self.context.set_in_no_type_check(InNoTypeCheck::Yes);
function_decorators |= FunctionDecorators::NO_TYPE_CHECK;
continue;
} else if function.is_known(self.db(), KnownFunction::Overload) {
function_decorators |= FunctionDecorators::OVERLOAD;
continue;
}
} else if let Type::ClassLiteral(class) = decorator_ty {
if class.class.is_known(self.db(), KnownClass::Classmethod) {
function_decorators |= FunctionDecorators::CLASSMETHOD;
continue;
}
}
decorator_types_and_nodes.push((decorator_ty, decorator));
}
for default in parameters
@@ -1430,31 +1407,18 @@ impl<'db> TypeInferenceBuilder<'db> {
.node_scope(NodeWithScopeRef::Function(function))
.to_scope_id(self.db(), self.file());
let mut inferred_ty = Type::FunctionLiteral(FunctionType::new(
let function_ty = Type::FunctionLiteral(FunctionType::new(
self.db(),
&name.id,
function_kind,
body_scope,
function_decorators,
decorator_tys.into_boxed_slice(),
));
for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() {
inferred_ty = match decorator_ty
.try_call(self.db(), CallArgumentTypes::positional([inferred_ty]))
.map(|bindings| bindings.return_type(self.db()))
{
Ok(return_ty) => return_ty,
Err(CallError(_, bindings)) => {
bindings.report_diagnostics(&self.context, (*decorator_node).into());
bindings.return_type(self.db())
}
};
}
self.add_declaration_with_binding(
function.into(),
definition,
&DeclaredAndInferredType::AreTheSame(inferred_ty),
&DeclaredAndInferredType::AreTheSame(function_ty),
);
}
@@ -1860,11 +1824,11 @@ impl<'db> TypeInferenceBuilder<'db> {
todo_type!("async `with` statement")
} else {
match with_item.target() {
TargetKind::Sequence(unpack_position, unpack) => {
TargetKind::Sequence(unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
if unpack_position == UnpackPosition::First {
self.context.extend(unpacked.diagnostics());
if with_item.is_first() {
self.context.extend(unpacked);
}
unpacked.expression_type(name_ast_id)
}
@@ -2017,7 +1981,6 @@ impl<'db> TypeInferenceBuilder<'db> {
let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new(
self.db(),
name.id.clone(),
definition,
bound_or_constraint,
default_ty,
)));
@@ -2156,11 +2119,6 @@ impl<'db> TypeInferenceBuilder<'db> {
}
self.infer_standalone_expression(cls);
}
ast::Pattern::MatchOr(match_or) => {
for pattern in &match_or.patterns {
self.infer_match_pattern(pattern);
}
}
_ => {
self.infer_nested_match_pattern(pattern);
}
@@ -2338,12 +2296,8 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::SliceLiteral(..)
| Type::Tuple(..)
| Type::KnownInstance(..)
| Type::PropertyInstance(..)
| Type::FunctionLiteral(..)
| Type::Callable(..)
| Type::BoundMethod(_)
| Type::MethodWrapper(_)
| Type::WrapperDescriptor(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy => match object_ty.class_member(db, attribute.into()) {
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
@@ -2669,12 +2623,12 @@ impl<'db> TypeInferenceBuilder<'db> {
let value_ty = self.infer_standalone_expression(value);
let mut target_ty = match assignment.target() {
TargetKind::Sequence(unpack_position, unpack) => {
TargetKind::Sequence(unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
// Only copy the diagnostics if this is the first assignment to avoid duplicating the
// unpack assignments.
if unpack_position == UnpackPosition::First {
self.context.extend(unpacked.diagnostics());
if assignment.is_first() {
self.context.extend(unpacked);
}
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
@@ -2865,7 +2819,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// Fall back to non-augmented binary operator inference.
let mut binary_return_ty = || {
self.infer_binary_expression_type(assignment.into(), false, target_type, value_type, op)
self.infer_binary_expression_type(target_type, value_type, op)
.unwrap_or_else(|| {
report_unsupported_augmented_op(&mut self.context);
Type::unknown()
@@ -2971,10 +2925,10 @@ impl<'db> TypeInferenceBuilder<'db> {
todo_type!("async iterables/iterators")
} else {
match for_stmt.target() {
TargetKind::Sequence(unpack_position, unpack) => {
TargetKind::Sequence(unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
if unpack_position == UnpackPosition::First {
self.context.extend(unpacked.diagnostics());
if for_stmt.is_first() {
self.context.extend(unpacked);
}
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
unpacked.expression_type(name_ast_id)
@@ -3626,7 +3580,8 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_first_comprehension_iter(generators);
todo_type!("generator type")
// TODO generator type
todo_type!()
}
fn infer_list_comprehension_expression(&mut self, listcomp: &ast::ExprListComp) -> Type<'db> {
@@ -3638,7 +3593,8 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_first_comprehension_iter(generators);
todo_type!("list comprehension type")
// TODO list type
todo_type!()
}
fn infer_dict_comprehension_expression(&mut self, dictcomp: &ast::ExprDictComp) -> Type<'db> {
@@ -3651,7 +3607,8 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_first_comprehension_iter(generators);
todo_type!("dict comprehension type")
// TODO dict type
todo_type!()
}
fn infer_set_comprehension_expression(&mut self, setcomp: &ast::ExprSetComp) -> Type<'db> {
@@ -3663,7 +3620,8 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_first_comprehension_iter(generators);
todo_type!("set comprehension type")
// TODO set type
todo_type!()
}
fn infer_generator_expression_scope(&mut self, generator: &ast::ExprGenerator) {
@@ -3918,10 +3876,10 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: Useful inference of a lambda's return type will require a different approach,
// which does the inference of the body expression based on arguments at each call site,
// rather than eagerly computing a return type without knowing the argument types.
Type::Callable(CallableType::new(
Type::Callable(CallableType::General(GeneralCallableType::new(
self.db(),
Signature::new(parameters, Some(Type::unknown())),
))
)))
}
fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> {
@@ -3967,7 +3925,7 @@ impl<'db> TypeInferenceBuilder<'db> {
"Revealed type is `{}`",
revealed_type.display(self.db())
),
&[],
vec![],
);
}
}
@@ -4048,22 +4006,6 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
}
KnownFunction::Cast => {
if let [Some(casted_ty), Some(source_ty)] = overload.parameter_types() {
if source_ty.is_gradual_equivalent_to(self.context.db(), *casted_ty)
&& !source_ty.contains_todo(self.context.db())
{
self.context.report_lint(
&REDUNDANT_CAST,
call_expression,
format_args!(
"Value is already of type `{}`",
casted_ty.display(self.context.db()),
),
);
}
}
}
_ => {}
}
}
@@ -4184,15 +4126,8 @@ impl<'db> TypeInferenceBuilder<'db> {
// Class scopes are not visible to nested scopes, and we need to handle global
// scope differently (because an unbound name there falls back to builtins), so
// check only function-like scopes.
// There is one exception to this rule: type parameter scopes can see
// names defined in an immediately-enclosing class scope.
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file);
let is_immediately_enclosing_scope = scope.is_type_parameter(db)
&& scope
.scope(db)
.parent()
.is_some_and(|parent| parent == enclosing_scope_file_id);
if !enclosing_scope_id.is_function_like(db) && !is_immediately_enclosing_scope {
if !enclosing_scope_id.is_function_like(db) {
continue;
}
@@ -4203,20 +4138,12 @@ impl<'db> TypeInferenceBuilder<'db> {
// enclosing scopes that actually contain bindings that we should use when
// resolving the reference.)
if !self.is_deferred() {
match self.index.eager_bindings(
if let Some(bindings) = self.index.eager_bindings(
enclosing_scope_file_id,
symbol_name,
file_scope_id,
) {
EagerBindingsResult::Found(bindings) => {
return symbol_from_bindings(db, bindings).into();
}
// There are no visible bindings here.
// Don't fall back to non-eager symbol resolution.
EagerBindingsResult::NotFound => {
continue;
}
EagerBindingsResult::NoLongerInEagerContext => {}
return symbol_from_bindings(db, bindings).into();
}
}
@@ -4244,19 +4171,12 @@ impl<'db> TypeInferenceBuilder<'db> {
}
if !self.is_deferred() {
match self.index.eager_bindings(
if let Some(bindings) = self.index.eager_bindings(
FileScopeId::global(),
symbol_name,
file_scope_id,
) {
EagerBindingsResult::Found(bindings) => {
return symbol_from_bindings(db, bindings).into();
}
// There are no visible bindings here.
EagerBindingsResult::NotFound => {
return Symbol::Unbound.into();
}
EagerBindingsResult::NoLongerInEagerContext => {}
return symbol_from_bindings(db, bindings).into();
}
}
@@ -4441,15 +4361,11 @@ impl<'db> TypeInferenceBuilder<'db> {
op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert),
Type::FunctionLiteral(_)
| Type::Callable(..)
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(_)
| Type::BoundMethod(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
| Type::SubclassOf(_)
| Type::Instance(_)
| Type::KnownInstance(_)
| Type::PropertyInstance(_)
| Type::Union(_)
| Type::Intersection(_)
| Type::AlwaysTruthy
@@ -4502,7 +4418,19 @@ impl<'db> TypeInferenceBuilder<'db> {
let left_ty = self.infer_expression(left);
let right_ty = self.infer_expression(right);
self.infer_binary_expression_type(binary.into(), false, left_ty, right_ty, *op)
// Check for division by zero; this doesn't change the inferred type for the expression, but
// may emit a diagnostic
if matches!(
(op, right_ty),
(
ast::Operator::Div | ast::Operator::FloorDiv | ast::Operator::Mod,
Type::IntLiteral(0) | Type::BooleanLiteral(false)
)
) {
self.check_division_by_zero(binary, left_ty);
}
self.infer_binary_expression_type(left_ty, right_ty, *op)
.unwrap_or_else(|| {
self.context.report_lint(
&UNSUPPORTED_OPERATOR,
@@ -4519,37 +4447,15 @@ impl<'db> TypeInferenceBuilder<'db> {
fn infer_binary_expression_type(
&mut self,
node: AnyNodeRef<'_>,
mut emitted_division_by_zero_diagnostic: bool,
left_ty: Type<'db>,
right_ty: Type<'db>,
op: ast::Operator,
) -> Option<Type<'db>> {
// Check for division by zero; this doesn't change the inferred type for the expression, but
// may emit a diagnostic
if !emitted_division_by_zero_diagnostic
&& matches!(
(op, right_ty),
(
ast::Operator::Div | ast::Operator::FloorDiv | ast::Operator::Mod,
Type::IntLiteral(0) | Type::BooleanLiteral(false)
)
)
{
emitted_division_by_zero_diagnostic = self.check_division_by_zero(node, op, left_ty);
}
match (left_ty, right_ty, op) {
(Type::Union(lhs_union), rhs, _) => {
let mut union = UnionBuilder::new(self.db());
for lhs in lhs_union.elements(self.db()) {
let result = self.infer_binary_expression_type(
node,
emitted_division_by_zero_diagnostic,
*lhs,
rhs,
op,
)?;
let result = self.infer_binary_expression_type(*lhs, rhs, op)?;
union = union.add(result);
}
Some(union.build())
@@ -4557,13 +4463,7 @@ impl<'db> TypeInferenceBuilder<'db> {
(lhs, Type::Union(rhs_union), _) => {
let mut union = UnionBuilder::new(self.db());
for rhs in rhs_union.elements(self.db()) {
let result = self.infer_binary_expression_type(
node,
emitted_division_by_zero_diagnostic,
lhs,
*rhs,
op,
)?;
let result = self.infer_binary_expression_type(lhs, *rhs, op)?;
union = union.add(result);
}
Some(union.build())
@@ -4615,17 +4515,16 @@ impl<'db> TypeInferenceBuilder<'db> {
.unwrap_or_else(|| KnownClass::Int.to_instance(self.db())),
),
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Pow) => Some({
if m < 0 {
KnownClass::Float.to_instance(self.db())
} else {
u32::try_from(m)
.ok()
.and_then(|m| n.checked_pow(m))
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Pow) => {
let m = u32::try_from(m);
Some(match m {
Ok(m) => n
.checked_pow(m)
.map(Type::IntLiteral)
.unwrap_or_else(|| KnownClass::Int.to_instance(self.db()))
}
}),
.unwrap_or_else(|| KnownClass::Int.to_instance(self.db())),
Err(_) => KnownClass::Int.to_instance(self.db()),
})
}
(Type::BytesLiteral(lhs), Type::BytesLiteral(rhs), ast::Operator::Add) => {
let bytes = [&**lhs.value(self.db()), &**rhs.value(self.db())].concat();
@@ -4683,19 +4582,13 @@ impl<'db> TypeInferenceBuilder<'db> {
}
(Type::BooleanLiteral(bool_value), right, op) => self.infer_binary_expression_type(
node,
emitted_division_by_zero_diagnostic,
Type::IntLiteral(i64::from(bool_value)),
right,
op,
),
(left, Type::BooleanLiteral(bool_value), op) => self.infer_binary_expression_type(
node,
emitted_division_by_zero_diagnostic,
left,
Type::IntLiteral(i64::from(bool_value)),
op,
),
(left, Type::BooleanLiteral(bool_value), op) => {
self.infer_binary_expression_type(left, Type::IntLiteral(i64::from(bool_value)), op)
}
(Type::Tuple(lhs), Type::Tuple(rhs), ast::Operator::Add) => {
// Note: this only works on heterogeneous tuples.
@@ -4716,15 +4609,11 @@ impl<'db> TypeInferenceBuilder<'db> {
(
Type::FunctionLiteral(_)
| Type::Callable(..)
| Type::BoundMethod(_)
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
| Type::SubclassOf(_)
| Type::Instance(_)
| Type::KnownInstance(_)
| Type::PropertyInstance(_)
| Type::Intersection(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
@@ -4736,15 +4625,11 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::Tuple(_),
Type::FunctionLiteral(_)
| Type::Callable(..)
| Type::BoundMethod(_)
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
| Type::SubclassOf(_)
| Type::Instance(_)
| Type::KnownInstance(_)
| Type::PropertyInstance(_)
| Type::Intersection(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
@@ -5399,11 +5284,12 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
// Lookup the rich comparison `__dunder__` methods
_ => {
let rich_comparison = |op| self.infer_rich_comparison(left, right, op);
// Lookup the rich comparison `__dunder__` methods on instances
(Type::Instance(left_instance), Type::Instance(right_instance)) => {
let rich_comparison =
|op| self.infer_rich_comparison(left_instance, right_instance, op);
let membership_test_comparison = |op, range: TextRange| {
self.infer_membership_test_comparison(left, right, op, range)
self.infer_membership_test_comparison(left_instance, right_instance, op, range)
};
match op {
ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq),
@@ -5442,27 +5328,37 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
}
_ => match op {
ast::CmpOp::Is | ast::CmpOp::IsNot => Ok(KnownClass::Bool.to_instance(self.db())),
_ => Ok(todo_type!("Binary comparisons between more types")),
},
}
}
/// Rich comparison in Python are the operators `==`, `!=`, `<`, `<=`, `>`, and `>=`. Their
/// behaviour can be edited for classes by implementing corresponding dunder methods.
/// This function performs rich comparison between two types and returns the resulting type.
/// This function performs rich comparison between two instances and returns the resulting type.
/// see `<https://docs.python.org/3/reference/datamodel.html#object.__lt__>`
fn infer_rich_comparison(
&self,
left: Type<'db>,
right: Type<'db>,
left: InstanceType<'db>,
right: InstanceType<'db>,
op: RichCompareOperator,
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
let db = self.db();
// The following resource has details about the rich comparison algorithm:
// https://snarky.ca/unravelling-rich-comparison-operators/
let call_dunder = |op: RichCompareOperator, left: Type<'db>, right: Type<'db>| {
left.try_call_dunder(db, op.dunder(), CallArgumentTypes::positional([right]))
.map(|outcome| outcome.return_type(db))
.ok()
};
let call_dunder =
|op: RichCompareOperator, left: InstanceType<'db>, right: InstanceType<'db>| {
Type::Instance(left)
.try_call_dunder(
db,
op.dunder(),
CallArgumentTypes::positional([Type::Instance(right)]),
)
.map(|outcome| outcome.return_type(db))
.ok()
};
// The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side.
if left != right && right.is_subtype_of(db, left) {
@@ -5482,8 +5378,8 @@ impl<'db> TypeInferenceBuilder<'db> {
})
.ok_or_else(|| CompareUnsupportedError {
op: op.into(),
left_ty: left,
right_ty: right,
left_ty: left.into(),
right_ty: right.into(),
})
}
@@ -5493,25 +5389,31 @@ impl<'db> TypeInferenceBuilder<'db> {
/// and `<https://docs.python.org/3/reference/expressions.html#membership-test-details>`
fn infer_membership_test_comparison(
&self,
left: Type<'db>,
right: Type<'db>,
left: InstanceType<'db>,
right: InstanceType<'db>,
op: MembershipTestCompareOperator,
range: TextRange,
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
let db = self.db();
let contains_dunder = right.class_member(db, "__contains__".into()).symbol;
let contains_dunder = right.class().class_member(db, "__contains__").symbol;
let compare_result_opt = match contains_dunder {
Symbol::Type(contains_dunder, Boundness::Bound) => {
// If `__contains__` is available, it is used directly for the membership test.
contains_dunder
.try_call(db, CallArgumentTypes::positional([right, left]))
.try_call(
db,
CallArgumentTypes::positional([
Type::Instance(right),
Type::Instance(left),
]),
)
.map(|bindings| bindings.return_type(db))
.ok()
}
_ => {
// iteration-based membership test
right
Type::Instance(right)
.try_iterate(db)
.map(|_| KnownClass::Bool.to_instance(db))
.ok()
@@ -5536,8 +5438,8 @@ impl<'db> TypeInferenceBuilder<'db> {
})
.ok_or_else(|| CompareUnsupportedError {
op: op.into(),
left_ty: left,
right_ty: right,
left_ty: left.into(),
right_ty: right.into(),
})
}
@@ -5641,20 +5543,6 @@ impl<'db> TypeInferenceBuilder<'db> {
slice_ty: Type<'db>,
) -> Type<'db> {
match (value_ty, slice_ty) {
(
Type::Instance(instance),
Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::SliceLiteral(_),
) if instance
.class()
.is_known(self.db(), KnownClass::VersionInfo) =>
{
self.infer_subscript_expression_types(
value_node,
Type::version_info_tuple(self.db()),
slice_ty,
)
}
// Ex) Given `("a", "b", "c", "d")[1]`, return `"b"`
(Type::Tuple(tuple_ty), Type::IntLiteral(int)) if i32::try_from(int).is_ok() => {
let elements = tuple_ty.elements(self.db());
@@ -5796,6 +5684,7 @@ impl<'db> TypeInferenceBuilder<'db> {
return err.fallback_return_type(self.db());
}
Err(CallDunderError::CallError(_, bindings)) => {
bindings.report_diagnostics(&self.context, value_node.into());
self.context.report_lint(
&CALL_NON_CALLABLE,
value_node,
@@ -6039,9 +5928,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
ast::ExprContext::Invalid => TypeAndQualifiers::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
todo_type!("Name expression annotation in Store/Del context").into()
}
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!().into(),
},
ast::Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => {
@@ -6198,9 +6085,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.in_type_expression(self.db())
.unwrap_or_else(|error| error.into_fallback_type(&self.context, expression)),
ast::ExprContext::Invalid => Type::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
todo_type!("Name expression annotation in Store/Del context")
}
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(),
},
ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
@@ -6209,9 +6094,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.in_type_expression(self.db())
.unwrap_or_else(|error| error.into_fallback_type(&self.context, expression)),
ast::ExprContext::Invalid => Type::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
todo_type!("Attribute expression annotation in Store/Del context")
}
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(),
},
ast::Expr::NoneLiteral(_literal) => Type::none(self.db()),
@@ -6517,25 +6400,14 @@ impl<'db> TypeInferenceBuilder<'db> {
/// homogeneous tuple and a partly homogeneous tuple (respectively) due to the `...`
/// and the starred expression (respectively), Neither is supported by us right now,
/// so we should infer `Todo` for the *entire* tuple if we encounter one of those elements.
fn element_could_alter_type_of_whole_tuple(
element: &ast::Expr,
element_ty: Type,
builder: &TypeInferenceBuilder,
) -> bool {
if !element_ty.is_todo() {
return false;
}
match element {
ast::Expr::EllipsisLiteral(_) | ast::Expr::Starred(_) => true,
ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => {
matches!(
builder.expression_type(value),
Type::KnownInstance(KnownInstanceType::Unpack)
)
}
_ => false,
}
/// Even a subscript subelement could alter the type of the entire tuple
/// if the subscript is `Unpack[]` (which again, we don't yet support).
fn element_could_alter_type_of_whole_tuple(element: &ast::Expr, element_ty: Type) -> bool {
element_ty.is_todo()
&& matches!(
element,
ast::Expr::EllipsisLiteral(_) | ast::Expr::Starred(_) | ast::Expr::Subscript(_)
)
}
// TODO:
@@ -6551,8 +6423,7 @@ impl<'db> TypeInferenceBuilder<'db> {
for element in elements {
let element_ty = self.infer_type_expression(element);
return_todo |=
element_could_alter_type_of_whole_tuple(element, element_ty, self);
return_todo |= element_could_alter_type_of_whole_tuple(element, element_ty);
element_types.push(element_ty);
}
@@ -6571,8 +6442,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
single_element => {
let single_element_ty = self.infer_type_expression(single_element);
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty, self)
{
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty) {
todo_type!("full tuple[...] support")
} else {
TupleType::from_elements(self.db(), std::iter::once(single_element_ty))
@@ -6588,14 +6458,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let name_ty = self.infer_expression(slice);
match name_ty {
Type::ClassLiteral(class_literal_ty) => {
if class_literal_ty
.class()
.is_known(self.db(), KnownClass::Any)
{
SubclassOfType::subclass_of_any()
} else {
SubclassOfType::from(self.db(), class_literal_ty.class())
}
SubclassOfType::from(self.db(), class_literal_ty.class())
}
Type::KnownInstance(KnownInstanceType::Any) => {
SubclassOfType::subclass_of_any()
@@ -6675,14 +6538,6 @@ impl<'db> TypeInferenceBuilder<'db> {
} = subscript;
match value_ty {
Type::ClassLiteral(literal) if literal.class().is_known(self.db(), KnownClass::Any) => {
self.context.report_lint(
&INVALID_TYPE_FORM,
subscript,
format_args!("Type `typing.Any` expected no type parameter",),
);
Type::unknown()
}
Type::KnownInstance(known_instance) => {
self.infer_parameterized_known_instance_type_expression(subscript, known_instance)
}
@@ -6810,12 +6665,12 @@ impl<'db> TypeInferenceBuilder<'db> {
let callable_type = if let (Some(parameters), Some(return_type), true) =
(parameters, return_type, correct_argument_number)
{
CallableType::new(db, Signature::new(parameters, Some(return_type)))
GeneralCallableType::new(db, Signature::new(parameters, Some(return_type)))
} else {
CallableType::unknown(db)
GeneralCallableType::unknown(db)
};
let callable_type = Type::Callable(callable_type);
let callable_type = Type::Callable(CallableType::General(callable_type));
// `Signature` / `Parameters` are not a `Type` variant, so we're storing
// the outer callable type on the these expressions instead.
@@ -6875,7 +6730,7 @@ impl<'db> TypeInferenceBuilder<'db> {
argument_type
}
},
KnownInstanceType::CallableTypeOf => match arguments_slice {
KnownInstanceType::CallableTypeFromFunction => match arguments_slice {
ast::Expr::Tuple(_) => {
self.context.report_lint(
&INVALID_TYPE_FORM,
@@ -6889,23 +6744,19 @@ impl<'db> TypeInferenceBuilder<'db> {
}
_ => {
let argument_type = self.infer_expression(arguments_slice);
let signatures = argument_type.signatures(db);
// TODO overloads
let Some(signature) = signatures.iter().flatten().next() else {
let Some(function_type) = argument_type.into_function_literal() else {
self.context.report_lint(
&INVALID_TYPE_FORM,
arguments_slice,
format_args!(
"Expected the first argument to `{}` to be a callable object, but got an object of type `{}`",
"Expected the first argument to `{}` to be a function literal, but got `{}`",
known_instance.repr(db),
argument_type.display(db)
),
);
return Type::unknown();
};
Type::Callable(CallableType::new(db, signature.clone()))
function_type.into_callable_type(db)
}
},
@@ -7373,7 +7224,6 @@ mod tests {
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
use crate::symbol::global_symbol;
use crate::types::check_types;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::DbWithWritableSystem as _;
use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run};
@@ -7408,7 +7258,7 @@ mod tests {
fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) {
let messages: Vec<&str> = diagnostics
.iter()
.map(Diagnostic::primary_message)
.map(|diagnostic| diagnostic.message())
.collect();
assert_eq!(&messages, expected);
}

View File

@@ -19,8 +19,6 @@ use rustc_hash::FxHashMap;
use std::collections::hash_map::Entry;
use std::sync::Arc;
use super::UnionType;
/// Return the type constraint that `test` (if true) would place on `definition`, if any.
///
/// For example, if we have this code:
@@ -113,15 +111,7 @@ impl KnownConstraintFunction {
}
Some(builder.build())
}
Type::ClassLiteral(class_literal) => {
// At runtime (on Python 3.11+), this will return `True` for classes that actually
// do inherit `typing.Any` and `False` otherwise. We could accurately model that?
if class_literal.class().is_known(db, KnownClass::Any) {
None
} else {
Some(constraint_fn(class_literal.class()))
}
}
Type::ClassLiteral(class_literal) => Some(constraint_fn(class_literal.class())),
Type::SubclassOf(subclass_of_ty) => {
subclass_of_ty.subclass_of().into_class().map(constraint_fn)
}
@@ -235,31 +225,22 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
}
}
fn evaluate_pattern_predicate_kind(
&mut self,
pattern_predicate_kind: &PatternPredicateKind<'db>,
subject: Expression<'db>,
) -> Option<NarrowingConstraints<'db>> {
match pattern_predicate_kind {
PatternPredicateKind::Singleton(singleton) => {
self.evaluate_match_pattern_singleton(subject, *singleton)
}
PatternPredicateKind::Class(cls) => self.evaluate_match_pattern_class(subject, *cls),
PatternPredicateKind::Value(expr) => self.evaluate_match_pattern_value(subject, *expr),
PatternPredicateKind::Or(predicates) => {
self.evaluate_match_pattern_or(subject, predicates)
}
PatternPredicateKind::Unsupported => None,
}
}
fn evaluate_pattern_predicate(
&mut self,
pattern: PatternPredicate<'db>,
) -> Option<NarrowingConstraints<'db>> {
let subject = pattern.subject(self.db);
self.evaluate_pattern_predicate_kind(pattern.kind(self.db), subject)
match pattern.kind(self.db) {
PatternPredicateKind::Singleton(singleton, _guard) => {
self.evaluate_match_pattern_singleton(subject, *singleton)
}
PatternPredicateKind::Class(cls, _guard) => {
self.evaluate_match_pattern_class(subject, *cls)
}
// TODO: support more pattern kinds
PatternPredicateKind::Value(..) | PatternPredicateKind::Unsupported => None,
}
}
fn symbols(&self) -> Arc<SymbolTable> {
@@ -273,13 +254,6 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
}
}
#[track_caller]
fn expect_expr_name_symbol(&self, symbol: &str) -> ScopedSymbolId {
self.symbols()
.symbol_id_by_name(symbol)
.expect("We should always have a symbol for every `Name` node")
}
fn evaluate_expr_name(
&mut self,
expr_name: &ast::ExprName,
@@ -287,37 +261,22 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
) -> NarrowingConstraints<'db> {
let ast::ExprName { id, .. } = expr_name;
let symbol = self.expect_expr_name_symbol(id);
let symbol = self
.symbols()
.symbol_id_by_name(id)
.expect("Should always have a symbol for every Name node");
let mut constraints = NarrowingConstraints::default();
let ty = if is_positive {
Type::AlwaysFalsy.negate(self.db)
} else {
Type::AlwaysTruthy.negate(self.db)
};
constraints.insert(
symbol,
if is_positive {
Type::AlwaysFalsy.negate(self.db)
} else {
Type::AlwaysTruthy.negate(self.db)
},
);
NarrowingConstraints::from_iter([(symbol, ty)])
}
fn evaluate_expr_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option<Type<'db>> {
if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) {
match rhs_ty {
Type::Tuple(rhs_tuple) => Some(UnionType::from_elements(
self.db,
rhs_tuple.elements(self.db),
)),
Type::StringLiteral(string_literal) => Some(UnionType::from_elements(
self.db,
string_literal
.iter_each_char(self.db)
.map(Type::StringLiteral),
)),
_ => None,
}
} else {
None
}
constraints
}
fn evaluate_expr_compare(
@@ -376,7 +335,10 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
id,
ctx: _,
}) => {
let symbol = self.expect_expr_name_symbol(id);
let symbol = self
.symbols()
.symbol_id_by_name(id)
.expect("Should always have a symbol for every Name node");
match if is_positive { *op } else { op.negate() } {
ast::CmpOp::IsNot => {
@@ -403,16 +365,6 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
ast::CmpOp::Eq if lhs_ty.is_literal_string() => {
constraints.insert(symbol, rhs_ty);
}
ast::CmpOp::In => {
if let Some(ty) = self.evaluate_expr_in(lhs_ty, rhs_ty) {
constraints.insert(symbol, ty);
}
}
ast::CmpOp::NotIn => {
if let Some(ty) = self.evaluate_expr_in(lhs_ty, rhs_ty) {
constraints.insert(symbol, ty.negate(self.db));
}
}
_ => {
// TODO other comparison types
}
@@ -453,7 +405,10 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
.into_class_literal()
.is_some_and(|c| c.class().is_known(self.db, KnownClass::Type))
{
let symbol = self.expect_expr_name_symbol(id);
let symbol = self
.symbols()
.symbol_id_by_name(id)
.expect("Should always have a symbol for every Name node");
constraints.insert(symbol, Type::instance(rhs_class));
}
}
@@ -487,7 +442,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
return None;
};
let symbol = self.expect_expr_name_symbol(id);
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let class_info_ty =
inference.expression_type(class_info.scoped_expression_id(self.db, scope));
@@ -495,10 +450,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
function
.generate_constraint(self.db, class_info_ty)
.map(|constraint| {
NarrowingConstraints::from_iter([(
symbol,
constraint.negate_if(self.db, !is_positive),
)])
let mut constraints = NarrowingConstraints::default();
constraints.insert(symbol, constraint.negate_if(self.db, !is_positive));
constraints
})
}
// for the expression `bool(E)`, we further narrow the type based on `E`
@@ -522,14 +476,21 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
subject: Expression<'db>,
singleton: ast::Singleton,
) -> Option<NarrowingConstraints<'db>> {
let symbol = self.expect_expr_name_symbol(&subject.node_ref(self.db).as_name_expr()?.id);
if let Some(ast::ExprName { id, .. }) = subject.node_ref(self.db).as_name_expr() {
// SAFETY: we should always have a symbol for every Name node.
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let ty = match singleton {
ast::Singleton::None => Type::none(self.db),
ast::Singleton::True => Type::BooleanLiteral(true),
ast::Singleton::False => Type::BooleanLiteral(false),
};
Some(NarrowingConstraints::from_iter([(symbol, ty)]))
let ty = match singleton {
ast::Singleton::None => Type::none(self.db),
ast::Singleton::True => Type::BooleanLiteral(true),
ast::Singleton::False => Type::BooleanLiteral(false),
};
let mut constraints = NarrowingConstraints::default();
constraints.insert(symbol, ty);
Some(constraints)
} else {
None
}
}
fn evaluate_match_pattern_class(
@@ -537,36 +498,16 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
subject: Expression<'db>,
cls: Expression<'db>,
) -> Option<NarrowingConstraints<'db>> {
let symbol = self.expect_expr_name_symbol(&subject.node_ref(self.db).as_name_expr()?.id);
let ast::ExprName { id, .. } = subject.node_ref(self.db).as_name_expr()?;
let symbol = self
.symbols()
.symbol_id_by_name(id)
.expect("We should always have a symbol for every `Name` node");
let ty = infer_same_file_expression_type(self.db, cls).to_instance(self.db)?;
Some(NarrowingConstraints::from_iter([(symbol, ty)]))
}
fn evaluate_match_pattern_value(
&mut self,
subject: Expression<'db>,
value: Expression<'db>,
) -> Option<NarrowingConstraints<'db>> {
let symbol = self.expect_expr_name_symbol(&subject.node_ref(self.db).as_name_expr()?.id);
let ty = infer_same_file_expression_type(self.db, value);
Some(NarrowingConstraints::from_iter([(symbol, ty)]))
}
fn evaluate_match_pattern_or(
&mut self,
subject: Expression<'db>,
predicates: &Vec<PatternPredicateKind<'db>>,
) -> Option<NarrowingConstraints<'db>> {
let db = self.db;
predicates
.iter()
.filter_map(|predicate| self.evaluate_pattern_predicate_kind(predicate, subject))
.reduce(|mut constraints, constraints_| {
merge_constraints_or(&mut constraints, &constraints_, db);
constraints
})
let mut constraints = NarrowingConstraints::default();
constraints.insert(symbol, ty);
Some(constraints)
}
fn evaluate_bool_op(

View File

@@ -1,13 +1,11 @@
use crate::db::tests::TestDb;
use crate::symbol::{builtins_symbol, known_module_symbol};
use crate::types::{
BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType, Parameter,
Parameters, Signature, SubclassOfType, TupleType, Type, UnionType,
BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType,
SubclassOfType, TupleType, Type, UnionType,
};
use crate::{Db, KnownModule};
use hashbrown::HashSet;
use quickcheck::{Arbitrary, Gen};
use ruff_python_ast::name::Name;
/// A test representation of a type that can be transformed unambiguously into a real Type,
/// given a db.
@@ -47,59 +45,6 @@ pub(crate) enum Ty {
class: &'static str,
method: &'static str,
},
Callable {
params: CallableParams,
returns: Option<Box<Ty>>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum CallableParams {
GradualForm,
List(Vec<Param>),
}
impl CallableParams {
pub(crate) fn into_parameters(self, db: &TestDb) -> Parameters<'_> {
match self {
CallableParams::GradualForm => Parameters::gradual_form(),
CallableParams::List(params) => Parameters::new(params.into_iter().map(|param| {
let mut parameter = match param.kind {
ParamKind::PositionalOnly => Parameter::positional_only(param.name),
ParamKind::PositionalOrKeyword => {
Parameter::positional_or_keyword(param.name.unwrap())
}
ParamKind::Variadic => Parameter::variadic(param.name.unwrap()),
ParamKind::KeywordOnly => Parameter::keyword_only(param.name.unwrap()),
ParamKind::KeywordVariadic => Parameter::keyword_variadic(param.name.unwrap()),
};
if let Some(annotated_ty) = param.annotated_ty {
parameter = parameter.with_annotated_type(annotated_ty.into_type(db));
}
if let Some(default_ty) = param.default_ty {
parameter = parameter.with_default_type(default_ty.into_type(db));
}
parameter
})),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Param {
kind: ParamKind,
name: Option<Name>,
annotated_ty: Option<Ty>,
default_ty: Option<Ty>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ParamKind {
PositionalOnly,
PositionalOrKeyword,
Variadic,
KeywordOnly,
KeywordVariadic,
}
#[salsa::tracked]
@@ -108,11 +53,11 @@ fn create_bound_method<'db>(
function: Type<'db>,
builtins_class: Type<'db>,
) -> Type<'db> {
Type::BoundMethod(BoundMethodType::new(
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
db,
function.expect_function_literal(),
builtins_class.to_instance(db).unwrap(),
))
)))
}
impl Ty {
@@ -186,13 +131,6 @@ impl Ty {
create_bound_method(db, function, builtins_class)
}
Ty::Callable { params, returns } => Type::Callable(CallableType::new(
db,
Signature::new(
params.into_parameters(db),
returns.map(|ty| ty.into_type(db)),
),
)),
}
}
}
@@ -267,7 +205,7 @@ fn arbitrary_type(g: &mut Gen, size: u32) -> Ty {
if size == 0 {
arbitrary_core_type(g)
} else {
match u32::arbitrary(g) % 5 {
match u32::arbitrary(g) % 4 {
0 => arbitrary_core_type(g),
1 => Ty::Union(
(0..*g.choose(&[2, 3]).unwrap())
@@ -287,103 +225,11 @@ fn arbitrary_type(g: &mut Gen, size: u32) -> Ty {
.map(|_| arbitrary_type(g, size - 1))
.collect(),
},
4 => Ty::Callable {
params: match u32::arbitrary(g) % 2 {
0 => CallableParams::GradualForm,
1 => CallableParams::List(arbitrary_parameter_list(g, size)),
_ => unreachable!(),
},
returns: arbitrary_optional_type(g, size - 1).map(Box::new),
},
_ => unreachable!(),
}
}
}
fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec<Param> {
let mut params: Vec<Param> = vec![];
let mut used_names = HashSet::new();
// First, choose the number of parameters to generate.
for _ in 0..*g.choose(&[0, 1, 2, 3, 4, 5]).unwrap() {
// Next, choose the kind of parameters that can be generated based on the last parameter.
let next_kind = match params.last().map(|p| p.kind) {
None | Some(ParamKind::PositionalOnly) => *g
.choose(&[
ParamKind::PositionalOnly,
ParamKind::PositionalOrKeyword,
ParamKind::Variadic,
ParamKind::KeywordOnly,
ParamKind::KeywordVariadic,
])
.unwrap(),
Some(ParamKind::PositionalOrKeyword) => *g
.choose(&[
ParamKind::PositionalOrKeyword,
ParamKind::Variadic,
ParamKind::KeywordOnly,
ParamKind::KeywordVariadic,
])
.unwrap(),
Some(ParamKind::Variadic | ParamKind::KeywordOnly) => *g
.choose(&[ParamKind::KeywordOnly, ParamKind::KeywordVariadic])
.unwrap(),
Some(ParamKind::KeywordVariadic) => {
// There can't be any other parameter kind after a keyword variadic parameter.
break;
}
};
let name = loop {
let name = if matches!(next_kind, ParamKind::PositionalOnly) {
arbitrary_optional_name(g)
} else {
Some(arbitrary_name(g))
};
if let Some(name) = name {
if used_names.insert(name.clone()) {
break Some(name);
}
} else {
break None;
}
};
params.push(Param {
kind: next_kind,
name,
annotated_ty: arbitrary_optional_type(g, size),
default_ty: if matches!(next_kind, ParamKind::Variadic | ParamKind::KeywordVariadic) {
None
} else {
arbitrary_optional_type(g, size)
},
});
}
params
}
fn arbitrary_optional_type(g: &mut Gen, size: u32) -> Option<Ty> {
match u32::arbitrary(g) % 2 {
0 => None,
1 => Some(arbitrary_type(g, size)),
_ => unreachable!(),
}
}
fn arbitrary_name(g: &mut Gen) -> Name {
Name::new(format!("n{}", u32::arbitrary(g) % 10))
}
fn arbitrary_optional_name(g: &mut Gen) -> Option<Name> {
match u32::arbitrary(g) % 2 {
0 => None,
1 => Some(arbitrary_name(g)),
_ => unreachable!(),
}
}
impl Arbitrary for Ty {
fn arbitrary(g: &mut Gen) -> Ty {
const MAX_SIZE: u32 = 2;

View File

@@ -265,13 +265,6 @@ impl<'db> Signature<'db> {
pub(crate) fn parameters(&self) -> &Parameters<'db> {
&self.parameters
}
pub(crate) fn bind_self(&self) -> Self {
Self {
parameters: Parameters::new(self.parameters().iter().skip(1).cloned()),
return_ty: self.return_ty,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
@@ -511,12 +504,6 @@ impl<'db, 'a> IntoIterator for &'a Parameters<'db> {
}
}
impl<'db> FromIterator<Parameter<'db>> for Parameters<'db> {
fn from_iter<T: IntoIterator<Item = Parameter<'db>>>(iter: T) -> Self {
Self::new(iter)
}
}
impl<'db> std::ops::Index<usize> for Parameters<'db> {
type Output = Parameter<'db>;
@@ -606,56 +593,6 @@ impl<'db> Parameter<'db> {
self
}
/// Strip information from the parameter so that two equivalent parameters compare equal.
/// Normalize nested unions and intersections in the annotated type, if any.
///
/// See [`Type::normalized`] for more details.
pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self {
let Parameter {
annotated_type,
kind,
form,
} = self;
// Ensure unions and intersections are ordered in the annotated type (if there is one)
let annotated_type = annotated_type.map(|ty| ty.normalized(db));
// Ensure that parameter names are stripped from positional-only, variadic and keyword-variadic parameters.
// Ensure that we only record whether a parameter *has* a default
// (strip the precise *type* of the default from the parameter, replacing it with `Never`).
let kind = match kind {
ParameterKind::PositionalOnly {
name: _,
default_type,
} => ParameterKind::PositionalOnly {
name: None,
default_type: default_type.map(|_| Type::Never),
},
ParameterKind::PositionalOrKeyword { name, default_type } => {
ParameterKind::PositionalOrKeyword {
name: name.clone(),
default_type: default_type.map(|_| Type::Never),
}
}
ParameterKind::KeywordOnly { name, default_type } => ParameterKind::KeywordOnly {
name: name.clone(),
default_type: default_type.map(|_| Type::Never),
},
ParameterKind::Variadic { name: _ } => ParameterKind::Variadic {
name: Name::new_static("args"),
},
ParameterKind::KeywordVariadic { name: _ } => ParameterKind::KeywordVariadic {
name: Name::new_static("kwargs"),
},
};
Self {
annotated_type,
kind,
form: *form,
}
}
fn from_node_and_kind(
db: &'db dyn Db,
definition: Definition<'db>,
@@ -1053,4 +990,25 @@ mod tests {
// With no decorators, internal and external signature are the same
assert_eq!(func.signature(&db), &expected_sig);
}
#[test]
fn external_signature_decorated() {
let mut db = setup_db();
db.write_dedented(
"/src/a.py",
"
def deco(func): ...
@deco
def f(a: int) -> int: ...
",
)
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let expected_sig = Signature::todo("return type of decorated function");
// With no decorators, internal and external signature are the same
assert_eq!(func.signature(&db), &expected_sig);
}
}

View File

@@ -52,7 +52,7 @@ impl<'db> SubclassOfType<'db> {
}
/// Return the inner [`ClassBase`] value wrapped by this `SubclassOfType`.
pub const fn subclass_of(self) -> ClassBase<'db> {
pub(crate) const fn subclass_of(self) -> ClassBase<'db> {
self.subclass_of
}

View File

@@ -1,6 +1,7 @@
use std::cmp::Ordering;
use crate::db::Db;
use crate::types::CallableType;
use super::{
class_base::ClassBase, ClassLiteralType, DynamicType, InstanceType, KnownInstanceType,
@@ -61,29 +62,43 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::FunctionLiteral(_), _) => Ordering::Less,
(_, Type::FunctionLiteral(_)) => Ordering::Greater,
(Type::BoundMethod(left), Type::BoundMethod(right)) => left.cmp(right),
(Type::BoundMethod(_), _) => Ordering::Less,
(_, Type::BoundMethod(_)) => Ordering::Greater,
(
Type::Callable(CallableType::BoundMethod(left)),
Type::Callable(CallableType::BoundMethod(right)),
) => left.cmp(right),
(Type::Callable(CallableType::BoundMethod(_)), _) => Ordering::Less,
(_, Type::Callable(CallableType::BoundMethod(_))) => Ordering::Greater,
(Type::MethodWrapper(left), Type::MethodWrapper(right)) => left.cmp(right),
(Type::MethodWrapper(_), _) => Ordering::Less,
(_, Type::MethodWrapper(_)) => Ordering::Greater,
(
Type::Callable(CallableType::MethodWrapperDunderGet(left)),
Type::Callable(CallableType::MethodWrapperDunderGet(right)),
) => left.cmp(right),
(Type::Callable(CallableType::MethodWrapperDunderGet(_)), _) => Ordering::Less,
(_, Type::Callable(CallableType::MethodWrapperDunderGet(_))) => Ordering::Greater,
(Type::WrapperDescriptor(left), Type::WrapperDescriptor(right)) => left.cmp(right),
(Type::WrapperDescriptor(_), _) => Ordering::Less,
(_, Type::WrapperDescriptor(_)) => Ordering::Greater,
(
Type::Callable(CallableType::WrapperDescriptorDunderGet),
Type::Callable(CallableType::WrapperDescriptorDunderGet),
) => Ordering::Equal,
(Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less,
(_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater,
(Type::Callable(left), Type::Callable(right)) => {
debug_assert_eq!(*left, left.normalized(db));
debug_assert_eq!(*right, right.normalized(db));
left.cmp(right)
(
Type::Callable(CallableType::SpecializedGetitem),
Type::Callable(CallableType::SpecializedGetitem),
) => Ordering::Equal,
(Type::Callable(CallableType::SpecializedGetitem), _) => Ordering::Less,
(_, Type::Callable(CallableType::SpecializedGetitem)) => Ordering::Greater,
(Type::Callable(CallableType::General(_)), Type::Callable(CallableType::General(_))) => {
Ordering::Equal
}
(Type::Callable(_), _) => Ordering::Less,
(_, Type::Callable(_)) => Ordering::Greater,
(Type::Callable(CallableType::General(_)), _) => Ordering::Less,
(_, Type::Callable(CallableType::General(_))) => Ordering::Greater,
(Type::Tuple(left), Type::Tuple(right)) => {
debug_assert_eq!(*left, left.normalized(db));
debug_assert_eq!(*right, right.normalized(db));
debug_assert_eq!(*left, left.with_sorted_unions_and_intersections(db));
debug_assert_eq!(*right, right.with_sorted_unions_and_intersections(db));
left.cmp(right)
}
(Type::Tuple(_), _) => Ordering::Less,
@@ -222,8 +237,8 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(KnownInstanceType::TypeOf, _) => Ordering::Less,
(_, KnownInstanceType::TypeOf) => Ordering::Greater,
(KnownInstanceType::CallableTypeOf, _) => Ordering::Less,
(_, KnownInstanceType::CallableTypeOf) => Ordering::Greater,
(KnownInstanceType::CallableTypeFromFunction, _) => Ordering::Less,
(_, KnownInstanceType::CallableTypeFromFunction) => Ordering::Greater,
(KnownInstanceType::Unpack, _) => Ordering::Less,
(_, KnownInstanceType::Unpack) => Ordering::Greater,
@@ -262,10 +277,6 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::KnownInstance(_), _) => Ordering::Less,
(_, Type::KnownInstance(_)) => Ordering::Greater,
(Type::PropertyInstance(left), Type::PropertyInstance(right)) => left.cmp(right),
(Type::PropertyInstance(_), _) => Ordering::Less,
(_, Type::PropertyInstance(_)) => Ordering::Greater,
(Type::Dynamic(left), Type::Dynamic(right)) => dynamic_elements_ordering(*left, *right),
(Type::Dynamic(_), _) => Ordering::Less,
(_, Type::Dynamic(_)) => Ordering::Greater,
@@ -275,8 +286,8 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
}
(Type::Intersection(left), Type::Intersection(right)) => {
debug_assert_eq!(*left, left.normalized(db));
debug_assert_eq!(*right, right.normalized(db));
debug_assert_eq!(*left, left.to_sorted_intersection(db));
debug_assert_eq!(*right, right.to_sorted_intersection(db));
if left == right {
return Ordering::Equal;
@@ -321,7 +332,18 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
(_, DynamicType::Unknown) => Ordering::Greater,
#[cfg(debug_assertions)]
(DynamicType::Todo(TodoType(left)), DynamicType::Todo(TodoType(right))) => left.cmp(right),
(DynamicType::Todo(left), DynamicType::Todo(right)) => match (left, right) {
(
TodoType::FileAndLine(left_file, left_line),
TodoType::FileAndLine(right_file, right_line),
) => left_file
.cmp(right_file)
.then_with(|| left_line.cmp(&right_line)),
(TodoType::FileAndLine(..), _) => Ordering::Less,
(_, TodoType::FileAndLine(..)) => Ordering::Greater,
(TodoType::Message(left), TodoType::Message(right)) => left.cmp(right),
},
#[cfg(not(debug_assertions))]
(DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal,

View File

@@ -8,10 +8,10 @@ use ruff_python_ast::{self as ast, AnyNodeRef};
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::symbol::ScopeId;
use crate::types::{infer_expression_types, todo_type, Type, TypeCheckDiagnostics};
use crate::unpack::{UnpackKind, UnpackValue};
use crate::unpack::UnpackValue;
use crate::Db;
use super::context::InferContext;
use super::context::{InferContext, WithDiagnostics};
use super::diagnostic::INVALID_ASSIGNMENT;
use super::{TupleType, UnionType};
@@ -45,27 +45,30 @@ impl<'db> Unpacker<'db> {
let value_type = infer_expression_types(self.db(), value.expression())
.expression_type(value.scoped_expression_id(self.db(), self.scope));
let value_type = match value.kind() {
UnpackKind::Assign => {
let value_type = match value {
UnpackValue::Assign(expression) => {
if self.context.in_stub()
&& value
.expression()
.node_ref(self.db())
.is_ellipsis_literal_expr()
&& expression.node_ref(self.db()).is_ellipsis_literal_expr()
{
Type::unknown()
} else {
value_type
}
}
UnpackKind::Iterable => value_type.try_iterate(self.db()).unwrap_or_else(|err| {
UnpackValue::Iterable(_) => value_type.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, value_type, value.as_any_node_ref(self.db()));
err.fallback_element_type(self.db())
}),
UnpackKind::ContextManager => value_type.try_enter(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, value_type, value.as_any_node_ref(self.db()));
err.fallback_enter_type(self.db())
}),
UnpackValue::ContextManager(_) => {
value_type.try_enter(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(
&self.context,
value_type,
value.as_any_node_ref(self.db()),
);
err.fallback_enter_type(self.db())
})
}
};
self.unpack_inner(target, value.as_any_node_ref(self.db()), value_type);
@@ -283,9 +286,10 @@ impl<'db> UnpackResult<'db> {
pub(crate) fn expression_type(&self, expr_id: ScopedExpressionId) -> Type<'db> {
self.targets[&expr_id]
}
}
/// Returns the diagnostics in this unpacking assignment.
pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics {
impl WithDiagnostics for UnpackResult<'_> {
fn diagnostics(&self) -> &TypeCheckDiagnostics {
&self.diagnostics
}
}

View File

@@ -60,21 +60,23 @@ impl<'db> Unpack<'db> {
/// The expression that is being unpacked.
#[derive(Clone, Copy, Debug, Hash, salsa::Update)]
pub(crate) struct UnpackValue<'db> {
/// The kind of unpack expression
kind: UnpackKind,
/// The expression we are unpacking
expression: Expression<'db>,
pub(crate) enum UnpackValue<'db> {
/// An iterable expression like the one in a `for` loop or a comprehension.
Iterable(Expression<'db>),
/// An context manager expression like the one in a `with` statement.
ContextManager(Expression<'db>),
/// An expression that is being assigned to a target.
Assign(Expression<'db>),
}
impl<'db> UnpackValue<'db> {
pub(crate) fn new(kind: UnpackKind, expression: Expression<'db>) -> Self {
Self { kind, expression }
}
/// Returns the underlying [`Expression`] that is being unpacked.
pub(crate) const fn expression(self) -> Expression<'db> {
self.expression
match self {
UnpackValue::Assign(expr)
| UnpackValue::Iterable(expr)
| UnpackValue::ContextManager(expr) => expr,
}
}
/// Returns the [`ScopedExpressionId`] of the underlying expression.
@@ -92,27 +94,4 @@ impl<'db> UnpackValue<'db> {
pub(crate) fn as_any_node_ref(self, db: &'db dyn Db) -> AnyNodeRef<'db> {
self.expression().node_ref(db).node().into()
}
pub(crate) const fn kind(self) -> UnpackKind {
self.kind
}
}
#[derive(Clone, Copy, Debug, Hash, salsa::Update)]
pub(crate) enum UnpackKind {
/// An iterable expression like the one in a `for` loop or a comprehension.
Iterable,
/// An context manager expression like the one in a `with` statement.
ContextManager,
/// An expression that is being assigned to a target.
Assign,
}
/// The position of the target element in an unpacking.
#[derive(Clone, Copy, Debug, Hash, PartialEq, salsa::Update)]
pub(crate) enum UnpackPosition {
/// The target element is in the first position of the unpacking.
First,
/// The target element is in the position other than the first position of the unpacking.
Other,
}

View File

@@ -1,6 +1,5 @@
use camino::Utf8Path;
use dir_test::{dir_test, Fixture};
use red_knot_test::OutputFormat;
/// See `crates/red_knot_test/README.md` for documentation on these tests.
#[dir_test(
@@ -19,19 +18,12 @@ fn mdtest(fixture: Fixture<&str>) {
let test_name = test_name("mdtest", absolute_fixture_path);
let output_format = if std::env::var("MDTEST_GITHUB_ANNOTATIONS_FORMAT").is_ok() {
OutputFormat::GitHub
} else {
OutputFormat::Cli
};
red_knot_test::run(
absolute_fixture_path,
relative_fixture_path,
&snapshot_path,
short_title,
&test_name,
output_format,
);
}

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