Compare commits
43 Commits
deprecated
...
charlierma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fd6f969e3 | ||
|
|
1feb3cf41a | ||
|
|
7778d1d646 | ||
|
|
fb58a9b610 | ||
|
|
17a8a55f08 | ||
|
|
99d8ec6769 | ||
|
|
34cc3cab98 | ||
|
|
9384ba4b91 | ||
|
|
2b3550c85f | ||
|
|
90589372da | ||
|
|
b5ffb404de | ||
|
|
cffd1866ce | ||
|
|
569060f46c | ||
|
|
15394a8028 | ||
|
|
fc2ebea736 | ||
|
|
43160b4c3e | ||
|
|
0173738eef | ||
|
|
05ea77b1d4 | ||
|
|
1e790d3885 | ||
|
|
7855f03735 | ||
|
|
84301a7300 | ||
|
|
7b17c9c445 | ||
|
|
23c222368e | ||
|
|
1ecd97855e | ||
|
|
39e2df7ada | ||
|
|
ce8110332c | ||
|
|
555b3a6a2c | ||
|
|
05abd642a8 | ||
|
|
bb6fb4686d | ||
|
|
b4877f1661 | ||
|
|
3235cd8019 | ||
|
|
13e7afca42 | ||
|
|
f349dab4fc | ||
|
|
df713bc507 | ||
|
|
043ff61a0b | ||
|
|
792f9e357e | ||
|
|
6fe404a40f | ||
|
|
770b844fa5 | ||
|
|
7841cddb34 | ||
|
|
d4efef2382 | ||
|
|
e220c74163 | ||
|
|
f54b82147e | ||
|
|
1e053531b6 |
@@ -6,3 +6,10 @@ failure-output = "immediate-final"
|
||||
fail-fast = false
|
||||
|
||||
status-level = "skip"
|
||||
|
||||
# Mark tests that take longer than 1s as slow.
|
||||
# Terminate after 60s as a stop-gap measure to terminate on deadlock.
|
||||
slow-timeout = { period = "1s", terminate-after = 60 }
|
||||
|
||||
# Show slow jobs in the final summary
|
||||
final-status-level = "slow"
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
2
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# This file cannot use the extension `.yaml`.
|
||||
blank_issues_enabled: false
|
||||
22
.github/ISSUE_TEMPLATE/issue.yaml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/issue.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: New issue
|
||||
description: A generic issue
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to report an issue! We're glad to have you involved with Ruff.
|
||||
|
||||
If you're filing a bug report, please consider including the following information:
|
||||
|
||||
* List of keywords you searched for before creating this issue. Write them down here so that others can find this issue more easily and help provide feedback.
|
||||
e.g. "RUF001", "unused variable", "Jupyter notebook"
|
||||
* A minimal code snippet that reproduces the bug.
|
||||
* The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag.
|
||||
* The current Ruff settings (any relevant sections from your `pyproject.toml`).
|
||||
* The current Ruff version (`ruff --version`).
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: A description of the issue
|
||||
2
.github/workflows/build-binaries.yml
vendored
2
.github/workflows/build-binaries.yml
vendored
@@ -23,6 +23,8 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
PACKAGE_NAME: ruff
|
||||
MODULE_NAME: ruff
|
||||
|
||||
2
.github/workflows/build-docker.yml
vendored
2
.github/workflows/build-docker.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
env:
|
||||
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}
|
||||
run: |
|
||||
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
|
||||
version=$(grep -m 1 "^version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
|
||||
if [ "${TAG}" != "${version}" ]; then
|
||||
echo "The input tag does not match the version from pyproject.toml:" >&2
|
||||
echo "${TAG}" >&2
|
||||
|
||||
58
.github/workflows/ci.yaml
vendored
58
.github/workflows/ci.yaml
vendored
@@ -1,5 +1,7 @@
|
||||
name: CI
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -59,6 +61,7 @@ jobs:
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- crates/**
|
||||
- "!crates/red_knot*/**"
|
||||
- "!crates/ruff_python_formatter/**"
|
||||
- "!crates/ruff_formatter/**"
|
||||
- "!crates/ruff_dev/**"
|
||||
@@ -116,11 +119,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: |
|
||||
rustup component add clippy
|
||||
rustup target add wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Clippy"
|
||||
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||
- name: "Clippy (wasm)"
|
||||
@@ -130,12 +133,13 @@ jobs:
|
||||
name: "cargo test (linux)"
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -148,7 +152,6 @@ jobs:
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -176,12 +179,13 @@ jobs:
|
||||
name: "cargo test (linux, release)"
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -194,7 +198,6 @@ jobs:
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -205,22 +208,23 @@ jobs:
|
||||
name: "cargo test (windows)"
|
||||
runs-on: github-windows-2025-x86_64-16
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
|
||||
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
|
||||
run: |
|
||||
@@ -231,12 +235,13 @@ jobs:
|
||||
name: "cargo test (wasm)"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -247,7 +252,6 @@ jobs:
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Test ruff_wasm"
|
||||
run: |
|
||||
cd crates/ruff_wasm
|
||||
@@ -266,11 +270,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Build"
|
||||
run: cargo build --release --locked
|
||||
|
||||
@@ -278,7 +282,7 @@ jobs:
|
||||
name: "cargo build (msrv)"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -289,6 +293,7 @@ jobs:
|
||||
with:
|
||||
file: "Cargo.toml"
|
||||
field: "workspace.package.rust-version"
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
env:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
@@ -303,7 +308,6 @@ jobs:
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -321,11 +325,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "fuzz -> target"
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
with:
|
||||
@@ -341,7 +345,7 @@ jobs:
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.parser == 'true' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && needs.determine_changes.outputs.parser == 'true' }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
@@ -377,15 +381,15 @@ jobs:
|
||||
name: "test scripts"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
# Run all code generation scripts, and verify that the current output is
|
||||
# already checked into git.
|
||||
- run: python crates/ruff_python_ast/generate.py
|
||||
@@ -409,7 +413,7 @@ jobs:
|
||||
- determine_changes
|
||||
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
|
||||
# Ecosystem check needs linter and/or formatter changes.
|
||||
if: ${{ github.event_name == 'pull_request' && needs.determine_changes.outputs.code == 'true' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.code == 'true' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -543,6 +547,7 @@ jobs:
|
||||
name: "python package"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -577,9 +582,9 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install pre-commit"
|
||||
run: pip install pre-commit
|
||||
- name: "Cache pre-commit"
|
||||
@@ -611,6 +616,7 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
@@ -620,7 +626,6 @@ jobs:
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
@@ -644,16 +649,15 @@ jobs:
|
||||
name: "formatter instabilities and black similarity"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Cache rust"
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: "Run checks"
|
||||
run: scripts/formatter_ecosystem_checks.sh
|
||||
- name: "Github step summary"
|
||||
@@ -668,7 +672,7 @@ jobs:
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
steps:
|
||||
- uses: extractions/setup-just@v2
|
||||
env:
|
||||
@@ -710,7 +714,7 @@ jobs:
|
||||
benchmarks:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: determine_changes
|
||||
if: ${{ github.repository == 'astral-sh/ruff' && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
@@ -718,6 +722,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
@@ -726,8 +732,6 @@ jobs:
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: "Build benchmarks"
|
||||
run: cargo codspeed build --features codspeed -p ruff_benchmark
|
||||
|
||||
|
||||
7
.github/zizmor.yml
vendored
7
.github/zizmor.yml
vendored
@@ -10,3 +10,10 @@ rules:
|
||||
ignore:
|
||||
- build-docker.yml
|
||||
- publish-playground.yml
|
||||
excessive-permissions:
|
||||
# it's hard to test what the impact of removing these ignores would be
|
||||
# without actually running the release workflow...
|
||||
ignore:
|
||||
- build-docker.yml
|
||||
- publish-playground.yml
|
||||
- publish-docs.yml
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -29,6 +29,10 @@ tracing.folded
|
||||
tracing-flamechart.svg
|
||||
tracing-flamegraph.svg
|
||||
|
||||
# insta
|
||||
.rs.pending-snap
|
||||
|
||||
|
||||
###
|
||||
# Rust.gitignore
|
||||
###
|
||||
|
||||
@@ -91,7 +91,7 @@ repos:
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.1.1
|
||||
rev: v1.2.2
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
|
||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,5 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Argument `fail_stop` in DAG has been renamed as `fail_fast` (`AIR302`) ([#15633](https://github.com/astral-sh/ruff/pull/15633))
|
||||
- \[`airflow`\] Extend `AIR303` with more symbols ([#15611](https://github.com/astral-sh/ruff/pull/15611))
|
||||
- \[`flake8-bandit`\] Report all references to suspicious functions (`S3`) ([#15541](https://github.com/astral-sh/ruff/pull/15541))
|
||||
- \[`flake8-pytest-style`\] Do not emit diagnostics for empty `for` loops (`PT012`, `PT031`) ([#15542](https://github.com/astral-sh/ruff/pull/15542))
|
||||
- \[`flake8-simplify`\] Avoid double negations (`SIM103`) ([#15562](https://github.com/astral-sh/ruff/pull/15562))
|
||||
- \[`pyflakes`\] Fix infinite loop with unused local import in `__init__.py` (`F401`) ([#15517](https://github.com/astral-sh/ruff/pull/15517))
|
||||
- \[`pylint`\] Do not report methods with only one `EM101`-compatible `raise` (`PLR6301`) ([#15507](https://github.com/astral-sh/ruff/pull/15507))
|
||||
- \[`pylint`\] Implement `redefined-slots-in-subclass` (`W0244`) ([#9640](https://github.com/astral-sh/ruff/pull/9640))
|
||||
- \[`pyupgrade`\] Add rules to use PEP 695 generics in classes and functions (`UP046`, `UP047`) ([#15565](https://github.com/astral-sh/ruff/pull/15565), [#15659](https://github.com/astral-sh/ruff/pull/15659))
|
||||
- \[`refurb`\] Implement `for-loop-writes` (`FURB122`) ([#10630](https://github.com/astral-sh/ruff/pull/10630))
|
||||
- \[`ruff`\] Implement `needless-else` clause (`RUF047`) ([#15051](https://github.com/astral-sh/ruff/pull/15051))
|
||||
- \[`ruff`\] Implement `starmap-zip` (`RUF058`) ([#15483](https://github.com/astral-sh/ruff/pull/15483))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-bugbear`\] Do not raise error if keyword argument is present and target-python version is less or equals than 3.9 (`B903`) ([#15549](https://github.com/astral-sh/ruff/pull/15549))
|
||||
- \[`flake8-comprehensions`\] strip parentheses around generators in `unnecessary-generator-set` (`C401`) ([#15553](https://github.com/astral-sh/ruff/pull/15553))
|
||||
- \[`flake8-pytest-style`\] Rewrite references to `.exception` (`PT027`) ([#15680](https://github.com/astral-sh/ruff/pull/15680))
|
||||
- \[`flake8-simplify`\] Mark fixes as unsafe (`SIM201`, `SIM202`) ([#15626](https://github.com/astral-sh/ruff/pull/15626))
|
||||
- \[`flake8-type-checking`\] Fix some safe fixes being labeled unsafe (`TC006`,`TC008`) ([#15638](https://github.com/astral-sh/ruff/pull/15638))
|
||||
- \[`isort`\] Omit trailing whitespace in `unsorted-imports` (`I001`) ([#15518](https://github.com/astral-sh/ruff/pull/15518))
|
||||
- \[`pydoclint`\] Allow ignoring one line docstrings for `DOC` rules ([#13302](https://github.com/astral-sh/ruff/pull/13302))
|
||||
- \[`pyflakes`\] Apply redefinition fixes by source code order (`F811`) ([#15575](https://github.com/astral-sh/ruff/pull/15575))
|
||||
- \[`pyflakes`\] Avoid removing too many imports in `redefined-while-unused` (`F811`) ([#15585](https://github.com/astral-sh/ruff/pull/15585))
|
||||
- \[`pyflakes`\] Group redefinition fixes by source statement (`F811`) ([#15574](https://github.com/astral-sh/ruff/pull/15574))
|
||||
- \[`pylint`\] Include name of base class in message for `redefined-slots-in-subclass` (`W0244`) ([#15559](https://github.com/astral-sh/ruff/pull/15559))
|
||||
- \[`ruff`\] Update fix for `RUF055` to use `var == value` ([#15605](https://github.com/astral-sh/ruff/pull/15605))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Fix bracket spacing for single-element tuples in f-string expressions ([#15537](https://github.com/astral-sh/ruff/pull/15537))
|
||||
- Fix unstable f-string formatting for expressions containing a trailing comma ([#15545](https://github.com/astral-sh/ruff/pull/15545))
|
||||
|
||||
### Performance
|
||||
|
||||
- Avoid quadratic membership check in import fixes ([#15576](https://github.com/astral-sh/ruff/pull/15576))
|
||||
|
||||
### Server
|
||||
|
||||
- Allow `unsafe-fixes` settings for code actions ([#15666](https://github.com/astral-sh/ruff/pull/15666))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-bandit`\] Add missing single-line/dotall regex flag (`S608`) ([#15654](https://github.com/astral-sh/ruff/pull/15654))
|
||||
- \[`flake8-import-conventions`\] Fix infinite loop between `ICN001` and `I002` (`ICN001`) ([#15480](https://github.com/astral-sh/ruff/pull/15480))
|
||||
- \[`flake8-simplify`\] Do not emit diagnostics for expressions inside string type annotations (`SIM222`, `SIM223`) ([#15405](https://github.com/astral-sh/ruff/pull/15405))
|
||||
- \[`pyflakes`\] Treat arguments passed to the `default=` parameter of `TypeVar` as type expressions (`F821`) ([#15679](https://github.com/astral-sh/ruff/pull/15679))
|
||||
- \[`pyupgrade`\] Avoid syntax error when the iterable is a non-parenthesized tuple (`UP028`) ([#15543](https://github.com/astral-sh/ruff/pull/15543))
|
||||
- \[`ruff`\] Exempt `NewType` calls where the original type is immutable (`RUF009`) ([#15588](https://github.com/astral-sh/ruff/pull/15588))
|
||||
- Preserve raw string prefix and escapes in all codegen fixes ([#15694](https://github.com/astral-sh/ruff/pull/15694))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Generate documentation redirects for lowercase rule codes ([#15564](https://github.com/astral-sh/ruff/pull/15564))
|
||||
- `TRY300`: Add some extra notes on not catching exceptions you didn't expect ([#15036](https://github.com/astral-sh/ruff/pull/15036))
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### Preview features
|
||||
|
||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -2332,6 +2332,7 @@ dependencies = [
|
||||
"red_knot_server",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_python_trivia",
|
||||
"salsa",
|
||||
"tempfile",
|
||||
"toml",
|
||||
@@ -2583,7 +2584,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2817,7 +2818,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3134,7 +3135,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
|
||||
@@ -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.9.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.2/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.9.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.3/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.9.2
|
||||
rev: v0.9.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -33,6 +33,7 @@ tracing-tree = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["testing"] }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
insta-cmd = { workspace = true }
|
||||
|
||||
@@ -7,6 +7,7 @@ use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use python_version::PythonVersion;
|
||||
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use red_knot_project::watch;
|
||||
use red_knot_project::watch::ProjectWatcher;
|
||||
use red_knot_project::{ProjectDatabase, ProjectMetadata};
|
||||
@@ -69,22 +70,18 @@ struct Args {
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn to_options(&self, cli_cwd: &SystemPath) -> Options {
|
||||
fn to_options(&self) -> Options {
|
||||
Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: self.python_version.map(Into::into),
|
||||
venv_path: self
|
||||
.venv_path
|
||||
.as_ref()
|
||||
.map(|venv_path| SystemPath::absolute(venv_path, cli_cwd)),
|
||||
typeshed: self
|
||||
.typeshed
|
||||
.as_ref()
|
||||
.map(|typeshed| SystemPath::absolute(typeshed, cli_cwd)),
|
||||
python_version: self
|
||||
.python_version
|
||||
.map(|version| RangedValue::cli(version.into())),
|
||||
venv_path: self.venv_path.as_ref().map(RelativePathBuf::cli),
|
||||
typeshed: self.typeshed.as_ref().map(RelativePathBuf::cli),
|
||||
extra_paths: self.extra_search_path.as_ref().map(|extra_search_paths| {
|
||||
extra_search_paths
|
||||
.iter()
|
||||
.map(|path| SystemPath::absolute(path, cli_cwd))
|
||||
.map(RelativePathBuf::cli)
|
||||
.collect()
|
||||
}),
|
||||
..EnvironmentOptions::default()
|
||||
@@ -158,8 +155,8 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| cli_base_path.clone());
|
||||
|
||||
let system = OsSystem::new(cwd.clone());
|
||||
let cli_options = args.to_options(&cwd);
|
||||
let system = OsSystem::new(cwd);
|
||||
let cli_options = args.to_options();
|
||||
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
|
||||
workspace_metadata.apply_cli_options(cli_options.clone());
|
||||
|
||||
|
||||
@@ -1,60 +1,327 @@
|
||||
use anyhow::Context;
|
||||
use insta::Settings;
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Specifying an option on the CLI should take precedence over the same setting in the
|
||||
/// project's configuration.
|
||||
#[test]
|
||||
fn test_config_override() -> anyhow::Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
fn config_override() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.knot.environment]
|
||||
python-version = "3.11"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r#"
|
||||
import sys
|
||||
|
||||
std::fs::write(
|
||||
tempdir.path().join("pyproject.toml"),
|
||||
r#"
|
||||
[tool.knot.environment]
|
||||
python-version = "3.11"
|
||||
"#,
|
||||
)
|
||||
.context("Failed to write settings")?;
|
||||
# Access `sys.last_exc` that was only added in Python 3.12
|
||||
print(sys.last_exc)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
std::fs::write(
|
||||
tempdir.path().join("test.py"),
|
||||
r#"
|
||||
import sys
|
||||
case.insta_settings().bind(|| {
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-attribute] <temp_dir>/test.py:5:7 Type `<module 'sys'>` has no attribute `last_exc`
|
||||
|
||||
# Access `sys.last_exc` that was only added in Python 3.12
|
||||
print(sys.last_exc)
|
||||
"#,
|
||||
)
|
||||
.context("Failed to write test.py")?;
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
insta::with_settings!({filters => vec![(&*tempdir_filter(&tempdir), "<temp_dir>/")]}, {
|
||||
assert_cmd_snapshot!(knot().arg("--project").arg(tempdir.path()), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-attribute] <temp_dir>/test.py:5:7 Type `<module 'sys'>` has no attribute `last_exc`
|
||||
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
assert_cmd_snapshot!(knot().arg("--project").arg(tempdir.path()).arg("--python-version").arg("3.12"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn knot() -> Command {
|
||||
Command::new(get_cargo_bin("red_knot"))
|
||||
/// Paths specified on the CLI are relative to the current working directory and not the project root.
|
||||
///
|
||||
/// We test this by adding an extra search path from the CLI to the libs directory when
|
||||
/// running the CLI from the child directory (using relative paths).
|
||||
///
|
||||
/// Project layout:
|
||||
/// ```
|
||||
/// - libs
|
||||
/// |- utils.py
|
||||
/// - child
|
||||
/// | - test.py
|
||||
/// - pyproject.toml
|
||||
/// ```
|
||||
///
|
||||
/// And the command is run in the `child` directory.
|
||||
#[test]
|
||||
fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.knot.environment]
|
||||
python-version = "3.11"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"libs/utils.py",
|
||||
r#"
|
||||
def add(a: int, b: int) -> int:
|
||||
a + b
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"child/test.py",
|
||||
r#"
|
||||
from utils import add
|
||||
|
||||
stat = add(10, 15)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
case.insta_settings().bind(|| {
|
||||
// Make sure that the CLI fails when the `libs` directory is not in the search path.
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-import] <temp_dir>/child/test.py:2:1 Cannot resolve import `utils`
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")).arg("--extra-search-path").arg("../libs"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tempdir_filter(tempdir: &TempDir) -> String {
|
||||
format!(r"{}\\?/?", regex::escape(tempdir.path().to_str().unwrap()))
|
||||
/// Paths specified in a configuration file are relative to the project root.
|
||||
///
|
||||
/// We test this by adding `libs` (as a relative path) to the extra search path in the configuration and run
|
||||
/// the CLI from a subdirectory.
|
||||
///
|
||||
/// Project layout:
|
||||
/// ```
|
||||
/// - libs
|
||||
/// |- utils.py
|
||||
/// - child
|
||||
/// | - test.py
|
||||
/// - pyproject.toml
|
||||
/// ```
|
||||
#[test]
|
||||
fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.knot.environment]
|
||||
python-version = "3.11"
|
||||
extra-paths = ["libs"]
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"libs/utils.py",
|
||||
r#"
|
||||
def add(a: int, b: int) -> int:
|
||||
a + b
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"child/test.py",
|
||||
r#"
|
||||
from utils import add
|
||||
|
||||
stat = add(10, 15)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
case.insta_settings().bind(|| {
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The rule severity can be changed in the configuration file
|
||||
#[test]
|
||||
fn rule_severity() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, y):
|
||||
x = a
|
||||
|
||||
print(x) # possibly-unresolved-reference
|
||||
"#,
|
||||
)?;
|
||||
|
||||
case.insta_settings().bind(|| {
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:7:7 Name `x` used when possibly not defined
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
case.write_file("pyproject.toml", r#"
|
||||
[tool.knot.rules]
|
||||
division-by-zero = "warn" # demote to warn
|
||||
possibly-unresolved-reference = "ignore"
|
||||
"#)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Red Knot warns about unknown rules
|
||||
#[test]
|
||||
fn unknown_rules() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.knot.rules]
|
||||
division-by-zer = "warn" # incorrect rule name
|
||||
"#,
|
||||
),
|
||||
("test.py", "print(10)"),
|
||||
])?;
|
||||
|
||||
case.insta_settings().bind(|| {
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unknown-rule] <temp_dir>/pyproject.toml:3:1 Unknown lint rule `division-by-zer`
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
_temp_dir: TempDir,
|
||||
project_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl TestCase {
|
||||
fn new() -> anyhow::Result<Self> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
// Canonicalize the tempdir path because macos uses symlinks for tempdirs
|
||||
// and that doesn't play well with our snapshot filtering.
|
||||
let project_dir = temp_dir
|
||||
.path()
|
||||
.canonicalize()
|
||||
.context("Failed to canonicalize project path")?;
|
||||
|
||||
Ok(Self {
|
||||
project_dir,
|
||||
_temp_dir: temp_dir,
|
||||
})
|
||||
}
|
||||
|
||||
fn with_files<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<Self> {
|
||||
let case = Self::new()?;
|
||||
case.write_files(files)?;
|
||||
Ok(case)
|
||||
}
|
||||
|
||||
fn with_file(path: impl AsRef<Path>, content: &str) -> anyhow::Result<Self> {
|
||||
let case = Self::new()?;
|
||||
case.write_file(path, content)?;
|
||||
Ok(case)
|
||||
}
|
||||
|
||||
fn write_files<'a>(
|
||||
&self,
|
||||
files: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> anyhow::Result<()> {
|
||||
for (path, content) in files {
|
||||
self.write_file(path, content)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_file(&self, path: impl AsRef<Path>, content: &str) -> anyhow::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let path = self.project_dir.join(path);
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create directory `{}`", parent.display()))?;
|
||||
}
|
||||
std::fs::write(&path, &*ruff_python_trivia::textwrap::dedent(content))
|
||||
.with_context(|| format!("Failed to write file `{path}`", path = path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn project_dir(&self) -> &Path {
|
||||
&self.project_dir
|
||||
}
|
||||
|
||||
// Returns the insta filters to escape paths in snapshots
|
||||
fn insta_settings(&self) -> Settings {
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(&tempdir_filter(&self.project_dir), "<temp_dir>/");
|
||||
settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
||||
settings
|
||||
}
|
||||
|
||||
fn command(&self) -> Command {
|
||||
let mut command = Command::new(get_cargo_bin("red_knot"));
|
||||
command.current_dir(&self.project_dir);
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
fn tempdir_filter(path: &Path) -> String {
|
||||
format!(r"{}\\?/?", regex::escape(path.to_str().unwrap()))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::time::{Duration, Instant};
|
||||
use anyhow::{anyhow, Context};
|
||||
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use red_knot_project::metadata::pyproject::{PyProject, Tool};
|
||||
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use red_knot_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher};
|
||||
use red_knot_project::{Db, ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform, PythonVersion};
|
||||
@@ -321,7 +322,7 @@ where
|
||||
.search_paths
|
||||
.extra_paths
|
||||
.iter()
|
||||
.chain(program_settings.search_paths.typeshed.as_ref())
|
||||
.chain(program_settings.search_paths.custom_typeshed.as_ref())
|
||||
{
|
||||
std::fs::create_dir_all(path.as_std_path())
|
||||
.with_context(|| format!("Failed to create search path `{path}`"))?;
|
||||
@@ -791,7 +792,7 @@ fn search_path() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_options([("bar.py", "import sub.a")], |root_path, _project_path| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![root_path.join("site_packages")]),
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
@@ -832,7 +833,7 @@ fn add_search_path() -> anyhow::Result<()> {
|
||||
// Register site-packages as a search path.
|
||||
case.update_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![site_packages.clone()]),
|
||||
extra_paths: Some(vec![RelativePathBuf::cli("site_packages")]),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
@@ -855,7 +856,7 @@ fn remove_search_path() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_options([("bar.py", "import sub.a")], |root_path, _project_path| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![root_path.join("site_packages")]),
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
@@ -896,8 +897,10 @@ print(sys.last_exc, os.getegid())
|
||||
|_root_path, _project_path| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(PythonVersion::PY311),
|
||||
python_platform: Some(PythonPlatform::Identifier("win32".to_string())),
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY311)),
|
||||
python_platform: Some(RangedValue::cli(PythonPlatform::Identifier(
|
||||
"win32".to_string(),
|
||||
))),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
@@ -920,8 +923,10 @@ print(sys.last_exc, os.getegid())
|
||||
// Change the python version
|
||||
case.update_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(PythonVersion::PY312),
|
||||
python_platform: Some(PythonPlatform::Identifier("linux".to_string())),
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
|
||||
python_platform: Some(RangedValue::cli(PythonPlatform::Identifier(
|
||||
"linux".to_string(),
|
||||
))),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
@@ -951,7 +956,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
||||
|root_path, _project_path| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
typeshed: Some(root_path.join("typeshed")),
|
||||
typeshed: Some(RelativePathBuf::cli(root_path.join("typeshed"))),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
@@ -1375,11 +1380,13 @@ mod unix {
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|_root, project| {
|
||||
|_root, _project| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![project.join(".venv/lib/python3.12/site-packages")]),
|
||||
python_version: Some(PythonVersion::PY312),
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(
|
||||
".venv/lib/python3.12/site-packages",
|
||||
)]),
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::panic::RefUnwindSafe;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::DEFAULT_LINT_REGISTRY;
|
||||
use crate::{check_file, Project, ProjectMetadata};
|
||||
use crate::{Project, ProjectMetadata};
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{Db as SemanticDb, Program};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
@@ -27,7 +27,6 @@ pub struct ProjectDatabase {
|
||||
storage: salsa::Storage<ProjectDatabase>,
|
||||
files: Files,
|
||||
system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
impl ProjectDatabase {
|
||||
@@ -35,14 +34,11 @@ impl ProjectDatabase {
|
||||
where
|
||||
S: System + 'static + Send + Sync + RefUnwindSafe,
|
||||
{
|
||||
let rule_selection = RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY);
|
||||
|
||||
let mut db = Self {
|
||||
project: None,
|
||||
storage: salsa::Storage::default(),
|
||||
files: Files::default(),
|
||||
system: Arc::new(system),
|
||||
rule_selection: Arc::new(rule_selection),
|
||||
};
|
||||
|
||||
// TODO: Use the `program_settings` to compute the key for the database's persistent
|
||||
@@ -66,7 +62,7 @@ impl ProjectDatabase {
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Box<dyn Diagnostic>>, Cancelled> {
|
||||
let _span = tracing::debug_span!("check_file", file=%file.path(self)).entered();
|
||||
|
||||
self.with_db(|db| check_file(db, file))
|
||||
self.with_db(|db| self.project().check_file(db, file))
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the system.
|
||||
@@ -119,7 +115,7 @@ impl SemanticDb for ProjectDatabase {
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
self.project().rule_selection(self)
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#![allow(clippy::ref_option)]
|
||||
|
||||
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder};
|
||||
use crate::metadata::options::OptionDiagnostic;
|
||||
pub use db::{Db, ProjectDatabase};
|
||||
use files::{Index, Indexed, IndexedFiles};
|
||||
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::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity};
|
||||
@@ -17,10 +21,6 @@ use salsa::Setter;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use db::{Db, ProjectDatabase};
|
||||
use files::{Index, Indexed, IndexedFiles};
|
||||
pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
|
||||
|
||||
pub mod combine;
|
||||
|
||||
mod db;
|
||||
@@ -68,6 +68,7 @@ pub struct Project {
|
||||
pub metadata: ProjectMetadata,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl Project {
|
||||
pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self {
|
||||
Project::builder(metadata)
|
||||
@@ -96,13 +97,34 @@ impl Project {
|
||||
self.reload_files(db);
|
||||
}
|
||||
|
||||
pub fn rule_selection(self, db: &dyn Db) -> &RuleSelection {
|
||||
let (selection, _) = self.rule_selection_with_diagnostics(db);
|
||||
selection
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn rule_selection_with_diagnostics(
|
||||
self,
|
||||
db: &dyn Db,
|
||||
) -> (RuleSelection, Vec<OptionDiagnostic>) {
|
||||
self.metadata(db).options().to_rule_selection(db)
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
pub fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn Diagnostic>> {
|
||||
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn Diagnostic>> {
|
||||
let project_span = tracing::debug_span!("Project::check");
|
||||
let _span = project_span.enter();
|
||||
|
||||
tracing::debug!("Checking project '{name}'", name = self.name(db));
|
||||
let result = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
|
||||
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
|
||||
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
|
||||
diagnostics.extend(options_diagnostics.iter().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
let result = Arc::new(std::sync::Mutex::new(diagnostics));
|
||||
let inner_result = Arc::clone(&result);
|
||||
|
||||
let db = db.clone();
|
||||
@@ -119,7 +141,7 @@ impl Project {
|
||||
let check_file_span = tracing::debug_span!(parent: &project_span, "check_file", file=%file.path(&db));
|
||||
let _entered = check_file_span.entered();
|
||||
|
||||
let file_diagnostics = check_file(&db, file);
|
||||
let file_diagnostics = check_file_impl(&db, file);
|
||||
result.lock().unwrap().extend(file_diagnostics);
|
||||
});
|
||||
}
|
||||
@@ -128,6 +150,23 @@ impl Project {
|
||||
Arc::into_inner(result).unwrap().into_inner().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
|
||||
|
||||
let mut file_diagnostics: Vec<_> = options_diagnostics
|
||||
.iter()
|
||||
.map(|diagnostic| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
})
|
||||
.collect();
|
||||
|
||||
let check_diagnostics = check_file_impl(db, file);
|
||||
file_diagnostics.extend(check_diagnostics);
|
||||
|
||||
file_diagnostics
|
||||
}
|
||||
|
||||
/// Opens a file in the project.
|
||||
///
|
||||
/// This changes the behavior of `check` to only check the open files rather than all files in the project.
|
||||
@@ -265,8 +304,9 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_file(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
|
||||
|
||||
// Abort checking if there are IO errors.
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
@@ -402,8 +442,8 @@ impl Diagnostic for IOErrorDiagnostic {
|
||||
self.error.to_string().into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
fn file(&self) -> Option<File> {
|
||||
Some(self.file)
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
@@ -418,7 +458,7 @@ impl Diagnostic for IOErrorDiagnostic {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::{check_file, ProjectMetadata};
|
||||
use crate::{check_file_impl, ProjectMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
@@ -442,7 +482,7 @@ mod tests {
|
||||
|
||||
assert_eq!(source_text(&db, file).as_str(), "");
|
||||
assert_eq!(
|
||||
check_file(&db, file)
|
||||
check_file_impl(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -458,7 +498,7 @@ mod tests {
|
||||
|
||||
assert_eq!(source_text(&db, file).as_str(), "");
|
||||
assert_eq!(
|
||||
check_file(&db, file)
|
||||
check_file_impl(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use red_knot_python_semantic::ProgramSettings;
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::name::Name;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::combine::Combine;
|
||||
use crate::metadata::pyproject::{Project, PyProject, PyProjectError};
|
||||
use crate::metadata::value::ValueSource;
|
||||
use options::KnotTomlError;
|
||||
use options::Options;
|
||||
|
||||
pub mod options;
|
||||
pub mod pyproject;
|
||||
pub mod value;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize))]
|
||||
@@ -52,7 +55,7 @@ impl ProjectMetadata {
|
||||
) -> Self {
|
||||
let name = project
|
||||
.and_then(|project| project.name.as_ref())
|
||||
.map(|name| Name::new(&**name))
|
||||
.map(|name| Name::new(&***name))
|
||||
.unwrap_or_else(|| Name::new(root.file_name().unwrap_or("root")));
|
||||
|
||||
// TODO(https://github.com/astral-sh/ruff/issues/15491): Respect requires-python
|
||||
@@ -87,7 +90,10 @@ impl ProjectMetadata {
|
||||
let pyproject_path = project_root.join("pyproject.toml");
|
||||
|
||||
let pyproject = if let Ok(pyproject_str) = system.read_to_string(&pyproject_path) {
|
||||
match PyProject::from_toml_str(&pyproject_str) {
|
||||
match PyProject::from_toml_str(
|
||||
&pyproject_str,
|
||||
ValueSource::File(Arc::new(pyproject_path.clone())),
|
||||
) {
|
||||
Ok(pyproject) => Some(pyproject),
|
||||
Err(error) => {
|
||||
return Err(ProjectDiscoveryError::InvalidPyProject {
|
||||
@@ -103,7 +109,10 @@ impl ProjectMetadata {
|
||||
// A `knot.toml` takes precedence over a `pyproject.toml`.
|
||||
let knot_toml_path = project_root.join("knot.toml");
|
||||
if let Ok(knot_str) = system.read_to_string(&knot_toml_path) {
|
||||
let options = match Options::from_toml_str(&knot_str) {
|
||||
let options = match Options::from_toml_str(
|
||||
&knot_str,
|
||||
ValueSource::File(Arc::new(knot_toml_path.clone())),
|
||||
) {
|
||||
Ok(options) => options,
|
||||
Err(error) => {
|
||||
return Err(ProjectDiscoveryError::InvalidKnotToml {
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
|
||||
use crate::Db;
|
||||
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
|
||||
use red_knot_python_semantic::{
|
||||
ProgramSettings, PythonPlatform, PythonVersion, SearchPathSettings, SitePackages,
|
||||
};
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::Combine;
|
||||
use ruff_text_size::TextRange;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
use thiserror::Error;
|
||||
|
||||
/// The options for the project.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct Options {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub environment: Option<EnvironmentOptions>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub src: Option<SrcOptions>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rules: Option<Rules>,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub(crate) fn from_toml_str(content: &str) -> Result<Self, KnotTomlError> {
|
||||
pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, KnotTomlError> {
|
||||
let _guard = ValueSourceGuard::new(source);
|
||||
let options = toml::from_str(content)?;
|
||||
Ok(options)
|
||||
}
|
||||
@@ -29,7 +44,12 @@ impl Options {
|
||||
let (python_version, python_platform) = self
|
||||
.environment
|
||||
.as_ref()
|
||||
.map(|env| (env.python_version, env.python_platform.as_ref()))
|
||||
.map(|env| {
|
||||
(
|
||||
env.python_version.as_deref().copied(),
|
||||
env.python_platform.as_deref(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
ProgramSettings {
|
||||
@@ -44,19 +64,19 @@ impl Options {
|
||||
project_root: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> SearchPathSettings {
|
||||
let src_roots =
|
||||
if let Some(src_root) = self.src.as_ref().and_then(|src| src.root.as_deref()) {
|
||||
vec![src_root.to_path_buf()]
|
||||
} else {
|
||||
let src = project_root.join("src");
|
||||
let src_roots = if let Some(src_root) = self.src.as_ref().and_then(|src| src.root.as_ref())
|
||||
{
|
||||
vec![src_root.absolute(project_root, system)]
|
||||
} else {
|
||||
let src = project_root.join("src");
|
||||
|
||||
// Default to `src` and the project root if `src` exists and the root hasn't been specified.
|
||||
if system.is_directory(&src) {
|
||||
vec![project_root.to_path_buf(), src]
|
||||
} else {
|
||||
vec![project_root.to_path_buf()]
|
||||
}
|
||||
};
|
||||
// Default to `src` and the project root if `src` exists and the root hasn't been specified.
|
||||
if system.is_directory(&src) {
|
||||
vec![project_root.to_path_buf(), src]
|
||||
} else {
|
||||
vec![project_root.to_path_buf()]
|
||||
}
|
||||
};
|
||||
|
||||
let (extra_paths, python, typeshed) = self
|
||||
.environment
|
||||
@@ -71,43 +91,119 @@ impl Options {
|
||||
.unwrap_or_default();
|
||||
|
||||
SearchPathSettings {
|
||||
extra_paths: extra_paths.unwrap_or_default(),
|
||||
extra_paths: extra_paths
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|path| path.absolute(project_root, system))
|
||||
.collect(),
|
||||
src_roots,
|
||||
typeshed,
|
||||
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
|
||||
site_packages: python
|
||||
.map(|venv_path| SitePackages::Derived { venv_path })
|
||||
.map(|venv_path| SitePackages::Derived {
|
||||
venv_path: venv_path.absolute(project_root, system),
|
||||
})
|
||||
.unwrap_or(SitePackages::Known(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
|
||||
let registry = db.lint_registry();
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
// Initialize the selection with the defaults
|
||||
let mut selection = RuleSelection::from_registry(registry);
|
||||
|
||||
let rules = self
|
||||
.rules
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|rules| rules.inner.iter());
|
||||
|
||||
for (rule_name, level) in rules {
|
||||
let source = rule_name.source();
|
||||
match registry.get(rule_name) {
|
||||
Ok(lint) => {
|
||||
let lint_source = match source {
|
||||
ValueSource::File(_) => LintSource::File,
|
||||
ValueSource::Cli => LintSource::Cli,
|
||||
};
|
||||
if let Ok(severity) = Severity::try_from(**level) {
|
||||
selection.enable(lint, severity, lint_source);
|
||||
} else {
|
||||
selection.disable(lint);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// `system_path_to_file` can return `Err` if the file was deleted since the configuration
|
||||
// was read. This should be rare and it should be okay to default to not showing a configuration
|
||||
// file in that case.
|
||||
let file = source
|
||||
.file()
|
||||
.and_then(|path| system_path_to_file(db.upcast(), path).ok());
|
||||
|
||||
// TODO: Add a note if the value was configured on the CLI
|
||||
let diagnostic = match error {
|
||||
GetLintError::Unknown(_) => OptionDiagnostic::new(
|
||||
DiagnosticId::UnknownRule,
|
||||
format!("Unknown lint rule `{rule_name}`"),
|
||||
Severity::Warning,
|
||||
),
|
||||
GetLintError::Removed(_) => OptionDiagnostic::new(
|
||||
DiagnosticId::UnknownRule,
|
||||
format!("Unknown lint rule `{rule_name}`"),
|
||||
Severity::Warning,
|
||||
),
|
||||
};
|
||||
|
||||
diagnostics.push(diagnostic.with_file(file).with_range(rule_name.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(selection, diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct EnvironmentOptions {
|
||||
pub python_version: Option<PythonVersion>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub python_version: Option<RangedValue<PythonVersion>>,
|
||||
|
||||
pub python_platform: Option<PythonPlatform>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub python_platform: Option<RangedValue<PythonPlatform>>,
|
||||
|
||||
/// List of user-provided paths that should take first priority in the module resolution.
|
||||
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
|
||||
/// or pyright's stubPath configuration setting.
|
||||
pub extra_paths: Option<Vec<SystemPathBuf>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_paths: Option<Vec<RelativePathBuf>>,
|
||||
|
||||
/// Optional path to a "typeshed" directory on disk for us to use for standard-library types.
|
||||
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
|
||||
/// bundled as a zip file in the binary
|
||||
pub typeshed: Option<SystemPathBuf>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub typeshed: Option<RelativePathBuf>,
|
||||
|
||||
// TODO: Rename to python, see https://github.com/astral-sh/ruff/issues/15530
|
||||
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
|
||||
pub venv_path: Option<SystemPathBuf>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub venv_path: Option<RelativePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SrcOptions {
|
||||
/// The root of the project, used for finding first-party modules.
|
||||
pub root: Option<SystemPathBuf>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub root: Option<RelativePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", transparent)]
|
||||
pub struct Rules {
|
||||
inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -115,3 +211,58 @@ pub enum KnotTomlError {
|
||||
#[error(transparent)]
|
||||
TomlSyntax(#[from] toml::de::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct OptionDiagnostic {
|
||||
id: DiagnosticId,
|
||||
message: String,
|
||||
severity: Severity,
|
||||
file: Option<File>,
|
||||
range: Option<TextRange>,
|
||||
}
|
||||
|
||||
impl OptionDiagnostic {
|
||||
pub fn new(id: DiagnosticId, message: String, severity: Severity) -> Self {
|
||||
Self {
|
||||
id,
|
||||
message,
|
||||
severity,
|
||||
file: None,
|
||||
range: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn with_file(mut self, file: Option<File>) -> Self {
|
||||
self.file = file;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn with_range(mut self, range: Option<TextRange>) -> Self {
|
||||
self.range = range;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for OptionDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
Cow::Borrowed(&self.message)
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
self.range
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
self.severity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::ops::Deref;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::metadata::options::Options;
|
||||
use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard};
|
||||
|
||||
/// A `pyproject.toml` as specified in PEP 517.
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
|
||||
@@ -28,7 +29,11 @@ pub enum PyProjectError {
|
||||
}
|
||||
|
||||
impl PyProject {
|
||||
pub(crate) fn from_toml_str(content: &str) -> Result<Self, PyProjectError> {
|
||||
pub(crate) fn from_toml_str(
|
||||
content: &str,
|
||||
source: ValueSource,
|
||||
) -> Result<Self, PyProjectError> {
|
||||
let _guard = ValueSourceGuard::new(source);
|
||||
toml::from_str(content).map_err(PyProjectError::TomlSyntax)
|
||||
}
|
||||
}
|
||||
@@ -43,11 +48,11 @@ pub struct Project {
|
||||
///
|
||||
/// Note: Intentionally option to be more permissive during deserialization.
|
||||
/// `PackageMetadata::from_pyproject` reports missing names.
|
||||
pub name: Option<PackageName>,
|
||||
pub name: Option<RangedValue<PackageName>>,
|
||||
/// The version of the project
|
||||
pub version: Option<Version>,
|
||||
pub version: Option<RangedValue<Version>>,
|
||||
/// The Python versions this project is compatible with.
|
||||
pub requires_python: Option<VersionSpecifiers>,
|
||||
pub requires_python: Option<RangedValue<VersionSpecifiers>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
|
||||
337
crates/red_knot_project/src/metadata/value.rs
Normal file
337
crates/red_knot_project/src/metadata/value.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
use crate::combine::Combine;
|
||||
use crate::Db;
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
use toml::Spanned;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ValueSource {
|
||||
/// Value loaded from a project's configuration file.
|
||||
///
|
||||
/// Ideally, we'd use [`ruff_db::files::File`] but we can't because the database hasn't been
|
||||
/// created when loading the configuration.
|
||||
File(Arc<SystemPathBuf>),
|
||||
/// The value comes from a CLI argument, while it's left open if specified using a short argument,
|
||||
/// long argument (`--extra-paths`) or `--config key=value`.
|
||||
Cli,
|
||||
}
|
||||
|
||||
impl ValueSource {
|
||||
pub fn file(&self) -> Option<&SystemPath> {
|
||||
match self {
|
||||
ValueSource::File(path) => Some(&**path),
|
||||
ValueSource::Cli => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Serde doesn't provide any easy means to pass a value to a [`Deserialize`] implementation,
|
||||
/// but we want to associate each deserialized [`RelativePath`] with the source from
|
||||
/// which it originated. We use a thread local variable to work around this limitation.
|
||||
///
|
||||
/// Use the [`ValueSourceGuard`] to initialize the thread local before calling into any
|
||||
/// deserialization code. It ensures that the thread local variable gets cleaned up
|
||||
/// once deserialization is done (once the guard gets dropped).
|
||||
static VALUE_SOURCE: RefCell<Option<ValueSource>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Guard to safely change the [`VALUE_SOURCE`] for the current thread.
|
||||
#[must_use]
|
||||
pub(super) struct ValueSourceGuard {
|
||||
prev_value: Option<ValueSource>,
|
||||
}
|
||||
|
||||
impl ValueSourceGuard {
|
||||
pub(super) fn new(source: ValueSource) -> Self {
|
||||
let prev = VALUE_SOURCE.replace(Some(source));
|
||||
Self { prev_value: prev }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ValueSourceGuard {
|
||||
fn drop(&mut self) {
|
||||
VALUE_SOURCE.set(self.prev_value.take());
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that "remembers" where it comes from (source) and its range in source.
|
||||
///
|
||||
/// ## Equality, Hash, and Ordering
|
||||
/// The equality, hash, and ordering are solely based on the value. They disregard the value's range
|
||||
/// or source.
|
||||
///
|
||||
/// This ensures that two resolved configurations are identical even if the position of a value has changed
|
||||
/// or if the values were loaded from different sources.
|
||||
#[derive(Clone)]
|
||||
pub struct RangedValue<T> {
|
||||
value: T,
|
||||
source: ValueSource,
|
||||
|
||||
/// The byte range of `value` in `source`.
|
||||
///
|
||||
/// Can be `None` because not all sources support a range.
|
||||
/// For example, arguments provided on the CLI won't have a range attached.
|
||||
range: Option<TextRange>,
|
||||
}
|
||||
|
||||
impl<T> RangedValue<T> {
|
||||
pub fn new(value: T, source: ValueSource) -> Self {
|
||||
Self::with_range(value, source, TextRange::default())
|
||||
}
|
||||
|
||||
pub fn cli(value: T) -> Self {
|
||||
Self::with_range(value, ValueSource::Cli, TextRange::default())
|
||||
}
|
||||
|
||||
pub fn with_range(value: T, source: ValueSource, range: TextRange) -> Self {
|
||||
Self {
|
||||
value,
|
||||
range: Some(range),
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range(&self) -> Option<TextRange> {
|
||||
self.range
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &ValueSource {
|
||||
&self.source
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_source(mut self, source: ValueSource) -> Self {
|
||||
self.source = source;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Combine for RangedValue<T> {
|
||||
fn combine(self, _other: Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self
|
||||
}
|
||||
fn combine_with(&mut self, _other: Self) {}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// The type already has an `iter` method thanks to `Deref`.
|
||||
#[allow(clippy::into_iter_without_iter)]
|
||||
impl<'a, T> IntoIterator for &'a RangedValue<T>
|
||||
where
|
||||
&'a T: IntoIterator,
|
||||
{
|
||||
type Item = <&'a T as IntoIterator>::Item;
|
||||
type IntoIter = <&'a T as IntoIterator>::IntoIter;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.value.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
// The type already has a `into_iter_mut` method thanks to `DerefMut`.
|
||||
#[allow(clippy::into_iter_without_iter)]
|
||||
impl<'a, T> IntoIterator for &'a mut RangedValue<T>
|
||||
where
|
||||
&'a mut T: IntoIterator,
|
||||
{
|
||||
type Item = <&'a mut T as IntoIterator>::Item;
|
||||
type IntoIter = <&'a mut T as IntoIterator>::IntoIter;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.value.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for RangedValue<T>
|
||||
where
|
||||
T: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.value.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Display for RangedValue<T>
|
||||
where
|
||||
T: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.value.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
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 T {
|
||||
&mut self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U: ?Sized> AsRef<U> for RangedValue<T>
|
||||
where
|
||||
T: AsRef<U>,
|
||||
{
|
||||
fn as_ref(&self) -> &U {
|
||||
self.value.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq for RangedValue<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.value.eq(&other.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq<T>> PartialEq<T> for RangedValue<T> {
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
self.value.eq(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Eq> Eq for RangedValue<T> {}
|
||||
|
||||
impl<T: Hash> Hash for RangedValue<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.value.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialOrd> PartialOrd for RangedValue<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.value.partial_cmp(&other.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialOrd<T>> PartialOrd<T> for RangedValue<T> {
|
||||
fn partial_cmp(&self, other: &T) -> Option<Ordering> {
|
||||
self.value.partial_cmp(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ord> Ord for RangedValue<T> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.value.cmp(&other.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Deserialize<'de> for RangedValue<T>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let spanned: Spanned<T> = Spanned::deserialize(deserializer)?;
|
||||
let span = spanned.span();
|
||||
let range = TextRange::new(
|
||||
TextSize::try_from(span.start).expect("Configuration file to be smaller than 4GB"),
|
||||
TextSize::try_from(span.end).expect("Configuration file to be smaller than 4GB"),
|
||||
);
|
||||
|
||||
Ok(VALUE_SOURCE.with_borrow(|source| {
|
||||
let source = source.clone().unwrap();
|
||||
|
||||
Self::with_range(spanned.into_inner(), source, range)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Serialize for RangedValue<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.value.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// A possibly relative path in a configuration file.
|
||||
///
|
||||
/// Relative paths in configuration files or from CLI options
|
||||
/// require different anchoring:
|
||||
///
|
||||
/// * CLI: The path is relative to the current working directory
|
||||
/// * Configuration file: The path is relative to the project's root.
|
||||
#[derive(
|
||||
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct RelativePathBuf(RangedValue<SystemPathBuf>);
|
||||
|
||||
impl RelativePathBuf {
|
||||
pub fn new(path: impl AsRef<SystemPath>, source: ValueSource) -> Self {
|
||||
Self(RangedValue::new(path.as_ref().to_path_buf(), source))
|
||||
}
|
||||
|
||||
pub fn cli(path: impl AsRef<SystemPath>) -> Self {
|
||||
Self::new(path, ValueSource::Cli)
|
||||
}
|
||||
|
||||
/// Returns the relative path as specified by the user.
|
||||
pub fn path(&self) -> &SystemPath {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Returns the owned relative path.
|
||||
pub fn into_path_buf(self) -> SystemPathBuf {
|
||||
self.0.into_inner()
|
||||
}
|
||||
|
||||
/// Resolves the absolute path for `self` based on its origin.
|
||||
pub fn absolute_with_db(&self, db: &dyn Db) -> SystemPathBuf {
|
||||
self.absolute(db.project().root(db), db.system())
|
||||
}
|
||||
|
||||
/// Resolves the absolute path for `self` based on its origin.
|
||||
pub fn absolute(&self, project_root: &SystemPath, system: &dyn System) -> SystemPathBuf {
|
||||
let relative_to = match &self.0.source {
|
||||
ValueSource::File(_) => project_root,
|
||||
ValueSource::Cli => system.current_directory(),
|
||||
};
|
||||
|
||||
SystemPath::absolute(&self.0, relative_to)
|
||||
}
|
||||
}
|
||||
|
||||
impl Combine for RelativePathBuf {
|
||||
fn combine(self, other: Self) -> Self {
|
||||
Self(self.0.combine(other.0))
|
||||
}
|
||||
|
||||
fn combine_with(&mut self, other: Self) {
|
||||
self.0.combine_with(other.0);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ ProjectMetadata(
|
||||
name: Name("project-root"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: Some(SrcOptions(
|
||||
root: Some("src"),
|
||||
)),
|
||||
|
||||
@@ -6,7 +6,6 @@ ProjectMetadata(
|
||||
name: Name("nested-project"),
|
||||
root: "/app/packages/a",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: Some(SrcOptions(
|
||||
root: Some("src"),
|
||||
)),
|
||||
|
||||
@@ -8,11 +8,6 @@ ProjectMetadata(
|
||||
options: Options(
|
||||
environment: Some(EnvironmentOptions(
|
||||
r#python-version: Some("3.10"),
|
||||
r#python-platform: None,
|
||||
r#extra-paths: None,
|
||||
typeshed: None,
|
||||
r#venv-path: None,
|
||||
)),
|
||||
src: None,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -5,8 +5,5 @@ expression: sub_project
|
||||
ProjectMetadata(
|
||||
name: Name("nested-project"),
|
||||
root: "/app/packages/a",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: None,
|
||||
),
|
||||
options: Options(),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ ProjectMetadata(
|
||||
name: Name("super-app"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: Some(SrcOptions(
|
||||
root: Some("src"),
|
||||
)),
|
||||
|
||||
@@ -5,8 +5,5 @@ expression: project
|
||||
ProjectMetadata(
|
||||
name: Name("backend"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: None,
|
||||
),
|
||||
options: Options(),
|
||||
)
|
||||
|
||||
@@ -5,8 +5,5 @@ expression: project
|
||||
ProjectMetadata(
|
||||
name: Name("app"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: None,
|
||||
),
|
||||
options: Options(),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use red_knot_project::{ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_python_semantic::{HasTy, SemanticModel};
|
||||
use red_knot_python_semantic::{HasType, SemanticModel};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem};
|
||||
@@ -197,10 +197,10 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
||||
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||
match stmt {
|
||||
Stmt::FunctionDef(function) => {
|
||||
let _ty = function.ty(&self.model);
|
||||
let _ty = function.inferred_type(&self.model);
|
||||
}
|
||||
Stmt::ClassDef(class) => {
|
||||
let _ty = class.ty(&self.model);
|
||||
let _ty = class.inferred_type(&self.model);
|
||||
}
|
||||
Stmt::Assign(assign) => {
|
||||
for target in &assign.targets {
|
||||
@@ -243,25 +243,25 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &Expr) {
|
||||
let _ty = expr.ty(&self.model);
|
||||
let _ty = expr.inferred_type(&self.model);
|
||||
|
||||
source_order::walk_expr(self, expr);
|
||||
}
|
||||
|
||||
fn visit_parameter(&mut self, parameter: &Parameter) {
|
||||
let _ty = parameter.ty(&self.model);
|
||||
let _ty = parameter.inferred_type(&self.model);
|
||||
|
||||
source_order::walk_parameter(self, parameter);
|
||||
}
|
||||
|
||||
fn visit_parameter_with_default(&mut self, parameter_with_default: &ParameterWithDefault) {
|
||||
let _ty = parameter_with_default.ty(&self.model);
|
||||
let _ty = parameter_with_default.inferred_type(&self.model);
|
||||
|
||||
source_order::walk_parameter_with_default(self, parameter_with_default);
|
||||
}
|
||||
|
||||
fn visit_alias(&mut self, alias: &Alias) {
|
||||
let _ty = alias.ty(&self.model);
|
||||
let _ty = alias.inferred_type(&self.model);
|
||||
|
||||
source_order::walk_alias(self, alias);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,13 @@ class MDTestRunner:
|
||||
return False
|
||||
|
||||
# Run it again with 'json' format to find the mdtest executable:
|
||||
json_output = self._run_cargo_test(message_format="json")
|
||||
try:
|
||||
json_output = self._run_cargo_test(message_format="json")
|
||||
except subprocess.CalledProcessError as _:
|
||||
# `cargo test` can still fail if something changed in between the two runs.
|
||||
# Here we don't have a human-readable output, so just show a generic message:
|
||||
self.console.print("[red]Error[/red]: Failed to compile tests")
|
||||
return False
|
||||
|
||||
if json_output:
|
||||
self._get_executable_path_from_json(json_output)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Deferred annotations
|
||||
|
||||
## Deferred annotations in stubs always resolve
|
||||
|
||||
```pyi path=mod.pyi
|
||||
def get_foo() -> Foo: ...
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import get_foo
|
||||
|
||||
reveal_type(get_foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## Deferred annotations in regular code fail
|
||||
|
||||
In (regular) source files, annotations are *not* deferred. This also tests that imports from
|
||||
`__future__` that are not `annotations` are ignored.
|
||||
|
||||
```py
|
||||
from __future__ import with_statement as annotations
|
||||
|
||||
# error: [unresolved-reference]
|
||||
def get_foo() -> Foo: ...
|
||||
|
||||
class Foo: ...
|
||||
|
||||
reveal_type(get_foo()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Deferred annotations in regular code with `__future__.annotations`
|
||||
|
||||
If `__future__.annotations` is imported, annotations *are* deferred.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
def get_foo() -> Foo: ...
|
||||
|
||||
class Foo: ...
|
||||
|
||||
reveal_type(get_foo()) # revealed: Foo
|
||||
```
|
||||
@@ -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: Literal[0]
|
||||
reveal_type(b1) # revealed: Unknown | Literal[0]
|
||||
|
||||
# error: [invalid-type-form]
|
||||
invalid1: Literal[3 + 4]
|
||||
|
||||
@@ -102,7 +102,7 @@ reveal_type(C.pure_instance_variable) # revealed: str
|
||||
# and pyright allow this.
|
||||
C.pure_instance_variable = "overwritten on class"
|
||||
|
||||
# TODO: this should be an error (incompatible types in assignment)
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `pure_instance_variable` of type `str`"
|
||||
c_instance.pure_instance_variable = 1
|
||||
```
|
||||
|
||||
@@ -175,7 +175,7 @@ class C:
|
||||
|
||||
reveal_type(C.pure_class_variable1) # revealed: str
|
||||
|
||||
# TODO: this should be `Literal[1]`, or `Unknown | Literal[1]`.
|
||||
# TODO: Should be `Unknown | Literal[1]`.
|
||||
reveal_type(C.pure_class_variable2) # revealed: Unknown
|
||||
|
||||
c_instance = C()
|
||||
@@ -191,7 +191,7 @@ c_instance.pure_class_variable1 = "value set on instance"
|
||||
|
||||
C.pure_class_variable1 = "overwritten on class"
|
||||
|
||||
# TODO: should raise an error (incompatible types in assignment)
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `pure_class_variable1` of type `str`"
|
||||
C.pure_class_variable1 = 1
|
||||
|
||||
class Subclass(C):
|
||||
@@ -252,8 +252,7 @@ class C:
|
||||
|
||||
reveal_type(C.variable_with_class_default1) # revealed: str
|
||||
|
||||
# TODO: this should be `Unknown | Literal[1]`.
|
||||
reveal_type(C.variable_with_class_default2) # revealed: Literal[1]
|
||||
reveal_type(C.variable_with_class_default2) # revealed: Unknown | Literal[1]
|
||||
|
||||
c_instance = C()
|
||||
|
||||
@@ -296,8 +295,8 @@ def _(flag: bool):
|
||||
else:
|
||||
x = 4
|
||||
|
||||
reveal_type(C1.x) # revealed: Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Literal[3, 4]
|
||||
reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Unknown | Literal[3, 4]
|
||||
```
|
||||
|
||||
## Inherited class attributes
|
||||
@@ -311,7 +310,7 @@ class A:
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
|
||||
reveal_type(C.X) # revealed: Literal["foo"]
|
||||
reveal_type(C.X) # revealed: Unknown | Literal["foo"]
|
||||
```
|
||||
|
||||
### Multiple inheritance
|
||||
@@ -334,7 +333,7 @@ class A(B, C): ...
|
||||
reveal_type(A.__mro__)
|
||||
|
||||
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
|
||||
reveal_type(A.X) # revealed: Literal[42]
|
||||
reveal_type(A.X) # revealed: Unknown | Literal[42]
|
||||
```
|
||||
|
||||
## Unions with possibly unbound paths
|
||||
@@ -356,7 +355,7 @@ def _(flag1: bool, flag2: bool):
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 3]
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
|
||||
```
|
||||
|
||||
### Possibly-unbound within a class
|
||||
@@ -379,7 +378,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 2, 3]
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
### Unions with all paths unbound
|
||||
@@ -436,6 +435,64 @@ class Foo: ...
|
||||
reveal_type(Foo.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## Module attributes
|
||||
|
||||
```py path=mod.py
|
||||
global_symbol: str = "a"
|
||||
```
|
||||
|
||||
```py
|
||||
import mod
|
||||
|
||||
reveal_type(mod.global_symbol) # revealed: str
|
||||
mod.global_symbol = "b"
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `global_symbol` of type `str`"
|
||||
mod.global_symbol = 1
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `global_symbol` of type `str`"
|
||||
(_, mod.global_symbol) = (..., 1)
|
||||
|
||||
# TODO: this should be an error, but we do not understand list unpackings yet.
|
||||
[_, mod.global_symbol] = [1, 2]
|
||||
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# error: [invalid-assignment] "Object of type `int` is not assignable to attribute `global_symbol` of type `str`"
|
||||
for mod.global_symbol in IntIterable():
|
||||
pass
|
||||
```
|
||||
|
||||
## Nested attributes
|
||||
|
||||
```py path=outer/__init__.py
|
||||
```
|
||||
|
||||
```py path=outer/nested/__init__.py
|
||||
```
|
||||
|
||||
```py path=outer/nested/inner.py
|
||||
class Outer:
|
||||
class Nested:
|
||||
class Inner:
|
||||
attr: int = 1
|
||||
```
|
||||
|
||||
```py
|
||||
import outer.nested.inner
|
||||
|
||||
reveal_type(outer.nested.inner.Outer.Nested.Inner.attr) # revealed: int
|
||||
|
||||
# error: [invalid-assignment]
|
||||
outer.nested.inner.Outer.Nested.Inner.attr = "a"
|
||||
```
|
||||
|
||||
## Literal types
|
||||
|
||||
### Function-literal attributes
|
||||
|
||||
@@ -50,46 +50,44 @@ reveal_type(b | b) # revealed: Literal[False]
|
||||
## Arithmetic with a variable
|
||||
|
||||
```py
|
||||
a = True
|
||||
b = False
|
||||
def _(a: bool):
|
||||
def lhs_is_int(x: int):
|
||||
reveal_type(x + a) # revealed: int
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: float
|
||||
reveal_type(x % a) # revealed: int
|
||||
|
||||
def lhs_is_int(x: int):
|
||||
reveal_type(x + a) # revealed: int
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: float
|
||||
reveal_type(x % a) # revealed: int
|
||||
def rhs_is_int(x: int):
|
||||
reveal_type(a + x) # revealed: int
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def rhs_is_int(x: int):
|
||||
reveal_type(a + x) # revealed: int
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: float
|
||||
reveal_type(a % x) # revealed: int
|
||||
def lhs_is_bool(x: bool):
|
||||
reveal_type(x + a) # revealed: int
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: float
|
||||
reveal_type(x % a) # revealed: int
|
||||
|
||||
def lhs_is_bool(x: bool):
|
||||
reveal_type(x + a) # revealed: int
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: float
|
||||
reveal_type(x % a) # revealed: int
|
||||
def rhs_is_bool(x: bool):
|
||||
reveal_type(a + x) # revealed: int
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def rhs_is_bool(x: bool):
|
||||
reveal_type(a + x) # revealed: int
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def both_are_bool(x: bool, y: bool):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(x - y) # revealed: int
|
||||
reveal_type(x * y) # revealed: int
|
||||
reveal_type(x // y) # revealed: int
|
||||
reveal_type(x / y) # revealed: float
|
||||
reveal_type(x % y) # revealed: int
|
||||
def both_are_bool(x: bool, y: bool):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(x - y) # revealed: int
|
||||
reveal_type(x * y) # revealed: int
|
||||
reveal_type(x // y) # revealed: int
|
||||
reveal_type(x / y) # revealed: float
|
||||
reveal_type(x % y) # revealed: int
|
||||
```
|
||||
|
||||
@@ -262,7 +262,8 @@ class A:
|
||||
class B:
|
||||
__add__ = A()
|
||||
|
||||
reveal_type(B() + B()) # revealed: int
|
||||
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type
|
||||
reveal_type(B() + B()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Integration test: numbers from typeshed
|
||||
|
||||
@@ -5,6 +5,11 @@ that is, a use of a symbol from another scope. If a symbol has a declared type i
|
||||
(e.g. `int`), we use that as the symbol's "public type" (the type of the symbol from the perspective
|
||||
of other scopes) even if there is a more precise local inferred type for the symbol (`Literal[1]`).
|
||||
|
||||
If a symbol has no declared type, we use the union of `Unknown` with the inferred type as the public
|
||||
type. If there is no declaration, then the symbol can be reassigned to any type from another scope;
|
||||
the union with `Unknown` reflects that its type must at least be as large as the type of the
|
||||
assigned value, but could be arbitrarily larger.
|
||||
|
||||
We test the whole matrix of possible boundness and declaredness states. The current behavior is
|
||||
summarized in the following table, while the tests below demonstrate each case. Note that some of
|
||||
this behavior is questionable and might change in the future. See the TODOs in `symbol_by_id`
|
||||
@@ -12,11 +17,11 @@ this behavior is questionable and might change in the future. See the TODOs in `
|
||||
In particular, we should raise errors in the "possibly-undeclared-and-unbound" as well as the
|
||||
"undeclared-and-possibly-unbound" cases (marked with a "?").
|
||||
|
||||
| **Public type** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | ------------ | -------------------------- | ------------ |
|
||||
| bound | `T_declared` | `T_declared \| T_inferred` | `T_inferred` |
|
||||
| possibly-unbound | `T_declared` | `T_declared \| T_inferred` | `T_inferred` |
|
||||
| unbound | `T_declared` | `T_declared` | `Unknown` |
|
||||
| **Public type** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | ------------ | -------------------------- | ----------------------- |
|
||||
| bound | `T_declared` | `T_declared \| T_inferred` | `Unknown \| T_inferred` |
|
||||
| possibly-unbound | `T_declared` | `T_declared \| T_inferred` | `Unknown \| T_inferred` |
|
||||
| unbound | `T_declared` | `T_declared` | `Unknown` |
|
||||
|
||||
| **Diagnostic** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | -------- | ------------------------- | ------------------- |
|
||||
@@ -97,17 +102,24 @@ def flag() -> bool: ...
|
||||
|
||||
x = 1
|
||||
y = 2
|
||||
z = 3
|
||||
if flag():
|
||||
x: Any
|
||||
x: int
|
||||
y: Any
|
||||
# error: [invalid-declaration]
|
||||
y: str
|
||||
z: str
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x, y
|
||||
from mod import x, y, z
|
||||
|
||||
reveal_type(x) # revealed: Literal[1] | Any
|
||||
reveal_type(y) # revealed: Literal[2] | Unknown
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: Literal[2] | Any
|
||||
reveal_type(z) # revealed: Literal[3] | Unknown
|
||||
|
||||
# External modifications of `x` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
x = None
|
||||
```
|
||||
|
||||
### Possibly undeclared and possibly unbound
|
||||
@@ -134,6 +146,10 @@ from mod import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[1] | Any
|
||||
reveal_type(y) # revealed: Literal[2] | str
|
||||
|
||||
# External modifications of `y` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
y = None
|
||||
```
|
||||
|
||||
### Possibly undeclared and unbound
|
||||
@@ -154,14 +170,16 @@ if flag():
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# External modifications to `x` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
x = None
|
||||
```
|
||||
|
||||
## Undeclared
|
||||
|
||||
### Undeclared but bound
|
||||
|
||||
We use the inferred type as the public type, if a symbol has no declared type.
|
||||
|
||||
```py path=mod.py
|
||||
x = 1
|
||||
```
|
||||
@@ -169,7 +187,10 @@ x = 1
|
||||
```py
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
|
||||
# All external modifications of `x` are allowed:
|
||||
x = None
|
||||
```
|
||||
|
||||
### Undeclared and possibly unbound
|
||||
@@ -189,7 +210,10 @@ if flag:
|
||||
# on top of this document.
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
|
||||
# All external modifications of `x` are allowed:
|
||||
x = None
|
||||
```
|
||||
|
||||
### Undeclared and unbound
|
||||
@@ -206,4 +230,7 @@ if False:
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
# Modifications allowed in this case:
|
||||
x = None
|
||||
```
|
||||
|
||||
@@ -52,7 +52,7 @@ class NonCallable:
|
||||
__call__ = 1
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `NonCallable` is not callable"
|
||||
# error: "Object of type `Unknown | Literal[1]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Comprehensions
|
||||
|
||||
## Basic comprehensions
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# revealed: int
|
||||
[reveal_type(x) for x in IntIterable()]
|
||||
|
||||
class IteratorOfIterables:
|
||||
def __next__(self) -> IntIterable:
|
||||
return IntIterable()
|
||||
|
||||
class IterableOfIterables:
|
||||
def __iter__(self) -> IteratorOfIterables:
|
||||
return IteratorOfIterables()
|
||||
|
||||
# revealed: tuple[int, IntIterable]
|
||||
[reveal_type((x, y)) for y in IterableOfIterables() for x in y]
|
||||
|
||||
# revealed: int
|
||||
{reveal_type(x): 0 for x in IntIterable()}
|
||||
|
||||
# revealed: int
|
||||
{0: reveal_type(x) for x in IntIterable()}
|
||||
```
|
||||
|
||||
## Nested comprehension
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# TODO: This could be a `tuple[int, int]` if we model that `y` can not be modified in the outer comprehension scope
|
||||
# revealed: tuple[int, Unknown | int]
|
||||
[[reveal_type((x, y)) for x in IntIterable()] for y in IntIterable()]
|
||||
```
|
||||
|
||||
## Comprehension referencing outer comprehension
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
class IteratorOfIterables:
|
||||
def __next__(self) -> IntIterable:
|
||||
return IntIterable()
|
||||
|
||||
class IterableOfIterables:
|
||||
def __iter__(self) -> IteratorOfIterables:
|
||||
return IteratorOfIterables()
|
||||
|
||||
# TODO: This could be a `tuple[int, int]` (see above)
|
||||
# revealed: tuple[int, Unknown | IntIterable]
|
||||
[[reveal_type((x, y)) for x in y] for y in IterableOfIterables()]
|
||||
```
|
||||
|
||||
## Comprehension with unbound iterable
|
||||
|
||||
Iterating over an unbound iterable yields `Unknown`:
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference] "Name `x` used when not defined"
|
||||
# revealed: Unknown
|
||||
[reveal_type(z) for z in x]
|
||||
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# error: [not-iterable] "Object of type `int` is not iterable"
|
||||
# revealed: tuple[int, Unknown]
|
||||
[reveal_type((x, z)) for x in IntIterable() for z in x]
|
||||
```
|
||||
|
||||
## Starred expressions
|
||||
|
||||
Starred expressions must be iterable
|
||||
|
||||
```py
|
||||
class NotIterable: ...
|
||||
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator: ...
|
||||
|
||||
# This is fine:
|
||||
x = [*Iterable()]
|
||||
|
||||
# error: [not-iterable] "Object of type `NotIterable` is not iterable"
|
||||
y = [*NotIterable()]
|
||||
```
|
||||
|
||||
## Async comprehensions
|
||||
|
||||
### Basic
|
||||
|
||||
```py
|
||||
class AsyncIterator:
|
||||
async def __anext__(self) -> int:
|
||||
return 42
|
||||
|
||||
class AsyncIterable:
|
||||
def __aiter__(self) -> AsyncIterator:
|
||||
return AsyncIterator()
|
||||
|
||||
# revealed: @Todo(async iterables/iterators)
|
||||
[reveal_type(x) async for x in AsyncIterable()]
|
||||
```
|
||||
|
||||
### Invalid async comprehension
|
||||
|
||||
This tests that we understand that `async` comprehensions do *not* work according to the synchronous
|
||||
iteration protocol
|
||||
|
||||
```py
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
# revealed: @Todo(async iterables/iterators)
|
||||
[reveal_type(x) async for x in Iterable()]
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# Comprehensions with invalid syntax
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# Missing 'in' keyword.
|
||||
|
||||
# It's reasonably clear here what they *meant* to write,
|
||||
# so we'll still infer the correct type:
|
||||
|
||||
# error: [invalid-syntax] "Expected 'in', found name"
|
||||
# revealed: int
|
||||
[reveal_type(a) for a IntIterable()]
|
||||
|
||||
|
||||
# Missing iteration variable
|
||||
|
||||
# error: [invalid-syntax] "Expected an identifier, but found a keyword 'in' that cannot be used here"
|
||||
# error: [invalid-syntax] "Expected 'in', found name"
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unknown
|
||||
[reveal_type(b) for in IntIterable()]
|
||||
|
||||
|
||||
# Missing iterable
|
||||
|
||||
# error: [invalid-syntax] "Expected an expression"
|
||||
# revealed: Unknown
|
||||
[reveal_type(c) for c in]
|
||||
|
||||
|
||||
# Missing 'in' keyword and missing iterable
|
||||
|
||||
# error: [invalid-syntax] "Expected 'in', found ']'"
|
||||
# revealed: Unknown
|
||||
[reveal_type(d) for d]
|
||||
```
|
||||
@@ -5,7 +5,7 @@
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A:
|
||||
always_bound = 1
|
||||
always_bound: int = 1
|
||||
|
||||
if flag:
|
||||
union = 1
|
||||
@@ -13,14 +13,21 @@ def _(flag: bool):
|
||||
union = "abc"
|
||||
|
||||
if flag:
|
||||
possibly_unbound = "abc"
|
||||
union_declared: int = 1
|
||||
else:
|
||||
union_declared: str = "abc"
|
||||
|
||||
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||
if flag:
|
||||
possibly_unbound: str = "abc"
|
||||
|
||||
reveal_type(A.union) # revealed: Literal[1, "abc"]
|
||||
reveal_type(A.always_bound) # revealed: int
|
||||
|
||||
reveal_type(A.union) # revealed: Unknown | Literal[1, "abc"]
|
||||
|
||||
reveal_type(A.union_declared) # revealed: int | str
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||
reveal_type(A.possibly_unbound) # revealed: str
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||
reveal_type(A.non_existent) # revealed: Unknown
|
||||
|
||||
@@ -55,7 +55,7 @@ reveal_type("x" or "y" and "") # revealed: Literal["x"]
|
||||
## Evaluates to builtin
|
||||
|
||||
```py path=a.py
|
||||
redefined_builtin_bool = bool
|
||||
redefined_builtin_bool: type[bool] = bool
|
||||
|
||||
def my_bool(x) -> bool:
|
||||
return True
|
||||
|
||||
@@ -172,10 +172,10 @@ class IntUnion:
|
||||
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: ...
|
||||
|
||||
reveal_type(len(Auto())) # revealed: int
|
||||
reveal_type(len(Int())) # revealed: Literal[2]
|
||||
reveal_type(len(Int())) # revealed: int
|
||||
reveal_type(len(Str())) # revealed: int
|
||||
reveal_type(len(Tuple())) # revealed: int
|
||||
reveal_type(len(IntUnion())) # revealed: Literal[2, 32]
|
||||
reveal_type(len(IntUnion())) # revealed: int
|
||||
```
|
||||
|
||||
### Negative integers
|
||||
|
||||
@@ -20,7 +20,7 @@ wrong_innards: MyBox[int] = MyBox("five")
|
||||
# TODO reveal int, do not leak the typevar
|
||||
reveal_type(box.data) # revealed: T
|
||||
|
||||
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||
reveal_type(MyBox.box_model_number) # revealed: Unknown | Literal[695]
|
||||
```
|
||||
|
||||
## Subclassing
|
||||
|
||||
@@ -1,8 +1,70 @@
|
||||
# Importing builtin module
|
||||
# Builtins
|
||||
|
||||
## Importing builtin module
|
||||
|
||||
Builtin symbols can be explicitly imported:
|
||||
|
||||
```py
|
||||
import builtins
|
||||
|
||||
x = builtins.chr
|
||||
reveal_type(x) # revealed: Literal[chr]
|
||||
reveal_type(builtins.chr) # revealed: Literal[chr]
|
||||
```
|
||||
|
||||
## Implicit use of builtin
|
||||
|
||||
Or used implicitly:
|
||||
|
||||
```py
|
||||
reveal_type(chr) # revealed: Literal[chr]
|
||||
reveal_type(str) # revealed: Literal[str]
|
||||
```
|
||||
|
||||
## Builtin symbol from custom typeshed
|
||||
|
||||
If we specify a custom typeshed, we can use the builtin symbol from it, and no longer access the
|
||||
builtins from the "actual" vendored typeshed:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/builtins.pyi
|
||||
class Custom: ...
|
||||
|
||||
custom_builtin: Custom
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/typing_extensions.pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
```py
|
||||
reveal_type(custom_builtin) # revealed: Custom
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(str) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unknown builtin (later defined)
|
||||
|
||||
`foo` has a type of `Unknown` in this example, as it relies on `bar` which has not been defined at
|
||||
that point:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/builtins.pyi
|
||||
foo = bar
|
||||
bar = 1
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/typing_extensions.pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
```py
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -23,8 +23,8 @@ reveal_type(y)
|
||||
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound` is possibly unbound"
|
||||
from maybe_unbound import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(y) # revealed: Literal[3]
|
||||
reveal_type(x) # revealed: Unknown | Literal[3]
|
||||
reveal_type(y) # revealed: Unknown | Literal[3]
|
||||
```
|
||||
|
||||
## Maybe unbound annotated
|
||||
@@ -52,7 +52,7 @@ Importing an annotated name prefers the declared type over the inferred type:
|
||||
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound_annotated` is possibly unbound"
|
||||
from maybe_unbound_annotated import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(x) # revealed: Unknown | Literal[3]
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
b: int = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
@@ -20,11 +20,11 @@ b = 42
|
||||
```py
|
||||
from a import b
|
||||
|
||||
reveal_type(b) # revealed: Literal[42]
|
||||
reveal_type(b) # revealed: int
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
b: int = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
@@ -41,7 +41,7 @@ reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
b: int = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
@@ -60,13 +60,13 @@ sees the submodule as the value of `b` instead of the integer.
|
||||
from a import b
|
||||
import a.b
|
||||
|
||||
# Python would say `Literal[42]` for `b`
|
||||
# Python would say `int` for `b`
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
b: int = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
|
||||
@@ -20,12 +20,12 @@ from a import b.c
|
||||
|
||||
# TODO: Should these be inferred as Unknown?
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(b.c) # revealed: Literal[1]
|
||||
reveal_type(b.c) # revealed: int
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
c = 1
|
||||
c: int = 1
|
||||
```
|
||||
|
||||
@@ -17,13 +17,13 @@ reveal_type(X) # revealed: Unknown
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from .foo import X
|
||||
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
reveal_type(X) # revealed: int
|
||||
```
|
||||
|
||||
## Dotted
|
||||
@@ -32,25 +32,25 @@ reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
```py path=package/foo/bar/baz.py
|
||||
X = 42
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from .foo.bar.baz import X
|
||||
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
reveal_type(X) # revealed: int
|
||||
```
|
||||
|
||||
## Bare to package
|
||||
|
||||
```py path=package/__init__.py
|
||||
X = 42
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from . import X
|
||||
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
reveal_type(X) # revealed: int
|
||||
```
|
||||
|
||||
## Non-existent + bare to package
|
||||
@@ -66,11 +66,11 @@ reveal_type(X) # revealed: Unknown
|
||||
```py path=package/__init__.py
|
||||
from .foo import X
|
||||
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
reveal_type(X) # revealed: int
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
## Non-existent + dunder init
|
||||
@@ -87,13 +87,13 @@ reveal_type(X) # revealed: Unknown
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/subpackage/subsubpackage/bar.py
|
||||
from ...foo import X
|
||||
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
reveal_type(X) # revealed: int
|
||||
```
|
||||
|
||||
## Unbound symbol
|
||||
@@ -117,13 +117,13 @@ reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from . import foo
|
||||
|
||||
reveal_type(foo.X) # revealed: Literal[42]
|
||||
reveal_type(foo.X) # revealed: int
|
||||
```
|
||||
|
||||
## Non-existent + bare to module
|
||||
@@ -152,7 +152,7 @@ submodule via the attribute on its parent package.
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
|
||||
@@ -109,9 +109,9 @@ reveal_type(x)
|
||||
def _(flag: bool):
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
__iter__: int = 1
|
||||
else:
|
||||
__iter__ = None
|
||||
__iter__: None = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
@@ -135,7 +135,7 @@ for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
|
||||
class NotIterable:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
__iter__ = None
|
||||
__iter__: None = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Custom typeshed
|
||||
|
||||
The `environment.typeshed` configuration option can be used to specify a custom typeshed directory
|
||||
for Markdown-based tests. Custom typeshed stubs can then be placed in the specified directory using
|
||||
fenced code blocks with language `pyi`, and will be used instead of the vendored copy of typeshed.
|
||||
|
||||
A fenced code block with language `text` can be used to provide a `stdlib/VERSIONS` file in the
|
||||
custom typeshed root. If no such file is created explicitly, it will be automatically created with
|
||||
entries enabling all specified `<typeshed-root>/stdlib` files for all supported Python versions.
|
||||
|
||||
## Basic example (auto-generated `VERSIONS` file)
|
||||
|
||||
First, we specify `/typeshed` as the custom typeshed directory:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
We can then place custom stub files in `/typeshed/stdlib`, for example:
|
||||
|
||||
```pyi path=/typeshed/stdlib/builtins.pyi
|
||||
class BuiltinClass: ...
|
||||
|
||||
builtin_symbol: BuiltinClass
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/sys/__init__.pyi
|
||||
version = "my custom Python"
|
||||
```
|
||||
|
||||
And finally write a normal Python code block that makes use of the custom stubs:
|
||||
|
||||
```py
|
||||
b: BuiltinClass = builtin_symbol
|
||||
|
||||
class OtherClass: ...
|
||||
|
||||
o: OtherClass = builtin_symbol # error: [invalid-assignment]
|
||||
|
||||
# Make sure that 'sys' has a proper entry in the auto-generated 'VERSIONS' file
|
||||
import sys
|
||||
```
|
||||
|
||||
## Custom `VERSIONS` file
|
||||
|
||||
If we want to specify a custom `VERSIONS` file, we can do so by creating a fenced code block with
|
||||
language `text`. In the following test, we set the Python version to `3.10` and then make sure that
|
||||
we can *not* import `new_module` with a version requirement of `3.11-`:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/old_module.pyi
|
||||
class OldClass: ...
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/new_module.pyi
|
||||
class NewClass: ...
|
||||
```
|
||||
|
||||
```text path=/typeshed/stdlib/VERSIONS
|
||||
old_module: 3.0-
|
||||
new_module: 3.11-
|
||||
```
|
||||
|
||||
```py
|
||||
from old_module import OldClass
|
||||
|
||||
# error: [unresolved-import] "Cannot resolve import `new_module`"
|
||||
from new_module import NewClass
|
||||
```
|
||||
|
||||
## Using `reveal_type` with a custom typeshed
|
||||
|
||||
When providing a custom typeshed directory, basic things like `reveal_type` will stop working
|
||||
because we rely on being able to import it from `typing_extensions`. The actual definition of
|
||||
`reveal_type` in typeshed is slightly involved (depends on generics, `TypeVar`, etc.), but a very
|
||||
simple untyped definition is enough to make `reveal_type` work in tests:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/typing_extensions.pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
```py
|
||||
reveal_type(()) # revealed: tuple[()]
|
||||
```
|
||||
@@ -99,9 +99,9 @@ def _(x: str | int):
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
alias_for_type = type
|
||||
|
||||
def _(x: A | B):
|
||||
alias_for_type = type
|
||||
|
||||
if alias_for_type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
||||
@@ -10,10 +10,10 @@ def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
chr = 1
|
||||
chr: int = 1
|
||||
|
||||
def f():
|
||||
reveal_type(chr) # revealed: Literal[chr] | Literal[1]
|
||||
reveal_type(chr) # revealed: Literal[chr] | int
|
||||
```
|
||||
|
||||
## Conditionally global or builtin, with annotation
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Two levels up
|
||||
@@ -16,7 +16,7 @@ def f():
|
||||
x = 1
|
||||
def g():
|
||||
def h():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Skips class scope
|
||||
@@ -28,7 +28,7 @@ def f():
|
||||
class C:
|
||||
x = 2
|
||||
def g():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Skips annotation-only assignment
|
||||
@@ -41,5 +41,16 @@ def f():
|
||||
# name is otherwise not defined; maybe should be an error?
|
||||
x: int
|
||||
def h():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Implicit global in function
|
||||
|
||||
A name reference to a never-defined symbol in a function is implicitly a global lookup.
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
@@ -17,8 +17,8 @@ class C:
|
||||
x = 2
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[2]
|
||||
reveal_type(C.y) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Possibly unbound in class and global scope
|
||||
@@ -37,7 +37,7 @@ class C:
|
||||
# error: [possibly-unresolved-reference]
|
||||
y = x
|
||||
|
||||
reveal_type(C.y) # revealed: Literal[1, "abc"]
|
||||
reveal_type(C.y) # revealed: Unknown | Literal[1, "abc"]
|
||||
```
|
||||
|
||||
## Unbound function local
|
||||
|
||||
@@ -182,3 +182,34 @@ class C(A, B): ...
|
||||
# False negative: [incompatible-slots]
|
||||
class A(int, str): ...
|
||||
```
|
||||
|
||||
### Diagnostic if `__slots__` is externally modified
|
||||
|
||||
We special-case type inference for `__slots__` and return the pure inferred type, even if the symbol
|
||||
is not declared — a case in which we union with `Unknown` for other public symbols. The reason for
|
||||
this is that `__slots__` has a special handling in the runtime. Modifying it externally is actually
|
||||
allowed, but those changes do not take effect. If you have a class `C` with `__slots__ = ("foo",)`
|
||||
and externally set `C.__slots__ = ("bar",)`, you still can't access `C.bar`. And you can still
|
||||
access `C.foo`. We therefore issue a diagnostic for such assignments:
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ("a",)
|
||||
|
||||
# Modifying `__slots__` from within the class body is fine:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
# No `Unknown` here:
|
||||
reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]]
|
||||
|
||||
# But modifying it externally is not:
|
||||
|
||||
# error: [invalid-assignment]
|
||||
A.__slots__ = ("a",)
|
||||
|
||||
# error: [invalid-assignment]
|
||||
A.__slots__ = ("a", "b_new")
|
||||
|
||||
# error: [invalid-assignment]
|
||||
A.__slots__ = ("a", "b", "c")
|
||||
```
|
||||
|
||||
@@ -11,7 +11,7 @@ version:
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
SomeFeature = "available"
|
||||
SomeFeature: str = "available"
|
||||
```
|
||||
|
||||
If we can statically determine that the condition is always true, then we can also understand that
|
||||
@@ -21,7 +21,7 @@ If we can statically determine that the condition is always true, then we can al
|
||||
from module1 import SomeFeature
|
||||
|
||||
# SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
|
||||
reveal_type(SomeFeature) # revealed: Literal["available"]
|
||||
reveal_type(SomeFeature) # revealed: str
|
||||
```
|
||||
|
||||
Another scenario where this is useful is for `typing.TYPE_CHECKING` branches, which are often used
|
||||
|
||||
@@ -31,7 +31,7 @@ def _(n: int):
|
||||
## Slices
|
||||
|
||||
```py
|
||||
b = b"\x00abc\xff"
|
||||
b: bytes = b"\x00abc\xff"
|
||||
|
||||
reveal_type(b[0:2]) # revealed: Literal[b"\x00a"]
|
||||
reveal_type(b[-3:]) # revealed: Literal[b"bc\xff"]
|
||||
|
||||
@@ -14,7 +14,8 @@ a = NotSubscriptable()[0] # error: "Cannot subscript object of type `NotSubscri
|
||||
class NotSubscriptable:
|
||||
__getitem__ = None
|
||||
|
||||
a = NotSubscriptable()[0] # error: "Method `__getitem__` of type `None` is not callable on object of type `NotSubscriptable`"
|
||||
# error: "Method `__getitem__` of type `Unknown | None` is not callable on object of type `NotSubscriptable`"
|
||||
a = NotSubscriptable()[0]
|
||||
```
|
||||
|
||||
## Valid getitem
|
||||
|
||||
@@ -28,52 +28,52 @@ def _(n: int):
|
||||
## Slices
|
||||
|
||||
```py
|
||||
s = "abcde"
|
||||
|
||||
reveal_type(s[0:0]) # revealed: Literal[""]
|
||||
reveal_type(s[0:1]) # revealed: Literal["a"]
|
||||
reveal_type(s[0:2]) # revealed: Literal["ab"]
|
||||
reveal_type(s[0:5]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[0:6]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[1:3]) # revealed: Literal["bc"]
|
||||
|
||||
reveal_type(s[-3:5]) # revealed: Literal["cde"]
|
||||
reveal_type(s[-4:-2]) # revealed: Literal["bc"]
|
||||
reveal_type(s[-10:10]) # revealed: Literal["abcde"]
|
||||
|
||||
reveal_type(s[0:]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[2:]) # revealed: Literal["cde"]
|
||||
reveal_type(s[5:]) # revealed: Literal[""]
|
||||
reveal_type(s[:2]) # revealed: Literal["ab"]
|
||||
reveal_type(s[:0]) # revealed: Literal[""]
|
||||
reveal_type(s[:2]) # revealed: Literal["ab"]
|
||||
reveal_type(s[:10]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[:]) # revealed: Literal["abcde"]
|
||||
|
||||
reveal_type(s[::-1]) # revealed: Literal["edcba"]
|
||||
reveal_type(s[::2]) # revealed: Literal["ace"]
|
||||
reveal_type(s[-2:-5:-1]) # revealed: Literal["dcb"]
|
||||
reveal_type(s[::-2]) # revealed: Literal["eca"]
|
||||
reveal_type(s[-1::-3]) # revealed: Literal["eb"]
|
||||
|
||||
reveal_type(s[None:2:None]) # revealed: Literal["ab"]
|
||||
reveal_type(s[1:None:1]) # revealed: Literal["bcde"]
|
||||
reveal_type(s[None:None:None]) # revealed: Literal["abcde"]
|
||||
|
||||
start = 1
|
||||
stop = None
|
||||
step = 2
|
||||
reveal_type(s[start:stop:step]) # revealed: Literal["bd"]
|
||||
|
||||
reveal_type(s[False:True]) # revealed: Literal["a"]
|
||||
reveal_type(s[True:3]) # revealed: Literal["bc"]
|
||||
|
||||
s[0:4:0] # error: [zero-stepsize-in-slice]
|
||||
s[:4:0] # error: [zero-stepsize-in-slice]
|
||||
s[0::0] # error: [zero-stepsize-in-slice]
|
||||
s[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def _(m: int, n: int, s2: str):
|
||||
s = "abcde"
|
||||
|
||||
reveal_type(s[0:0]) # revealed: Literal[""]
|
||||
reveal_type(s[0:1]) # revealed: Literal["a"]
|
||||
reveal_type(s[0:2]) # revealed: Literal["ab"]
|
||||
reveal_type(s[0:5]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[0:6]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[1:3]) # revealed: Literal["bc"]
|
||||
|
||||
reveal_type(s[-3:5]) # revealed: Literal["cde"]
|
||||
reveal_type(s[-4:-2]) # revealed: Literal["bc"]
|
||||
reveal_type(s[-10:10]) # revealed: Literal["abcde"]
|
||||
|
||||
reveal_type(s[0:]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[2:]) # revealed: Literal["cde"]
|
||||
reveal_type(s[5:]) # revealed: Literal[""]
|
||||
reveal_type(s[:2]) # revealed: Literal["ab"]
|
||||
reveal_type(s[:0]) # revealed: Literal[""]
|
||||
reveal_type(s[:2]) # revealed: Literal["ab"]
|
||||
reveal_type(s[:10]) # revealed: Literal["abcde"]
|
||||
reveal_type(s[:]) # revealed: Literal["abcde"]
|
||||
|
||||
reveal_type(s[::-1]) # revealed: Literal["edcba"]
|
||||
reveal_type(s[::2]) # revealed: Literal["ace"]
|
||||
reveal_type(s[-2:-5:-1]) # revealed: Literal["dcb"]
|
||||
reveal_type(s[::-2]) # revealed: Literal["eca"]
|
||||
reveal_type(s[-1::-3]) # revealed: Literal["eb"]
|
||||
|
||||
reveal_type(s[None:2:None]) # revealed: Literal["ab"]
|
||||
reveal_type(s[1:None:1]) # revealed: Literal["bcde"]
|
||||
reveal_type(s[None:None:None]) # revealed: Literal["abcde"]
|
||||
|
||||
start = 1
|
||||
stop = None
|
||||
step = 2
|
||||
reveal_type(s[start:stop:step]) # revealed: Literal["bd"]
|
||||
|
||||
reveal_type(s[False:True]) # revealed: Literal["a"]
|
||||
reveal_type(s[True:3]) # revealed: Literal["bc"]
|
||||
|
||||
s[0:4:0] # error: [zero-stepsize-in-slice]
|
||||
s[:4:0] # error: [zero-stepsize-in-slice]
|
||||
s[0::0] # error: [zero-stepsize-in-slice]
|
||||
s[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
substring1 = s[m:n]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo(return type)
|
||||
|
||||
@@ -23,51 +23,51 @@ reveal_type(b) # revealed: Unknown
|
||||
## Slices
|
||||
|
||||
```py
|
||||
t = (1, "a", None, b"b")
|
||||
|
||||
reveal_type(t[0:0]) # revealed: tuple[()]
|
||||
reveal_type(t[0:1]) # revealed: tuple[Literal[1]]
|
||||
reveal_type(t[0:2]) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(t[0:4]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[0:5]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[1:3]) # revealed: tuple[Literal["a"], None]
|
||||
|
||||
reveal_type(t[-2:4]) # revealed: tuple[None, Literal[b"b"]]
|
||||
reveal_type(t[-3:-1]) # revealed: tuple[Literal["a"], None]
|
||||
reveal_type(t[-10:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
|
||||
reveal_type(t[0:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[2:]) # revealed: tuple[None, Literal[b"b"]]
|
||||
reveal_type(t[4:]) # revealed: tuple[()]
|
||||
reveal_type(t[:0]) # revealed: tuple[()]
|
||||
reveal_type(t[:2]) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(t[:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
|
||||
reveal_type(t[::-1]) # revealed: tuple[Literal[b"b"], None, Literal["a"], Literal[1]]
|
||||
reveal_type(t[::2]) # revealed: tuple[Literal[1], None]
|
||||
reveal_type(t[-2:-5:-1]) # revealed: tuple[None, Literal["a"], Literal[1]]
|
||||
reveal_type(t[::-2]) # revealed: tuple[Literal[b"b"], Literal["a"]]
|
||||
reveal_type(t[-1::-3]) # revealed: tuple[Literal[b"b"], Literal[1]]
|
||||
|
||||
reveal_type(t[None:2:None]) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(t[1:None:1]) # revealed: tuple[Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[None:None:None]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
|
||||
start = 1
|
||||
stop = None
|
||||
step = 2
|
||||
reveal_type(t[start:stop:step]) # revealed: tuple[Literal["a"], Literal[b"b"]]
|
||||
|
||||
reveal_type(t[False:True]) # revealed: tuple[Literal[1]]
|
||||
reveal_type(t[True:3]) # revealed: tuple[Literal["a"], None]
|
||||
|
||||
t[0:4:0] # error: [zero-stepsize-in-slice]
|
||||
t[:4:0] # error: [zero-stepsize-in-slice]
|
||||
t[0::0] # error: [zero-stepsize-in-slice]
|
||||
t[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def _(m: int, n: int):
|
||||
t = (1, "a", None, b"b")
|
||||
|
||||
reveal_type(t[0:0]) # revealed: tuple[()]
|
||||
reveal_type(t[0:1]) # revealed: tuple[Literal[1]]
|
||||
reveal_type(t[0:2]) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(t[0:4]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[0:5]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[1:3]) # revealed: tuple[Literal["a"], None]
|
||||
|
||||
reveal_type(t[-2:4]) # revealed: tuple[None, Literal[b"b"]]
|
||||
reveal_type(t[-3:-1]) # revealed: tuple[Literal["a"], None]
|
||||
reveal_type(t[-10:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
|
||||
reveal_type(t[0:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[2:]) # revealed: tuple[None, Literal[b"b"]]
|
||||
reveal_type(t[4:]) # revealed: tuple[()]
|
||||
reveal_type(t[:0]) # revealed: tuple[()]
|
||||
reveal_type(t[:2]) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(t[:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
|
||||
reveal_type(t[::-1]) # revealed: tuple[Literal[b"b"], None, Literal["a"], Literal[1]]
|
||||
reveal_type(t[::2]) # revealed: tuple[Literal[1], None]
|
||||
reveal_type(t[-2:-5:-1]) # revealed: tuple[None, Literal["a"], Literal[1]]
|
||||
reveal_type(t[::-2]) # revealed: tuple[Literal[b"b"], Literal["a"]]
|
||||
reveal_type(t[-1::-3]) # revealed: tuple[Literal[b"b"], Literal[1]]
|
||||
|
||||
reveal_type(t[None:2:None]) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(t[1:None:1]) # revealed: tuple[Literal["a"], None, Literal[b"b"]]
|
||||
reveal_type(t[None:None:None]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
|
||||
|
||||
start = 1
|
||||
stop = None
|
||||
step = 2
|
||||
reveal_type(t[start:stop:step]) # revealed: tuple[Literal["a"], Literal[b"b"]]
|
||||
|
||||
reveal_type(t[False:True]) # revealed: tuple[Literal[1]]
|
||||
reveal_type(t[True:3]) # revealed: tuple[Literal["a"], None]
|
||||
|
||||
t[0:4:0] # error: [zero-stepsize-in-slice]
|
||||
t[:4:0] # error: [zero-stepsize-in-slice]
|
||||
t[0::0] # error: [zero-stepsize-in-slice]
|
||||
t[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
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)
|
||||
|
||||
@@ -263,15 +263,12 @@ static_assert(not is_assignable_to(int, Not[int]))
|
||||
static_assert(not is_assignable_to(int, Not[Literal[1]]))
|
||||
|
||||
static_assert(not is_assignable_to(Intersection[Any, Parent], Unrelated))
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
|
||||
|
||||
# TODO: The following assertions should not fail (see https://github.com/astral-sh/ruff/issues/14899)
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Any, int], int))
|
||||
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]]))
|
||||
```
|
||||
|
||||
@@ -139,7 +139,9 @@ reveal_type(not AlwaysFalse())
|
||||
|
||||
# We don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
|
||||
class BoolIsBool:
|
||||
__bool__ = bool
|
||||
# TODO: The `type[bool]` declaration here is a workaround to avoid running into
|
||||
# https://github.com/astral-sh/ruff/issues/15672
|
||||
__bool__: type[bool] = bool
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not BoolIsBool())
|
||||
|
||||
@@ -76,11 +76,11 @@ with Manager():
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
__enter__ = 42
|
||||
__enter__: int = 42
|
||||
|
||||
def __exit__(self, exc_tpe, exc_value, traceback): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` of type `Literal[42]` is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` of type `int` is not callable"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
@@ -91,9 +91,9 @@ with Manager():
|
||||
class Manager:
|
||||
def __enter__(self) -> Self: ...
|
||||
|
||||
__exit__ = 32
|
||||
__exit__: int = 32
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `Literal[32]` is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `int` is not callable"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
@@ -156,11 +156,6 @@ pub(crate) mod tests {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_custom_typeshed(mut self, path: &str) -> Self {
|
||||
self.custom_typeshed = Some(SystemPathBuf::from(path));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_file(mut self, path: &'a str, content: &'a str) -> Self {
|
||||
self.files.push((path, content));
|
||||
self
|
||||
@@ -176,7 +171,7 @@ pub(crate) mod tests {
|
||||
.context("Failed to write test files")?;
|
||||
|
||||
let mut search_paths = SearchPathSettings::new(vec![src_root]);
|
||||
search_paths.typeshed = self.custom_typeshed;
|
||||
search_paths.custom_typeshed = self.custom_typeshed;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
|
||||
@@ -10,7 +10,7 @@ pub use module_resolver::{resolve_module, system_module_search_paths, KnownModul
|
||||
pub use program::{Program, ProgramSettings, SearchPathSettings, SitePackages};
|
||||
pub use python_platform::PythonPlatform;
|
||||
pub use python_version::PythonVersion;
|
||||
pub use semantic_model::{HasTy, SemanticModel};
|
||||
pub use semantic_model::{HasType, SemanticModel};
|
||||
|
||||
pub mod ast_node_ref;
|
||||
mod db;
|
||||
|
||||
@@ -31,6 +31,11 @@ pub struct LintMetadata {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(rename_all = "kebab-case")
|
||||
)]
|
||||
pub enum Level {
|
||||
/// The lint is disabled and should not run.
|
||||
Ignore,
|
||||
@@ -404,12 +409,12 @@ impl From<&'static LintMetadata> for LintEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct RuleSelection {
|
||||
/// Map with the severity for each enabled lint rule.
|
||||
///
|
||||
/// If a rule isn't present in this map, then it should be considered disabled.
|
||||
lints: FxHashMap<LintId, Severity>,
|
||||
lints: FxHashMap<LintId, (Severity, LintSource)>,
|
||||
}
|
||||
|
||||
impl RuleSelection {
|
||||
@@ -422,7 +427,7 @@ impl RuleSelection {
|
||||
.filter_map(|lint| {
|
||||
Severity::try_from(lint.default_level())
|
||||
.ok()
|
||||
.map(|severity| (*lint, severity))
|
||||
.map(|severity| (*lint, (severity, LintSource::Default)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -436,12 +441,14 @@ impl RuleSelection {
|
||||
|
||||
/// Returns an iterator over all enabled lints and their severity.
|
||||
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + '_ {
|
||||
self.lints.iter().map(|(&lint, &severity)| (lint, severity))
|
||||
self.lints
|
||||
.iter()
|
||||
.map(|(&lint, &(severity, _))| (lint, severity))
|
||||
}
|
||||
|
||||
/// Returns the configured severity for the lint with the given id or `None` if the lint is disabled.
|
||||
pub fn severity(&self, lint: LintId) -> Option<Severity> {
|
||||
self.lints.get(&lint).copied()
|
||||
self.lints.get(&lint).map(|(severity, _)| *severity)
|
||||
}
|
||||
|
||||
/// Returns `true` if the `lint` is enabled.
|
||||
@@ -452,19 +459,25 @@ impl RuleSelection {
|
||||
/// Enables `lint` and configures with the given `severity`.
|
||||
///
|
||||
/// Overrides any previous configuration for the lint.
|
||||
pub fn enable(&mut self, lint: LintId, severity: Severity) {
|
||||
self.lints.insert(lint, severity);
|
||||
pub fn enable(&mut self, lint: LintId, severity: Severity, source: LintSource) {
|
||||
self.lints.insert(lint, (severity, source));
|
||||
}
|
||||
|
||||
/// Disables `lint` if it was previously enabled.
|
||||
pub fn disable(&mut self, lint: LintId) {
|
||||
self.lints.remove(&lint);
|
||||
}
|
||||
|
||||
/// Merges the enabled lints from `other` into this selection.
|
||||
///
|
||||
/// Lints from `other` will override any existing configuration.
|
||||
pub fn merge(&mut self, other: &RuleSelection) {
|
||||
self.lints.extend(other.iter());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LintSource {
|
||||
/// The user didn't enable the rule explicitly, instead it's enabled by default.
|
||||
#[default]
|
||||
Default,
|
||||
|
||||
/// The rule was enabled by using a CLI argument
|
||||
Cli,
|
||||
|
||||
/// The rule was enabled in a configuration file.
|
||||
File,
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ impl SearchPaths {
|
||||
let SearchPathSettings {
|
||||
extra_paths,
|
||||
src_roots,
|
||||
typeshed,
|
||||
custom_typeshed: typeshed,
|
||||
site_packages: site_packages_paths,
|
||||
} = settings;
|
||||
|
||||
@@ -1308,7 +1308,7 @@ mod tests {
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![src.clone()],
|
||||
typeshed: Some(custom_typeshed),
|
||||
custom_typeshed: Some(custom_typeshed),
|
||||
site_packages: SitePackages::Known(vec![site_packages]),
|
||||
},
|
||||
},
|
||||
@@ -1814,7 +1814,7 @@ not_a_directory
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![SystemPathBuf::from("/src")],
|
||||
typeshed: None,
|
||||
custom_typeshed: None,
|
||||
site_packages: SitePackages::Known(vec![
|
||||
venv_site_packages,
|
||||
system_site_packages,
|
||||
|
||||
@@ -73,7 +73,7 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
///
|
||||
/// For tests checking that standard-library module resolution is working
|
||||
/// correctly, you should usually create a [`MockedTypeshed`] instance
|
||||
/// and pass it to the [`TestCaseBuilder::with_custom_typeshed`] method.
|
||||
/// and pass it to the [`TestCaseBuilder::with_mocked_typeshed`] method.
|
||||
/// If you need to check something that involves the vendored typeshed stubs
|
||||
/// we include as part of the binary, you can instead use the
|
||||
/// [`TestCaseBuilder::with_vendored_typeshed`] method.
|
||||
@@ -238,7 +238,7 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![src.clone()],
|
||||
typeshed: Some(typeshed.clone()),
|
||||
custom_typeshed: Some(typeshed.clone()),
|
||||
site_packages: SitePackages::Known(vec![site_packages.clone()]),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -108,7 +108,7 @@ pub struct SearchPathSettings {
|
||||
/// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types.
|
||||
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
|
||||
/// bundled as a zip file in the binary
|
||||
pub typeshed: Option<SystemPathBuf>,
|
||||
pub custom_typeshed: Option<SystemPathBuf>,
|
||||
|
||||
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
|
||||
pub site_packages: SitePackages,
|
||||
@@ -119,7 +119,7 @@ impl SearchPathSettings {
|
||||
Self {
|
||||
src_roots,
|
||||
extra_paths: vec![],
|
||||
typeshed: None,
|
||||
custom_typeshed: None,
|
||||
site_packages: SitePackages::Known(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,8 +603,8 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
|
||||
let definition = self.add_definition(symbol, parameter);
|
||||
|
||||
// Insert a mapping from the inner Parameter node to the same definition.
|
||||
// This ensures that calling `HasTy::ty` on the inner parameter returns
|
||||
// Insert a mapping from the inner Parameter node to the same definition. This
|
||||
// ensures that calling `HasType::inferred_type` on the inner parameter returns
|
||||
// a valid type (and doesn't panic)
|
||||
let existing_definition = self
|
||||
.definitions_by_node
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{resolve_module, Module};
|
||||
use crate::semantic_index::ast_ids::HasScopedExpressionId;
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::types::{binding_ty, infer_scope_types, Type};
|
||||
use crate::types::{binding_type, infer_scope_types, Type};
|
||||
use crate::Db;
|
||||
|
||||
pub struct SemanticModel<'db> {
|
||||
@@ -40,117 +40,117 @@ impl<'db> SemanticModel<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasTy {
|
||||
pub trait HasType {
|
||||
/// Returns the inferred type of `self`.
|
||||
///
|
||||
/// ## Panics
|
||||
/// May panic if `self` is from another file than `model`.
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>;
|
||||
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>;
|
||||
}
|
||||
|
||||
impl HasTy for ast::ExprRef<'_> {
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
impl HasType for ast::ExprRef<'_> {
|
||||
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
let index = semantic_index(model.db, model.file);
|
||||
let file_scope = index.expression_scope_id(*self);
|
||||
let scope = file_scope.to_scope_id(model.db, model.file);
|
||||
|
||||
let expression_id = self.scoped_expression_id(model.db, scope);
|
||||
infer_scope_types(model.db, scope).expression_ty(expression_id)
|
||||
infer_scope_types(model.db, scope).expression_type(expression_id)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_expression_has_ty {
|
||||
macro_rules! impl_expression_has_type {
|
||||
($ty: ty) => {
|
||||
impl HasTy for $ty {
|
||||
impl HasType for $ty {
|
||||
#[inline]
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
let expression_ref = ExprRef::from(self);
|
||||
expression_ref.ty(model)
|
||||
expression_ref.inferred_type(model)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_expression_has_ty!(ast::ExprBoolOp);
|
||||
impl_expression_has_ty!(ast::ExprNamed);
|
||||
impl_expression_has_ty!(ast::ExprBinOp);
|
||||
impl_expression_has_ty!(ast::ExprUnaryOp);
|
||||
impl_expression_has_ty!(ast::ExprLambda);
|
||||
impl_expression_has_ty!(ast::ExprIf);
|
||||
impl_expression_has_ty!(ast::ExprDict);
|
||||
impl_expression_has_ty!(ast::ExprSet);
|
||||
impl_expression_has_ty!(ast::ExprListComp);
|
||||
impl_expression_has_ty!(ast::ExprSetComp);
|
||||
impl_expression_has_ty!(ast::ExprDictComp);
|
||||
impl_expression_has_ty!(ast::ExprGenerator);
|
||||
impl_expression_has_ty!(ast::ExprAwait);
|
||||
impl_expression_has_ty!(ast::ExprYield);
|
||||
impl_expression_has_ty!(ast::ExprYieldFrom);
|
||||
impl_expression_has_ty!(ast::ExprCompare);
|
||||
impl_expression_has_ty!(ast::ExprCall);
|
||||
impl_expression_has_ty!(ast::ExprFString);
|
||||
impl_expression_has_ty!(ast::ExprStringLiteral);
|
||||
impl_expression_has_ty!(ast::ExprBytesLiteral);
|
||||
impl_expression_has_ty!(ast::ExprNumberLiteral);
|
||||
impl_expression_has_ty!(ast::ExprBooleanLiteral);
|
||||
impl_expression_has_ty!(ast::ExprNoneLiteral);
|
||||
impl_expression_has_ty!(ast::ExprEllipsisLiteral);
|
||||
impl_expression_has_ty!(ast::ExprAttribute);
|
||||
impl_expression_has_ty!(ast::ExprSubscript);
|
||||
impl_expression_has_ty!(ast::ExprStarred);
|
||||
impl_expression_has_ty!(ast::ExprName);
|
||||
impl_expression_has_ty!(ast::ExprList);
|
||||
impl_expression_has_ty!(ast::ExprTuple);
|
||||
impl_expression_has_ty!(ast::ExprSlice);
|
||||
impl_expression_has_ty!(ast::ExprIpyEscapeCommand);
|
||||
impl_expression_has_type!(ast::ExprBoolOp);
|
||||
impl_expression_has_type!(ast::ExprNamed);
|
||||
impl_expression_has_type!(ast::ExprBinOp);
|
||||
impl_expression_has_type!(ast::ExprUnaryOp);
|
||||
impl_expression_has_type!(ast::ExprLambda);
|
||||
impl_expression_has_type!(ast::ExprIf);
|
||||
impl_expression_has_type!(ast::ExprDict);
|
||||
impl_expression_has_type!(ast::ExprSet);
|
||||
impl_expression_has_type!(ast::ExprListComp);
|
||||
impl_expression_has_type!(ast::ExprSetComp);
|
||||
impl_expression_has_type!(ast::ExprDictComp);
|
||||
impl_expression_has_type!(ast::ExprGenerator);
|
||||
impl_expression_has_type!(ast::ExprAwait);
|
||||
impl_expression_has_type!(ast::ExprYield);
|
||||
impl_expression_has_type!(ast::ExprYieldFrom);
|
||||
impl_expression_has_type!(ast::ExprCompare);
|
||||
impl_expression_has_type!(ast::ExprCall);
|
||||
impl_expression_has_type!(ast::ExprFString);
|
||||
impl_expression_has_type!(ast::ExprStringLiteral);
|
||||
impl_expression_has_type!(ast::ExprBytesLiteral);
|
||||
impl_expression_has_type!(ast::ExprNumberLiteral);
|
||||
impl_expression_has_type!(ast::ExprBooleanLiteral);
|
||||
impl_expression_has_type!(ast::ExprNoneLiteral);
|
||||
impl_expression_has_type!(ast::ExprEllipsisLiteral);
|
||||
impl_expression_has_type!(ast::ExprAttribute);
|
||||
impl_expression_has_type!(ast::ExprSubscript);
|
||||
impl_expression_has_type!(ast::ExprStarred);
|
||||
impl_expression_has_type!(ast::ExprName);
|
||||
impl_expression_has_type!(ast::ExprList);
|
||||
impl_expression_has_type!(ast::ExprTuple);
|
||||
impl_expression_has_type!(ast::ExprSlice);
|
||||
impl_expression_has_type!(ast::ExprIpyEscapeCommand);
|
||||
|
||||
impl HasTy for ast::Expr {
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
impl HasType for ast::Expr {
|
||||
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
match self {
|
||||
Expr::BoolOp(inner) => inner.ty(model),
|
||||
Expr::Named(inner) => inner.ty(model),
|
||||
Expr::BinOp(inner) => inner.ty(model),
|
||||
Expr::UnaryOp(inner) => inner.ty(model),
|
||||
Expr::Lambda(inner) => inner.ty(model),
|
||||
Expr::If(inner) => inner.ty(model),
|
||||
Expr::Dict(inner) => inner.ty(model),
|
||||
Expr::Set(inner) => inner.ty(model),
|
||||
Expr::ListComp(inner) => inner.ty(model),
|
||||
Expr::SetComp(inner) => inner.ty(model),
|
||||
Expr::DictComp(inner) => inner.ty(model),
|
||||
Expr::Generator(inner) => inner.ty(model),
|
||||
Expr::Await(inner) => inner.ty(model),
|
||||
Expr::Yield(inner) => inner.ty(model),
|
||||
Expr::YieldFrom(inner) => inner.ty(model),
|
||||
Expr::Compare(inner) => inner.ty(model),
|
||||
Expr::Call(inner) => inner.ty(model),
|
||||
Expr::FString(inner) => inner.ty(model),
|
||||
Expr::StringLiteral(inner) => inner.ty(model),
|
||||
Expr::BytesLiteral(inner) => inner.ty(model),
|
||||
Expr::NumberLiteral(inner) => inner.ty(model),
|
||||
Expr::BooleanLiteral(inner) => inner.ty(model),
|
||||
Expr::NoneLiteral(inner) => inner.ty(model),
|
||||
Expr::EllipsisLiteral(inner) => inner.ty(model),
|
||||
Expr::Attribute(inner) => inner.ty(model),
|
||||
Expr::Subscript(inner) => inner.ty(model),
|
||||
Expr::Starred(inner) => inner.ty(model),
|
||||
Expr::Name(inner) => inner.ty(model),
|
||||
Expr::List(inner) => inner.ty(model),
|
||||
Expr::Tuple(inner) => inner.ty(model),
|
||||
Expr::Slice(inner) => inner.ty(model),
|
||||
Expr::IpyEscapeCommand(inner) => inner.ty(model),
|
||||
Expr::BoolOp(inner) => inner.inferred_type(model),
|
||||
Expr::Named(inner) => inner.inferred_type(model),
|
||||
Expr::BinOp(inner) => inner.inferred_type(model),
|
||||
Expr::UnaryOp(inner) => inner.inferred_type(model),
|
||||
Expr::Lambda(inner) => inner.inferred_type(model),
|
||||
Expr::If(inner) => inner.inferred_type(model),
|
||||
Expr::Dict(inner) => inner.inferred_type(model),
|
||||
Expr::Set(inner) => inner.inferred_type(model),
|
||||
Expr::ListComp(inner) => inner.inferred_type(model),
|
||||
Expr::SetComp(inner) => inner.inferred_type(model),
|
||||
Expr::DictComp(inner) => inner.inferred_type(model),
|
||||
Expr::Generator(inner) => inner.inferred_type(model),
|
||||
Expr::Await(inner) => inner.inferred_type(model),
|
||||
Expr::Yield(inner) => inner.inferred_type(model),
|
||||
Expr::YieldFrom(inner) => inner.inferred_type(model),
|
||||
Expr::Compare(inner) => inner.inferred_type(model),
|
||||
Expr::Call(inner) => inner.inferred_type(model),
|
||||
Expr::FString(inner) => inner.inferred_type(model),
|
||||
Expr::StringLiteral(inner) => inner.inferred_type(model),
|
||||
Expr::BytesLiteral(inner) => inner.inferred_type(model),
|
||||
Expr::NumberLiteral(inner) => inner.inferred_type(model),
|
||||
Expr::BooleanLiteral(inner) => inner.inferred_type(model),
|
||||
Expr::NoneLiteral(inner) => inner.inferred_type(model),
|
||||
Expr::EllipsisLiteral(inner) => inner.inferred_type(model),
|
||||
Expr::Attribute(inner) => inner.inferred_type(model),
|
||||
Expr::Subscript(inner) => inner.inferred_type(model),
|
||||
Expr::Starred(inner) => inner.inferred_type(model),
|
||||
Expr::Name(inner) => inner.inferred_type(model),
|
||||
Expr::List(inner) => inner.inferred_type(model),
|
||||
Expr::Tuple(inner) => inner.inferred_type(model),
|
||||
Expr::Slice(inner) => inner.inferred_type(model),
|
||||
Expr::IpyEscapeCommand(inner) => inner.inferred_type(model),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_binding_has_ty {
|
||||
($ty: ty) => {
|
||||
impl HasTy for $ty {
|
||||
impl HasType for $ty {
|
||||
#[inline]
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
let index = semantic_index(model.db, model.file);
|
||||
let binding = index.definition(self);
|
||||
binding_ty(model.db, binding)
|
||||
binding_type(model.db, binding)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -168,10 +168,10 @@ mod tests {
|
||||
use ruff_db::parsed::parsed_module;
|
||||
|
||||
use crate::db::tests::TestDbBuilder;
|
||||
use crate::{HasTy, SemanticModel};
|
||||
use crate::{HasType, SemanticModel};
|
||||
|
||||
#[test]
|
||||
fn function_ty() -> anyhow::Result<()> {
|
||||
fn function_type() -> anyhow::Result<()> {
|
||||
let db = TestDbBuilder::new()
|
||||
.with_file("/src/foo.py", "def test(): pass")
|
||||
.build()?;
|
||||
@@ -182,7 +182,7 @@ mod tests {
|
||||
|
||||
let function = ast.suite()[0].as_function_def_stmt().unwrap();
|
||||
let model = SemanticModel::new(&db, foo);
|
||||
let ty = function.ty(&model);
|
||||
let ty = function.inferred_type(&model);
|
||||
|
||||
assert!(ty.is_function_literal());
|
||||
|
||||
@@ -190,7 +190,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_ty() -> anyhow::Result<()> {
|
||||
fn class_type() -> anyhow::Result<()> {
|
||||
let db = TestDbBuilder::new()
|
||||
.with_file("/src/foo.py", "class Test: pass")
|
||||
.build()?;
|
||||
@@ -201,7 +201,7 @@ mod tests {
|
||||
|
||||
let class = ast.suite()[0].as_class_def_stmt().unwrap();
|
||||
let model = SemanticModel::new(&db, foo);
|
||||
let ty = class.ty(&model);
|
||||
let ty = class.inferred_type(&model);
|
||||
|
||||
assert!(ty.is_class_literal());
|
||||
|
||||
@@ -209,7 +209,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_ty() -> anyhow::Result<()> {
|
||||
fn alias_type() -> anyhow::Result<()> {
|
||||
let db = TestDbBuilder::new()
|
||||
.with_file("/src/foo.py", "class Test: pass")
|
||||
.with_file("/src/bar.py", "from foo import Test")
|
||||
@@ -222,7 +222,7 @@ mod tests {
|
||||
let import = ast.suite()[0].as_import_from_stmt().unwrap();
|
||||
let alias = &import.names[0];
|
||||
let model = SemanticModel::new(&db, bar);
|
||||
let ty = alias.ty(&model);
|
||||
let ty = alias.inferred_type(&model);
|
||||
|
||||
assert!(ty.is_class_literal());
|
||||
|
||||
|
||||
@@ -85,6 +85,14 @@ impl<'db> Symbol<'db> {
|
||||
Symbol::Unbound => self,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Symbol<'db> {
|
||||
match self {
|
||||
Symbol::Type(ty, boundness) => Symbol::Type(f(ty), boundness),
|
||||
Symbol::Unbound => Symbol::Unbound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -80,29 +80,54 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
|
||||
diagnostics
|
||||
}
|
||||
|
||||
/// Computes a possibly-widened type `Unknown | T_inferred` from the inferred type `T_inferred`
|
||||
/// of a symbol, unless the type is a known-instance type (e.g. `typing.Any`) or the symbol is
|
||||
/// considered non-modifiable (e.g. when the symbol is `@Final`). We need this for public uses
|
||||
/// of symbols that have no declared type.
|
||||
fn widen_type_for_undeclared_public_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
inferred: Symbol<'db>,
|
||||
is_considered_non_modifiable: bool,
|
||||
) -> Symbol<'db> {
|
||||
// We special-case known-instance types here since symbols like `typing.Any` are typically
|
||||
// not declared in the stubs (e.g. `Any = object()`), but we still want to treat them as
|
||||
// such.
|
||||
let is_known_instance = inferred
|
||||
.ignore_possibly_unbound()
|
||||
.is_some_and(|ty| matches!(ty, Type::KnownInstance(_)));
|
||||
|
||||
if is_considered_non_modifiable || is_known_instance {
|
||||
inferred
|
||||
} else {
|
||||
inferred.map_type(|ty| UnionType::from_elements(db, [Type::unknown(), ty]))
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer the public type of a symbol (its type as seen from outside its scope).
|
||||
fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
|
||||
#[salsa::tracked]
|
||||
fn symbol_by_id<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
symbol: ScopedSymbolId,
|
||||
is_dunder_slots: bool,
|
||||
symbol_id: ScopedSymbolId,
|
||||
) -> Symbol<'db> {
|
||||
let use_def = use_def_map(db, scope);
|
||||
|
||||
// If the symbol is declared, the public type is based on declarations; otherwise, it's based
|
||||
// on inference from bindings.
|
||||
|
||||
let declarations = use_def.public_declarations(symbol);
|
||||
let declared =
|
||||
symbol_from_declarations(db, declarations).map(|SymbolAndQualifiers(ty, _)| ty);
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
let declared = symbol_from_declarations(db, declarations);
|
||||
let is_final = declared.as_ref().is_ok_and(SymbolAndQualifiers::is_final);
|
||||
let declared = declared.map(|SymbolAndQualifiers(symbol, _)| symbol);
|
||||
|
||||
match declared {
|
||||
// Symbol is declared, trust the declared type
|
||||
Ok(symbol @ Symbol::Type(_, Boundness::Bound)) => symbol,
|
||||
// Symbol is possibly declared
|
||||
Ok(Symbol::Type(declared_ty, Boundness::PossiblyUnbound)) => {
|
||||
let bindings = use_def.public_bindings(symbol);
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
match inferred {
|
||||
@@ -120,16 +145,18 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
),
|
||||
}
|
||||
}
|
||||
// Symbol is undeclared, return the inferred type
|
||||
// Symbol is undeclared, return the union of `Unknown` with the inferred type
|
||||
Ok(Symbol::Unbound) => {
|
||||
let bindings = use_def.public_bindings(symbol);
|
||||
symbol_from_bindings(db, bindings)
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, is_dunder_slots || is_final)
|
||||
}
|
||||
// Symbol is possibly undeclared
|
||||
// Symbol has conflicting declared types
|
||||
Err((declared_ty, _)) => {
|
||||
// Intentionally ignore conflicting declared types; that's not our problem,
|
||||
// it's the problem of the module we are importing from.
|
||||
declared_ty.inner_ty().into()
|
||||
declared_ty.inner_type().into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,9 +204,15 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
}
|
||||
|
||||
let table = symbol_table(db, scope);
|
||||
// `__slots__` is a symbol with special behavior in Python's runtime. It can be
|
||||
// modified externally, but those changes do not take effect. We therefore issue
|
||||
// a diagnostic if we see it being modified externally. In type inference, we
|
||||
// can assign a "narrow" type to it even if it is not *declared*. This means, we
|
||||
// do not have to call [`widen_type_for_undeclared_public_symbol`].
|
||||
let is_dunder_slots = name == "__slots__";
|
||||
table
|
||||
.symbol_id_by_name(name)
|
||||
.map(|symbol| symbol_by_id(db, scope, symbol))
|
||||
.map(|symbol| symbol_by_id(db, scope, is_dunder_slots, symbol))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
@@ -243,15 +276,15 @@ pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Sym
|
||||
}
|
||||
|
||||
/// Infer the type of a binding.
|
||||
pub(crate) fn binding_ty<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> {
|
||||
pub(crate) fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> {
|
||||
let inference = infer_definition_types(db, definition);
|
||||
inference.binding_ty(definition)
|
||||
inference.binding_type(definition)
|
||||
}
|
||||
|
||||
/// Infer the type of a declaration.
|
||||
fn declaration_ty<'db>(db: &'db dyn Db, definition: Definition<'db>) -> TypeAndQualifiers<'db> {
|
||||
fn declaration_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> TypeAndQualifiers<'db> {
|
||||
let inference = infer_definition_types(db, definition);
|
||||
inference.declaration_ty(definition)
|
||||
inference.declaration_type(definition)
|
||||
}
|
||||
|
||||
/// Infer the type of a (possibly deferred) sub-expression of a [`Definition`].
|
||||
@@ -260,7 +293,7 @@ fn declaration_ty<'db>(db: &'db dyn Db, definition: Definition<'db>) -> TypeAndQ
|
||||
///
|
||||
/// ## Panics
|
||||
/// If the given expression is not a sub-expression of the given [`Definition`].
|
||||
fn definition_expression_ty<'db>(
|
||||
fn definition_expression_type<'db>(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
expression: &ast::Expr,
|
||||
@@ -273,14 +306,14 @@ fn definition_expression_ty<'db>(
|
||||
if scope == definition.scope(db) {
|
||||
// expression is in the definition scope
|
||||
let inference = infer_definition_types(db, definition);
|
||||
if let Some(ty) = inference.try_expression_ty(expr_id) {
|
||||
if let Some(ty) = inference.try_expression_type(expr_id) {
|
||||
ty
|
||||
} else {
|
||||
infer_deferred_types(db, definition).expression_ty(expr_id)
|
||||
infer_deferred_types(db, definition).expression_type(expr_id)
|
||||
}
|
||||
} else {
|
||||
// expression is in a type-params sub-scope
|
||||
infer_scope_types(db, scope).expression_ty(expr_id)
|
||||
infer_scope_types(db, scope).expression_type(expr_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +356,7 @@ fn symbol_from_bindings<'db>(
|
||||
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
|
||||
.peekable();
|
||||
|
||||
let binding_ty = binding_ty(db, binding);
|
||||
let binding_ty = binding_type(db, binding);
|
||||
if constraint_tys.peek().is_some() {
|
||||
let intersection_ty = constraint_tys
|
||||
.fold(
|
||||
@@ -378,6 +411,10 @@ impl SymbolAndQualifiers<'_> {
|
||||
fn is_class_var(&self) -> bool {
|
||||
self.1.contains(TypeQualifiers::CLASS_VAR)
|
||||
}
|
||||
|
||||
fn is_final(&self) -> bool {
|
||||
self.1.contains(TypeQualifiers::FINAL)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Symbol<'db>> for SymbolAndQualifiers<'db> {
|
||||
@@ -432,7 +469,7 @@ fn symbol_from_declarations<'db>(
|
||||
if static_visibility.is_always_false() {
|
||||
None
|
||||
} else {
|
||||
Some(declaration_ty(db, declaration))
|
||||
Some(declaration_type(db, declaration))
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -440,12 +477,12 @@ fn symbol_from_declarations<'db>(
|
||||
if let Some(first) = types.next() {
|
||||
let mut conflicting: Vec<Type<'db>> = vec![];
|
||||
let declared_ty = if let Some(second) = types.next() {
|
||||
let ty_first = first.inner_ty();
|
||||
let ty_first = first.inner_type();
|
||||
let mut qualifiers = first.qualifiers();
|
||||
|
||||
let mut builder = UnionBuilder::new(db).add(ty_first);
|
||||
for other in std::iter::once(second).chain(types) {
|
||||
let other_ty = other.inner_ty();
|
||||
let other_ty = other.inner_type();
|
||||
if !ty_first.is_equivalent_to(db, other_ty) {
|
||||
conflicting.push(other_ty);
|
||||
}
|
||||
@@ -466,13 +503,13 @@ fn symbol_from_declarations<'db>(
|
||||
};
|
||||
|
||||
Ok(SymbolAndQualifiers(
|
||||
Symbol::Type(declared_ty.inner_ty(), boundness),
|
||||
Symbol::Type(declared_ty.inner_type(), boundness),
|
||||
declared_ty.qualifiers(),
|
||||
))
|
||||
} else {
|
||||
Err((
|
||||
declared_ty,
|
||||
std::iter::once(first.inner_ty())
|
||||
std::iter::once(first.inner_type())
|
||||
.chain(conflicting)
|
||||
.collect(),
|
||||
))
|
||||
@@ -1008,7 +1045,7 @@ impl<'db> Type<'db> {
|
||||
///
|
||||
/// [assignable to]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
if self.is_equivalent_to(db, target) {
|
||||
if self.is_gradual_equivalent_to(db, target) {
|
||||
return true;
|
||||
}
|
||||
match (self, target) {
|
||||
@@ -1787,7 +1824,7 @@ impl<'db> Type<'db> {
|
||||
|
||||
if let Some(Type::BooleanLiteral(bool_val)) = bool_method
|
||||
.call(db, &CallArguments::positional([*instance_ty]))
|
||||
.return_ty(db)
|
||||
.return_type(db)
|
||||
{
|
||||
bool_val.into()
|
||||
} else {
|
||||
@@ -1864,7 +1901,7 @@ impl<'db> Type<'db> {
|
||||
CallDunderResult::MethodNotAvailable => return None,
|
||||
|
||||
CallDunderResult::CallOutcome(outcome) | CallDunderResult::PossiblyUnbound(outcome) => {
|
||||
outcome.return_ty(db)?
|
||||
outcome.return_type(db)?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1879,11 +1916,11 @@ impl<'db> Type<'db> {
|
||||
let mut binding = bind_call(db, arguments, function_type.signature(db), Some(self));
|
||||
match function_type.known(db) {
|
||||
Some(KnownFunction::RevealType) => {
|
||||
let revealed_ty = binding.one_parameter_ty().unwrap_or(Type::unknown());
|
||||
let revealed_ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
CallOutcome::revealed(binding, revealed_ty)
|
||||
}
|
||||
Some(KnownFunction::StaticAssert) => {
|
||||
if let Some((parameter_ty, message)) = binding.two_parameter_tys() {
|
||||
if let Some((parameter_ty, message)) = binding.two_parameter_types() {
|
||||
let truthiness = parameter_ty.bool(db);
|
||||
|
||||
if truthiness.is_always_true() {
|
||||
@@ -1914,64 +1951,64 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
Some(KnownFunction::IsEquivalentTo) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
.two_parameter_tys()
|
||||
.two_parameter_types()
|
||||
.unwrap_or((Type::unknown(), Type::unknown()));
|
||||
binding
|
||||
.set_return_ty(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b)));
|
||||
.set_return_type(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsSubtypeOf) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
.two_parameter_tys()
|
||||
.two_parameter_types()
|
||||
.unwrap_or((Type::unknown(), Type::unknown()));
|
||||
binding.set_return_ty(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b)));
|
||||
binding.set_return_type(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsAssignableTo) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
.two_parameter_tys()
|
||||
.two_parameter_types()
|
||||
.unwrap_or((Type::unknown(), Type::unknown()));
|
||||
binding
|
||||
.set_return_ty(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b)));
|
||||
.set_return_type(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsDisjointFrom) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
.two_parameter_tys()
|
||||
.two_parameter_types()
|
||||
.unwrap_or((Type::unknown(), Type::unknown()));
|
||||
binding
|
||||
.set_return_ty(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b)));
|
||||
.set_return_type(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsGradualEquivalentTo) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
.two_parameter_tys()
|
||||
.two_parameter_types()
|
||||
.unwrap_or((Type::unknown(), Type::unknown()));
|
||||
binding.set_return_ty(Type::BooleanLiteral(
|
||||
binding.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_gradual_equivalent_to(db, ty_b),
|
||||
));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsFullyStatic) => {
|
||||
let ty = binding.one_parameter_ty().unwrap_or(Type::unknown());
|
||||
binding.set_return_ty(Type::BooleanLiteral(ty.is_fully_static(db)));
|
||||
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
binding.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsSingleton) => {
|
||||
let ty = binding.one_parameter_ty().unwrap_or(Type::unknown());
|
||||
binding.set_return_ty(Type::BooleanLiteral(ty.is_singleton(db)));
|
||||
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
binding.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsSingleValued) => {
|
||||
let ty = binding.one_parameter_ty().unwrap_or(Type::unknown());
|
||||
binding.set_return_ty(Type::BooleanLiteral(ty.is_single_valued(db)));
|
||||
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
binding.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
|
||||
Some(KnownFunction::Len) => {
|
||||
if let Some(first_arg) = binding.one_parameter_ty() {
|
||||
if let Some(first_arg) = binding.one_parameter_type() {
|
||||
if let Some(len_ty) = first_arg.len(db) {
|
||||
binding.set_return_ty(len_ty);
|
||||
binding.set_return_type(len_ty);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1979,15 +2016,15 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
|
||||
Some(KnownFunction::Repr) => {
|
||||
if let Some(first_arg) = binding.one_parameter_ty() {
|
||||
binding.set_return_ty(first_arg.repr(db));
|
||||
if let Some(first_arg) = binding.one_parameter_type() {
|
||||
binding.set_return_type(first_arg.repr(db));
|
||||
};
|
||||
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
|
||||
Some(KnownFunction::AssertType) => {
|
||||
let Some((_, asserted_ty)) = binding.two_parameter_tys() else {
|
||||
let Some((_, asserted_ty)) = binding.two_parameter_types() else {
|
||||
return CallOutcome::callable(binding);
|
||||
};
|
||||
|
||||
@@ -1997,12 +2034,12 @@ impl<'db> Type<'db> {
|
||||
Some(KnownFunction::Cast) => {
|
||||
// TODO: Use `.two_parameter_tys()` exclusively
|
||||
// when overloads are supported.
|
||||
if binding.two_parameter_tys().is_none() {
|
||||
if binding.two_parameter_types().is_none() {
|
||||
return CallOutcome::callable(binding);
|
||||
};
|
||||
|
||||
if let Some(casted_ty) = arguments.first_argument() {
|
||||
binding.set_return_ty(casted_ty);
|
||||
binding.set_return_type(casted_ty);
|
||||
};
|
||||
|
||||
CallOutcome::callable(binding)
|
||||
@@ -2015,7 +2052,7 @@ impl<'db> Type<'db> {
|
||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
||||
// TODO check call vs signatures of `__new__` and/or `__init__`
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||
CallOutcome::callable(CallBinding::from_return_ty(match class.known(db) {
|
||||
CallOutcome::callable(CallBinding::from_return_type(match class.known(db) {
|
||||
// If the class is the builtin-bool class (for example `bool(1)`), we try to
|
||||
// return the specific truthiness value of the input arg, `Literal[True]` for
|
||||
// the example above.
|
||||
@@ -2061,7 +2098,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
|
||||
// Dynamic types are callable, and the return type is the same dynamic type
|
||||
Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_ty(self)),
|
||||
Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_type(self)),
|
||||
|
||||
Type::Union(union) => CallOutcome::union(
|
||||
self,
|
||||
@@ -2071,7 +2108,7 @@ impl<'db> Type<'db> {
|
||||
.map(|elem| elem.call(db, arguments)),
|
||||
),
|
||||
|
||||
Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_ty(
|
||||
Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_type(
|
||||
todo_type!("Type::Intersection.call()"),
|
||||
)),
|
||||
|
||||
@@ -2117,7 +2154,7 @@ impl<'db> Type<'db> {
|
||||
match dunder_iter_result {
|
||||
CallDunderResult::CallOutcome(ref call_outcome)
|
||||
| CallDunderResult::PossiblyUnbound(ref call_outcome) => {
|
||||
let Some(iterator_ty) = call_outcome.return_ty(db) else {
|
||||
let Some(iterator_ty) = call_outcome.return_type(db) else {
|
||||
return IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
};
|
||||
@@ -2125,7 +2162,7 @@ impl<'db> Type<'db> {
|
||||
|
||||
return if let Some(element_ty) = iterator_ty
|
||||
.call_dunder(db, "__next__", &CallArguments::positional([iterator_ty]))
|
||||
.return_ty(db)
|
||||
.return_type(db)
|
||||
{
|
||||
if matches!(dunder_iter_result, CallDunderResult::PossiblyUnbound(..)) {
|
||||
IterationOutcome::PossiblyUnboundDunderIter {
|
||||
@@ -2156,7 +2193,7 @@ impl<'db> Type<'db> {
|
||||
"__getitem__",
|
||||
&CallArguments::positional([self, KnownClass::Int.to_instance(db)]),
|
||||
)
|
||||
.return_ty(db)
|
||||
.return_type(db)
|
||||
{
|
||||
IterationOutcome::Iterable { element_ty }
|
||||
} else {
|
||||
@@ -2262,7 +2299,9 @@ impl<'db> Type<'db> {
|
||||
Type::Dynamic(_) => Ok(*self),
|
||||
// TODO map this to a new `Type::TypeVar` variant
|
||||
Type::KnownInstance(KnownInstanceType::TypeVar(_)) => Ok(*self),
|
||||
Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => Ok(alias.value_ty(db)),
|
||||
Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => {
|
||||
Ok(alias.value_type(db))
|
||||
}
|
||||
Type::KnownInstance(KnownInstanceType::Never | KnownInstanceType::NoReturn) => {
|
||||
Ok(Type::Never)
|
||||
}
|
||||
@@ -2358,7 +2397,8 @@ impl<'db> Type<'db> {
|
||||
ClassBase::Dynamic(_) => *self,
|
||||
ClassBase::Class(class) => SubclassOfType::from(
|
||||
db,
|
||||
ClassBase::try_from_ty(db, class.metaclass(db)).unwrap_or(ClassBase::unknown()),
|
||||
ClassBase::try_from_type(db, class.metaclass(db))
|
||||
.unwrap_or(ClassBase::unknown()),
|
||||
),
|
||||
},
|
||||
|
||||
@@ -2367,7 +2407,7 @@ impl<'db> Type<'db> {
|
||||
// TODO intersections
|
||||
Type::Intersection(_) => SubclassOfType::from(
|
||||
db,
|
||||
ClassBase::try_from_ty(db, todo_type!("Intersection meta-type"))
|
||||
ClassBase::try_from_type(db, todo_type!("Intersection meta-type"))
|
||||
.expect("Type::Todo should be a valid ClassBase"),
|
||||
),
|
||||
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
|
||||
@@ -2491,7 +2531,7 @@ impl<'db> TypeAndQualifiers<'db> {
|
||||
}
|
||||
|
||||
/// Forget about type qualifiers and only return the inner type.
|
||||
pub(crate) fn inner_ty(&self) -> Type<'db> {
|
||||
pub(crate) fn inner_type(&self) -> Type<'db> {
|
||||
self.inner
|
||||
}
|
||||
|
||||
@@ -3310,6 +3350,14 @@ impl<'db> IterationOutcome<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_without_diagnostic(self) -> Type<'db> {
|
||||
match self {
|
||||
Self::Iterable { element_ty } => element_ty,
|
||||
Self::NotIterable { .. } => Type::unknown(),
|
||||
Self::PossiblyUnboundDunderIter { element_ty, .. } => element_ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
@@ -3778,7 +3826,7 @@ impl<'db> Class<'db> {
|
||||
class_stmt
|
||||
.bases()
|
||||
.iter()
|
||||
.map(|base_node| definition_expression_ty(db, class_definition, base_node))
|
||||
.map(|base_node| definition_expression_type(db, class_definition, base_node))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -3807,7 +3855,7 @@ impl<'db> Class<'db> {
|
||||
.decorator_list
|
||||
.iter()
|
||||
.map(|decorator_node| {
|
||||
definition_expression_ty(db, class_definition, &decorator_node.expression)
|
||||
definition_expression_type(db, class_definition, &decorator_node.expression)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -3866,7 +3914,7 @@ impl<'db> Class<'db> {
|
||||
.find_keyword("metaclass")?
|
||||
.value;
|
||||
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
|
||||
let metaclass_ty = definition_expression_ty(db, class_definition, metaclass_node);
|
||||
let metaclass_ty = definition_expression_type(db, class_definition, metaclass_node);
|
||||
Some(metaclass_ty)
|
||||
}
|
||||
|
||||
@@ -3926,7 +3974,7 @@ impl<'db> Class<'db> {
|
||||
let return_ty = outcomes
|
||||
.iter()
|
||||
.fold(None, |acc, outcome| {
|
||||
let ty = outcome.return_ty(db);
|
||||
let ty = outcome.return_type(db);
|
||||
|
||||
match (acc, ty) {
|
||||
(acc, None) => {
|
||||
@@ -3957,7 +4005,7 @@ impl<'db> Class<'db> {
|
||||
CallOutcome::Callable { binding }
|
||||
| CallOutcome::RevealType { binding, .. }
|
||||
| CallOutcome::StaticAssertionError { binding, .. }
|
||||
| CallOutcome::AssertType { binding, .. } => Ok(binding.return_ty()),
|
||||
| CallOutcome::AssertType { binding, .. } => Ok(binding.return_type()),
|
||||
};
|
||||
|
||||
return return_ty_result.map(|ty| ty.to_meta_type(db));
|
||||
@@ -4065,7 +4113,7 @@ impl<'db> Class<'db> {
|
||||
/// this class, not on its superclasses.
|
||||
fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
||||
// TODO: There are many things that are not yet implemented here:
|
||||
// - `typing.ClassVar` and `typing.Final`
|
||||
// - `typing.Final`
|
||||
// - Proper diagnostics
|
||||
// - Handling of possibly-undeclared/possibly-unbound attributes
|
||||
// - The descriptor protocol
|
||||
@@ -4073,10 +4121,10 @@ impl<'db> Class<'db> {
|
||||
let body_scope = self.body_scope(db);
|
||||
let table = symbol_table(db, body_scope);
|
||||
|
||||
if let Some(symbol) = table.symbol_id_by_name(name) {
|
||||
if let Some(symbol_id) = table.symbol_id_by_name(name) {
|
||||
let use_def = use_def_map(db, body_scope);
|
||||
|
||||
let declarations = use_def.public_declarations(symbol);
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
|
||||
match symbol_from_declarations(db, declarations) {
|
||||
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, _), qualifiers)) => {
|
||||
@@ -4093,24 +4141,18 @@ impl<'db> Class<'db> {
|
||||
SymbolAndQualifiers(Symbol::Type(declared_ty, Boundness::Bound), qualifiers)
|
||||
}
|
||||
}
|
||||
Ok(SymbolAndQualifiers(Symbol::Unbound, qualifiers)) => {
|
||||
let bindings = use_def.public_bindings(symbol);
|
||||
Ok(symbol @ SymbolAndQualifiers(Symbol::Unbound, qualifiers)) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
match inferred {
|
||||
Symbol::Type(ty, _) => SymbolAndQualifiers(
|
||||
Symbol::Type(
|
||||
UnionType::from_elements(db, [Type::unknown(), ty]),
|
||||
Boundness::Bound,
|
||||
),
|
||||
qualifiers,
|
||||
),
|
||||
Symbol::Unbound => SymbolAndQualifiers(Symbol::Unbound, qualifiers),
|
||||
}
|
||||
SymbolAndQualifiers(
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, symbol.is_final()),
|
||||
qualifiers,
|
||||
)
|
||||
}
|
||||
Err((declared_ty, _conflicting_declarations)) => {
|
||||
// Ignore conflicting declarations
|
||||
SymbolAndQualifiers(declared_ty.inner_ty().into(), declared_ty.qualifiers())
|
||||
SymbolAndQualifiers(declared_ty.inner_type().into(), declared_ty.qualifiers())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -4176,13 +4218,13 @@ pub struct TypeAliasType<'db> {
|
||||
#[salsa::tracked]
|
||||
impl<'db> TypeAliasType<'db> {
|
||||
#[salsa::tracked]
|
||||
pub fn value_ty(self, db: &'db dyn Db) -> Type<'db> {
|
||||
pub fn value_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
let scope = self.rhs_scope(db);
|
||||
|
||||
let type_alias_stmt_node = scope.node(db).expect_type_alias();
|
||||
let definition = semantic_index(db, scope.file(db)).definition(type_alias_stmt_node);
|
||||
|
||||
definition_expression_ty(db, definition, &type_alias_stmt_node.value)
|
||||
definition_expression_type(db, definition, &type_alias_stmt_node.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4683,7 +4725,10 @@ pub(crate) mod tests {
|
||||
let bar = system_path_to_file(&db, "src/bar.py")?;
|
||||
let a = global_symbol(&db, bar, "a");
|
||||
|
||||
assert_eq!(a.expect_type(), KnownClass::Int.to_instance(&db));
|
||||
assert_eq!(
|
||||
a.expect_type(),
|
||||
UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)])
|
||||
);
|
||||
|
||||
// Add a docstring to foo to trigger a re-run.
|
||||
// The bar-call site of foo should not be re-run because of that
|
||||
@@ -4699,7 +4744,10 @@ pub(crate) mod tests {
|
||||
|
||||
let a = global_symbol(&db, bar, "a");
|
||||
|
||||
assert_eq!(a.expect_type(), KnownClass::Int.to_instance(&db));
|
||||
assert_eq!(
|
||||
a.expect_type(),
|
||||
UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)])
|
||||
);
|
||||
let events = db.take_salsa_events();
|
||||
|
||||
let call = &*parsed_module(&db, bar).syntax().body[1]
|
||||
|
||||
@@ -89,13 +89,13 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
|
||||
/// Get the return type of the call, or `None` if not callable.
|
||||
pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Self::Callable { binding } => Some(binding.return_ty()),
|
||||
Self::Callable { binding } => Some(binding.return_type()),
|
||||
Self::RevealType {
|
||||
binding,
|
||||
revealed_ty: _,
|
||||
} => Some(binding.return_ty()),
|
||||
} => Some(binding.return_type()),
|
||||
Self::NotCallable { not_callable_ty: _ } => None,
|
||||
Self::Union {
|
||||
outcomes,
|
||||
@@ -105,7 +105,7 @@ impl<'db> CallOutcome<'db> {
|
||||
// If all outcomes are NotCallable, we return None; if some outcomes are callable
|
||||
// and some are not, we return a union including Unknown.
|
||||
.fold(None, |acc, outcome| {
|
||||
let ty = outcome.return_ty(db);
|
||||
let ty = outcome.return_type(db);
|
||||
match (acc, ty) {
|
||||
(None, None) => None,
|
||||
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
|
||||
@@ -113,12 +113,12 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
})
|
||||
.map(UnionBuilder::build),
|
||||
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db),
|
||||
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_type(db),
|
||||
Self::StaticAssertionError { .. } => Some(Type::none(db)),
|
||||
Self::AssertType {
|
||||
binding,
|
||||
asserted_ty: _,
|
||||
} => Some(binding.return_ty()),
|
||||
} => Some(binding.return_type()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ impl<'db> CallOutcome<'db> {
|
||||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
) -> Type<'db> {
|
||||
match self.return_ty_result(context, node) {
|
||||
match self.return_type_result(context, node) {
|
||||
Ok(return_ty) => return_ty,
|
||||
Err(NotCallableError::Type {
|
||||
not_callable_ty,
|
||||
@@ -194,7 +194,7 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
|
||||
/// Get the return type of the call as a result.
|
||||
pub(super) fn return_ty_result(
|
||||
pub(super) fn return_type_result(
|
||||
&self,
|
||||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
@@ -205,7 +205,7 @@ impl<'db> CallOutcome<'db> {
|
||||
match self {
|
||||
Self::Callable { binding } => {
|
||||
binding.report_diagnostics(context, node);
|
||||
Ok(binding.return_ty())
|
||||
Ok(binding.return_type())
|
||||
}
|
||||
Self::RevealType {
|
||||
binding,
|
||||
@@ -218,7 +218,7 @@ impl<'db> CallOutcome<'db> {
|
||||
Severity::Info,
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
|
||||
);
|
||||
Ok(binding.return_ty())
|
||||
Ok(binding.return_type())
|
||||
}
|
||||
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
|
||||
not_callable_ty: *not_callable_ty,
|
||||
@@ -230,7 +230,7 @@ impl<'db> CallOutcome<'db> {
|
||||
} => Err(NotCallableError::PossiblyUnboundDunderCall {
|
||||
callable_ty: *called_ty,
|
||||
return_ty: call_outcome
|
||||
.return_ty(context.db())
|
||||
.return_type(context.db())
|
||||
.unwrap_or(Type::unknown()),
|
||||
}),
|
||||
Self::Union {
|
||||
@@ -251,7 +251,7 @@ impl<'db> CallOutcome<'db> {
|
||||
revealed_ty: _,
|
||||
} => {
|
||||
if revealed {
|
||||
binding.return_ty()
|
||||
binding.return_type()
|
||||
} else {
|
||||
revealed = true;
|
||||
outcome.unwrap_with_diagnostic(context, node)
|
||||
@@ -329,8 +329,8 @@ impl<'db> CallOutcome<'db> {
|
||||
binding,
|
||||
asserted_ty,
|
||||
} => {
|
||||
let [actual_ty, _asserted] = binding.parameter_tys() else {
|
||||
return Ok(binding.return_ty());
|
||||
let [actual_ty, _asserted] = binding.parameter_types() else {
|
||||
return Ok(binding.return_type());
|
||||
};
|
||||
|
||||
if !actual_ty.is_gradual_equivalent_to(context.db(), *asserted_ty) {
|
||||
@@ -345,7 +345,7 @@ impl<'db> CallOutcome<'db> {
|
||||
);
|
||||
}
|
||||
|
||||
Ok(binding.return_ty())
|
||||
Ok(binding.return_type())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,9 +358,9 @@ pub(super) enum CallDunderResult<'db> {
|
||||
}
|
||||
|
||||
impl<'db> CallDunderResult<'db> {
|
||||
pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Self::CallOutcome(outcome) => outcome.return_ty(db),
|
||||
Self::CallOutcome(outcome) => outcome.return_type(db),
|
||||
Self::PossiblyUnbound { .. } => None,
|
||||
Self::MethodNotAvailable => None,
|
||||
}
|
||||
@@ -394,7 +394,7 @@ pub(super) enum NotCallableError<'db> {
|
||||
|
||||
impl<'db> NotCallableError<'db> {
|
||||
/// The return type that should be used when a call is not callable.
|
||||
pub(super) fn return_ty(&self) -> Type<'db> {
|
||||
pub(super) fn return_type(&self) -> Type<'db> {
|
||||
match self {
|
||||
Self::Type { return_ty, .. } => *return_ty,
|
||||
Self::UnionElement { return_ty, .. } => *return_ty,
|
||||
@@ -407,7 +407,7 @@ impl<'db> NotCallableError<'db> {
|
||||
///
|
||||
/// For unions, returns the union type itself, which may contain a mix of callable and
|
||||
/// non-callable types.
|
||||
pub(super) fn called_ty(&self) -> Type<'db> {
|
||||
pub(super) fn called_type(&self) -> Type<'db> {
|
||||
match self {
|
||||
Self::Type {
|
||||
not_callable_ty, ..
|
||||
|
||||
@@ -73,7 +73,7 @@ pub(crate) fn bind_call<'db>(
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(expected_ty) = parameter.annotated_ty() {
|
||||
if let Some(expected_ty) = parameter.annotated_type() {
|
||||
if !argument_ty.is_assignable_to(db, expected_ty) {
|
||||
errors.push(CallBindingError::InvalidArgumentType {
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
@@ -109,7 +109,8 @@ pub(crate) fn bind_call<'db>(
|
||||
for (index, bound_ty) in parameter_tys.iter().enumerate() {
|
||||
if bound_ty.is_none() {
|
||||
let param = ¶meters[index];
|
||||
if param.is_variadic() || param.is_keyword_variadic() || param.default_ty().is_some() {
|
||||
if param.is_variadic() || param.is_keyword_variadic() || param.default_type().is_some()
|
||||
{
|
||||
// variadic/keywords and defaulted arguments are not required
|
||||
continue;
|
||||
}
|
||||
@@ -151,7 +152,7 @@ pub(crate) struct CallBinding<'db> {
|
||||
|
||||
impl<'db> CallBinding<'db> {
|
||||
// TODO remove this constructor and construct always from `bind_call`
|
||||
pub(crate) fn from_return_ty(return_ty: Type<'db>) -> Self {
|
||||
pub(crate) fn from_return_type(return_ty: Type<'db>) -> Self {
|
||||
Self {
|
||||
callable_ty: None,
|
||||
return_ty,
|
||||
@@ -160,27 +161,27 @@ impl<'db> CallBinding<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_return_ty(&mut self, return_ty: Type<'db>) {
|
||||
pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) {
|
||||
self.return_ty = return_ty;
|
||||
}
|
||||
|
||||
pub(crate) fn return_ty(&self) -> Type<'db> {
|
||||
pub(crate) fn return_type(&self) -> Type<'db> {
|
||||
self.return_ty
|
||||
}
|
||||
|
||||
pub(crate) fn parameter_tys(&self) -> &[Type<'db>] {
|
||||
pub(crate) fn parameter_types(&self) -> &[Type<'db>] {
|
||||
&self.parameter_tys
|
||||
}
|
||||
|
||||
pub(crate) fn one_parameter_ty(&self) -> Option<Type<'db>> {
|
||||
match self.parameter_tys() {
|
||||
pub(crate) fn one_parameter_type(&self) -> Option<Type<'db>> {
|
||||
match self.parameter_types() {
|
||||
[ty] => Some(*ty),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn two_parameter_tys(&self) -> Option<(Type<'db>, Type<'db>)> {
|
||||
match self.parameter_tys() {
|
||||
pub(crate) fn two_parameter_types(&self) -> Option<(Type<'db>, Type<'db>)> {
|
||||
match self.parameter_types() {
|
||||
[first, second] => Some((*first, *second)),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ impl<'db> ClassBase<'db> {
|
||||
/// Attempt to resolve `ty` into a `ClassBase`.
|
||||
///
|
||||
/// Return `None` if `ty` is not an acceptable type for a class base.
|
||||
pub(super) fn try_from_ty(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
|
||||
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(ClassLiteralType { class }) => Some(Self::Class(class)),
|
||||
@@ -112,40 +112,40 @@ impl<'db> ClassBase<'db> {
|
||||
KnownInstanceType::Any => Some(Self::any()),
|
||||
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
|
||||
KnownInstanceType::Dict => {
|
||||
Self::try_from_ty(db, KnownClass::Dict.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::Dict.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::List => {
|
||||
Self::try_from_ty(db, KnownClass::List.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::List.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Type => {
|
||||
Self::try_from_ty(db, KnownClass::Type.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::Type.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Tuple => {
|
||||
Self::try_from_ty(db, KnownClass::Tuple.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Set => {
|
||||
Self::try_from_ty(db, KnownClass::Set.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::Set.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::FrozenSet => {
|
||||
Self::try_from_ty(db, KnownClass::FrozenSet.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::FrozenSet.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::ChainMap => {
|
||||
Self::try_from_ty(db, KnownClass::ChainMap.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::ChainMap.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Counter => {
|
||||
Self::try_from_ty(db, KnownClass::Counter.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::Counter.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::DefaultDict => {
|
||||
Self::try_from_ty(db, KnownClass::DefaultDict.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::DefaultDict.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Deque => {
|
||||
Self::try_from_ty(db, KnownClass::Deque.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::Deque.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::OrderedDict => {
|
||||
Self::try_from_ty(db, KnownClass::OrderedDict.to_class_literal(db))
|
||||
Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Callable => {
|
||||
Self::try_from_ty(db, todo_type!("Support for Callable as a base class"))
|
||||
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use ruff_db::{
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use super::{binding_ty, KnownFunction, TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
use super::{binding_type, KnownFunction, TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
@@ -144,7 +144,7 @@ impl<'db> InferContext<'db> {
|
||||
.ancestor_scopes(scope_id)
|
||||
.filter_map(|(_, scope)| scope.node().as_function())
|
||||
.filter_map(|function| {
|
||||
binding_ty(self.db, index.definition(function)).into_function_literal()
|
||||
binding_type(self.db, index.definition(function)).into_function_literal()
|
||||
});
|
||||
|
||||
// Iterate over all functions and test if any is decorated with `@no_type_check`.
|
||||
|
||||
@@ -802,8 +802,8 @@ impl Diagnostic for TypeCheckDiagnostic {
|
||||
TypeCheckDiagnostic::message(self).into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
TypeCheckDiagnostic::file(self)
|
||||
fn file(&self) -> Option<File> {
|
||||
Some(TypeCheckDiagnostic::file(self))
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
@@ -984,13 +984,13 @@ pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeR
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn report_invalid_assignment(
|
||||
fn report_invalid_assignment_with_message(
|
||||
context: &InferContext,
|
||||
node: AnyNodeRef,
|
||||
declared_ty: Type,
|
||||
assigned_ty: Type,
|
||||
target_ty: Type,
|
||||
message: std::fmt::Arguments,
|
||||
) {
|
||||
match declared_ty {
|
||||
match target_ty {
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||
context.report_lint(&INVALID_ASSIGNMENT, node, format_args!(
|
||||
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
|
||||
@@ -1002,19 +1002,48 @@ pub(super) fn report_invalid_assignment(
|
||||
function.name(context.db())));
|
||||
}
|
||||
_ => {
|
||||
context.report_lint(
|
||||
&INVALID_ASSIGNMENT,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not assignable to `{}`",
|
||||
assigned_ty.display(context.db()),
|
||||
declared_ty.display(context.db()),
|
||||
),
|
||||
);
|
||||
context.report_lint(&INVALID_ASSIGNMENT, node, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn report_invalid_assignment(
|
||||
context: &InferContext,
|
||||
node: AnyNodeRef,
|
||||
target_ty: Type,
|
||||
source_ty: Type,
|
||||
) {
|
||||
report_invalid_assignment_with_message(
|
||||
context,
|
||||
node,
|
||||
target_ty,
|
||||
format_args!(
|
||||
"Object of type `{}` is not assignable to `{}`",
|
||||
source_ty.display(context.db()),
|
||||
target_ty.display(context.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn report_invalid_attribute_assignment(
|
||||
context: &InferContext,
|
||||
node: AnyNodeRef,
|
||||
target_ty: Type,
|
||||
source_ty: Type,
|
||||
attribute_name: &'_ str,
|
||||
) {
|
||||
report_invalid_assignment_with_message(
|
||||
context,
|
||||
node,
|
||||
target_ty,
|
||||
format_args!(
|
||||
"Object of type `{}` is not assignable to attribute `{attribute_name}` of type `{}`",
|
||||
source_ty.display(context.db()),
|
||||
target_ty.display(context.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn report_possibly_unresolved_reference(
|
||||
context: &InferContext,
|
||||
expr_name_node: &ast::ExprName,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,7 @@ impl<'db> Mro<'db> {
|
||||
// This *could* theoretically be handled by the final branch below,
|
||||
// but it's a common case (i.e., worth optimizing for),
|
||||
// and the `c3_merge` function requires lots of allocations.
|
||||
[single_base] => ClassBase::try_from_ty(db, *single_base).map_or_else(
|
||||
[single_base] => ClassBase::try_from_type(db, *single_base).map_or_else(
|
||||
|| Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
|
||||
|single_base| {
|
||||
Ok(std::iter::once(ClassBase::Class(class))
|
||||
@@ -95,7 +95,7 @@ impl<'db> Mro<'db> {
|
||||
let mut invalid_bases = vec![];
|
||||
|
||||
for (i, base) in multiple_bases.iter().enumerate() {
|
||||
match ClassBase::try_from_ty(db, *base) {
|
||||
match ClassBase::try_from_type(db, *base) {
|
||||
Some(valid_base) => valid_bases.push(valid_base),
|
||||
None => invalid_bases.push((i, *base)),
|
||||
}
|
||||
|
||||
@@ -322,9 +322,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
|
||||
for (op, (left, right)) in std::iter::zip(&**ops, comparator_tuples) {
|
||||
let lhs_ty = last_rhs_ty.unwrap_or_else(|| {
|
||||
inference.expression_ty(left.scoped_expression_id(self.db, scope))
|
||||
inference.expression_type(left.scoped_expression_id(self.db, scope))
|
||||
});
|
||||
let rhs_ty = inference.expression_ty(right.scoped_expression_id(self.db, scope));
|
||||
let rhs_ty = inference.expression_type(right.scoped_expression_id(self.db, scope));
|
||||
last_rhs_ty = Some(rhs_ty);
|
||||
|
||||
match left {
|
||||
@@ -393,7 +393,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
|
||||
let callable_ty =
|
||||
inference.expression_ty(callable.scoped_expression_id(self.db, scope));
|
||||
inference.expression_type(callable.scoped_expression_id(self.db, scope));
|
||||
|
||||
if callable_ty
|
||||
.into_class_literal()
|
||||
@@ -422,7 +422,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
|
||||
let callable_ty =
|
||||
inference.expression_ty(expr_call.func.scoped_expression_id(self.db, scope));
|
||||
inference.expression_type(expr_call.func.scoped_expression_id(self.db, scope));
|
||||
|
||||
// TODO: add support for PEP 604 union types on the right hand side of `isinstance`
|
||||
// and `issubclass`, for example `isinstance(x, str | (int | float))`.
|
||||
@@ -441,7 +441,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
|
||||
let class_info_ty =
|
||||
inference.expression_ty(class_info.scoped_expression_id(self.db, scope));
|
||||
inference.expression_type(class_info.scoped_expression_id(self.db, scope));
|
||||
|
||||
function
|
||||
.generate_constraint(self.db, class_info_ty)
|
||||
@@ -500,7 +500,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
let scope = self.scope();
|
||||
let inference = infer_expression_types(self.db, cls);
|
||||
let ty = inference
|
||||
.expression_ty(cls.node_ref(self.db).scoped_expression_id(self.db, scope))
|
||||
.expression_type(cls.node_ref(self.db).scoped_expression_id(self.db, scope))
|
||||
.to_instance(self.db);
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
constraints.insert(symbol, ty);
|
||||
@@ -524,7 +524,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
// filter our arms with statically known truthiness
|
||||
.filter(|expr| {
|
||||
inference
|
||||
.expression_ty(expr.scoped_expression_id(self.db, scope))
|
||||
.expression_type(expr.scoped_expression_id(self.db, scope))
|
||||
.bool(self.db)
|
||||
!= match expr_bool_op.op {
|
||||
BoolOp::And => Truthiness::AlwaysTrue,
|
||||
|
||||
@@ -461,6 +461,12 @@ mod stable {
|
||||
forall types s, t.
|
||||
s.is_fully_static(db) && s.is_gradual_equivalent_to(db, t) => s.is_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// `T` can be assigned to itself.
|
||||
type_property_test!(
|
||||
assignable_to_is_reflexive, db,
|
||||
forall types t. t.is_assignable_to(db, t)
|
||||
);
|
||||
}
|
||||
|
||||
/// This module contains property tests that currently lead to many false positives.
|
||||
@@ -475,13 +481,6 @@ mod flaky {
|
||||
|
||||
use super::{intersection, union};
|
||||
|
||||
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
|
||||
// `T` can be assigned to itself.
|
||||
type_property_test!(
|
||||
assignable_to_is_reflexive, db,
|
||||
forall types t. t.is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// Negating `T` twice is equivalent to `T`.
|
||||
type_property_test!(
|
||||
double_negation_is_identity, db,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{definition_expression_ty, Type};
|
||||
use super::{definition_expression_type, Type};
|
||||
use crate::Db;
|
||||
use crate::{semantic_index::definition::Definition, types::todo_type};
|
||||
use ruff_python_ast::{self as ast, name::Name};
|
||||
@@ -39,7 +39,7 @@ impl<'db> Signature<'db> {
|
||||
if function_node.is_async {
|
||||
todo_type!("generic types.CoroutineType")
|
||||
} else {
|
||||
definition_expression_ty(db, definition, returns.as_ref())
|
||||
definition_expression_type(db, definition, returns.as_ref())
|
||||
}
|
||||
});
|
||||
|
||||
@@ -97,7 +97,7 @@ impl<'db> Parameters<'db> {
|
||||
parameter_with_default
|
||||
.default
|
||||
.as_deref()
|
||||
.map(|default| definition_expression_ty(db, definition, default))
|
||||
.map(|default| definition_expression_type(db, definition, default))
|
||||
};
|
||||
let positional_only = posonlyargs.iter().map(|arg| {
|
||||
Parameter::from_node_and_kind(
|
||||
@@ -245,7 +245,7 @@ impl<'db> Parameter<'db> {
|
||||
annotated_ty: parameter
|
||||
.annotation
|
||||
.as_deref()
|
||||
.map(|annotation| definition_expression_ty(db, definition, annotation)),
|
||||
.map(|annotation| definition_expression_type(db, definition, annotation)),
|
||||
kind,
|
||||
}
|
||||
}
|
||||
@@ -276,7 +276,7 @@ impl<'db> Parameter<'db> {
|
||||
}
|
||||
|
||||
/// Annotated type of the parameter, if annotated.
|
||||
pub(crate) fn annotated_ty(&self) -> Option<Type<'db>> {
|
||||
pub(crate) fn annotated_type(&self) -> Option<Type<'db>> {
|
||||
self.annotated_ty
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ impl<'db> Parameter<'db> {
|
||||
}
|
||||
|
||||
/// Default-value type of the parameter, if any.
|
||||
pub(crate) fn default_ty(&self) -> Option<Type<'db>> {
|
||||
pub(crate) fn default_type(&self) -> Option<Type<'db>> {
|
||||
match self.kind {
|
||||
ParameterKind::PositionalOnly { default_ty } => default_ty,
|
||||
ParameterKind::PositionalOrKeyword { default_ty } => default_ty,
|
||||
@@ -543,7 +543,10 @@ mod tests {
|
||||
assert_eq!(a_name, "a");
|
||||
assert_eq!(b_name, "b");
|
||||
// TODO resolution should not be deferred; we should see A not B
|
||||
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B");
|
||||
assert_eq!(
|
||||
a_annotated_ty.unwrap().display(&db).to_string(),
|
||||
"Unknown | B"
|
||||
);
|
||||
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T");
|
||||
}
|
||||
|
||||
@@ -583,7 +586,10 @@ mod tests {
|
||||
assert_eq!(a_name, "a");
|
||||
assert_eq!(b_name, "b");
|
||||
// Parameter resolution deferred; we should see B
|
||||
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B");
|
||||
assert_eq!(
|
||||
a_annotated_ty.unwrap().display(&db).to_string(),
|
||||
"Unknown | B"
|
||||
);
|
||||
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T");
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ impl<'db> Unpacker<'db> {
|
||||
);
|
||||
|
||||
let mut value_ty = infer_expression_types(self.db(), value.expression())
|
||||
.expression_ty(value.scoped_expression_id(self.db(), self.scope));
|
||||
.expression_type(value.scoped_expression_id(self.db(), self.scope));
|
||||
|
||||
if value.is_assign()
|
||||
&& self.context.in_stub()
|
||||
|
||||
@@ -294,8 +294,8 @@ impl<'db> VisibilityConstraints<'db> {
|
||||
ConstraintNode::Expression(test_expr) => {
|
||||
let inference = infer_expression_types(db, test_expr);
|
||||
let scope = test_expr.scope(db);
|
||||
let ty =
|
||||
inference.expression_ty(test_expr.node_ref(db).scoped_expression_id(db, scope));
|
||||
let ty = inference
|
||||
.expression_type(test_expr.node_ref(db).scoped_expression_id(db, scope));
|
||||
|
||||
ty.bool(db).negate_if(!constraint.is_positive)
|
||||
}
|
||||
@@ -304,7 +304,7 @@ impl<'db> VisibilityConstraints<'db> {
|
||||
let subject_expression = inner.subject(db);
|
||||
let inference = infer_expression_types(db, *subject_expression);
|
||||
let scope = subject_expression.scope(db);
|
||||
let subject_ty = inference.expression_ty(
|
||||
let subject_ty = inference.expression_type(
|
||||
subject_expression
|
||||
.node_ref(db)
|
||||
.scoped_expression_id(db, scope),
|
||||
@@ -312,8 +312,8 @@ impl<'db> VisibilityConstraints<'db> {
|
||||
|
||||
let inference = infer_expression_types(db, *value);
|
||||
let scope = value.scope(db);
|
||||
let value_ty =
|
||||
inference.expression_ty(value.node_ref(db).scoped_expression_id(db, scope));
|
||||
let value_ty = inference
|
||||
.expression_type(value.node_ref(db).scoped_expression_id(db, scope));
|
||||
|
||||
if subject_ty.is_single_valued(db) {
|
||||
let truthiness =
|
||||
|
||||
@@ -75,9 +75,9 @@ fn to_lsp_diagnostic(
|
||||
diagnostic: &dyn ruff_db::diagnostic::Diagnostic,
|
||||
encoding: crate::PositionEncoding,
|
||||
) -> Diagnostic {
|
||||
let range = if let Some(range) = diagnostic.range() {
|
||||
let index = line_index(db.upcast(), diagnostic.file());
|
||||
let source = source_text(db.upcast(), diagnostic.file());
|
||||
let range = if let (Some(file), Some(range)) = (diagnostic.file(), diagnostic.range()) {
|
||||
let index = line_index(db.upcast(), file);
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
range.to_range(&source, &index, encoding)
|
||||
} else {
|
||||
|
||||
@@ -8,8 +8,8 @@ under a certain directory as test suites.
|
||||
|
||||
A Markdown test suite can contain any number of tests. A test consists of one or more embedded
|
||||
"files", each defined by a triple-backticks fenced code block. The code block must have a tag string
|
||||
specifying its language; currently only `py` (Python files) and `pyi` (type stub files) are
|
||||
supported.
|
||||
specifying its language. We currently support `py` (Python files) and `pyi` (type stub files), as
|
||||
well as [typeshed `VERSIONS`] files and `toml` for configuration.
|
||||
|
||||
The simplest possible test suite consists of just a single test, with a single embedded file:
|
||||
|
||||
@@ -243,6 +243,20 @@ section. Nested sections can override configurations from their parent sections.
|
||||
|
||||
See [`MarkdownTestConfig`](https://github.com/astral-sh/ruff/blob/main/crates/red_knot_test/src/config.rs) for the full list of supported configuration options.
|
||||
|
||||
### Specifying a custom typeshed
|
||||
|
||||
Some tests will need to override the default typeshed with custom files. The `[environment]`
|
||||
configuration option `typeshed` can be used to do this:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[environment]
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
````
|
||||
|
||||
For more details, take a look at the [custom-typeshed Markdown test].
|
||||
|
||||
## Documentation of tests
|
||||
|
||||
Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by
|
||||
@@ -294,36 +308,6 @@ The column assertion `6` on the ending line should be optional.
|
||||
In cases of overlapping such assertions, resolve ambiguity using more angle brackets: `<<<<` begins
|
||||
an assertion ended by `>>>>`, etc.
|
||||
|
||||
### Non-Python files
|
||||
|
||||
Some tests may need to specify non-Python embedded files: typeshed `stdlib/VERSIONS`, `pth` files,
|
||||
`py.typed` files, `pyvenv.cfg` files...
|
||||
|
||||
We will allow specifying any of these using the `text` language in the code block tag string:
|
||||
|
||||
````markdown
|
||||
```text path=/third-party/foo/py.typed
|
||||
partial
|
||||
```
|
||||
````
|
||||
|
||||
We may want to also support testing Jupyter notebooks as embedded files; exact syntax for this is
|
||||
yet to be determined.
|
||||
|
||||
Of course, red-knot is only run directly on `py` and `pyi` files, and assertion comments are only
|
||||
possible in these files.
|
||||
|
||||
A fenced code block with no language will always be an error.
|
||||
|
||||
### Running just a single test from a suite
|
||||
|
||||
Having each test in a suite always run as a distinct Rust test would require writing our own test
|
||||
runner or code-generating tests in a build script; neither of these is planned.
|
||||
|
||||
We could still allow running just a single test from a suite, for debugging purposes, either via
|
||||
some "focus" syntax that could be easily temporarily added to a test, or via an environment
|
||||
variable.
|
||||
|
||||
### Configuring search paths and kinds
|
||||
|
||||
The red-knot TOML configuration format hasn't been finalized, and we may want to implement
|
||||
@@ -346,38 +330,6 @@ Paths for `workspace-root` and `third-party-root` must be absolute.
|
||||
Relative embedded-file paths are relative to the workspace root, even if it is explicitly set to a
|
||||
non-default value using the `workspace-root` config.
|
||||
|
||||
### Specifying a custom typeshed
|
||||
|
||||
Some tests will need to override the default typeshed with custom files. The `[environment]`
|
||||
configuration option `typeshed-path` can be used to do this:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[environment]
|
||||
typeshed-path = "/typeshed"
|
||||
```
|
||||
|
||||
This file is importable as part of our custom typeshed, because it is within `/typeshed`, which we
|
||||
configured above as our custom typeshed root:
|
||||
|
||||
```py path=/typeshed/stdlib/builtins.pyi
|
||||
I_AM_THE_ONLY_BUILTIN = 1
|
||||
```
|
||||
|
||||
This file is written to `/src/test.py`, because the default workspace root is `/src/ and the default
|
||||
file path is `test.py`:
|
||||
|
||||
```py
|
||||
reveal_type(I_AM_THE_ONLY_BUILTIN) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
A fenced code block with language `text` can be used to provide a `stdlib/VERSIONS` file in the
|
||||
custom typeshed root. If no such file is created explicitly, one should be created implicitly
|
||||
including entries enabling all specified `<typeshed-root>/stdlib` files for all supported Python
|
||||
versions.
|
||||
|
||||
### I/O errors
|
||||
|
||||
We could use an `error=` configuration option in the tag string to make an embedded file cause an
|
||||
@@ -480,3 +432,6 @@ cold, to validate equivalence of cold and incremental check results.
|
||||
|
||||
[^extensions]: `typing-extensions` is a third-party module, but typeshed, and thus type checkers
|
||||
also, treat it as part of the standard library.
|
||||
|
||||
[custom-typeshed markdown test]: ../red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md
|
||||
[typeshed `versions`]: https://github.com/python/typeshed/blob/c546278aae47de0b2b664973da4edb613400f6ce/stdlib/VERSIONS#L1-L18%3E
|
||||
|
||||
@@ -34,6 +34,12 @@ impl MarkdownTestConfig {
|
||||
.as_ref()
|
||||
.and_then(|env| env.python_platform.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn typeshed(&self) -> Option<&str> {
|
||||
self.environment
|
||||
.as_ref()
|
||||
.and_then(|env| env.typeshed.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
@@ -44,6 +50,9 @@ pub(crate) struct Environment {
|
||||
|
||||
/// Target platform to assume when resolving types.
|
||||
pub(crate) python_platform: Option<PythonPlatform>,
|
||||
|
||||
/// Path to a custom typeshed directory.
|
||||
pub(crate) typeshed: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
|
||||
@@ -198,8 +198,8 @@ mod tests {
|
||||
"dummy".into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
fn file(&self) -> Option<File> {
|
||||
Some(self.file)
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
|
||||
@@ -3,7 +3,7 @@ use camino::Utf8Path;
|
||||
use colored::Colorize;
|
||||
use parser as test_parser;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use red_knot_python_semantic::Program;
|
||||
use red_knot_python_semantic::{Program, ProgramSettings, SearchPathSettings, SitePackages};
|
||||
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic};
|
||||
use ruff_db::files::{system_path_to_file, File, Files};
|
||||
use ruff_db::panic::catch_unwind;
|
||||
@@ -12,7 +12,7 @@ use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::TextSize;
|
||||
use salsa::Setter;
|
||||
use std::fmt::Write;
|
||||
|
||||
mod assertion;
|
||||
mod config;
|
||||
@@ -50,13 +50,6 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
|
||||
Log::Filter(filter) => setup_logging_with_filter(filter),
|
||||
});
|
||||
|
||||
Program::get(&db)
|
||||
.set_python_version(&mut db)
|
||||
.to(test.configuration().python_version().unwrap_or_default());
|
||||
Program::get(&db)
|
||||
.set_python_platform(&mut db)
|
||||
.to(test.configuration().python_platform().unwrap_or_default());
|
||||
|
||||
// Remove all files so that the db is in a "fresh" state.
|
||||
db.memory_file_system().remove_all();
|
||||
Files::sync_all(&mut db);
|
||||
@@ -98,6 +91,10 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
|
||||
|
||||
fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures> {
|
||||
let project_root = db.project_root().to_path_buf();
|
||||
let src_path = SystemPathBuf::from("/src");
|
||||
let custom_typeshed_path = test.configuration().typeshed().map(SystemPathBuf::from);
|
||||
let mut typeshed_files = vec![];
|
||||
let mut has_custom_versions_file = false;
|
||||
|
||||
let test_files: Vec<_> = test
|
||||
.files()
|
||||
@@ -107,11 +104,33 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
}
|
||||
|
||||
assert!(
|
||||
matches!(embedded.lang, "py" | "pyi"),
|
||||
"Non-Python files not supported yet."
|
||||
matches!(embedded.lang, "py" | "pyi" | "text"),
|
||||
"Supported file types are: py, pyi, text"
|
||||
);
|
||||
let full_path = project_root.join(embedded.path);
|
||||
|
||||
let full_path = if embedded.path.starts_with('/') {
|
||||
SystemPathBuf::from(embedded.path)
|
||||
} else {
|
||||
project_root.join(embedded.path)
|
||||
};
|
||||
|
||||
if let Some(ref typeshed_path) = custom_typeshed_path {
|
||||
if let Ok(relative_path) = full_path.strip_prefix(typeshed_path.join("stdlib")) {
|
||||
if relative_path.as_str() == "VERSIONS" {
|
||||
has_custom_versions_file = true;
|
||||
} else if relative_path.extension().is_some_and(|ext| ext == "pyi") {
|
||||
typeshed_files.push(relative_path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.write_file(&full_path, embedded.code).unwrap();
|
||||
|
||||
if !full_path.starts_with(&src_path) || embedded.lang == "text" {
|
||||
// These files need to be written to the file system (above), but we don't run any checks on them.
|
||||
return None;
|
||||
}
|
||||
|
||||
let file = system_path_to_file(db, full_path).unwrap();
|
||||
|
||||
Some(TestFile {
|
||||
@@ -121,6 +140,42 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create a custom typeshed `VERSIONS` file if none was provided.
|
||||
if let Some(ref typeshed_path) = custom_typeshed_path {
|
||||
if !has_custom_versions_file {
|
||||
let versions_file = typeshed_path.join("stdlib/VERSIONS");
|
||||
let contents = typeshed_files
|
||||
.iter()
|
||||
.fold(String::new(), |mut content, path| {
|
||||
// This is intentionally kept simple:
|
||||
let module_name = path
|
||||
.as_str()
|
||||
.trim_end_matches(".pyi")
|
||||
.trim_end_matches("/__init__")
|
||||
.replace('/', ".");
|
||||
let _ = writeln!(content, "{module_name}: 3.8-");
|
||||
content
|
||||
});
|
||||
db.write_file(&versions_file, contents).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Program::get(db)
|
||||
.update_from_settings(
|
||||
db,
|
||||
ProgramSettings {
|
||||
python_version: test.configuration().python_version().unwrap_or_default(),
|
||||
python_platform: test.configuration().python_platform().unwrap_or_default(),
|
||||
search_paths: SearchPathSettings {
|
||||
src_roots: vec![src_path],
|
||||
extra_paths: vec![],
|
||||
custom_typeshed: custom_typeshed_path,
|
||||
site_packages: SitePackages::Known(vec![]),
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("Failed to update Program settings in TestDb");
|
||||
|
||||
let failures: Failures = test_files
|
||||
.into_iter()
|
||||
.filter_map(|test_file| {
|
||||
|
||||
@@ -385,8 +385,8 @@ mod tests {
|
||||
self.message.into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
fn file(&self) -> Option<File> {
|
||||
Some(self.file)
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
|
||||
@@ -133,12 +133,17 @@ struct EmbeddedFileId;
|
||||
|
||||
/// A single file embedded in a [`Section`] as a fenced code block.
|
||||
///
|
||||
/// Currently must be a Python file (`py` language) or type stub (`pyi`). In the future we plan
|
||||
/// support other kinds of files as well (TOML configuration, typeshed VERSIONS, `pth` files...).
|
||||
/// Currently must be a Python file (`py` language), a type stub (`pyi`) or a [typeshed `VERSIONS`]
|
||||
/// file.
|
||||
///
|
||||
/// TOML configuration blocks are also supported, but are not stored as `EmbeddedFile`s. In the
|
||||
/// future we plan to support `pth` files as well.
|
||||
///
|
||||
/// A Python embedded file makes its containing [`Section`] into a [`MarkdownTest`], and will be
|
||||
/// type-checked and searched for inline-comment assertions to match against the diagnostics from
|
||||
/// type checking.
|
||||
///
|
||||
/// [typeshed `VERSIONS`]: https://github.com/python/typeshed/blob/c546278aae47de0b2b664973da4edb613400f6ce/stdlib/VERSIONS#L1-L18
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EmbeddedFile<'s> {
|
||||
section: SectionId,
|
||||
|
||||
@@ -4,6 +4,7 @@ use js_sys::Error;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use red_knot_project::metadata::value::RangedValue;
|
||||
use red_knot_project::ProjectMetadata;
|
||||
use red_knot_project::{Db, ProjectDatabase};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
@@ -48,7 +49,7 @@ impl Workspace {
|
||||
|
||||
workspace.apply_cli_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(settings.python_version.into()),
|
||||
python_version: Some(RangedValue::cli(settings.python_version.into())),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use red_knot_project::metadata::value::RangedValue;
|
||||
use red_knot_project::watch::{ChangeEvent, ChangedKind};
|
||||
use red_knot_project::{Db, ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
@@ -42,10 +43,10 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
|
||||
// We don't handle intersections in `is_assignable_to` yet
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:626:46 Object of type `@Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_datetime`; expected type `Match`",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:626:46 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_datetime`; expected type `Match`",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:632:58 Object of type `@Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_localtime`; expected type `Match`",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:639:52 Object of type `@Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_number`; expected type `Match`",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:632:58 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_localtime`; expected type `Match`",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:639:52 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_number`; expected type `Match`",
|
||||
"warning[lint:unused-ignore-comment] /src/tomllib/_parser.py:682:31 Unused blanket `type: ignore` directive",
|
||||
];
|
||||
|
||||
@@ -76,7 +77,7 @@ fn setup_case() -> Case {
|
||||
let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap();
|
||||
metadata.apply_cli_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(PythonVersion::PY312),
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
|
||||
@@ -73,6 +73,9 @@ pub enum DiagnosticId {
|
||||
|
||||
/// A revealed type: Created by `reveal_type(expression)`.
|
||||
RevealedType,
|
||||
|
||||
/// No rule with the given name exists.
|
||||
UnknownRule,
|
||||
}
|
||||
|
||||
impl DiagnosticId {
|
||||
@@ -112,15 +115,18 @@ impl DiagnosticId {
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> Result<&str, DiagnosticAsStrError> {
|
||||
match self {
|
||||
DiagnosticId::Io => Ok("io"),
|
||||
DiagnosticId::InvalidSyntax => Ok("invalid-syntax"),
|
||||
DiagnosticId::Lint(name) => Err(DiagnosticAsStrError::Category {
|
||||
category: "lint",
|
||||
name: name.as_str(),
|
||||
}),
|
||||
DiagnosticId::RevealedType => Ok("revealed-type"),
|
||||
}
|
||||
Ok(match self {
|
||||
DiagnosticId::Io => "io",
|
||||
DiagnosticId::InvalidSyntax => "invalid-syntax",
|
||||
DiagnosticId::Lint(name) => {
|
||||
return Err(DiagnosticAsStrError::Category {
|
||||
category: "lint",
|
||||
name: name.as_str(),
|
||||
})
|
||||
}
|
||||
DiagnosticId::RevealedType => "revealed-type",
|
||||
DiagnosticId::UnknownRule => "unknown-rule",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,8 +158,18 @@ pub trait Diagnostic: Send + Sync + std::fmt::Debug {
|
||||
|
||||
fn message(&self) -> Cow<str>;
|
||||
|
||||
fn file(&self) -> File;
|
||||
/// The file this diagnostic is associated with.
|
||||
///
|
||||
/// File can be `None` for diagnostics that don't originate from a file.
|
||||
/// For example:
|
||||
/// * A diagnostic indicating that a directory couldn't be read.
|
||||
/// * A diagnostic related to a CLI argument
|
||||
fn file(&self) -> Option<File>;
|
||||
|
||||
/// The primary range of the diagnostic in `file`.
|
||||
///
|
||||
/// The range can be `None` if the diagnostic doesn't have a file
|
||||
/// or it applies to the entire file (e.g. the file should be executable but isn't).
|
||||
fn range(&self) -> Option<TextRange>;
|
||||
|
||||
fn severity(&self) -> Severity;
|
||||
@@ -197,16 +213,15 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
Severity::Fatal => f.write_str("fatal")?,
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"[{rule}] {path}",
|
||||
rule = self.diagnostic.id(),
|
||||
path = self.diagnostic.file().path(self.db)
|
||||
)?;
|
||||
write!(f, "[{rule}]", rule = self.diagnostic.id())?;
|
||||
|
||||
if let Some(range) = self.diagnostic.range() {
|
||||
let index = line_index(self.db, self.diagnostic.file());
|
||||
let source = source_text(self.db, self.diagnostic.file());
|
||||
if let Some(file) = self.diagnostic.file() {
|
||||
write!(f, " {path}", path = file.path(self.db))?;
|
||||
}
|
||||
|
||||
if let (Some(file), Some(range)) = (self.diagnostic.file(), self.diagnostic.range()) {
|
||||
let index = line_index(self.db, file);
|
||||
let source = source_text(self.db, file);
|
||||
|
||||
let start = index.source_location(range.start(), &source);
|
||||
|
||||
@@ -229,7 +244,7 @@ where
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
fn file(&self) -> Option<File> {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
@@ -254,7 +269,7 @@ where
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
fn file(&self) -> Option<File> {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
@@ -276,7 +291,7 @@ impl Diagnostic for Box<dyn Diagnostic> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
fn file(&self) -> Option<File> {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
@@ -310,8 +325,8 @@ impl Diagnostic for ParseDiagnostic {
|
||||
self.error.error.to_string().into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
fn file(&self) -> Option<File> {
|
||||
Some(self.file)
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -26,6 +26,8 @@ def sla_callback(*arg, **kwargs):
|
||||
|
||||
DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback)
|
||||
|
||||
DAG(dag_id="class_sla_callback", fail_stop=True)
|
||||
|
||||
|
||||
@dag(schedule="0 * * * *")
|
||||
def decorator_schedule():
|
||||
|
||||
127
crates/ruff_linter/resources/test/fixtures/airflow/AIR302_context.py
vendored
Normal file
127
crates/ruff_linter/resources/test/fixtures/airflow/AIR302_context.py
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pendulum
|
||||
|
||||
from airflow.decorators import dag, task
|
||||
from airflow.models import DAG
|
||||
from airflow.models.baseoperator import BaseOperator
|
||||
from airflow.operators.dummy import DummyOperator
|
||||
from airflow.plugins_manager import AirflowPlugin
|
||||
from airflow.providers.standard.operators.python import PythonOperator
|
||||
from airflow.utils.context import get_current_context
|
||||
|
||||
|
||||
def access_invalid_key_in_context(**context):
|
||||
print("access invalid key", context["conf"])
|
||||
|
||||
@task
|
||||
def access_invalid_key_task_out_of_dag(**context):
|
||||
print("access invalid key", context.get("conf"))
|
||||
|
||||
@dag(
|
||||
schedule=None,
|
||||
start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
|
||||
catchup=False,
|
||||
tags=[""],
|
||||
)
|
||||
def invalid_dag():
|
||||
@task()
|
||||
def access_invalid_key_task(**context):
|
||||
print("access invalid key", context.get("conf"))
|
||||
|
||||
task1 = PythonOperator(
|
||||
task_id="task1",
|
||||
python_callable=access_invalid_key_in_context,
|
||||
)
|
||||
access_invalid_key_task() >> task1
|
||||
access_invalid_key_task_out_of_dag()
|
||||
|
||||
invalid_dag()
|
||||
|
||||
@task
|
||||
def print_config(**context):
|
||||
# This should not throw an error as logical_date is part of airflow context.
|
||||
logical_date = context["logical_date"]
|
||||
|
||||
# Removed usage - should trigger violations
|
||||
execution_date = context["execution_date"]
|
||||
next_ds = context["next_ds"]
|
||||
next_ds_nodash = context["next_ds_nodash"]
|
||||
next_execution_date = context["next_execution_date"]
|
||||
prev_ds = context["prev_ds"]
|
||||
prev_ds_nodash = context["prev_ds_nodash"]
|
||||
prev_execution_date = context["prev_execution_date"]
|
||||
prev_execution_date_success = context["prev_execution_date_success"]
|
||||
tomorrow_ds = context["tomorrow_ds"]
|
||||
yesterday_ds = context["yesterday_ds"]
|
||||
yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
||||
with DAG(
|
||||
dag_id="example_dag",
|
||||
schedule_interval="@daily",
|
||||
start_date=datetime(2023, 1, 1),
|
||||
template_searchpath=["/templates"],
|
||||
) as dag:
|
||||
task1 = DummyOperator(
|
||||
task_id="task1",
|
||||
params={
|
||||
# Removed variables in template
|
||||
"execution_date": "{{ execution_date }}",
|
||||
"next_ds": "{{ next_ds }}",
|
||||
"prev_ds": "{{ prev_ds }}"
|
||||
},
|
||||
)
|
||||
|
||||
class CustomMacrosPlugin(AirflowPlugin):
|
||||
name = "custom_macros"
|
||||
macros = {
|
||||
"execution_date_macro": lambda context: context["execution_date"],
|
||||
"next_ds_macro": lambda context: context["next_ds"]
|
||||
}
|
||||
|
||||
@task
|
||||
def print_config():
|
||||
context = get_current_context()
|
||||
execution_date = context["execution_date"]
|
||||
next_ds = context["next_ds"]
|
||||
next_ds_nodash = context["next_ds_nodash"]
|
||||
next_execution_date = context["next_execution_date"]
|
||||
prev_ds = context["prev_ds"]
|
||||
prev_ds_nodash = context["prev_ds_nodash"]
|
||||
prev_execution_date = context["prev_execution_date"]
|
||||
prev_execution_date_success = context["prev_execution_date_success"]
|
||||
tomorrow_ds = context["tomorrow_ds"]
|
||||
yesterday_ds = context["yesterday_ds"]
|
||||
yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
||||
class CustomOperator(BaseOperator):
|
||||
def execute(self, context):
|
||||
execution_date = context["execution_date"]
|
||||
next_ds = context["next_ds"]
|
||||
next_ds_nodash = context["next_ds_nodash"]
|
||||
next_execution_date = context["next_execution_date"]
|
||||
prev_ds = context["prev_ds"]
|
||||
prev_ds_nodash = context["prev_ds_nodash"]
|
||||
prev_execution_date = context["prev_execution_date"]
|
||||
prev_execution_date_success = context["prev_execution_date_success"]
|
||||
tomorrow_ds = context["tomorrow_ds"]
|
||||
yesterday_ds = context["yesterday_ds"]
|
||||
yesterday_ds_nodash = context["yesterday_ds_nodash"]
|
||||
|
||||
@task
|
||||
def access_invalid_argument_task_out_of_dag(execution_date, tomorrow_ds, logical_date, **context):
|
||||
print("execution date", execution_date)
|
||||
print("access invalid key", context.get("conf"))
|
||||
|
||||
@task(task_id="print_the_context")
|
||||
def print_context(ds=None, **kwargs):
|
||||
"""Print the Airflow context and ds variable from the context."""
|
||||
print(ds)
|
||||
print(kwargs.get("tomorrow_ds"))
|
||||
c = get_current_context()
|
||||
c.get("execution_date")
|
||||
|
||||
class CustomOperatorNew(BaseOperator):
|
||||
def execute(self, context):
|
||||
execution_date = context.get("execution_date")
|
||||
next_ds = context.get("next_ds")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user