Compare commits
126 Commits
refactor-A
...
micha/dont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcde25773d | ||
|
|
c39ca8fe6d | ||
|
|
2d82445794 | ||
|
|
398f2e8b0c | ||
|
|
232fbc1300 | ||
|
|
c82932e580 | ||
|
|
12f86f39a4 | ||
|
|
2b28d566a4 | ||
|
|
adca7bd95c | ||
|
|
6b98a26452 | ||
|
|
c87463842a | ||
|
|
c364b586f9 | ||
|
|
73d424ee5e | ||
|
|
6e9ff445fd | ||
|
|
f2c3ddc5ea | ||
|
|
b861551b6a | ||
|
|
443bf38565 | ||
|
|
23ad319b55 | ||
|
|
3d9433ca66 | ||
|
|
baf068361a | ||
|
|
b33cf5baba | ||
|
|
b0905c4b04 | ||
|
|
d0b2bbd55e | ||
|
|
8628f169e9 | ||
|
|
8bc11c49b2 | ||
|
|
bf5b0c2688 | ||
|
|
097aa04c04 | ||
|
|
f706c3fdf2 | ||
|
|
29f6653318 | ||
|
|
d645525afc | ||
|
|
6dcf7b35b9 | ||
|
|
943d4fc160 | ||
|
|
3ea4c63d2c | ||
|
|
8e8a07144d | ||
|
|
225dd0a027 | ||
|
|
52aeb8ae11 | ||
|
|
71b6ac81a6 | ||
|
|
75fc2c3116 | ||
|
|
9c4d124ba0 | ||
|
|
8c620b9b4b | ||
|
|
1eda27d1a5 | ||
|
|
aaa86cf38d | ||
|
|
7821206b7b | ||
|
|
b76d05e283 | ||
|
|
424b720c19 | ||
|
|
a95deec00f | ||
|
|
21aa12a073 | ||
|
|
5f5eb7c0dd | ||
|
|
b6562ed57e | ||
|
|
4fd82d5f35 | ||
|
|
beb8e2dfe0 | ||
|
|
88d07202c1 | ||
|
|
2ca31e4b43 | ||
|
|
450d4e0e0c | ||
|
|
7284d68157 | ||
|
|
3820af2f1b | ||
|
|
ee9a912f47 | ||
|
|
1447553bc2 | ||
|
|
9a27b37a91 | ||
|
|
487f2f5df0 | ||
|
|
339167d372 | ||
|
|
235fdfc57a | ||
|
|
03ff883626 | ||
|
|
fdca2b422e | ||
|
|
71ad9a2ab1 | ||
|
|
3b3c2c5aa4 | ||
|
|
b2a0d68d70 | ||
|
|
f2a86fcfda | ||
|
|
ac72aca27c | ||
|
|
a876090715 | ||
|
|
e4139568b8 | ||
|
|
5567e7c26b | ||
|
|
95294e657c | ||
|
|
0dc00e63f4 | ||
|
|
ce9c4968ae | ||
|
|
066239fe5b | ||
|
|
1e948f739c | ||
|
|
78e26cec02 | ||
|
|
e7248ee43e | ||
|
|
065274d353 | ||
|
|
75a24bbc67 | ||
|
|
5e9259c96c | ||
|
|
d45c1ee44f | ||
|
|
636288038f | ||
|
|
832c0fa04b | ||
|
|
f29c9e48a4 | ||
|
|
bafe8714a8 | ||
|
|
e5270e2ac2 | ||
|
|
643fd7fe07 | ||
|
|
bd02beec85 | ||
|
|
7f299fceef | ||
|
|
6e2800df85 | ||
|
|
391332a835 | ||
|
|
84e13cea14 | ||
|
|
bcb5f621c5 | ||
|
|
47c8f1ad65 | ||
|
|
a4f8b9311e | ||
|
|
6097fd9bbe | ||
|
|
0743838438 | ||
|
|
980ce941c7 | ||
|
|
b26448926a | ||
|
|
2ea63620cf | ||
|
|
00aa387d9d | ||
|
|
eb82089551 | ||
|
|
f144b9684d | ||
|
|
df6e5c0293 | ||
|
|
8f0e01787f | ||
|
|
6b907c1305 | ||
|
|
f319531632 | ||
|
|
e4d9fe036a | ||
|
|
baf0d660eb | ||
|
|
bde8ecddca | ||
|
|
842f882ef0 | ||
|
|
75015b0ed9 | ||
|
|
706d87f239 | ||
|
|
0837cdd931 | ||
|
|
0dbfa8d0e0 | ||
|
|
1218bc65ed | ||
|
|
6180f78da4 | ||
|
|
835b453bfd | ||
|
|
a3d873ef66 | ||
|
|
d464ef67cf | ||
|
|
2355472d61 | ||
|
|
3c3f35a548 | ||
|
|
2327082c43 | ||
|
|
7671a3bbc7 |
10
.github/renovate.json5
vendored
10
.github/renovate.json5
vendored
@@ -45,7 +45,7 @@
|
||||
groupName: "Artifact GitHub Actions dependencies",
|
||||
matchManagers: ["github-actions"],
|
||||
matchDatasources: ["gitea-tags", "github-tags"],
|
||||
matchPackagePatterns: ["actions/.*-artifact"],
|
||||
matchPackageNames: ["actions/.*-artifact"],
|
||||
description: "Weekly update of artifact-related GitHub Actions dependencies",
|
||||
},
|
||||
{
|
||||
@@ -61,7 +61,7 @@
|
||||
{
|
||||
// Disable updates of `zip-rs`; intentionally pinned for now due to ownership change
|
||||
// See: https://github.com/astral-sh/uv/issues/3642
|
||||
matchPackagePatterns: ["zip"],
|
||||
matchPackageNames: ["zip"],
|
||||
matchManagers: ["cargo"],
|
||||
enabled: false,
|
||||
},
|
||||
@@ -70,7 +70,7 @@
|
||||
// with `mkdocs-material-insider`.
|
||||
// See: https://squidfunk.github.io/mkdocs-material/insiders/upgrade/
|
||||
matchManagers: ["pip_requirements"],
|
||||
matchPackagePatterns: ["mkdocs-material"],
|
||||
matchPackageNames: ["mkdocs-material"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
@@ -87,13 +87,13 @@
|
||||
{
|
||||
groupName: "Monaco",
|
||||
matchManagers: ["npm"],
|
||||
matchPackagePatterns: ["monaco"],
|
||||
matchPackageNames: ["monaco"],
|
||||
description: "Weekly update of the Monaco editor",
|
||||
},
|
||||
{
|
||||
groupName: "strum",
|
||||
matchManagers: ["cargo"],
|
||||
matchPackagePatterns: ["strum"],
|
||||
matchPackageNames: ["strum"],
|
||||
description: "Weekly update of strum dependencies",
|
||||
},
|
||||
{
|
||||
|
||||
12
.github/workflows/build-docker.yml
vendored
12
.github/workflows/build-docker.yml
vendored
@@ -48,11 +48,13 @@ jobs:
|
||||
|
||||
- name: Check tag consistency
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
env:
|
||||
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}
|
||||
run: |
|
||||
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
|
||||
if [ "${{ fromJson(inputs.plan).announcement_tag }}" != "${version}" ]; then
|
||||
if [ "${TAG}" != "${version}" ]; then
|
||||
echo "The input tag does not match the version from pyproject.toml:" >&2
|
||||
echo "${{ fromJson(inputs.plan).announcement_tag }}" >&2
|
||||
echo "${TAG}" >&2
|
||||
echo "${version}" >&2
|
||||
exit 1
|
||||
else
|
||||
@@ -175,6 +177,8 @@ jobs:
|
||||
|
||||
- name: Generate Dynamic Dockerfile Tags
|
||||
shell: bash
|
||||
env:
|
||||
TAG_VALUE: ${{ fromJson(inputs.plan).announcement_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -195,8 +199,8 @@ jobs:
|
||||
# Loop through all base tags and append its docker metadata pattern to the list
|
||||
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
|
||||
IFS=','; for TAG in ${BASE_TAGS}; do
|
||||
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
|
||||
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
|
||||
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${TAG_VALUE}\n"
|
||||
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${TAG_VALUE}\n"
|
||||
TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n"
|
||||
done
|
||||
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -386,7 +386,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: ./scripts/add_rule.py --name DoTheThing --prefix PL --code C0999 --linter pylint
|
||||
- run: ./scripts/add_rule.py --name DoTheThing --prefix F --code 999 --linter pyflakes
|
||||
- run: cargo check
|
||||
- run: cargo fmt --all --check
|
||||
- run: |
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -73,6 +73,6 @@ jobs:
|
||||
owner: "astral-sh",
|
||||
repo: "ruff",
|
||||
title: `Daily parser fuzz failed on ${new Date().toDateString()}`,
|
||||
body: "Runs listed here: https://github.com/astral-sh/ruff/actions/workflows/daily_fuzz.yml",
|
||||
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
||||
labels: ["bug", "parser", "fuzzer"],
|
||||
})
|
||||
|
||||
71
.github/workflows/daily_property_tests.yaml
vendored
Normal file
71
.github/workflows/daily_property_tests.yaml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Daily property test run
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 12 * * *"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/daily_property_tests.yaml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
property_tests:
|
||||
name: Property tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
# Don't run the cron job on forks:
|
||||
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build Red Knot
|
||||
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
|
||||
# mode (1.5 min vs 14 min), so the overall time is shorter with a release build.
|
||||
run: cargo build --locked --release --package red_knot_python_semantic --tests
|
||||
- name: Run property tests
|
||||
shell: bash
|
||||
run: |
|
||||
export QUICKCHECK_TESTS=100000
|
||||
for _ in {1..5}; do
|
||||
cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable
|
||||
done
|
||||
|
||||
create-issue-on-failure:
|
||||
name: Create an issue if the daily property test run surfaced any bugs
|
||||
runs-on: ubuntu-latest
|
||||
needs: property_tests
|
||||
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.property_tests.result == 'failure' }}
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
await github.rest.issues.create({
|
||||
owner: "astral-sh",
|
||||
repo: "ruff",
|
||||
title: `Daily property test run failed on ${new Date().toDateString()}`,
|
||||
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
||||
labels: ["bug", "red-knot", "testing"],
|
||||
})
|
||||
3
.github/workflows/publish-docs.yml
vendored
3
.github/workflows/publish-docs.yml
vendored
@@ -33,8 +33,9 @@ jobs:
|
||||
python-version: 3.12
|
||||
|
||||
- name: "Set docs version"
|
||||
env:
|
||||
version: ${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}
|
||||
run: |
|
||||
version="${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}"
|
||||
# if version is missing, use 'latest'
|
||||
if [ -z "$version" ]; then
|
||||
echo "Using 'latest' as version"
|
||||
|
||||
6
.github/zizmor.yml
vendored
6
.github/zizmor.yml
vendored
@@ -1,6 +1,12 @@
|
||||
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
|
||||
# https://woodruffw.github.io/zizmor/configuration/
|
||||
#
|
||||
# TODO: can we remove the ignores here so that our workflows are more secure?
|
||||
rules:
|
||||
dangerous-triggers:
|
||||
ignore:
|
||||
- pr-comment.yaml
|
||||
cache-poisoning:
|
||||
ignore:
|
||||
- build-docker.yml
|
||||
- publish-playground.yml
|
||||
|
||||
@@ -59,7 +59,7 @@ repos:
|
||||
- black==24.10.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.28.4
|
||||
rev: v1.29.4
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -73,7 +73,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.4
|
||||
rev: v0.8.6
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -91,7 +91,7 @@ repos:
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v0.10.0
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
@@ -103,7 +103,7 @@ repos:
|
||||
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
|
||||
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
rev: v1.7.5
|
||||
rev: v1.7.6
|
||||
hooks:
|
||||
- id: actionlint
|
||||
stages:
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.9.0
|
||||
|
||||
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the [changelog](./CHANGELOG.md#090) for a detailed list of changes.
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- **Default to Python 3.9**
|
||||
|
||||
134
CHANGELOG.md
134
CHANGELOG.md
@@ -1,5 +1,139 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`pycodestyle`\] Run `too-many-newlines-at-end-of-file` on each cell in notebooks (`W391`) ([#15308](https://github.com/astral-sh/ruff/pull/15308))
|
||||
- \[`ruff`\] Omit diagnostic for shadowed private function parameters in `used-dummy-variable` (`RUF052`) ([#15376](https://github.com/astral-sh/ruff/pull/15376))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-bugbear`\] Improve `assert-raises-exception` message (`B017`) ([#15389](https://github.com/astral-sh/ruff/pull/15389))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Preserve trailing end-of line comments for the last string literal in implicitly concatenated strings ([#15378](https://github.com/astral-sh/ruff/pull/15378))
|
||||
|
||||
### Server
|
||||
|
||||
- Fix a bug where the server and client notebooks were out of sync after reordering cells ([#15398](https://github.com/astral-sh/ruff/pull/15398))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-pie`\] Correctly remove wrapping parentheses (`PIE800`) ([#15394](https://github.com/astral-sh/ruff/pull/15394))
|
||||
- \[`pyupgrade`\] Handle comments and multiline expressions correctly (`UP037`) ([#15337](https://github.com/astral-sh/ruff/pull/15337))
|
||||
|
||||
## 0.9.0
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.9.0) for a migration guide and overview of the changes!
|
||||
|
||||
### Breaking changes
|
||||
|
||||
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the formatter section for a detailed list of changes.
|
||||
|
||||
This release doesn’t remove or remap any existing stable rules.
|
||||
|
||||
### Stabilization
|
||||
|
||||
The following rules have been stabilized and are no longer in preview:
|
||||
|
||||
- [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`).
|
||||
This rule has also been renamed: previously, it was called `builtin-module-shadowing`.
|
||||
- [`builtin-lambda-argument-shadowing`](https://docs.astral.sh/ruff/rules/builtin-lambda-argument-shadowing/) (`A006`)
|
||||
- [`slice-to-remove-prefix-or-suffix`](https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/) (`FURB188`)
|
||||
- [`boolean-chained-comparison`](https://docs.astral.sh/ruff/rules/boolean-chained-comparison/) (`PLR1716`)
|
||||
- [`decimal-from-float-literal`](https://docs.astral.sh/ruff/rules/decimal-from-float-literal/) (`RUF032`)
|
||||
- [`post-init-default`](https://docs.astral.sh/ruff/rules/post-init-default/) (`RUF033`)
|
||||
- [`useless-if-else`](https://docs.astral.sh/ruff/rules/useless-if-else/) (`RUF034`)
|
||||
|
||||
The following behaviors have been stabilized:
|
||||
|
||||
- [`pytest-parametrize-names-wrong-type`](https://docs.astral.sh/ruff/rules/pytest-parametrize-names-wrong-type/) (`PT006`): Detect [`pytest.parametrize`](https://docs.pytest.org/en/7.1.x/how-to/parametrize.html#parametrize) calls outside decorators and calls with keyword arguments.
|
||||
- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`): Ignore [`pytest.importorskip`](https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest-importorskip) calls between import statements.
|
||||
- [`mutable-dataclass-default`](https://docs.astral.sh/ruff/rules/mutable-dataclass-default/) (`RUF008`) and [`function-call-in-dataclass-default-argument`](https://docs.astral.sh/ruff/rules/function-call-in-dataclass-default-argument/) (`RUF009`): Add support for [`attrs`](https://www.attrs.org/en/stable/).
|
||||
- [`bad-version-info-comparison`](https://docs.astral.sh/ruff/rules/bad-version-info-comparison/) (`PYI006`): Extend the rule to check non-stub files.
|
||||
|
||||
The following fixes or improvements to fixes have been stabilized:
|
||||
|
||||
- [`redundant-numeric-union`](https://docs.astral.sh/ruff/rules/redundant-numeric-union/) (`PYI041`)
|
||||
- [`duplicate-union-members`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`)
|
||||
|
||||
### Formatter
|
||||
|
||||
This release introduces the new 2025 stable style ([#13371](https://github.com/astral-sh/ruff/issues/13371)), stabilizing the following changes:
|
||||
|
||||
- Format expressions in f-string elements ([#7594](https://github.com/astral-sh/ruff/issues/7594))
|
||||
- Alternate quotes for strings inside f-strings ([#13860](https://github.com/astral-sh/ruff/pull/13860))
|
||||
- Preserve the casing of hex codes in f-string debug expressions ([#14766](https://github.com/astral-sh/ruff/issues/14766))
|
||||
- Choose the quote style for each string literal in an implicitly concatenated f-string rather than for the entire string ([#13539](https://github.com/astral-sh/ruff/pull/13539))
|
||||
- Automatically join an implicitly concatenated string into a single string literal if it fits on a single line ([#9457](https://github.com/astral-sh/ruff/issues/9457))
|
||||
- Remove the [`ISC001`](https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/) incompatibility warning ([#15123](https://github.com/astral-sh/ruff/pull/15123))
|
||||
- Prefer parenthesizing the `assert` message over breaking the assertion expression ([#9457](https://github.com/astral-sh/ruff/issues/9457))
|
||||
- Automatically parenthesize over-long `if` guards in `match` `case` clauses ([#13513](https://github.com/astral-sh/ruff/pull/13513))
|
||||
- More consistent formatting for `match` `case` patterns ([#6933](https://github.com/astral-sh/ruff/issues/6933))
|
||||
- Avoid unnecessary parentheses around return type annotations ([#13381](https://github.com/astral-sh/ruff/pull/13381))
|
||||
- Keep the opening parentheses on the same line as the `if` keyword for comprehensions where the condition has a leading comment ([#12282](https://github.com/astral-sh/ruff/pull/12282))
|
||||
- More consistent formatting for `with` statements with a single context manager for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276))
|
||||
- Correctly calculate the line-width for code blocks in docstrings when using `max-doc-code-line-length = "dynamic"` ([#13523](https://github.com/astral-sh/ruff/pull/13523))
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-bugbear`\] Implement `class-as-data-structure` (`B903`) ([#9601](https://github.com/astral-sh/ruff/pull/9601))
|
||||
- \[`flake8-type-checking`\] Apply `quoted-type-alias` more eagerly in `TYPE_CHECKING` blocks and ignore it in stubs (`TC008`) ([#15180](https://github.com/astral-sh/ruff/pull/15180))
|
||||
- \[`pylint`\] Ignore `eq-without-hash` in stub files (`PLW1641`) ([#15310](https://github.com/astral-sh/ruff/pull/15310))
|
||||
- \[`pyupgrade`\] Split `UP007` into two individual rules: `UP007` for `Union` and `UP045` for `Optional` (`UP007`, `UP045`) ([#15313](https://github.com/astral-sh/ruff/pull/15313))
|
||||
- \[`ruff`\] New rule that detects classes that are both an enum and a `dataclass` (`RUF049`) ([#15299](https://github.com/astral-sh/ruff/pull/15299))
|
||||
- \[`ruff`\] Recode `RUF025` to `RUF037` (`RUF037`) ([#15258](https://github.com/astral-sh/ruff/pull/15258))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-builtins`\] Ignore [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) in stub files(`A005`) ([#15350](https://github.com/astral-sh/ruff/pull/15350))
|
||||
- \[`flake8-return`\] Add support for functions returning `typing.Never` (`RET503`) ([#15298](https://github.com/astral-sh/ruff/pull/15298))
|
||||
|
||||
### Server
|
||||
|
||||
- Improve the observability by removing the need for the ["trace" value](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#traceValue) to turn on or off logging. The server logging is solely controlled using the [`logLevel` server setting](https://docs.astral.sh/ruff/editors/settings/#loglevel)
|
||||
which defaults to `info`. This addresses the issue where users were notified about an error and told to consult the log, but it didn’t contain any messages. ([#15232](https://github.com/astral-sh/ruff/pull/15232))
|
||||
- Ignore diagnostics from other sources for code action requests ([#15373](https://github.com/astral-sh/ruff/pull/15373))
|
||||
|
||||
### CLI
|
||||
|
||||
- Improve the error message for `--config key=value` when the `key` is for a table and it’s a simple `value`
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`eradicate`\] Ignore metadata blocks directly followed by normal blocks (`ERA001`) ([#15330](https://github.com/astral-sh/ruff/pull/15330))
|
||||
- \[`flake8-django`\] Recognize other magic methods (`DJ012`) ([#15365](https://github.com/astral-sh/ruff/pull/15365))
|
||||
- \[`pycodestyle`\] Avoid false positives related to type aliases (`E252`) ([#15356](https://github.com/astral-sh/ruff/pull/15356))
|
||||
- \[`pydocstyle`\] Avoid treating newline-separated sections as sub-sections (`D405`) ([#15311](https://github.com/astral-sh/ruff/pull/15311))
|
||||
- \[`pyflakes`\] Remove call when removing final argument from `format` (`F523`) ([#15309](https://github.com/astral-sh/ruff/pull/15309))
|
||||
- \[`refurb`\] Mark fix as unsafe when the right-hand side is a string (`FURB171`) ([#15273](https://github.com/astral-sh/ruff/pull/15273))
|
||||
- \[`ruff`\] Treat `)` as a regex metacharacter (`RUF043`, `RUF055`) ([#15318](https://github.com/astral-sh/ruff/pull/15318))
|
||||
- \[`ruff`\] Parenthesize the `int`-call argument when removing the `int` call would change semantics (`RUF046`) ([#15277](https://github.com/astral-sh/ruff/pull/15277))
|
||||
|
||||
## 0.8.6
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`format`\]: Preserve multiline implicit concatenated strings in docstring positions ([#15126](https://github.com/astral-sh/ruff/pull/15126))
|
||||
- \[`ruff`\] Add rule to detect empty literal in deque call (`RUF025`) ([#15104](https://github.com/astral-sh/ruff/pull/15104))
|
||||
- \[`ruff`\] Avoid reporting when `ndigits` is possibly negative (`RUF057`) ([#15234](https://github.com/astral-sh/ruff/pull/15234))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-todos`\] remove issue code length restriction (`TD003`) ([#15175](https://github.com/astral-sh/ruff/pull/15175))
|
||||
- \[`pyflakes`\] Ignore errors in `@no_type_check` string annotations (`F722`, `F821`) ([#15215](https://github.com/astral-sh/ruff/pull/15215))
|
||||
|
||||
### CLI
|
||||
|
||||
- Show errors for attempted fixes only when passed `--verbose` ([#15237](https://github.com/astral-sh/ruff/pull/15237))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`ruff`\] Avoid syntax error when removing int over multiple lines (`RUF046`) ([#15230](https://github.com/astral-sh/ruff/pull/15230))
|
||||
- \[`pyupgrade`\] Revert "Add all PEP-585 names to `UP006` rule" ([#15250](https://github.com/astral-sh/ruff/pull/15250))
|
||||
|
||||
## 0.8.5
|
||||
|
||||
### Preview features
|
||||
|
||||
202
Cargo.lock
generated
202
Cargo.lock
generated
@@ -220,9 +220,9 @@ checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.11.1"
|
||||
version = "1.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
|
||||
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
@@ -291,12 +291,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
@@ -413,7 +407,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -424,15 +418,15 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "clearscreen"
|
||||
version = "3.0.0"
|
||||
version = "4.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f8c93eb5f77c9050c7750e14f13ef1033a40a0aac70c6371535b6763a01438c"
|
||||
checksum = "8c41dc435a7b98e4608224bbf65282309f5403719df9113621b30f8b6f74e2f4"
|
||||
dependencies = [
|
||||
"nix 0.28.0",
|
||||
"nix",
|
||||
"terminfo",
|
||||
"thiserror 1.0.67",
|
||||
"thiserror 2.0.9",
|
||||
"which",
|
||||
"winapi",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -470,7 +464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -668,7 +662,7 @@ version = "3.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
|
||||
dependencies = [
|
||||
"nix 0.29.0",
|
||||
"nix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -693,7 +687,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -704,7 +698,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -774,16 +768,7 @@ dependencies = [
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||
dependencies = [
|
||||
"dirs-sys 0.3.7",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -792,18 +777,7 @@ version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -826,7 +800,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -875,6 +849,12 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.6"
|
||||
@@ -1246,7 +1226,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1365,14 +1345,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.41.1"
|
||||
version = "1.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8"
|
||||
checksum = "6513e4067e16e69ed1db5ab56048ed65db32d10ba5fc1217f5393f8f17d8b5a5"
|
||||
dependencies = [
|
||||
"console",
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"linked-hash-map",
|
||||
"once_cell",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"regex",
|
||||
@@ -1420,7 +1400,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1462,6 +1442,15 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
@@ -1547,7 +1536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1647,9 +1636,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd0aa4b8ca861b08d68afc8702af3250776898c1508b278e1da9d01e01d4b45c"
|
||||
checksum = "2f926ade0c4e170215ae43342bf13b9310a437609c81f29f86c5df6657582ef9"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@@ -1719,18 +1708,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.1.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
@@ -1739,7 +1716,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.2.1",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -2015,7 +1992,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2293,7 +2270,7 @@ dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
"indexmap",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"memchr",
|
||||
"ordermap",
|
||||
"quickcheck",
|
||||
@@ -2520,7 +2497,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.5"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2540,7 +2517,7 @@ dependencies = [
|
||||
"insta",
|
||||
"insta-cmd",
|
||||
"is-macro",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"mimalloc",
|
||||
"notify",
|
||||
@@ -2606,7 +2583,7 @@ dependencies = [
|
||||
"filetime",
|
||||
"glob",
|
||||
"globset",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"regex",
|
||||
"ruff_macros",
|
||||
"seahash",
|
||||
@@ -2655,7 +2632,7 @@ dependencies = [
|
||||
"imara-diff",
|
||||
"indicatif",
|
||||
"indoc",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"libcst",
|
||||
"pretty_assertions",
|
||||
"rayon",
|
||||
@@ -2739,7 +2716,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.8.5"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2755,7 +2732,7 @@ dependencies = [
|
||||
"insta",
|
||||
"is-macro",
|
||||
"is-wsl",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"libcst",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -2768,6 +2745,7 @@ dependencies = [
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
"ruff_diagnostics",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -2802,11 +2780,11 @@ dependencies = [
|
||||
name = "ruff_macros"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2814,7 +2792,7 @@ name = "ruff_notebook"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"rand",
|
||||
"ruff_diagnostics",
|
||||
"ruff_source_file",
|
||||
@@ -2835,7 +2813,7 @@ dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"compact_str",
|
||||
"is-macro",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"memchr",
|
||||
"ruff_cache",
|
||||
"ruff_macros",
|
||||
@@ -2877,7 +2855,7 @@ dependencies = [
|
||||
"clap",
|
||||
"countme",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"memchr",
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
@@ -2915,7 +2893,7 @@ name = "ruff_python_literal"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"ruff_python_ast",
|
||||
"unic-ucd-category",
|
||||
]
|
||||
@@ -2984,7 +2962,7 @@ dependencies = [
|
||||
name = "ruff_python_trivia"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"unicode-ident",
|
||||
@@ -3055,7 +3033,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.8.5"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3089,7 +3067,7 @@ dependencies = [
|
||||
"globset",
|
||||
"ignore",
|
||||
"is-macro",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"matchit",
|
||||
"path-absolutize",
|
||||
@@ -3197,7 +3175,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
@@ -3205,7 +3183,6 @@ dependencies = [
|
||||
"dashmap 6.1.0",
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"rustc-hash 2.1.0",
|
||||
@@ -3218,17 +3195,17 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -3262,7 +3239,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3311,7 +3288,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3322,7 +3299,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3345,7 +3322,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3386,7 +3363,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3415,7 +3392,7 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
|
||||
dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"dirs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3500,7 +3477,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3522,9 +3499,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.93"
|
||||
version = "2.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
|
||||
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3539,7 +3516,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3567,11 +3544,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "terminfo"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f"
|
||||
checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
|
||||
dependencies = [
|
||||
"dirs 4.0.0",
|
||||
"fnv",
|
||||
"nom",
|
||||
"phf",
|
||||
@@ -3602,7 +3578,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3613,7 +3589,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
@@ -3643,7 +3619,7 @@ checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3654,7 +3630,7 @@ checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3776,7 +3752,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4046,7 +4022,7 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4141,7 +4117,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -4176,7 +4152,7 @@ checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -4210,7 +4186,7 @@ checksum = "222ebde6ea87fbfa6bdd2e9f1fd8a91d60aee5db68792632176c4e16a74fc7d8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4244,12 +4220,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "6.0.1"
|
||||
version = "7.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7"
|
||||
checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"env_home",
|
||||
"rustix",
|
||||
"winsafe",
|
||||
]
|
||||
@@ -4513,7 +4489,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4534,7 +4510,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4554,7 +4530,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4583,7 +4559,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.93",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -55,7 +55,7 @@ camino = { version = "1.1.7" }
|
||||
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.5.3", features = ["derive"] }
|
||||
clap_complete_command = { version = "0.6.0" }
|
||||
clearscreen = { version = "3.0.0" }
|
||||
clearscreen = { version = "4.0.0" }
|
||||
codspeed-criterion-compat = { version = "2.6.0", default-features = false }
|
||||
colored = { version = "2.1.0" }
|
||||
console_error_panic_hook = { version = "0.1.7" }
|
||||
@@ -89,7 +89,7 @@ insta = { version = "1.35.1" }
|
||||
insta-cmd = { version = "0.6.0" }
|
||||
is-macro = { version = "0.3.5" }
|
||||
is-wsl = { version = "0.4.0" }
|
||||
itertools = { version = "0.13.0" }
|
||||
itertools = { version = "0.14.0" }
|
||||
js-sys = { version = "0.3.69" }
|
||||
jod-thread = { version = "0.1.2" }
|
||||
libc = { version = "0.2.153" }
|
||||
@@ -119,7 +119,7 @@ rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3c7f1694c9efba751dbeeacfbc93b227586e316a" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -211,6 +211,9 @@ redundant_clone = "warn"
|
||||
debug_assert_with_mut_call = "warn"
|
||||
unused_peekable = "warn"
|
||||
|
||||
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
|
||||
large_stack_arrays = "allow"
|
||||
|
||||
[profile.release]
|
||||
# Note that we set these explicitly, and these values
|
||||
# were chosen based on a trade-off between compile times
|
||||
|
||||
21
README.md
21
README.md
@@ -116,12 +116,21 @@ For more, see the [documentation](https://docs.astral.sh/ruff/).
|
||||
|
||||
### Installation
|
||||
|
||||
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
|
||||
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI.
|
||||
|
||||
Invoke Ruff directly with [`uvx`](https://docs.astral.sh/uv/):
|
||||
|
||||
```shell
|
||||
uvx ruff check # Lint all files in the current directory.
|
||||
uvx ruff format # Format all files in the current directory.
|
||||
```
|
||||
|
||||
Or install Ruff with `uv` (recommended), `pip`, or `pipx`:
|
||||
|
||||
```shell
|
||||
# With uv.
|
||||
uv add --dev ruff # to add ruff to your project
|
||||
uv tool install ruff # to install ruff globally
|
||||
uv tool install ruff@latest # Install Ruff globally.
|
||||
uv add --dev ruff # Or add Ruff to your project.
|
||||
|
||||
# With pip.
|
||||
pip install ruff
|
||||
@@ -140,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.8.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.5/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.9.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.1/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -174,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.5
|
||||
rev: v0.9.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -9,8 +9,6 @@ from typing import Literal
|
||||
from enum import Enum
|
||||
|
||||
mode: Literal["w", "r"]
|
||||
mode2: Literal["w"] | Literal["r"]
|
||||
union_var: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
|
||||
a1: Literal[26]
|
||||
a2: Literal[0x1A]
|
||||
a3: Literal[-4]
|
||||
@@ -19,7 +17,6 @@ a5: Literal[b"hello world"]
|
||||
a6: Literal[True]
|
||||
a7: Literal[None]
|
||||
a8: Literal[Literal[1]]
|
||||
a9: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]]
|
||||
|
||||
class Color(Enum):
|
||||
RED = 0
|
||||
@@ -30,9 +27,6 @@ b1: Literal[Color.RED]
|
||||
|
||||
def f():
|
||||
reveal_type(mode) # revealed: Literal["w", "r"]
|
||||
reveal_type(mode2) # revealed: Literal["w", "r"]
|
||||
# TODO: should be revealed: Literal[1, 2, 3, "foo", 5] | None
|
||||
reveal_type(union_var) # revealed: Literal[1, 2, 3, 5] | Literal["foo"] | None
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
reveal_type(a2) # revealed: Literal[26]
|
||||
reveal_type(a3) # revealed: Literal[-4]
|
||||
@@ -41,7 +35,6 @@ def f():
|
||||
reveal_type(a6) # revealed: Literal[True]
|
||||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
reveal_type(a9) # revealed: Literal["w", "r", "w+"]
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: Literal[0]
|
||||
|
||||
@@ -61,6 +54,63 @@ invalid4: Literal[
|
||||
]
|
||||
```
|
||||
|
||||
## Shortening unions of literals
|
||||
|
||||
When a Literal is parameterized with more than one value, it’s treated as exactly to equivalent to
|
||||
the union of those types.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def x(
|
||||
a1: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None],
|
||||
a2: Literal["w"] | Literal["r"],
|
||||
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
|
||||
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
|
||||
):
|
||||
reveal_type(a1) # revealed: Literal[1, 2, 3, "foo", 5] | None
|
||||
reveal_type(a2) # revealed: Literal["w", "r"]
|
||||
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
|
||||
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
|
||||
```
|
||||
|
||||
## Display of heterogeneous unions of literals
|
||||
|
||||
```py
|
||||
from typing import Literal, Union
|
||||
|
||||
def foo(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
def bar(s: str) -> str:
|
||||
return s
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def union_example(
|
||||
x: Union[
|
||||
# unknown type
|
||||
# error: [unresolved-reference]
|
||||
y,
|
||||
Literal[-1],
|
||||
Literal["A"],
|
||||
Literal[b"A"],
|
||||
Literal[b"\x00"],
|
||||
Literal[b"\x07"],
|
||||
Literal[0],
|
||||
Literal[1],
|
||||
Literal["B"],
|
||||
Literal["foo"],
|
||||
Literal["bar"],
|
||||
Literal["B"],
|
||||
Literal[True],
|
||||
None,
|
||||
]
|
||||
):
|
||||
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
|
||||
```
|
||||
|
||||
## Detecting Literal outside typing and typing_extensions
|
||||
|
||||
Only Literal that is defined in typing and typing_extension modules is detected as the special
|
||||
|
||||
@@ -107,7 +107,7 @@ def _(flag: bool):
|
||||
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
|
||||
|
||||
baz_3 = "foo" if flag else 1
|
||||
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
|
||||
reveal_type(baz_3) # revealed: Literal["foo", 1]
|
||||
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ def f1(
|
||||
from typing import Literal
|
||||
|
||||
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
|
||||
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
|
||||
reveal_type(v) # revealed: Literal["a", "b", b"c", "de", "f", "g", "h"]
|
||||
```
|
||||
|
||||
## Class variables
|
||||
|
||||
@@ -122,3 +122,10 @@ class Foo: ...
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
|
||||
## Annotated assignments in stub files are inferred correctly
|
||||
|
||||
```pyi path=main.pyi
|
||||
x: int = 1
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -40,9 +40,9 @@ class C:
|
||||
return 42
|
||||
|
||||
x = C()
|
||||
# error: [invalid-argument-type]
|
||||
x -= 1
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
|
||||
@@ -46,3 +46,50 @@ reveal_type(a | b) # revealed: Literal[True]
|
||||
reveal_type(b | a) # revealed: Literal[True]
|
||||
reveal_type(b | b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Arithmetic with a variable
|
||||
|
||||
```py
|
||||
a = True
|
||||
b = False
|
||||
|
||||
def lhs_is_int(x: int):
|
||||
reveal_type(x + a) # revealed: int
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: float
|
||||
reveal_type(x % a) # revealed: int
|
||||
|
||||
def rhs_is_int(x: int):
|
||||
reveal_type(a + x) # revealed: int
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def lhs_is_bool(x: bool):
|
||||
reveal_type(x + a) # revealed: int
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: float
|
||||
reveal_type(x % a) # revealed: int
|
||||
|
||||
def rhs_is_bool(x: bool):
|
||||
reveal_type(a + x) # revealed: int
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def both_are_bool(x: bool, y: bool):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(x - y) # revealed: int
|
||||
reveal_type(x * y) # revealed: int
|
||||
reveal_type(x // y) # revealed: int
|
||||
reveal_type(x / y) # revealed: float
|
||||
reveal_type(x % y) # revealed: int
|
||||
```
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Binary operations on classes
|
||||
|
||||
## Union of two classes
|
||||
|
||||
Unioning two classes via the `|` operator is only available in Python 3.10 and later.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
reveal_type(A | B) # revealed: UnionType
|
||||
```
|
||||
|
||||
## Union of two classes (prior to 3.10)
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A | B) # revealed: Unknown
|
||||
```
|
||||
@@ -0,0 +1,371 @@
|
||||
# Custom binary operations
|
||||
|
||||
## Class instances
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
def __sub__(self, other) -> Literal["-"]:
|
||||
return "-"
|
||||
|
||||
def __mul__(self, other) -> Literal["*"]:
|
||||
return "*"
|
||||
|
||||
def __matmul__(self, other) -> Literal["@"]:
|
||||
return "@"
|
||||
|
||||
def __truediv__(self, other) -> Literal["/"]:
|
||||
return "/"
|
||||
|
||||
def __mod__(self, other) -> Literal["%"]:
|
||||
return "%"
|
||||
|
||||
def __pow__(self, other) -> Literal["**"]:
|
||||
return "**"
|
||||
|
||||
def __lshift__(self, other) -> Literal["<<"]:
|
||||
return "<<"
|
||||
|
||||
def __rshift__(self, other) -> Literal[">>"]:
|
||||
return ">>"
|
||||
|
||||
def __or__(self, other) -> Literal["|"]:
|
||||
return "|"
|
||||
|
||||
def __xor__(self, other) -> Literal["^"]:
|
||||
return "^"
|
||||
|
||||
def __and__(self, other) -> Literal["&"]:
|
||||
return "&"
|
||||
|
||||
def __floordiv__(self, other) -> Literal["//"]:
|
||||
return "//"
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
# Yes implements all of the dunder methods.
|
||||
reveal_type(Yes() + Yes()) # revealed: Literal["+"]
|
||||
reveal_type(Yes() - Yes()) # revealed: Literal["-"]
|
||||
reveal_type(Yes() * Yes()) # revealed: Literal["*"]
|
||||
reveal_type(Yes() @ Yes()) # revealed: Literal["@"]
|
||||
reveal_type(Yes() / Yes()) # revealed: Literal["/"]
|
||||
reveal_type(Yes() % Yes()) # revealed: Literal["%"]
|
||||
reveal_type(Yes() ** Yes()) # revealed: Literal["**"]
|
||||
reveal_type(Yes() << Yes()) # revealed: Literal["<<"]
|
||||
reveal_type(Yes() >> Yes()) # revealed: Literal[">>"]
|
||||
reveal_type(Yes() | Yes()) # revealed: Literal["|"]
|
||||
reveal_type(Yes() ^ Yes()) # revealed: Literal["^"]
|
||||
reveal_type(Yes() & Yes()) # revealed: Literal["&"]
|
||||
reveal_type(Yes() // Yes()) # revealed: Literal["//"]
|
||||
|
||||
# Sub inherits Yes's implementation of the dunder methods.
|
||||
reveal_type(Sub() + Sub()) # revealed: Literal["+"]
|
||||
reveal_type(Sub() - Sub()) # revealed: Literal["-"]
|
||||
reveal_type(Sub() * Sub()) # revealed: Literal["*"]
|
||||
reveal_type(Sub() @ Sub()) # revealed: Literal["@"]
|
||||
reveal_type(Sub() / Sub()) # revealed: Literal["/"]
|
||||
reveal_type(Sub() % Sub()) # revealed: Literal["%"]
|
||||
reveal_type(Sub() ** Sub()) # revealed: Literal["**"]
|
||||
reveal_type(Sub() << Sub()) # revealed: Literal["<<"]
|
||||
reveal_type(Sub() >> Sub()) # revealed: Literal[">>"]
|
||||
reveal_type(Sub() | Sub()) # revealed: Literal["|"]
|
||||
reveal_type(Sub() ^ Sub()) # revealed: Literal["^"]
|
||||
reveal_type(Sub() & Sub()) # revealed: Literal["&"]
|
||||
reveal_type(Sub() // Sub()) # revealed: Literal["//"]
|
||||
|
||||
# No does not implement any of the dunder methods.
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() + No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() - No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() * No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() @ No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() / No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() % No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() ** No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() << No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() >> No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() | No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() ^ No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() & No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() // No()) # revealed: Unknown
|
||||
|
||||
# Yes does not implement any of the reflected dunder methods.
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() + Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() - Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() * Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() @ Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() / Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() % Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() ** Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() << Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() >> Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() | Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() ^ Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() & Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() // Yes()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Subclass reflections override superclass dunders
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
def __sub__(self, other) -> Literal["-"]:
|
||||
return "-"
|
||||
|
||||
def __mul__(self, other) -> Literal["*"]:
|
||||
return "*"
|
||||
|
||||
def __matmul__(self, other) -> Literal["@"]:
|
||||
return "@"
|
||||
|
||||
def __truediv__(self, other) -> Literal["/"]:
|
||||
return "/"
|
||||
|
||||
def __mod__(self, other) -> Literal["%"]:
|
||||
return "%"
|
||||
|
||||
def __pow__(self, other) -> Literal["**"]:
|
||||
return "**"
|
||||
|
||||
def __lshift__(self, other) -> Literal["<<"]:
|
||||
return "<<"
|
||||
|
||||
def __rshift__(self, other) -> Literal[">>"]:
|
||||
return ">>"
|
||||
|
||||
def __or__(self, other) -> Literal["|"]:
|
||||
return "|"
|
||||
|
||||
def __xor__(self, other) -> Literal["^"]:
|
||||
return "^"
|
||||
|
||||
def __and__(self, other) -> Literal["&"]:
|
||||
return "&"
|
||||
|
||||
def __floordiv__(self, other) -> Literal["//"]:
|
||||
return "//"
|
||||
|
||||
class Sub(Yes):
|
||||
def __radd__(self, other) -> Literal["r+"]:
|
||||
return "r+"
|
||||
|
||||
def __rsub__(self, other) -> Literal["r-"]:
|
||||
return "r-"
|
||||
|
||||
def __rmul__(self, other) -> Literal["r*"]:
|
||||
return "r*"
|
||||
|
||||
def __rmatmul__(self, other) -> Literal["r@"]:
|
||||
return "r@"
|
||||
|
||||
def __rtruediv__(self, other) -> Literal["r/"]:
|
||||
return "r/"
|
||||
|
||||
def __rmod__(self, other) -> Literal["r%"]:
|
||||
return "r%"
|
||||
|
||||
def __rpow__(self, other) -> Literal["r**"]:
|
||||
return "r**"
|
||||
|
||||
def __rlshift__(self, other) -> Literal["r<<"]:
|
||||
return "r<<"
|
||||
|
||||
def __rrshift__(self, other) -> Literal["r>>"]:
|
||||
return "r>>"
|
||||
|
||||
def __ror__(self, other) -> Literal["r|"]:
|
||||
return "r|"
|
||||
|
||||
def __rxor__(self, other) -> Literal["r^"]:
|
||||
return "r^"
|
||||
|
||||
def __rand__(self, other) -> Literal["r&"]:
|
||||
return "r&"
|
||||
|
||||
def __rfloordiv__(self, other) -> Literal["r//"]:
|
||||
return "r//"
|
||||
|
||||
class No:
|
||||
def __radd__(self, other) -> Literal["r+"]:
|
||||
return "r+"
|
||||
|
||||
def __rsub__(self, other) -> Literal["r-"]:
|
||||
return "r-"
|
||||
|
||||
def __rmul__(self, other) -> Literal["r*"]:
|
||||
return "r*"
|
||||
|
||||
def __rmatmul__(self, other) -> Literal["r@"]:
|
||||
return "r@"
|
||||
|
||||
def __rtruediv__(self, other) -> Literal["r/"]:
|
||||
return "r/"
|
||||
|
||||
def __rmod__(self, other) -> Literal["r%"]:
|
||||
return "r%"
|
||||
|
||||
def __rpow__(self, other) -> Literal["r**"]:
|
||||
return "r**"
|
||||
|
||||
def __rlshift__(self, other) -> Literal["r<<"]:
|
||||
return "r<<"
|
||||
|
||||
def __rrshift__(self, other) -> Literal["r>>"]:
|
||||
return "r>>"
|
||||
|
||||
def __ror__(self, other) -> Literal["r|"]:
|
||||
return "r|"
|
||||
|
||||
def __rxor__(self, other) -> Literal["r^"]:
|
||||
return "r^"
|
||||
|
||||
def __rand__(self, other) -> Literal["r&"]:
|
||||
return "r&"
|
||||
|
||||
def __rfloordiv__(self, other) -> Literal["r//"]:
|
||||
return "r//"
|
||||
|
||||
# Subclass reflected dunder methods take precedence over the superclass's regular dunders.
|
||||
reveal_type(Yes() + Sub()) # revealed: Literal["r+"]
|
||||
reveal_type(Yes() - Sub()) # revealed: Literal["r-"]
|
||||
reveal_type(Yes() * Sub()) # revealed: Literal["r*"]
|
||||
reveal_type(Yes() @ Sub()) # revealed: Literal["r@"]
|
||||
reveal_type(Yes() / Sub()) # revealed: Literal["r/"]
|
||||
reveal_type(Yes() % Sub()) # revealed: Literal["r%"]
|
||||
reveal_type(Yes() ** Sub()) # revealed: Literal["r**"]
|
||||
reveal_type(Yes() << Sub()) # revealed: Literal["r<<"]
|
||||
reveal_type(Yes() >> Sub()) # revealed: Literal["r>>"]
|
||||
reveal_type(Yes() | Sub()) # revealed: Literal["r|"]
|
||||
reveal_type(Yes() ^ Sub()) # revealed: Literal["r^"]
|
||||
reveal_type(Yes() & Sub()) # revealed: Literal["r&"]
|
||||
reveal_type(Yes() // Sub()) # revealed: Literal["r//"]
|
||||
|
||||
# But for an unrelated class, the superclass regular dunders are used.
|
||||
reveal_type(Yes() + No()) # revealed: Literal["+"]
|
||||
reveal_type(Yes() - No()) # revealed: Literal["-"]
|
||||
reveal_type(Yes() * No()) # revealed: Literal["*"]
|
||||
reveal_type(Yes() @ No()) # revealed: Literal["@"]
|
||||
reveal_type(Yes() / No()) # revealed: Literal["/"]
|
||||
reveal_type(Yes() % No()) # revealed: Literal["%"]
|
||||
reveal_type(Yes() ** No()) # revealed: Literal["**"]
|
||||
reveal_type(Yes() << No()) # revealed: Literal["<<"]
|
||||
reveal_type(Yes() >> No()) # revealed: Literal[">>"]
|
||||
reveal_type(Yes() | No()) # revealed: Literal["|"]
|
||||
reveal_type(Yes() ^ No()) # revealed: Literal["^"]
|
||||
reveal_type(Yes() & No()) # revealed: Literal["&"]
|
||||
reveal_type(Yes() // No()) # revealed: Literal["//"]
|
||||
```
|
||||
|
||||
## Classes
|
||||
|
||||
Dunder methods defined in a class are available to instances of that class, but not to the class
|
||||
itself. (For these operators to work on the class itself, they would have to be defined on the
|
||||
class's type, i.e. `type`.)
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`"
|
||||
reveal_type(Yes + Yes) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`"
|
||||
reveal_type(Sub + Sub) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`"
|
||||
reveal_type(No + No) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Subclass
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
def yes() -> type[Yes]:
|
||||
return Yes
|
||||
|
||||
def sub() -> type[Sub]:
|
||||
return Sub
|
||||
|
||||
def no() -> type[No]:
|
||||
return No
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`"
|
||||
reveal_type(yes() + yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`"
|
||||
reveal_type(sub() + sub()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`"
|
||||
reveal_type(no() + no()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Function literals
|
||||
|
||||
```py
|
||||
def f():
|
||||
pass
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f + f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f - f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f * f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f @ f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f / f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f % f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f**f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f << f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f >> f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f | f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f ^ f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f & f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
reveal_type(f // f) # revealed: Unknown
|
||||
```
|
||||
@@ -9,6 +9,34 @@ reveal_type(3 * -1) # revealed: Literal[-3]
|
||||
reveal_type(-3 // 3) # revealed: Literal[-1]
|
||||
reveal_type(-3 / 3) # revealed: float
|
||||
reveal_type(5 % 3) # revealed: Literal[2]
|
||||
|
||||
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
|
||||
# formal parameter type.
|
||||
reveal_type(2 + "f") # revealed: int
|
||||
|
||||
def lhs(x: int):
|
||||
reveal_type(x + 1) # revealed: int
|
||||
reveal_type(x - 4) # revealed: int
|
||||
reveal_type(x * -1) # revealed: int
|
||||
reveal_type(x // 3) # revealed: int
|
||||
reveal_type(x / 3) # revealed: float
|
||||
reveal_type(x % 3) # revealed: int
|
||||
|
||||
def rhs(x: int):
|
||||
reveal_type(2 + x) # revealed: int
|
||||
reveal_type(3 - x) # revealed: int
|
||||
reveal_type(3 * x) # revealed: int
|
||||
reveal_type(-3 // x) # revealed: int
|
||||
reveal_type(-3 / x) # revealed: float
|
||||
reveal_type(5 % x) # revealed: int
|
||||
|
||||
def both(x: int):
|
||||
reveal_type(x + x) # revealed: int
|
||||
reveal_type(x - x) # revealed: int
|
||||
reveal_type(x * x) # revealed: int
|
||||
reveal_type(x // x) # revealed: int
|
||||
reveal_type(x / x) # revealed: float
|
||||
reveal_type(x % x) # revealed: int
|
||||
```
|
||||
|
||||
## Power
|
||||
@@ -21,6 +49,11 @@ largest_u32 = 4_294_967_295
|
||||
reveal_type(2**2) # revealed: Literal[4]
|
||||
reveal_type(1 ** (largest_u32 + 1)) # revealed: int
|
||||
reveal_type(2**largest_u32) # revealed: int
|
||||
|
||||
def variable(x: int):
|
||||
reveal_type(x**2) # revealed: @Todo(return type)
|
||||
reveal_type(2**x) # revealed: @Todo(return type)
|
||||
reveal_type(x**x) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Division by Zero
|
||||
|
||||
@@ -70,3 +70,32 @@ def _(flag: bool):
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Call binding errors
|
||||
|
||||
### Wrong argument type
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __call__(self, x: int) -> int:
|
||||
return 1
|
||||
|
||||
c = C()
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`"
|
||||
reveal_type(c("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Wrong argument type on `self`
|
||||
|
||||
```py
|
||||
class C:
|
||||
# TODO this definition should also be an error; `C` must be assignable to type of `self`
|
||||
def __call__(self: int) -> int:
|
||||
return 1
|
||||
|
||||
c = C()
|
||||
|
||||
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
|
||||
reveal_type(c()) # revealed: int
|
||||
```
|
||||
|
||||
@@ -64,3 +64,260 @@ def _(flag: bool):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
## Wrong argument type
|
||||
|
||||
### Positional argument, positional-or-keyword parameter
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Positional argument, positional-only parameter
|
||||
|
||||
```py
|
||||
def f(x: int, /) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Positional argument, variadic parameter
|
||||
|
||||
```py
|
||||
def f(*args: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `*args` of function `f`; expected type `int`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Keyword argument, positional-or-keyword parameter
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Keyword argument, keyword-only parameter
|
||||
|
||||
```py
|
||||
def f(*, x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Keyword argument, keywords parameter
|
||||
|
||||
```py
|
||||
def f(**kwargs: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `**kwargs` of function `f`; expected type `int`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Correctly match keyword out-of-order
|
||||
|
||||
```py
|
||||
def f(x: int = 1, y: str = "foo") -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal[2]` cannot be assigned to parameter `y` of function `f`; expected type `str`"
|
||||
# error: 20 [invalid-argument-type] "Object of type `Literal["bar"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
|
||||
reveal_type(f(y=2, x="bar")) # revealed: int
|
||||
```
|
||||
|
||||
## Too many positional arguments
|
||||
|
||||
### One too many
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Two too many
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2"
|
||||
reveal_type(f("foo", "bar")) # revealed: int
|
||||
```
|
||||
|
||||
### No too-many-positional if variadic is taken
|
||||
|
||||
```py
|
||||
def f(*args: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f(1, 2, 3)) # revealed: int
|
||||
```
|
||||
|
||||
## Missing arguments
|
||||
|
||||
### No defaults or variadic
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### With default
|
||||
|
||||
```py
|
||||
def f(x: int, y: str = "foo") -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Defaulted argument is not required
|
||||
|
||||
```py
|
||||
def f(x: int = 1) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### With variadic
|
||||
|
||||
```py
|
||||
def f(x: int, *y: str) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Variadic argument is not required
|
||||
|
||||
```py
|
||||
def f(*args: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Keywords argument is not required
|
||||
|
||||
```py
|
||||
def f(**kwargs: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Multiple
|
||||
|
||||
```py
|
||||
def f(x: int, y: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
## Unknown argument
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`"
|
||||
reveal_type(f(x=1, y=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Parameter already assigned
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
|
||||
reveal_type(f(1, x=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Special functions
|
||||
|
||||
Some functions require special handling in type inference. Here, we make sure that we still emit
|
||||
proper diagnostics in case of missing or superfluous arguments.
|
||||
|
||||
### `reveal_type`
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
|
||||
reveal_type() # revealed: Unknown
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
|
||||
reveal_type(1, 2) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### `static_assert`
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
|
||||
# error: [static-assert-error]
|
||||
static_assert()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"
|
||||
static_assert(True, 2, 3)
|
||||
```
|
||||
|
||||
### `len`
|
||||
|
||||
```py
|
||||
# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`"
|
||||
len()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2"
|
||||
len([], 1)
|
||||
```
|
||||
|
||||
### Type API predicates
|
||||
|
||||
```py
|
||||
from knot_extensions import is_subtype_of, is_fully_static
|
||||
|
||||
# error: [missing-argument]
|
||||
is_subtype_of()
|
||||
|
||||
# error: [missing-argument]
|
||||
is_subtype_of(int)
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_subtype_of(int, int, int)
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_subtype_of(int, int, int, int)
|
||||
|
||||
# error: [missing-argument]
|
||||
is_fully_static()
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_fully_static(int, int)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Invalid signatures
|
||||
|
||||
## Multiple arguments with the same name
|
||||
|
||||
We always map a keyword argument to the first parameter of that name.
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "Duplicate parameter "x""
|
||||
def f(x: int, x: str) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
|
||||
reveal_type(f(1, x=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Positional after non-positional
|
||||
|
||||
When parameter kinds are given in an invalid order, we emit a diagnostic and implicitly reorder them
|
||||
to the valid order:
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "Parameter cannot follow var-keyword parameter"
|
||||
def f(**kw: int, x: str) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `str`"
|
||||
reveal_type(f(1)) # revealed: int
|
||||
```
|
||||
|
||||
## Non-defaulted after defaulted
|
||||
|
||||
We emit a syntax diagnostic for this, but it doesn't cause any problems for binding.
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "Parameter without a default cannot follow a parameter with a default"
|
||||
def f(x: int = 1, y: str) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f(y="foo")) # revealed: int
|
||||
# error: [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
|
||||
# error: [missing-argument] "No argument provided for required parameter `y` of function `f`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
@@ -56,7 +56,7 @@ def _(flag: bool, flag2: bool):
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
# revealed: Unknown | int
|
||||
reveal_type(f())
|
||||
```
|
||||
@@ -72,6 +72,6 @@ def _(flag: bool):
|
||||
else:
|
||||
f = "foo"
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
|
||||
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -22,7 +22,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
reveal_type(d) # revealed: bool
|
||||
|
||||
int_literal_or_str_literal = 1 if flag else "foo"
|
||||
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
|
||||
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
reveal_type(e) # revealed: bool
|
||||
|
||||
|
||||
@@ -115,3 +115,35 @@ def _(flag: bool, flag2: bool):
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## if-elif with assignment expressions in tests
|
||||
|
||||
```py
|
||||
def check(x: int) -> bool:
|
||||
return bool(x)
|
||||
|
||||
if check(x := 1):
|
||||
x = 2
|
||||
elif check(x := 3):
|
||||
x = 4
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## constraints apply to later test expressions
|
||||
|
||||
```py
|
||||
def check(x) -> bool:
|
||||
return bool(x)
|
||||
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else None
|
||||
y = 0
|
||||
|
||||
if x is None:
|
||||
pass
|
||||
elif check(y := x):
|
||||
pass
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# `assert_type`
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(x: int):
|
||||
assert_type(x, int) # fine
|
||||
assert_type(x, str) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
The asserted type is checked against the inferred type, not the declared type.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(x: int | str):
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: int
|
||||
assert_type(x, int) # fine
|
||||
```
|
||||
|
||||
## Equivalence
|
||||
|
||||
The actual type must match the asserted type precisely.
|
||||
|
||||
```py
|
||||
from typing import Any, Type, Union
|
||||
from typing_extensions import assert_type
|
||||
|
||||
# Subtype does not count
|
||||
def _(x: bool):
|
||||
assert_type(x, int) # error: [type-assertion-failure]
|
||||
|
||||
def _(a: type[int], b: type[Any]):
|
||||
assert_type(a, type[Any]) # error: [type-assertion-failure]
|
||||
assert_type(b, type[int]) # error: [type-assertion-failure]
|
||||
|
||||
# The expression constructing the type is not taken into account
|
||||
def _(a: type[int]):
|
||||
assert_type(a, Type[int]) # fine
|
||||
```
|
||||
|
||||
## Gradual types
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import Literal, assert_type
|
||||
|
||||
from knot_extensions import Unknown
|
||||
|
||||
# Any and Unknown are considered equivalent
|
||||
def _(a: Unknown, b: Any):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
assert_type(a, Any) # fine
|
||||
|
||||
reveal_type(b) # revealed: Any
|
||||
assert_type(b, Unknown) # fine
|
||||
|
||||
def _(a: type[Unknown], b: type[Any]):
|
||||
reveal_type(a) # revealed: type[Unknown]
|
||||
assert_type(a, type[Any]) # fine
|
||||
|
||||
reveal_type(b) # revealed: type[Any]
|
||||
assert_type(b, type[Unknown]) # fine
|
||||
```
|
||||
|
||||
## Tuples
|
||||
|
||||
Tuple types with the same elements are the same.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(a: tuple[int, str, bytes]):
|
||||
assert_type(a, tuple[int, str, bytes]) # fine
|
||||
|
||||
assert_type(a, tuple[int, str]) # error: [type-assertion-failure]
|
||||
assert_type(a, tuple[int, str, bytes, None]) # error: [type-assertion-failure]
|
||||
assert_type(a, tuple[int, bytes, str]) # error: [type-assertion-failure]
|
||||
|
||||
def _(a: tuple[Any, ...], b: tuple[Unknown, ...]):
|
||||
assert_type(a, tuple[Any, ...]) # fine
|
||||
assert_type(a, tuple[Unknown, ...]) # fine
|
||||
|
||||
assert_type(b, tuple[Unknown, ...]) # fine
|
||||
assert_type(b, tuple[Any, ...]) # fine
|
||||
```
|
||||
|
||||
## Unions
|
||||
|
||||
Unions with the same elements are the same, regardless of order.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(a: str | int):
|
||||
assert_type(a, str | int) # fine
|
||||
|
||||
# TODO: Order-independent union handling in type equivalence
|
||||
assert_type(a, int | str) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
## Intersections
|
||||
|
||||
Intersections are the same when their positive and negative parts are respectively the same,
|
||||
regardless of order.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
|
||||
def _(a: A):
|
||||
if isinstance(a, B) and not isinstance(a, C) and not isinstance(a, D):
|
||||
reveal_type(a) # revealed: A & B & ~C & ~D
|
||||
|
||||
assert_type(a, Intersection[A, B, Not[C], Not[D]]) # fine
|
||||
|
||||
# TODO: Order-independent intersection handling in type equivalence
|
||||
assert_type(a, Intersection[B, A, Not[D], Not[C]]) # error: [type-assertion-failure]
|
||||
```
|
||||
@@ -17,7 +17,7 @@ def _(flag: bool):
|
||||
|
||||
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||
|
||||
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
|
||||
reveal_type(A.union) # revealed: Literal[1, "abc"]
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||
|
||||
@@ -31,9 +31,9 @@ The test inside an if expression should not affect code outside of the expressio
|
||||
def _(flag: bool):
|
||||
x: Literal[42, "hello"] = 42 if flag else "hello"
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
reveal_type(x) # revealed: Literal[42, "hello"]
|
||||
|
||||
_ = ... if isinstance(x, str) else ...
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
reveal_type(x) # revealed: Literal[42, "hello"]
|
||||
```
|
||||
|
||||
@@ -119,7 +119,7 @@ class ZeroOrStr:
|
||||
reveal_type(len(Zero())) # revealed: Literal[0]
|
||||
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(OneOrFalse())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(OneOrFalse())) # revealed: Literal[1, 0]
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(OneOrFoo())) # revealed: int
|
||||
|
||||
@@ -0,0 +1,748 @@
|
||||
# Intersection types
|
||||
|
||||
## Introduction
|
||||
|
||||
This test suite covers certain properties of intersection types and makes sure that we can apply
|
||||
various simplification strategies. We use `Intersection` (`&`) and `Not` (`~`) to construct
|
||||
intersection types (note that we display negative contributions at the end; the order does not
|
||||
matter):
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Q],
|
||||
i2: Intersection[P, Not[Q]],
|
||||
i3: Intersection[Not[P], Q],
|
||||
i4: Intersection[Not[P], Not[Q]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P & Q
|
||||
reveal_type(i2) # revealed: P & ~Q
|
||||
reveal_type(i3) # revealed: Q & ~P
|
||||
reveal_type(i4) # revealed: ~P & ~Q
|
||||
```
|
||||
|
||||
## Notation
|
||||
|
||||
Throughout this document, we use the following types as representatives for certain equivalence
|
||||
classes.
|
||||
|
||||
### Non-disjoint types
|
||||
|
||||
We use `P`, `Q`, `R`, … to denote types that are non-disjoint:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_disjoint_from
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
static_assert(not is_disjoint_from(P, Q))
|
||||
static_assert(not is_disjoint_from(P, R))
|
||||
static_assert(not is_disjoint_from(Q, R))
|
||||
```
|
||||
|
||||
Although `P` is not a subtype of `Q` and `Q` is not a subtype of `P`, the two types are not disjoint
|
||||
because it would be possible to create a class `S` that inherits from both `P` and `Q` using
|
||||
multiple inheritance. An instance of `S` would be a member of the `P` type _and_ the `Q` type.
|
||||
|
||||
### Disjoint types
|
||||
|
||||
We use `Literal[1]`, `Literal[2]`, … as examples of pairwise-disjoint types, and `int` as a joint
|
||||
supertype of these:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_disjoint_from, is_subtype_of
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_disjoint_from(Literal[1], Literal[2]))
|
||||
static_assert(is_disjoint_from(Literal[1], Literal[3]))
|
||||
static_assert(is_disjoint_from(Literal[2], Literal[3]))
|
||||
|
||||
static_assert(is_subtype_of(Literal[1], int))
|
||||
static_assert(is_subtype_of(Literal[2], int))
|
||||
static_assert(is_subtype_of(Literal[3], int))
|
||||
```
|
||||
|
||||
### Subtypes
|
||||
|
||||
Finally, we use `A <: B <: C` and `A <: B1`, `A <: B2` to denote hierarchies of (proper) subtypes:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_subtype_of, is_disjoint_from
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
|
||||
static_assert(is_subtype_of(B, A))
|
||||
static_assert(is_subtype_of(C, B))
|
||||
static_assert(is_subtype_of(C, A))
|
||||
|
||||
static_assert(not is_subtype_of(A, B))
|
||||
static_assert(not is_subtype_of(B, C))
|
||||
static_assert(not is_subtype_of(A, C))
|
||||
|
||||
class B1(A): ...
|
||||
class B2(A): ...
|
||||
|
||||
static_assert(is_subtype_of(B1, A))
|
||||
static_assert(is_subtype_of(B2, A))
|
||||
|
||||
static_assert(not is_subtype_of(A, B1))
|
||||
static_assert(not is_subtype_of(A, B2))
|
||||
|
||||
static_assert(not is_subtype_of(B1, B2))
|
||||
static_assert(not is_subtype_of(B2, B1))
|
||||
```
|
||||
|
||||
## Structural properties
|
||||
|
||||
This section covers structural properties of intersection types and documents some decisions on how
|
||||
to represent mixtures of intersections and unions.
|
||||
|
||||
### Single-element intersections
|
||||
|
||||
If we have an intersection with a single element, we can simplify to that element. Similarly, we
|
||||
show an intersection with a single negative contribution as just the negation of that element.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P],
|
||||
i2: Intersection[Not[P]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P
|
||||
reveal_type(i2) # revealed: ~P
|
||||
```
|
||||
|
||||
### Flattening of nested intersections
|
||||
|
||||
We eagerly flatten nested intersections types.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
class S: ...
|
||||
|
||||
def positive_contributions(
|
||||
i1: Intersection[P, Intersection[Q, R]],
|
||||
i2: Intersection[Intersection[P, Q], R],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P & Q & R
|
||||
reveal_type(i2) # revealed: P & Q & R
|
||||
|
||||
def negative_contributions(
|
||||
i1: Intersection[Not[P], Intersection[Not[Q], Not[R]]],
|
||||
i2: Intersection[Intersection[Not[P], Not[Q]], Not[R]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: ~P & ~Q & ~R
|
||||
reveal_type(i2) # revealed: ~P & ~Q & ~R
|
||||
|
||||
def mixed(
|
||||
i1: Intersection[P, Intersection[Not[Q], R]],
|
||||
i2: Intersection[Intersection[P, Not[Q]], R],
|
||||
i3: Intersection[Not[P], Intersection[Q, Not[R]]],
|
||||
i4: Intersection[Intersection[Q, Not[R]], Not[P]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P & R & ~Q
|
||||
reveal_type(i2) # revealed: P & R & ~Q
|
||||
reveal_type(i3) # revealed: Q & ~P & ~R
|
||||
reveal_type(i4) # revealed: Q & ~R & ~P
|
||||
|
||||
def multiple(
|
||||
i1: Intersection[Intersection[P, Q], Intersection[R, S]],
|
||||
):
|
||||
reveal_type(i1) # revealed: P & Q & R & S
|
||||
|
||||
def nested(
|
||||
i1: Intersection[Intersection[Intersection[P, Q], R], S],
|
||||
i2: Intersection[P, Intersection[Q, Intersection[R, S]]],
|
||||
):
|
||||
reveal_type(i1) # revealed: P & Q & R & S
|
||||
reveal_type(i2) # revealed: P & Q & R & S
|
||||
```
|
||||
|
||||
### Union of intersections
|
||||
|
||||
We always normalize our representation to a _union of intersections_, so when we add a _union to an
|
||||
intersection_, we distribute the union over the respective elements:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
class S: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Q | R | S],
|
||||
i2: Intersection[P | Q | R, S],
|
||||
i3: Intersection[P | Q, R | S],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P & Q | P & R | P & S
|
||||
reveal_type(i2) # revealed: P & S | Q & S | R & S
|
||||
reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S
|
||||
|
||||
def simplifications_for_same_elements(
|
||||
i1: Intersection[P, Q | P],
|
||||
i2: Intersection[Q, P | Q],
|
||||
i3: Intersection[P | Q, Q | R],
|
||||
i4: Intersection[P | Q, P | Q],
|
||||
i5: Intersection[P | Q, Q | P],
|
||||
) -> None:
|
||||
# P & (Q | P)
|
||||
# = P & Q | P & P
|
||||
# = P & Q | P
|
||||
# = P
|
||||
# (because P is a supertype of P & Q)
|
||||
reveal_type(i1) # revealed: P
|
||||
# similar here:
|
||||
reveal_type(i2) # revealed: Q
|
||||
|
||||
# (P | Q) & (Q | R)
|
||||
# = P & Q | P & R | Q & Q | Q & R
|
||||
# = P & Q | P & R | Q | Q & R
|
||||
# = Q | P & R
|
||||
# (again, because Q is a supertype of P & Q and of Q & R)
|
||||
reveal_type(i3) # revealed: Q | P & R
|
||||
|
||||
# (P | Q) & (P | Q)
|
||||
# = P & P | P & Q | Q & P | Q & Q
|
||||
# = P | P & Q | Q
|
||||
# = P | Q
|
||||
reveal_type(i4) # revealed: P | Q
|
||||
```
|
||||
|
||||
### Negation distributes over union
|
||||
|
||||
Distribution also applies to a negation operation. This is a manifestation of one of
|
||||
[De Morgan's laws], namely `~(P | Q) = ~P & ~Q`:
|
||||
|
||||
```py
|
||||
from knot_extensions import Not
|
||||
from typing import Literal
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
def _(i1: Not[P | Q], i2: Not[P | Q | R]) -> None:
|
||||
reveal_type(i1) # revealed: ~P & ~Q
|
||||
reveal_type(i2) # revealed: ~P & ~Q & ~R
|
||||
|
||||
def example_literals(i: Not[Literal[1, 2]]) -> None:
|
||||
reveal_type(i) # revealed: ~Literal[1] & ~Literal[2]
|
||||
```
|
||||
|
||||
### Negation of intersections
|
||||
|
||||
The other of [De Morgan's laws], `~(P & Q) = ~P | ~Q`, also holds:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
def _(
|
||||
i1: Not[Intersection[P, Q]],
|
||||
i2: Not[Intersection[P, Q, R]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: ~P | ~Q
|
||||
reveal_type(i2) # revealed: ~P | ~Q | ~R
|
||||
```
|
||||
|
||||
### `Never` is dual to `object`
|
||||
|
||||
`Never` represents the empty set of values, while `object` represents the set of all values, so
|
||||
`~Never` is equivalent to `object`, and `~object` is equivalent to `Never`. This is a manifestation
|
||||
of the [complement laws] of set theory.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing_extensions import Never
|
||||
|
||||
def _(
|
||||
not_never: Not[Never],
|
||||
not_object: Not[object],
|
||||
) -> None:
|
||||
reveal_type(not_never) # revealed: object
|
||||
reveal_type(not_object) # revealed: Never
|
||||
```
|
||||
|
||||
### Intersection of a type and its negation
|
||||
|
||||
Continuing with more [complement laws], if we see both `P` and `~P` in an intersection, we can
|
||||
simplify to `Never`, even in the presence of other types:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Any
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Not[P]],
|
||||
i2: Intersection[Not[P], P],
|
||||
i3: Intersection[P, Q, Not[P]],
|
||||
i4: Intersection[Not[P], Q, P],
|
||||
i5: Intersection[P, Any, Not[P]],
|
||||
i6: Intersection[Not[P], Any, P],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Never
|
||||
reveal_type(i2) # revealed: Never
|
||||
reveal_type(i3) # revealed: Never
|
||||
reveal_type(i4) # revealed: Never
|
||||
reveal_type(i5) # revealed: Never
|
||||
reveal_type(i6) # revealed: Never
|
||||
```
|
||||
|
||||
### Union of a type and its negation
|
||||
|
||||
Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `object`.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
def _(
|
||||
i1: P | Not[P],
|
||||
i2: Not[P] | P,
|
||||
i3: P | Q | Not[P],
|
||||
i4: Not[P] | Q | P,
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: object
|
||||
reveal_type(i2) # revealed: object
|
||||
reveal_type(i3) # revealed: object
|
||||
reveal_type(i4) # revealed: object
|
||||
```
|
||||
|
||||
### Negation is an involution
|
||||
|
||||
The final of the [complement laws] states that negating twice is equivalent to not negating at all:
|
||||
|
||||
```py
|
||||
from knot_extensions import Not
|
||||
|
||||
class P: ...
|
||||
|
||||
def _(
|
||||
i1: Not[P],
|
||||
i2: Not[Not[P]],
|
||||
i3: Not[Not[Not[P]]],
|
||||
i4: Not[Not[Not[Not[P]]]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: ~P
|
||||
reveal_type(i2) # revealed: P
|
||||
reveal_type(i3) # revealed: ~P
|
||||
reveal_type(i4) # revealed: P
|
||||
```
|
||||
|
||||
## Simplification strategies
|
||||
|
||||
In this section, we present various simplification strategies that go beyond the structure of the
|
||||
representation.
|
||||
|
||||
### `Never` in intersections
|
||||
|
||||
If we intersect with `Never`, we can simplify the whole intersection to `Never`, even if there are
|
||||
dynamic types involved:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing_extensions import Never, Any
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Never],
|
||||
i2: Intersection[Never, P],
|
||||
i3: Intersection[Any, Never],
|
||||
i4: Intersection[Never, Not[Any]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Never
|
||||
reveal_type(i2) # revealed: Never
|
||||
reveal_type(i3) # revealed: Never
|
||||
reveal_type(i4) # revealed: Never
|
||||
```
|
||||
|
||||
### Simplifications using disjointness
|
||||
|
||||
#### Positive contributions
|
||||
|
||||
If we intersect disjoint types, we can simplify to `Never`, even in the presence of other types:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Literal, Any
|
||||
|
||||
class P: ...
|
||||
|
||||
def _(
|
||||
i01: Intersection[Literal[1], Literal[2]],
|
||||
i02: Intersection[Literal[2], Literal[1]],
|
||||
i03: Intersection[Literal[1], Literal[2], P],
|
||||
i04: Intersection[Literal[1], P, Literal[2]],
|
||||
i05: Intersection[P, Literal[1], Literal[2]],
|
||||
i06: Intersection[Literal[1], Literal[2], Any],
|
||||
i07: Intersection[Literal[1], Any, Literal[2]],
|
||||
i08: Intersection[Any, Literal[1], Literal[2]],
|
||||
) -> None:
|
||||
reveal_type(i01) # revealed: Never
|
||||
reveal_type(i02) # revealed: Never
|
||||
reveal_type(i03) # revealed: Never
|
||||
reveal_type(i04) # revealed: Never
|
||||
reveal_type(i05) # revealed: Never
|
||||
reveal_type(i06) # revealed: Never
|
||||
reveal_type(i07) # revealed: Never
|
||||
reveal_type(i08) # revealed: Never
|
||||
|
||||
# `bool` is final and can not be subclassed, so `type[bool]` is equivalent to `Literal[bool]`, which
|
||||
# is disjoint from `type[str]`:
|
||||
def example_type_bool_type_str(
|
||||
i: Intersection[type[bool], type[str]],
|
||||
) -> None:
|
||||
reveal_type(i) # revealed: Never
|
||||
```
|
||||
|
||||
#### Positive and negative contributions
|
||||
|
||||
If we intersect a type `X` with the negation `~Y` of a disjoint type `Y`, we can remove the negative
|
||||
contribution `~Y`, as `~Y` must fully contain the positive contribution `X` as a subtype:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Literal
|
||||
|
||||
def _(
|
||||
i1: Intersection[Literal[1], Not[Literal[2]]],
|
||||
i2: Intersection[Not[Literal[2]], Literal[1]],
|
||||
i3: Intersection[Literal[1], Not[Literal[2]], int],
|
||||
i4: Intersection[Literal[1], int, Not[Literal[2]]],
|
||||
i5: Intersection[int, Literal[1], Not[Literal[2]]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Literal[1]
|
||||
reveal_type(i2) # revealed: Literal[1]
|
||||
reveal_type(i3) # revealed: Literal[1]
|
||||
reveal_type(i4) # revealed: Literal[1]
|
||||
reveal_type(i5) # revealed: Literal[1]
|
||||
|
||||
# None is disjoint from int, so this simplification applies here
|
||||
def example_none(
|
||||
i1: Intersection[int, Not[None]],
|
||||
i2: Intersection[Not[None], int],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: int
|
||||
reveal_type(i2) # revealed: int
|
||||
```
|
||||
|
||||
### Simplifications using subtype relationships
|
||||
|
||||
#### Positive type and positive subtype
|
||||
|
||||
Subtypes are contained within their supertypes, so we can simplify intersections by removing
|
||||
superfluous supertypes:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _(
|
||||
i01: Intersection[A, B],
|
||||
i02: Intersection[B, A],
|
||||
i03: Intersection[A, C],
|
||||
i04: Intersection[C, A],
|
||||
i05: Intersection[B, C],
|
||||
i06: Intersection[C, B],
|
||||
i07: Intersection[A, B, C],
|
||||
i08: Intersection[C, B, A],
|
||||
i09: Intersection[B, C, A],
|
||||
i10: Intersection[A, B, Unrelated],
|
||||
i11: Intersection[B, A, Unrelated],
|
||||
i12: Intersection[B, Unrelated, A],
|
||||
i13: Intersection[A, Unrelated, B],
|
||||
i14: Intersection[Unrelated, A, B],
|
||||
i15: Intersection[Unrelated, B, A],
|
||||
i16: Intersection[A, B, Any],
|
||||
i17: Intersection[B, A, Any],
|
||||
i18: Intersection[B, Any, A],
|
||||
i19: Intersection[A, Any, B],
|
||||
i20: Intersection[Any, A, B],
|
||||
i21: Intersection[Any, B, A],
|
||||
) -> None:
|
||||
reveal_type(i01) # revealed: B
|
||||
reveal_type(i02) # revealed: B
|
||||
reveal_type(i03) # revealed: C
|
||||
reveal_type(i04) # revealed: C
|
||||
reveal_type(i05) # revealed: C
|
||||
reveal_type(i06) # revealed: C
|
||||
reveal_type(i07) # revealed: C
|
||||
reveal_type(i08) # revealed: C
|
||||
reveal_type(i09) # revealed: C
|
||||
reveal_type(i10) # revealed: B & Unrelated
|
||||
reveal_type(i11) # revealed: B & Unrelated
|
||||
reveal_type(i12) # revealed: B & Unrelated
|
||||
reveal_type(i13) # revealed: Unrelated & B
|
||||
reveal_type(i14) # revealed: Unrelated & B
|
||||
reveal_type(i15) # revealed: Unrelated & B
|
||||
reveal_type(i16) # revealed: B & Any
|
||||
reveal_type(i17) # revealed: B & Any
|
||||
reveal_type(i18) # revealed: B & Any
|
||||
reveal_type(i19) # revealed: Any & B
|
||||
reveal_type(i20) # revealed: Any & B
|
||||
reveal_type(i21) # revealed: Any & B
|
||||
```
|
||||
|
||||
#### Negative type and negative subtype
|
||||
|
||||
For negative contributions, this property is reversed. Here we can remove superfluous _subtypes_:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _(
|
||||
i01: Intersection[Not[B], Not[A]],
|
||||
i02: Intersection[Not[A], Not[B]],
|
||||
i03: Intersection[Not[A], Not[C]],
|
||||
i04: Intersection[Not[C], Not[A]],
|
||||
i05: Intersection[Not[B], Not[C]],
|
||||
i06: Intersection[Not[C], Not[B]],
|
||||
i07: Intersection[Not[A], Not[B], Not[C]],
|
||||
i08: Intersection[Not[C], Not[B], Not[A]],
|
||||
i09: Intersection[Not[B], Not[C], Not[A]],
|
||||
i10: Intersection[Not[B], Not[A], Unrelated],
|
||||
i11: Intersection[Not[A], Not[B], Unrelated],
|
||||
i12: Intersection[Not[A], Unrelated, Not[B]],
|
||||
i13: Intersection[Not[B], Unrelated, Not[A]],
|
||||
i14: Intersection[Unrelated, Not[A], Not[B]],
|
||||
i15: Intersection[Unrelated, Not[B], Not[A]],
|
||||
i16: Intersection[Not[B], Not[A], Any],
|
||||
i17: Intersection[Not[A], Not[B], Any],
|
||||
i18: Intersection[Not[A], Any, Not[B]],
|
||||
i19: Intersection[Not[B], Any, Not[A]],
|
||||
i20: Intersection[Any, Not[A], Not[B]],
|
||||
i21: Intersection[Any, Not[B], Not[A]],
|
||||
) -> None:
|
||||
reveal_type(i01) # revealed: ~A
|
||||
reveal_type(i02) # revealed: ~A
|
||||
reveal_type(i03) # revealed: ~A
|
||||
reveal_type(i04) # revealed: ~A
|
||||
reveal_type(i05) # revealed: ~B
|
||||
reveal_type(i06) # revealed: ~B
|
||||
reveal_type(i07) # revealed: ~A
|
||||
reveal_type(i08) # revealed: ~A
|
||||
reveal_type(i09) # revealed: ~A
|
||||
reveal_type(i10) # revealed: Unrelated & ~A
|
||||
reveal_type(i11) # revealed: Unrelated & ~A
|
||||
reveal_type(i12) # revealed: Unrelated & ~A
|
||||
reveal_type(i13) # revealed: Unrelated & ~A
|
||||
reveal_type(i14) # revealed: Unrelated & ~A
|
||||
reveal_type(i15) # revealed: Unrelated & ~A
|
||||
reveal_type(i16) # revealed: Any & ~A
|
||||
reveal_type(i17) # revealed: Any & ~A
|
||||
reveal_type(i18) # revealed: Any & ~A
|
||||
reveal_type(i19) # revealed: Any & ~A
|
||||
reveal_type(i20) # revealed: Any & ~A
|
||||
reveal_type(i21) # revealed: Any & ~A
|
||||
```
|
||||
|
||||
#### Negative type and multiple negative subtypes
|
||||
|
||||
If there are multiple negative subtypes, all of them can be removed:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class A: ...
|
||||
class B1(A): ...
|
||||
class B2(A): ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[Not[A], Not[B1], Not[B2]],
|
||||
i2: Intersection[Not[A], Not[B2], Not[B1]],
|
||||
i3: Intersection[Not[B1], Not[A], Not[B2]],
|
||||
i4: Intersection[Not[B1], Not[B2], Not[A]],
|
||||
i5: Intersection[Not[B2], Not[A], Not[B1]],
|
||||
i6: Intersection[Not[B2], Not[B1], Not[A]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: ~A
|
||||
reveal_type(i2) # revealed: ~A
|
||||
reveal_type(i3) # revealed: ~A
|
||||
reveal_type(i4) # revealed: ~A
|
||||
reveal_type(i5) # revealed: ~A
|
||||
reveal_type(i6) # revealed: ~A
|
||||
```
|
||||
|
||||
#### Negative type and positive subtype
|
||||
|
||||
When `A` is a supertype of `B`, its negation `~A` is disjoint from `B`, so we can simplify the
|
||||
intersection to `Never`:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[Not[A], B],
|
||||
i2: Intersection[B, Not[A]],
|
||||
i3: Intersection[Not[A], C],
|
||||
i4: Intersection[C, Not[A]],
|
||||
i5: Intersection[Unrelated, Not[A], B],
|
||||
i6: Intersection[B, Not[A], Not[Unrelated]],
|
||||
i7: Intersection[Any, Not[A], B],
|
||||
i8: Intersection[B, Not[A], Not[Any]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Never
|
||||
reveal_type(i2) # revealed: Never
|
||||
reveal_type(i3) # revealed: Never
|
||||
reveal_type(i4) # revealed: Never
|
||||
reveal_type(i5) # revealed: Never
|
||||
reveal_type(i6) # revealed: Never
|
||||
reveal_type(i7) # revealed: Never
|
||||
reveal_type(i8) # revealed: Never
|
||||
```
|
||||
|
||||
## Non fully-static types
|
||||
|
||||
### Negation of dynamic types
|
||||
|
||||
`Any` represents the dynamic type, an unknown set of runtime values. The negation of that, `~Any`,
|
||||
is still an unknown set of runtime values, so `~Any` is equivalent to `Any`. We therefore eagerly
|
||||
simplify `~Any` to `Any` in intersections. The same applies to `Unknown`.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, Unknown
|
||||
from typing_extensions import Any, Never
|
||||
|
||||
class P: ...
|
||||
|
||||
def any(
|
||||
i1: Not[Any],
|
||||
i2: Intersection[P, Not[Any]],
|
||||
i3: Intersection[Never, Not[Any]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Any
|
||||
reveal_type(i2) # revealed: P & Any
|
||||
reveal_type(i3) # revealed: Never
|
||||
|
||||
def unknown(
|
||||
i1: Not[Unknown],
|
||||
i2: Intersection[P, Not[Unknown]],
|
||||
i3: Intersection[Never, Not[Unknown]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Unknown
|
||||
reveal_type(i2) # revealed: P & Unknown
|
||||
reveal_type(i3) # revealed: Never
|
||||
```
|
||||
|
||||
### Collapsing of multiple `Any`/`Unknown` contributions
|
||||
|
||||
The intersection of an unknown set of runtime values with (another) unknown set of runtime values is
|
||||
still an unknown set of runtime values:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, Unknown
|
||||
from typing_extensions import Any
|
||||
|
||||
class P: ...
|
||||
|
||||
def any(
|
||||
i1: Intersection[Any, Any],
|
||||
i2: Intersection[P, Any, Any],
|
||||
i3: Intersection[Any, P, Any],
|
||||
i4: Intersection[Any, Any, P],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Any
|
||||
reveal_type(i2) # revealed: P & Any
|
||||
reveal_type(i3) # revealed: Any & P
|
||||
reveal_type(i4) # revealed: Any & P
|
||||
|
||||
def unknown(
|
||||
i1: Intersection[Unknown, Unknown],
|
||||
i2: Intersection[P, Unknown, Unknown],
|
||||
i3: Intersection[Unknown, P, Unknown],
|
||||
i4: Intersection[Unknown, Unknown, P],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Unknown
|
||||
reveal_type(i2) # revealed: P & Unknown
|
||||
reveal_type(i3) # revealed: Unknown & P
|
||||
reveal_type(i4) # revealed: Unknown & P
|
||||
```
|
||||
|
||||
### No self-cancellation
|
||||
|
||||
Dynamic types do not cancel each other out. Intersecting an unknown set of values with the negation
|
||||
of another unknown set of values is not necessarily empty, so we keep the positive contribution:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, Unknown
|
||||
|
||||
def any(
|
||||
i1: Intersection[Any, Not[Any]],
|
||||
i2: Intersection[Not[Any], Any],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Any
|
||||
reveal_type(i2) # revealed: Any
|
||||
|
||||
def unknown(
|
||||
i1: Intersection[Unknown, Not[Unknown]],
|
||||
i2: Intersection[Not[Unknown], Unknown],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Unknown
|
||||
reveal_type(i2) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Mixed dynamic types
|
||||
|
||||
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, Unknown
|
||||
|
||||
def mixed(
|
||||
i1: Intersection[Any, Unknown],
|
||||
i2: Intersection[Any, Not[Unknown]],
|
||||
i3: Intersection[Not[Any], Unknown],
|
||||
i4: Intersection[Not[Any], Not[Unknown]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Any & Unknown
|
||||
reveal_type(i2) # revealed: Any & Unknown
|
||||
reveal_type(i3) # revealed: Any & Unknown
|
||||
reveal_type(i4) # revealed: Any & Unknown
|
||||
```
|
||||
|
||||
[complement laws]: https://en.wikipedia.org/wiki/Complement_(set_theory)
|
||||
[de morgan's laws]: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
|
||||
@@ -98,7 +98,7 @@ reveal_type(x)
|
||||
for x in (1, "a", b"foo"):
|
||||
pass
|
||||
|
||||
# revealed: Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
# revealed: Literal[1, "a", b"foo"]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
@@ -41,7 +41,7 @@ def _(flag: bool, flag2: bool):
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
reveal_type(y) # revealed: Literal[4, 1, 2]
|
||||
```
|
||||
|
||||
## Nested `while` loops
|
||||
|
||||
@@ -170,8 +170,35 @@ def f(*args, **kwargs) -> int: ...
|
||||
|
||||
class A(metaclass=f): ...
|
||||
|
||||
# TODO should be `type[int]`
|
||||
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
|
||||
# TODO: Should be `int`
|
||||
reveal_type(A) # revealed: Literal[A]
|
||||
reveal_type(A.__class__) # revealed: type[int]
|
||||
|
||||
def _(n: int):
|
||||
# error: [invalid-metaclass]
|
||||
class B(metaclass=n): ...
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(B) # revealed: Literal[B]
|
||||
reveal_type(B.__class__) # revealed: type[Unknown]
|
||||
|
||||
def _(flag: bool):
|
||||
m = f if flag else 42
|
||||
|
||||
# error: [invalid-metaclass]
|
||||
class C(metaclass=m): ...
|
||||
# TODO: Should be `int | Unknown`
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
reveal_type(C.__class__) # revealed: type[Unknown]
|
||||
|
||||
class SignatureMismatch: ...
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
class D(metaclass=SignatureMismatch): ...
|
||||
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(D) # revealed: Literal[D]
|
||||
# TODO: Should be `type[Unknown]`
|
||||
reveal_type(D.__class__) # revealed: Literal[SignatureMismatch]
|
||||
```
|
||||
|
||||
## Cyclic
|
||||
|
||||
@@ -56,7 +56,7 @@ def _(x_flag: bool, y_flag: bool):
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = None if flag1 else (1 if flag2 else True)
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
|
||||
reveal_type(x) # revealed: None | Literal[1, True]
|
||||
if x is None:
|
||||
reveal_type(x) # revealed: None
|
||||
elif x is True:
|
||||
|
||||
@@ -17,7 +17,7 @@ def _(flag: bool):
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
```
|
||||
|
||||
## `classinfo` is a tuple of types
|
||||
@@ -30,7 +30,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, (int, str)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
@@ -43,19 +43,19 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
# No narrowing should occur if a larger type is also
|
||||
# one of the possibilities:
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
y = 1 if flag1 else "a" if flag2 else b"b"
|
||||
if isinstance(y, (int, str)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(y) # revealed: Literal[1, "a"]
|
||||
|
||||
if isinstance(y, (int, bytes)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
|
||||
reveal_type(y) # revealed: Literal[1, b"b"]
|
||||
|
||||
if isinstance(y, (str, bytes)):
|
||||
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
|
||||
reveal_type(y) # revealed: Literal["a", b"b"]
|
||||
```
|
||||
|
||||
## `classinfo` is a nested tuple of types
|
||||
@@ -107,7 +107,7 @@ def _(flag: bool):
|
||||
x = 1 if flag else "foo"
|
||||
|
||||
if isinstance(x, t):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
|
||||
reveal_type(x) # revealed: Literal[1, "foo"]
|
||||
```
|
||||
|
||||
## Do not use custom `isinstance` for narrowing
|
||||
@@ -119,7 +119,7 @@ def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
```
|
||||
|
||||
## Do support narrowing if `isinstance` is aliased
|
||||
@@ -155,12 +155,12 @@ def _(flag: bool):
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "a"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "int"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
```
|
||||
|
||||
## Do not narrow if there are keyword arguments
|
||||
@@ -169,8 +169,15 @@ def _(flag: bool):
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`isinstance` has no `foo` parameter)
|
||||
# error: [unknown-argument]
|
||||
if isinstance(x, int, foo="bar"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(x) # revealed: Literal[1, "a"]
|
||||
```
|
||||
|
||||
## `type[]` types are narrowed as well as class-literal types
|
||||
|
||||
```py
|
||||
def _(x: object, y: type[int]):
|
||||
if isinstance(x, y):
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
@@ -90,7 +90,7 @@ def _(t: type[object]):
|
||||
if issubclass(t, B):
|
||||
reveal_type(t) # revealed: type[A] & type[B]
|
||||
else:
|
||||
reveal_type(t) # revealed: type[object] & ~type[A]
|
||||
reveal_type(t) # revealed: type & ~type[A]
|
||||
```
|
||||
|
||||
### Handling of `None`
|
||||
@@ -146,7 +146,7 @@ class A: ...
|
||||
|
||||
t = object()
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
# error: [invalid-argument-type]
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
```
|
||||
@@ -160,7 +160,7 @@ branch:
|
||||
```py
|
||||
t = 1
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
# error: [invalid-argument-type]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
@@ -234,8 +234,15 @@ def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`issubclass` has no `foo` parameter)
|
||||
# error: [unknown-argument]
|
||||
if issubclass(t, int, foo="bar"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
|
||||
### `type[]` types are narrowed as well as class-literal types
|
||||
|
||||
```py
|
||||
def _(x: type, y: type[int]):
|
||||
if issubclass(x, y):
|
||||
reveal_type(x) # revealed: type[int]
|
||||
```
|
||||
|
||||
@@ -16,3 +16,48 @@ def _(flag: bool):
|
||||
|
||||
reveal_type(y) # revealed: Literal[0] | None
|
||||
```
|
||||
|
||||
## Class patterns
|
||||
|
||||
```py
|
||||
def get_object() -> object: ...
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case A():
|
||||
reveal_type(x) # revealed: A
|
||||
case B():
|
||||
# TODO could be `B & ~A`
|
||||
reveal_type(x) # revealed: B
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
## Class pattern with guard
|
||||
|
||||
```py
|
||||
def get_object() -> object: ...
|
||||
|
||||
class A:
|
||||
def y() -> int: ...
|
||||
|
||||
class B: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case A() if reveal_type(x): # revealed: A
|
||||
pass
|
||||
case B() if reveal_type(x): # revealed: B
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
@@ -9,39 +9,39 @@ def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[(
|
||||
x = foo()
|
||||
|
||||
if x:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
|
||||
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
|
||||
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
|
||||
|
||||
if not x:
|
||||
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
|
||||
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
|
||||
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]
|
||||
|
||||
if x and not x:
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
|
||||
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
|
||||
|
||||
if not (x and not x):
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
|
||||
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if x or not x:
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
|
||||
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if not (x or not x):
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
|
||||
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
|
||||
|
||||
if (isinstance(x, int) or isinstance(x, str)) and x:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"]
|
||||
reveal_type(x) # revealed: Literal[-1, True, "foo"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[b"", b"bar"] | None | tuple[()] | Literal[0] | Literal[False] | Literal[""]
|
||||
reveal_type(x) # revealed: Literal[b"", b"bar", 0, False, ""] | None | tuple[()]
|
||||
```
|
||||
|
||||
## Function Literals
|
||||
@@ -87,10 +87,10 @@ def f(x: A | B):
|
||||
if x and not x:
|
||||
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
|
||||
else:
|
||||
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy | A & ~AlwaysFalsy | B & ~AlwaysFalsy
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if x or not x:
|
||||
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy | A & ~AlwaysTruthy | B & ~AlwaysTruthy
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
|
||||
```
|
||||
@@ -166,16 +166,16 @@ y = literals()
|
||||
|
||||
if isinstance(x, str) and not isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & str & ~B
|
||||
reveal_type(y) # revealed: Literal[0, 42] | Literal["", "hello"]
|
||||
reveal_type(y) # revealed: Literal[0, 42, "", "hello"]
|
||||
|
||||
z = x if flag() else y
|
||||
|
||||
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42] | Literal["", "hello"]
|
||||
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"]
|
||||
|
||||
if z:
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42] | Literal["hello"]
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"]
|
||||
else:
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0] | Literal[""]
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""]
|
||||
```
|
||||
|
||||
## Narrowing Multiple Variables
|
||||
@@ -214,10 +214,9 @@ if x and not x:
|
||||
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
|
||||
else:
|
||||
y = x
|
||||
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
|
||||
reveal_type(y) # revealed: A
|
||||
|
||||
# TODO: It should be A. We should improve UnionBuilder or IntersectionBuilder. (issue #15023)
|
||||
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
|
||||
reveal_type(y) # revealed: A
|
||||
```
|
||||
|
||||
## Truthiness of classes
|
||||
|
||||
@@ -37,7 +37,7 @@ class C:
|
||||
# error: [possibly-unresolved-reference]
|
||||
y = x
|
||||
|
||||
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
|
||||
reveal_type(C.y) # revealed: Literal[1, "abc"]
|
||||
```
|
||||
|
||||
## Unbound function local
|
||||
|
||||
@@ -167,7 +167,7 @@ class A:
|
||||
__slots__ = ()
|
||||
__slots__ += ("a", "b")
|
||||
|
||||
reveal_type(A.__slots__) # revealed: @Todo(Support for more binary expressions)
|
||||
reveal_type(A.__slots__) # revealed: @Todo(return type)
|
||||
|
||||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Ellipsis
|
||||
|
||||
## Function and methods
|
||||
|
||||
The ellipsis literal `...` can be used as a placeholder default value for a function parameter, in a
|
||||
stub file only, regardless of the type of the parameter.
|
||||
|
||||
```py path=test.pyi
|
||||
def f(x: int = ...) -> None:
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def f2(x: str = ...) -> None:
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
## Class and module symbols
|
||||
|
||||
The ellipsis literal can be assigned to a class or module symbol, regardless of its declared type,
|
||||
in a stub file only.
|
||||
|
||||
```py path=test.pyi
|
||||
y: bytes = ...
|
||||
reveal_type(y) # revealed: bytes
|
||||
x = ...
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
class Foo:
|
||||
y: int = ...
|
||||
|
||||
reveal_type(Foo.y) # revealed: int
|
||||
```
|
||||
|
||||
## Unpacking ellipsis literal in assignment
|
||||
|
||||
No diagnostic is emitted if an ellipsis literal is "unpacked" in a stub file as part of an
|
||||
assignment statement:
|
||||
|
||||
```py path=test.pyi
|
||||
x, y = ...
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unpacking ellipsis literal in for loops
|
||||
|
||||
Iterating over an ellipsis literal as part of a `for` loop in a stub is invalid, however, and
|
||||
results in a diagnostic:
|
||||
|
||||
```py path=test.pyi
|
||||
# error: [not-iterable] "Object of type `ellipsis` is not iterable"
|
||||
for a, b in ...:
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Ellipsis usage in non stub file
|
||||
|
||||
In a non-stub file, there's no special treatment of ellipsis literals. An ellipsis literal can only
|
||||
be assigned if `EllipsisType` is actually assignable to the annotated type.
|
||||
|
||||
```py
|
||||
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
|
||||
def f(x: int = ...) -> None: ...
|
||||
|
||||
# error: 1 [invalid-assignment] "Object of type `ellipsis` is not assignable to `int`"
|
||||
a: int = ...
|
||||
b = ...
|
||||
reveal_type(b) # revealed: ellipsis
|
||||
```
|
||||
|
||||
## Use of `Ellipsis` symbol
|
||||
|
||||
There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals.
|
||||
|
||||
```py path=test.pyi
|
||||
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
|
||||
def f(x: int = Ellipsis) -> None: ...
|
||||
```
|
||||
352
crates/red_knot_python_semantic/resources/mdtest/type_api.md
Normal file
352
crates/red_knot_python_semantic/resources/mdtest/type_api.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# Type API (`knot_extensions`)
|
||||
|
||||
This document describes the internal `knot_extensions` API for creating and manipulating types as
|
||||
well as testing various type system properties.
|
||||
|
||||
## Type extensions
|
||||
|
||||
The Python language itself allows us to perform a variety of operations on types. For example, we
|
||||
can build a union of types like `int | None`, or we can use type constructors such as `list[int]`
|
||||
and `type[int]` to create new types. But some type-level operations that we rely on in Red Knot,
|
||||
like intersections, cannot yet be expressed in Python. The `knot_extensions` module provides the
|
||||
`Intersection` and `Not` type constructors (special forms) which allow us to construct these types
|
||||
directly.
|
||||
|
||||
### Negation
|
||||
|
||||
```py
|
||||
from knot_extensions import Not, static_assert
|
||||
|
||||
def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None:
|
||||
reveal_type(n1) # revealed: ~int
|
||||
reveal_type(n2) # revealed: int
|
||||
reveal_type(n3) # revealed: ~int
|
||||
|
||||
def static_truthiness(not_one: Not[Literal[1]]) -> None:
|
||||
static_assert(not_one != 1)
|
||||
static_assert(not (not_one == 1))
|
||||
|
||||
# error: "Special form `knot_extensions.Not` expected exactly one type parameter"
|
||||
n: Not[int, str]
|
||||
```
|
||||
|
||||
### Intersection
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
|
||||
from typing_extensions import Never
|
||||
|
||||
class S: ...
|
||||
class T: ...
|
||||
|
||||
def x(x1: Intersection[S, T], x2: Intersection[S, Not[T]]) -> None:
|
||||
reveal_type(x1) # revealed: S & T
|
||||
reveal_type(x2) # revealed: S & ~T
|
||||
|
||||
def y(y1: Intersection[int, object], y2: Intersection[int, bool], y3: Intersection[int, Never]) -> None:
|
||||
reveal_type(y1) # revealed: int
|
||||
reveal_type(y2) # revealed: bool
|
||||
reveal_type(y3) # revealed: Never
|
||||
|
||||
def z(z1: Intersection[int, Not[Literal[1]], Not[Literal[2]]]) -> None:
|
||||
reveal_type(z1) # revealed: int & ~Literal[1] & ~Literal[2]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
type ABC = Intersection[A, B, C]
|
||||
|
||||
static_assert(is_subtype_of(ABC, A))
|
||||
static_assert(is_subtype_of(ABC, B))
|
||||
static_assert(is_subtype_of(ABC, C))
|
||||
|
||||
class D: ...
|
||||
|
||||
static_assert(not is_subtype_of(ABC, D))
|
||||
```
|
||||
|
||||
### Unknown type
|
||||
|
||||
The `Unknown` type is a special type that we use to represent actually unknown types (no
|
||||
annotation), as opposed to `Any` which represents an explicitly unknown type.
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown, static_assert, is_assignable_to, is_fully_static
|
||||
|
||||
static_assert(is_assignable_to(Unknown, int))
|
||||
static_assert(is_assignable_to(int, Unknown))
|
||||
|
||||
static_assert(not is_fully_static(Unknown))
|
||||
|
||||
def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: tuple[str, Unknown]
|
||||
reveal_type(z) # revealed: Unknown | Literal[1]
|
||||
|
||||
# Unknown can be subclassed, just like Any
|
||||
class C(Unknown): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
reveal_type(C.__mro__)
|
||||
|
||||
# error: "Special form `knot_extensions.Unknown` expected no type parameter"
|
||||
u: Unknown[str]
|
||||
```
|
||||
|
||||
## Static assertions
|
||||
|
||||
### Basics
|
||||
|
||||
The `knot_extensions` module provides a `static_assert` function that can be used to enforce
|
||||
properties at type-check time. The function takes an arbitrary expression and raises a type error if
|
||||
the expression is not of statically known truthiness.
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
from typing import TYPE_CHECKING
|
||||
import sys
|
||||
|
||||
static_assert(True)
|
||||
static_assert(False) # error: "Static assertion error: argument evaluates to `False`"
|
||||
|
||||
static_assert(False or True)
|
||||
static_assert(True and True)
|
||||
static_assert(False or False) # error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(False and True) # error: "Static assertion error: argument evaluates to `False`"
|
||||
|
||||
static_assert(1 + 1 == 2)
|
||||
static_assert(1 + 1 == 3) # error: "Static assertion error: argument evaluates to `False`"
|
||||
|
||||
static_assert("a" in "abc")
|
||||
static_assert("d" in "abc") # error: "Static assertion error: argument evaluates to `False`"
|
||||
|
||||
n = None
|
||||
static_assert(n is None)
|
||||
|
||||
static_assert(TYPE_CHECKING)
|
||||
|
||||
static_assert(sys.version_info >= (3, 6))
|
||||
```
|
||||
|
||||
### Narrowing constraints
|
||||
|
||||
Static assertions can be used to enforce narrowing constraints:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
|
||||
def f(x: int) -> None:
|
||||
if x != 0:
|
||||
static_assert(x != 0)
|
||||
else:
|
||||
# `int` can be subclassed, so we cannot assert that `x == 0` here:
|
||||
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
|
||||
static_assert(x == 0)
|
||||
```
|
||||
|
||||
### Truthy expressions
|
||||
|
||||
See also: <https://docs.python.org/3/library/stdtypes.html#truth-value-testing>
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
|
||||
static_assert(True)
|
||||
static_assert(False) # error: "Static assertion error: argument evaluates to `False`"
|
||||
|
||||
static_assert(None) # error: "Static assertion error: argument of type `None` is statically known to be falsy"
|
||||
|
||||
static_assert(1)
|
||||
static_assert(0) # error: "Static assertion error: argument of type `Literal[0]` is statically known to be falsy"
|
||||
|
||||
static_assert((0,))
|
||||
static_assert(()) # error: "Static assertion error: argument of type `tuple[()]` is statically known to be falsy"
|
||||
|
||||
static_assert("a")
|
||||
static_assert("") # error: "Static assertion error: argument of type `Literal[""]` is statically known to be falsy"
|
||||
|
||||
static_assert(b"a")
|
||||
static_assert(b"") # error: "Static assertion error: argument of type `Literal[b""]` is statically known to be falsy"
|
||||
```
|
||||
|
||||
### Error messages
|
||||
|
||||
We provide various tailored error messages for wrong argument types to `static_assert`:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
|
||||
static_assert(2 * 3 == 6)
|
||||
|
||||
# error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(2 * 3 == 7)
|
||||
|
||||
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
|
||||
static_assert(int(2.0 * 3.0) == 6)
|
||||
|
||||
class InvalidBoolDunder:
|
||||
def __bool__(self) -> int:
|
||||
return 1
|
||||
|
||||
# error: "Static assertion error: argument of type `InvalidBoolDunder` has an ambiguous static truthiness"
|
||||
static_assert(InvalidBoolDunder())
|
||||
```
|
||||
|
||||
### Custom error messages
|
||||
|
||||
Alternatively, users can provide custom error messages:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert
|
||||
|
||||
# error: "Static assertion error: I really want this to be true"
|
||||
static_assert(1 + 1 == 3, "I really want this to be true")
|
||||
|
||||
error_message = "A custom message "
|
||||
error_message += "constructed from multiple string literals"
|
||||
# error: "Static assertion error: A custom message constructed from multiple string literals"
|
||||
static_assert(False, error_message)
|
||||
|
||||
# There are limitations to what we can still infer as a string literal. In those cases,
|
||||
# we simply fall back to the default message.
|
||||
shouted_message = "A custom message".upper()
|
||||
# error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(False, shouted_message)
|
||||
```
|
||||
|
||||
## Type predicates
|
||||
|
||||
The `knot_extensions` module also provides predicates to test various properties of types. These are
|
||||
implemented as functions that return `Literal[True]` or `Literal[False]` depending on the result of
|
||||
the test.
|
||||
|
||||
### Equivalence
|
||||
|
||||
```py
|
||||
from knot_extensions import is_equivalent_to, static_assert
|
||||
from typing_extensions import Never, Union
|
||||
|
||||
static_assert(is_equivalent_to(type, type[object]))
|
||||
static_assert(is_equivalent_to(tuple[int, Never], Never))
|
||||
static_assert(is_equivalent_to(int | str, Union[int, str]))
|
||||
|
||||
static_assert(not is_equivalent_to(int, str))
|
||||
static_assert(not is_equivalent_to(int | str, int | str | bytes))
|
||||
```
|
||||
|
||||
### Subtyping
|
||||
|
||||
```py
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(bool, int))
|
||||
static_assert(not is_subtype_of(str, int))
|
||||
|
||||
static_assert(is_subtype_of(bool, int | str))
|
||||
static_assert(is_subtype_of(str, int | str))
|
||||
static_assert(not is_subtype_of(bytes, int | str))
|
||||
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
static_assert(is_subtype_of(Derived, Base))
|
||||
static_assert(not is_subtype_of(Base, Derived))
|
||||
static_assert(is_subtype_of(Base, Base))
|
||||
|
||||
static_assert(not is_subtype_of(Unrelated, Base))
|
||||
static_assert(not is_subtype_of(Base, Unrelated))
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
```py
|
||||
from knot_extensions import is_assignable_to, static_assert
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_assignable_to(int, Any))
|
||||
static_assert(is_assignable_to(Any, str))
|
||||
static_assert(not is_assignable_to(int, str))
|
||||
```
|
||||
|
||||
### Disjointness
|
||||
|
||||
```py
|
||||
from knot_extensions import is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(None, int))
|
||||
static_assert(not is_disjoint_from(Literal[2] | str, int))
|
||||
```
|
||||
|
||||
### Fully static types
|
||||
|
||||
```py
|
||||
from knot_extensions import is_fully_static, static_assert
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_fully_static(int | str))
|
||||
static_assert(is_fully_static(type[int]))
|
||||
|
||||
static_assert(not is_fully_static(int | Any))
|
||||
static_assert(not is_fully_static(type[Any]))
|
||||
```
|
||||
|
||||
### Singleton types
|
||||
|
||||
```py
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(None))
|
||||
static_assert(is_singleton(Literal[True]))
|
||||
|
||||
static_assert(not is_singleton(int))
|
||||
static_assert(not is_singleton(Literal["a"]))
|
||||
```
|
||||
|
||||
### Single-valued types
|
||||
|
||||
```py
|
||||
from knot_extensions import is_single_valued, static_assert
|
||||
|
||||
static_assert(is_single_valued(None))
|
||||
static_assert(is_single_valued(Literal[True]))
|
||||
static_assert(is_single_valued(Literal["a"]))
|
||||
|
||||
static_assert(not is_single_valued(int))
|
||||
static_assert(not is_single_valued(Literal["a"] | Literal["b"]))
|
||||
```
|
||||
|
||||
## `TypeOf`
|
||||
|
||||
We use `TypeOf` to get the inferred type of an expression. This is useful when we want to refer to
|
||||
it in a type expression. For example, if we want to make sure that the class literal type `str` is a
|
||||
subtype of `type[str]`, we can not use `is_subtype_of(str, type[str])`, as that would test if the
|
||||
type `str` itself is a subtype of `type[str]`. Instead, we can use `TypeOf[str]` to get the type of
|
||||
the expression `str`:
|
||||
|
||||
```py
|
||||
from knot_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
# This is incorrect and therefore fails with ...
|
||||
# error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(is_subtype_of(str, type[str]))
|
||||
|
||||
# Correct, returns True:
|
||||
static_assert(is_subtype_of(TypeOf[str], type[str]))
|
||||
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
|
||||
# `TypeOf` can be used in annotations:
|
||||
def type_of_annotation() -> None:
|
||||
t1: TypeOf[Base] = Base
|
||||
t2: TypeOf[Base] = Derived # error: [invalid-assignment]
|
||||
|
||||
# Note how this is different from `type[…]` which includes subclasses:
|
||||
s1: type[Base] = Base
|
||||
s2: type[Base] = Derived # no error here
|
||||
|
||||
# error: "Special form `knot_extensions.TypeOf` expected exactly one type parameter"
|
||||
t: TypeOf[int, str, bytes]
|
||||
```
|
||||
@@ -142,3 +142,25 @@ class Foo(type[int]): ...
|
||||
# TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `@final` classes
|
||||
|
||||
`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is
|
||||
used as the type argument. This applies to standard-library classes and user-defined classes:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from types import EllipsisType
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class Foo: ...
|
||||
|
||||
def _(x: type[Foo], y: type[EllipsisType]):
|
||||
reveal_type(x) # revealed: Literal[Foo]
|
||||
reveal_type(y) # revealed: Literal[EllipsisType]
|
||||
```
|
||||
|
||||
@@ -47,9 +47,8 @@ x: type = A() # error: [invalid-assignment]
|
||||
|
||||
```py
|
||||
def f(x: type[object]):
|
||||
reveal_type(x) # revealed: type[object]
|
||||
# TODO: bound method types
|
||||
reveal_type(x.__repr__) # revealed: Literal[__repr__]
|
||||
reveal_type(x) # revealed: type
|
||||
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
|
||||
|
||||
class A: ...
|
||||
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
# Assignable-to relation
|
||||
|
||||
The `is_assignable_to(S, T)` relation below checks if type `S` is assignable to type `T` (target).
|
||||
This allows us to check if a type `S` can be used in a context where a type `T` is expected
|
||||
(function arguments, variable assignments). See the [typing documentation] for a precise definition
|
||||
of this concept.
|
||||
|
||||
## Basic types
|
||||
|
||||
### Fully static
|
||||
|
||||
Fully static types participate in subtyping. If a type `S` is a subtype of `T`, `S` will also be
|
||||
assignable to `T`. Two equivalent types are subtypes of each other:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
|
||||
class Parent: ...
|
||||
class Child1(Parent): ...
|
||||
class Child2(Parent): ...
|
||||
class Grandchild(Child1, Child2): ...
|
||||
class Unrelated: ...
|
||||
|
||||
static_assert(is_assignable_to(int, int))
|
||||
static_assert(is_assignable_to(Parent, Parent))
|
||||
static_assert(is_assignable_to(Child1, Parent))
|
||||
static_assert(is_assignable_to(Grandchild, Parent))
|
||||
static_assert(is_assignable_to(Unrelated, Unrelated))
|
||||
|
||||
static_assert(not is_assignable_to(str, int))
|
||||
static_assert(not is_assignable_to(object, int))
|
||||
static_assert(not is_assignable_to(Parent, Child1))
|
||||
static_assert(not is_assignable_to(Unrelated, Parent))
|
||||
static_assert(not is_assignable_to(Child1, Child2))
|
||||
```
|
||||
|
||||
### Gradual types
|
||||
|
||||
Gradual types do not participate in subtyping, but can still be assignable to other types (and
|
||||
static types can be assignable to gradual types):
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_assignable_to(Unknown, Literal[1]))
|
||||
static_assert(is_assignable_to(Any, Literal[1]))
|
||||
static_assert(is_assignable_to(Literal[1], Unknown))
|
||||
static_assert(is_assignable_to(Literal[1], Any))
|
||||
```
|
||||
|
||||
## Literal types
|
||||
|
||||
### Boolean literals
|
||||
|
||||
`Literal[True]` and `Literal[False]` are both subtypes of (and therefore assignable to) `bool`,
|
||||
which is in turn a subtype of `int`:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_assignable_to(Literal[True], Literal[True]))
|
||||
static_assert(is_assignable_to(Literal[True], bool))
|
||||
static_assert(is_assignable_to(Literal[True], int))
|
||||
|
||||
static_assert(not is_assignable_to(Literal[True], Literal[False]))
|
||||
static_assert(not is_assignable_to(bool, Literal[True]))
|
||||
```
|
||||
|
||||
### Integer literals
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_assignable_to(Literal[1], Literal[1]))
|
||||
static_assert(is_assignable_to(Literal[1], int))
|
||||
|
||||
static_assert(not is_assignable_to(Literal[1], Literal[2]))
|
||||
static_assert(not is_assignable_to(int, Literal[1]))
|
||||
static_assert(not is_assignable_to(Literal[1], str))
|
||||
```
|
||||
|
||||
### String literals and `LiteralString`
|
||||
|
||||
All string-literal types are subtypes of (and therefore assignable to) `LiteralString`, which is in
|
||||
turn a subtype of `str`:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
static_assert(is_assignable_to(Literal["foo"], Literal["foo"]))
|
||||
static_assert(is_assignable_to(Literal["foo"], LiteralString))
|
||||
static_assert(is_assignable_to(Literal["foo"], str))
|
||||
|
||||
static_assert(is_assignable_to(LiteralString, str))
|
||||
|
||||
static_assert(not is_assignable_to(Literal["foo"], Literal["bar"]))
|
||||
static_assert(not is_assignable_to(str, Literal["foo"]))
|
||||
static_assert(not is_assignable_to(str, LiteralString))
|
||||
```
|
||||
|
||||
### Byte literals
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
static_assert(is_assignable_to(Literal[b"foo"], bytes))
|
||||
static_assert(is_assignable_to(Literal[b"foo"], Literal[b"foo"]))
|
||||
|
||||
static_assert(not is_assignable_to(Literal[b"foo"], str))
|
||||
static_assert(not is_assignable_to(Literal[b"foo"], LiteralString))
|
||||
static_assert(not is_assignable_to(Literal[b"foo"], Literal[b"bar"]))
|
||||
static_assert(not is_assignable_to(Literal[b"foo"], Literal["foo"]))
|
||||
static_assert(not is_assignable_to(Literal["foo"], Literal[b"foo"]))
|
||||
```
|
||||
|
||||
## `type[…]` and class literals
|
||||
|
||||
In the following tests, `TypeOf[str]` is a singleton type with a single inhabitant, the class `str`.
|
||||
This contrasts with `type[str]`, which represents "all possible subclasses of `str`".
|
||||
|
||||
Both `TypeOf[str]` and `type[str]` are subtypes of `type` and `type[object]`, which both represent
|
||||
"all possible instances of `type`"; therefore both `type[str]` and `TypeOf[str]` are assignable to
|
||||
`type`. `type[Any]`, on the other hand, represents a type of unknown size or inhabitants, but which
|
||||
is known to be no larger than the set of possible objects represented by `type`.
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown, TypeOf
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_assignable_to(type, type))
|
||||
static_assert(is_assignable_to(type[object], type[object]))
|
||||
|
||||
static_assert(is_assignable_to(type, type[object]))
|
||||
static_assert(is_assignable_to(type[object], type))
|
||||
|
||||
static_assert(is_assignable_to(type[str], type[object]))
|
||||
static_assert(is_assignable_to(TypeOf[str], type[object]))
|
||||
static_assert(is_assignable_to(type[str], type))
|
||||
static_assert(is_assignable_to(TypeOf[str], type))
|
||||
|
||||
static_assert(is_assignable_to(type[str], type[str]))
|
||||
static_assert(is_assignable_to(TypeOf[str], type[str]))
|
||||
|
||||
static_assert(not is_assignable_to(TypeOf[int], type[str]))
|
||||
static_assert(not is_assignable_to(type, type[str]))
|
||||
static_assert(not is_assignable_to(type[object], type[str]))
|
||||
|
||||
static_assert(is_assignable_to(type[Any], type[Any]))
|
||||
static_assert(is_assignable_to(type[Any], type[object]))
|
||||
static_assert(is_assignable_to(type[object], type[Any]))
|
||||
static_assert(is_assignable_to(type, type[Any]))
|
||||
static_assert(is_assignable_to(type[Any], type[str]))
|
||||
static_assert(is_assignable_to(type[str], type[Any]))
|
||||
static_assert(is_assignable_to(TypeOf[str], type[Any]))
|
||||
|
||||
static_assert(is_assignable_to(type[Unknown], type[Unknown]))
|
||||
static_assert(is_assignable_to(type[Unknown], type[object]))
|
||||
static_assert(is_assignable_to(type[object], type[Unknown]))
|
||||
static_assert(is_assignable_to(type, type[Unknown]))
|
||||
static_assert(is_assignable_to(type[Unknown], type[str]))
|
||||
static_assert(is_assignable_to(type[str], type[Unknown]))
|
||||
static_assert(is_assignable_to(TypeOf[str], type[Unknown]))
|
||||
|
||||
static_assert(is_assignable_to(type[Unknown], type[Any]))
|
||||
static_assert(is_assignable_to(type[Any], type[Unknown]))
|
||||
|
||||
static_assert(not is_assignable_to(object, type[Any]))
|
||||
static_assert(not is_assignable_to(str, type[Any]))
|
||||
|
||||
class Meta(type): ...
|
||||
|
||||
static_assert(is_assignable_to(type[Any], Meta))
|
||||
static_assert(is_assignable_to(type[Unknown], Meta))
|
||||
static_assert(is_assignable_to(Meta, type[Any]))
|
||||
static_assert(is_assignable_to(Meta, type[Unknown]))
|
||||
```
|
||||
|
||||
## Tuple types
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to
|
||||
from typing import Literal, Any
|
||||
|
||||
static_assert(is_assignable_to(tuple[()], tuple[()]))
|
||||
static_assert(is_assignable_to(tuple[int], tuple[int]))
|
||||
static_assert(is_assignable_to(tuple[int], tuple[Any]))
|
||||
static_assert(is_assignable_to(tuple[Any], tuple[int]))
|
||||
static_assert(is_assignable_to(tuple[int, str], tuple[int, str]))
|
||||
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, int]))
|
||||
static_assert(is_assignable_to(tuple[Any, Literal[2]], tuple[int, int]))
|
||||
static_assert(is_assignable_to(tuple[Literal[1], Any], tuple[int, int]))
|
||||
|
||||
static_assert(not is_assignable_to(tuple[()], tuple[int]))
|
||||
static_assert(not is_assignable_to(tuple[int], tuple[str]))
|
||||
static_assert(not is_assignable_to(tuple[int], tuple[int, str]))
|
||||
static_assert(not is_assignable_to(tuple[int, str], tuple[int]))
|
||||
static_assert(not is_assignable_to(tuple[int, int], tuple[Literal[1], int]))
|
||||
static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str]))
|
||||
```
|
||||
|
||||
## Union types
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing import Literal, Any
|
||||
|
||||
static_assert(is_assignable_to(int, int | str))
|
||||
static_assert(is_assignable_to(str, int | str))
|
||||
static_assert(is_assignable_to(int | str, int | str))
|
||||
static_assert(is_assignable_to(str | int, int | str))
|
||||
static_assert(is_assignable_to(Literal[1], int | str))
|
||||
static_assert(is_assignable_to(Literal[1], Unknown | str))
|
||||
static_assert(is_assignable_to(Literal[1] | Literal[2], Literal[1] | Literal[2]))
|
||||
static_assert(is_assignable_to(Literal[1] | Literal[2], int))
|
||||
static_assert(is_assignable_to(Literal[1] | None, int | None))
|
||||
static_assert(is_assignable_to(Any, int | str))
|
||||
static_assert(is_assignable_to(Any | int, int))
|
||||
static_assert(is_assignable_to(str, int | Any))
|
||||
|
||||
static_assert(not is_assignable_to(int | None, int))
|
||||
static_assert(not is_assignable_to(int | None, str | None))
|
||||
static_assert(not is_assignable_to(Literal[1] | None, int))
|
||||
static_assert(not is_assignable_to(Literal[1] | None, str | None))
|
||||
static_assert(not is_assignable_to(Any | int | str, int))
|
||||
```
|
||||
|
||||
## Intersection types
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Intersection, Not
|
||||
from typing_extensions import Any, Literal
|
||||
|
||||
class Parent: ...
|
||||
class Child1(Parent): ...
|
||||
class Child2(Parent): ...
|
||||
class Grandchild(Child1, Child2): ...
|
||||
class Unrelated: ...
|
||||
|
||||
static_assert(is_assignable_to(Intersection[Child1, Child2], Child1))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Child2], Child2))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Child2], Parent))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Parent], Parent))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[Parent, Unrelated], Parent))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Unrelated], Child1))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Child1))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Parent))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Not[Grandchild]], Parent))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child1, Child2]))
|
||||
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child2, Child1]))
|
||||
static_assert(is_assignable_to(Grandchild, Intersection[Child1, Child2]))
|
||||
|
||||
static_assert(not is_assignable_to(Parent, Intersection[Parent, Unrelated]))
|
||||
static_assert(not is_assignable_to(int, Intersection[int, Not[Literal[1]]]))
|
||||
static_assert(not is_assignable_to(int, Not[int]))
|
||||
static_assert(not is_assignable_to(int, Not[Literal[1]]))
|
||||
|
||||
static_assert(not is_assignable_to(Intersection[Any, Parent], Unrelated))
|
||||
|
||||
# TODO: The following assertions should not fail (see https://github.com/astral-sh/ruff/issues/14899)
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Any, int], int))
|
||||
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]]))
|
||||
```
|
||||
|
||||
## General properties
|
||||
|
||||
See also: our property tests in `property_tests.rs`.
|
||||
|
||||
### Everything is assignable to `object`
|
||||
|
||||
`object` is Python's top type; the set of all possible objects at runtime:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing import Literal, Any
|
||||
|
||||
static_assert(is_assignable_to(str, object))
|
||||
static_assert(is_assignable_to(Literal[1], object))
|
||||
static_assert(is_assignable_to(object, object))
|
||||
static_assert(is_assignable_to(type, object))
|
||||
static_assert(is_assignable_to(Any, object))
|
||||
static_assert(is_assignable_to(Unknown, object))
|
||||
static_assert(is_assignable_to(type[object], object))
|
||||
static_assert(is_assignable_to(type[str], object))
|
||||
static_assert(is_assignable_to(type[Any], object))
|
||||
```
|
||||
|
||||
### Every type is assignable to `Any` / `Unknown`
|
||||
|
||||
`Any` and `Unknown` are gradual types. They could materialize to any given type at runtime, and so
|
||||
any type is assignable to them:
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing import Literal, Any
|
||||
|
||||
static_assert(is_assignable_to(str, Any))
|
||||
static_assert(is_assignable_to(Literal[1], Any))
|
||||
static_assert(is_assignable_to(object, Any))
|
||||
static_assert(is_assignable_to(type, Any))
|
||||
static_assert(is_assignable_to(Any, Any))
|
||||
static_assert(is_assignable_to(Unknown, Any))
|
||||
static_assert(is_assignable_to(type[object], Any))
|
||||
static_assert(is_assignable_to(type[str], Any))
|
||||
static_assert(is_assignable_to(type[Any], Any))
|
||||
|
||||
static_assert(is_assignable_to(str, Unknown))
|
||||
static_assert(is_assignable_to(Literal[1], Unknown))
|
||||
static_assert(is_assignable_to(object, Unknown))
|
||||
static_assert(is_assignable_to(type, Unknown))
|
||||
static_assert(is_assignable_to(Any, Unknown))
|
||||
static_assert(is_assignable_to(Unknown, Unknown))
|
||||
static_assert(is_assignable_to(type[object], Unknown))
|
||||
static_assert(is_assignable_to(type[str], Unknown))
|
||||
static_assert(is_assignable_to(type[Any], Unknown))
|
||||
```
|
||||
|
||||
### `Never` is assignable to every type
|
||||
|
||||
`Never` is Python's bottom type: the empty set, a type with no inhabitants. It is therefore
|
||||
assignable to any arbitrary type.
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing_extensions import Never, Any
|
||||
|
||||
static_assert(is_assignable_to(Never, str))
|
||||
static_assert(is_assignable_to(Never, Literal[1]))
|
||||
static_assert(is_assignable_to(Never, object))
|
||||
static_assert(is_assignable_to(Never, type))
|
||||
static_assert(is_assignable_to(Never, Any))
|
||||
static_assert(is_assignable_to(Never, Unknown))
|
||||
static_assert(is_assignable_to(Never, type[object]))
|
||||
static_assert(is_assignable_to(Never, type[str]))
|
||||
static_assert(is_assignable_to(Never, type[Any]))
|
||||
```
|
||||
|
||||
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
@@ -0,0 +1,33 @@
|
||||
# Tuples containing `Never`
|
||||
|
||||
A heterogeneous `tuple[…]` type that contains `Never` as a type argument simplifies to `Never`. One
|
||||
way to think about this is the following: in order to construct a tuple, you need to have an object
|
||||
of every element type. But since there is no object of type `Never`, you cannot construct the tuple.
|
||||
Such a tuple type is therefore uninhabited and equivalent to `Never`.
|
||||
|
||||
In the language of algebraic data types, a tuple type is a product type and `Never` acts like the
|
||||
zero element in multiplication, similar to how a Cartesian product with the empty set is the empty
|
||||
set.
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_equivalent_to
|
||||
from typing_extensions import Never, NoReturn
|
||||
|
||||
static_assert(is_equivalent_to(Never, tuple[Never]))
|
||||
static_assert(is_equivalent_to(Never, tuple[Never, int]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, Never]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, Never, str]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, tuple[str, Never]]))
|
||||
static_assert(is_equivalent_to(Never, tuple[tuple[str, Never], int]))
|
||||
|
||||
# The empty tuple is *not* equivalent to Never!
|
||||
static_assert(not is_equivalent_to(Never, tuple[()]))
|
||||
|
||||
# NoReturn is just a different spelling of Never, so the same is true for NoReturn
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn, int]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn, str]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[int, tuple[str, NoReturn]]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[tuple[str, NoReturn], int]))
|
||||
```
|
||||
@@ -34,6 +34,10 @@ reveal_type(~No()) # revealed: Unknown
|
||||
|
||||
## Classes
|
||||
|
||||
Dunder methods defined in a class are available to instances of that class, but not to the class
|
||||
itself. (For these operators to work on the class itself, they would have to be defined on the
|
||||
class's type, i.e. `type`.)
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __pos__(self) -> bool:
|
||||
|
||||
143
crates/red_knot_python_semantic/resources/mdtest/union_types.md
Normal file
143
crates/red_knot_python_semantic/resources/mdtest/union_types.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Union types
|
||||
|
||||
This test suite covers certain basic properties and simplification strategies for union types.
|
||||
|
||||
## Basic unions
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(u1: int | str, u2: Literal[0] | Literal[1]) -> None:
|
||||
reveal_type(u1) # revealed: int | str
|
||||
reveal_type(u2) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
## Duplicate elements are collapsed
|
||||
|
||||
```py
|
||||
def _(u1: int | int | str, u2: int | str | int) -> None:
|
||||
reveal_type(u1) # revealed: int | str
|
||||
reveal_type(u2) # revealed: int | str
|
||||
```
|
||||
|
||||
## `Never` is removed
|
||||
|
||||
`Never` is an empty set, a type with no inhabitants. Its presence in a union is always redundant,
|
||||
and so we eagerly simplify it away. `NoReturn` is equivalent to `Never`.
|
||||
|
||||
```py
|
||||
from typing_extensions import Never, NoReturn
|
||||
|
||||
def never(u1: int | Never, u2: int | Never | str) -> None:
|
||||
reveal_type(u1) # revealed: int
|
||||
reveal_type(u2) # revealed: int | str
|
||||
|
||||
def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None:
|
||||
reveal_type(u1) # revealed: int
|
||||
reveal_type(u2) # revealed: int | str
|
||||
```
|
||||
|
||||
## Flattening of nested unions
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(
|
||||
u1: (int | str) | bytes,
|
||||
u2: int | (str | bytes),
|
||||
u3: int | (str | (bytes | complex)),
|
||||
) -> None:
|
||||
reveal_type(u1) # revealed: int | str | bytes
|
||||
reveal_type(u2) # revealed: int | str | bytes
|
||||
reveal_type(u3) # revealed: int | str | bytes | complex
|
||||
```
|
||||
|
||||
## Simplification using subtyping
|
||||
|
||||
The type `S | T` can be simplified to `T` if `S` is a subtype of `T`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
def _(
|
||||
u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString
|
||||
) -> None:
|
||||
reveal_type(u1) # revealed: str
|
||||
reveal_type(u2) # revealed: str
|
||||
reveal_type(u3) # revealed: str
|
||||
reveal_type(u4) # revealed: str | bytes
|
||||
```
|
||||
|
||||
## Boolean literals
|
||||
|
||||
The union `Literal[True] | Literal[False]` is exactly equivalent to `bool`:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(
|
||||
u1: Literal[True, False],
|
||||
u2: bool | Literal[True],
|
||||
u3: Literal[True] | bool,
|
||||
u4: Literal[True] | Literal[True, 17],
|
||||
u5: Literal[True, False, True, 17],
|
||||
) -> None:
|
||||
reveal_type(u1) # revealed: bool
|
||||
reveal_type(u2) # revealed: bool
|
||||
reveal_type(u3) # revealed: bool
|
||||
reveal_type(u4) # revealed: Literal[True, 17]
|
||||
reveal_type(u5) # revealed: bool | Literal[17]
|
||||
```
|
||||
|
||||
## Do not erase `Unknown`
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(u1: Unknown | str, u2: str | Unknown) -> None:
|
||||
reveal_type(u1) # revealed: Unknown | str
|
||||
reveal_type(u2) # revealed: str | Unknown
|
||||
```
|
||||
|
||||
## Collapse multiple `Unknown`s
|
||||
|
||||
Since `Unknown` is a gradual type, it is not a subtype of anything, but multiple `Unknown`s in a
|
||||
union are still redundant:
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None:
|
||||
reveal_type(u1) # revealed: Unknown | str
|
||||
reveal_type(u2) # revealed: Unknown | str
|
||||
reveal_type(u3) # revealed: str | Unknown
|
||||
```
|
||||
|
||||
## Subsume multiple elements
|
||||
|
||||
Simplifications still apply when `Unknown` is present.
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(u1: str | Unknown | int | object):
|
||||
reveal_type(u1) # revealed: Unknown | object
|
||||
```
|
||||
|
||||
## Union of intersections
|
||||
|
||||
We can simplify unions of intersections:
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
def _(
|
||||
i1: Intersection[P, Q] | Intersection[P, Q],
|
||||
i2: Intersection[P, Q] | Intersection[Q, P],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: P & Q
|
||||
reveal_type(i2) # revealed: P & Q
|
||||
```
|
||||
@@ -426,8 +426,8 @@ def _(flag: bool):
|
||||
value = ("a", "b")
|
||||
|
||||
a, b = value
|
||||
reveal_type(a) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(b) # revealed: Literal[2] | Literal["b"]
|
||||
reveal_type(a) # revealed: Literal[1, "a"]
|
||||
reveal_type(b) # revealed: Literal[2, "b"]
|
||||
```
|
||||
|
||||
### Typing literal
|
||||
@@ -528,8 +528,8 @@ for a, b in ((1, 2), (3, 4)):
|
||||
|
||||
```py
|
||||
for a, b in ((1, 2), ("a", "b")):
|
||||
reveal_type(a) # revealed: Literal[1] | Literal["a"]
|
||||
reveal_type(b) # revealed: Literal[2] | Literal["b"]
|
||||
reveal_type(a) # revealed: Literal[1, "a"]
|
||||
reveal_type(b) # revealed: Literal[2, "b"]
|
||||
```
|
||||
|
||||
### Mixed literals values (2)
|
||||
|
||||
@@ -109,6 +109,7 @@ pub enum KnownModule {
|
||||
#[allow(dead_code)]
|
||||
Abc, // currently only used in tests
|
||||
Collections,
|
||||
KnotExtensions,
|
||||
}
|
||||
|
||||
impl KnownModule {
|
||||
@@ -122,6 +123,7 @@ impl KnownModule {
|
||||
Self::Sys => "sys",
|
||||
Self::Abc => "abc",
|
||||
Self::Collections => "collections",
|
||||
Self::KnotExtensions => "knot_extensions",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +149,7 @@ impl KnownModule {
|
||||
"sys" => Some(Self::Sys),
|
||||
"abc" => Some(Self::Abc),
|
||||
"collections" => Some(Self::Collections),
|
||||
"knot_extensions" => Some(Self::KnotExtensions),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -154,4 +157,8 @@ impl KnownModule {
|
||||
pub const fn is_typing(self) -> bool {
|
||||
matches!(self, Self::Typing)
|
||||
}
|
||||
|
||||
pub const fn is_knot_extensions(self) -> bool {
|
||||
matches!(self, Self::KnotExtensions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,6 +404,17 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
pattern: &ast::Pattern,
|
||||
guard: Option<&ast::Expr>,
|
||||
) -> Constraint<'db> {
|
||||
// This is called for the top-level pattern of each match arm. We need to create a
|
||||
// standalone expression for each arm of a match statement, since they can introduce
|
||||
// constraints on the match subject. (Or more accurately, for the match arm's pattern,
|
||||
// since its the pattern that introduces any constraints, not the body.) Ideally, that
|
||||
// standalone expression would wrap the match arm's pattern as a whole. But a standalone
|
||||
// expression can currently only wrap an ast::Expr, which patterns are not. So, we need to
|
||||
// choose an Expr that can “stand in” for the pattern, which we can wrap in a standalone
|
||||
// expression.
|
||||
//
|
||||
// See the comment in TypeInferenceBuilder::infer_match_pattern for more details.
|
||||
|
||||
let guard = guard.map(|guard| self.add_standalone_expression(guard));
|
||||
|
||||
let kind = match pattern {
|
||||
@@ -414,6 +425,10 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
ast::Pattern::MatchSingleton(singleton) => {
|
||||
PatternConstraintKind::Singleton(singleton.value, guard)
|
||||
}
|
||||
ast::Pattern::MatchClass(pattern) => {
|
||||
let cls = self.add_standalone_expression(&pattern.cls);
|
||||
PatternConstraintKind::Class(cls, guard)
|
||||
}
|
||||
_ => PatternConstraintKind::Unsupported,
|
||||
};
|
||||
|
||||
@@ -878,12 +893,11 @@ where
|
||||
}
|
||||
ast::Stmt::If(node) => {
|
||||
self.visit_expr(&node.test);
|
||||
let pre_if = self.flow_snapshot();
|
||||
let constraint = self.record_expression_constraint(&node.test);
|
||||
let mut constraints = vec![constraint];
|
||||
let mut no_branch_taken = self.flow_snapshot();
|
||||
let mut last_constraint = self.record_expression_constraint(&node.test);
|
||||
self.visit_body(&node.body);
|
||||
|
||||
let visibility_constraint_id = self.record_visibility_constraint(constraint);
|
||||
let visibility_constraint_id = self.record_visibility_constraint(last_constraint);
|
||||
let mut vis_constraints = vec![visibility_constraint_id];
|
||||
|
||||
let mut post_clauses: Vec<FlowSnapshot> = vec![];
|
||||
@@ -907,26 +921,27 @@ where
|
||||
// the state that we merge the other snapshots into
|
||||
post_clauses.push(self.flow_snapshot());
|
||||
// we can only take an elif/else branch if none of the previous ones were
|
||||
// taken, so the block entry state is always `pre_if`
|
||||
self.flow_restore(pre_if.clone());
|
||||
for constraint in &constraints {
|
||||
self.record_negated_constraint(*constraint);
|
||||
}
|
||||
// taken
|
||||
self.flow_restore(no_branch_taken.clone());
|
||||
self.record_negated_constraint(last_constraint);
|
||||
|
||||
let elif_constraint = if let Some(elif_test) = clause_test {
|
||||
self.visit_expr(elif_test);
|
||||
// A test expression is evaluated whether the branch is taken or not
|
||||
no_branch_taken = self.flow_snapshot();
|
||||
let constraint = self.record_expression_constraint(elif_test);
|
||||
constraints.push(constraint);
|
||||
Some(constraint)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.visit_body(clause_body);
|
||||
|
||||
for id in &vis_constraints {
|
||||
self.record_negated_visibility_constraint(*id);
|
||||
}
|
||||
if let Some(elif_constraint) = elif_constraint {
|
||||
last_constraint = elif_constraint;
|
||||
let id = self.record_visibility_constraint(elif_constraint);
|
||||
vis_constraints.push(id);
|
||||
}
|
||||
@@ -936,7 +951,7 @@ where
|
||||
self.flow_merge(post_clause_state);
|
||||
}
|
||||
|
||||
self.simplify_visibility_constraints(pre_if);
|
||||
self.simplify_visibility_constraints(no_branch_taken);
|
||||
}
|
||||
ast::Stmt::While(ast::StmtWhile {
|
||||
test,
|
||||
@@ -1089,37 +1104,35 @@ where
|
||||
cases,
|
||||
range: _,
|
||||
}) => {
|
||||
debug_assert_eq!(self.current_match_case, None);
|
||||
|
||||
let subject_expr = self.add_standalone_expression(subject);
|
||||
self.visit_expr(subject);
|
||||
|
||||
let after_subject = self.flow_snapshot();
|
||||
let Some((first, remaining)) = cases.split_first() else {
|
||||
if cases.is_empty() {
|
||||
return;
|
||||
};
|
||||
|
||||
let first_constraint_id = self.add_pattern_constraint(
|
||||
subject_expr,
|
||||
&first.pattern,
|
||||
first.guard.as_deref(),
|
||||
);
|
||||
|
||||
self.visit_match_case(first);
|
||||
|
||||
let first_vis_constraint_id =
|
||||
self.record_visibility_constraint(first_constraint_id);
|
||||
let mut vis_constraints = vec![first_vis_constraint_id];
|
||||
|
||||
let after_subject = self.flow_snapshot();
|
||||
let mut vis_constraints = vec![];
|
||||
let mut post_case_snapshots = vec![];
|
||||
for case in remaining {
|
||||
post_case_snapshots.push(self.flow_snapshot());
|
||||
self.flow_restore(after_subject.clone());
|
||||
for (i, case) in cases.iter().enumerate() {
|
||||
if i != 0 {
|
||||
post_case_snapshots.push(self.flow_snapshot());
|
||||
self.flow_restore(after_subject.clone());
|
||||
}
|
||||
|
||||
self.current_match_case = Some(CurrentMatchCase::new(&case.pattern));
|
||||
self.visit_pattern(&case.pattern);
|
||||
self.current_match_case = None;
|
||||
let constraint_id = self.add_pattern_constraint(
|
||||
subject_expr,
|
||||
&case.pattern,
|
||||
case.guard.as_deref(),
|
||||
);
|
||||
self.visit_match_case(case);
|
||||
|
||||
if let Some(expr) = &case.guard {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
self.visit_body(&case.body);
|
||||
for id in &vis_constraints {
|
||||
self.record_negated_visibility_constraint(*id);
|
||||
}
|
||||
@@ -1538,18 +1551,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_match_case(&mut self, match_case: &'ast ast::MatchCase) {
|
||||
debug_assert!(self.current_match_case.is_none());
|
||||
self.current_match_case = Some(CurrentMatchCase::new(&match_case.pattern));
|
||||
self.visit_pattern(&match_case.pattern);
|
||||
self.current_match_case = None;
|
||||
|
||||
if let Some(expr) = &match_case.guard {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
self.visit_body(&match_case.body);
|
||||
}
|
||||
|
||||
fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) {
|
||||
if let ast::Pattern::MatchStar(ast::PatternMatchStar {
|
||||
name: Some(name),
|
||||
@@ -1636,6 +1637,7 @@ impl<'a> From<&'a ast::ExprNamed> for CurrentAssignment<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct CurrentMatchCase<'a> {
|
||||
/// The pattern that's part of the current match case.
|
||||
pattern: &'a ast::Pattern,
|
||||
|
||||
@@ -22,6 +22,7 @@ pub(crate) enum ConstraintNode<'db> {
|
||||
pub(crate) enum PatternConstraintKind<'db> {
|
||||
Singleton(Singleton, Option<Expression<'db>>),
|
||||
Value(Expression<'db>, Option<Expression<'db>>),
|
||||
Class(Expression<'db>, Option<Expression<'db>>),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,11 @@ impl<'db> Definition<'db> {
|
||||
Some(KnownModule::Typing | KnownModule::TypingExtensions)
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_knot_extensions_definition(self, db: &'db dyn Db) -> bool {
|
||||
file_to_module(db, self.file(db))
|
||||
.is_some_and(|module| module.is_known(KnownModule::KnotExtensions))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
||||
@@ -310,7 +310,7 @@ impl SymbolState {
|
||||
visibility_constraints: VisibilityConstraintPerBinding::default(),
|
||||
},
|
||||
declarations: SymbolDeclarations {
|
||||
live_declarations: self.declarations.live_declarations.clone(),
|
||||
live_declarations: Declarations::default(),
|
||||
visibility_constraints: VisibilityConstraintPerDeclaration::default(),
|
||||
},
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,8 @@ impl<'db> UnionBuilder<'db> {
|
||||
|
||||
let mut to_add = ty;
|
||||
let mut to_remove = SmallVec::<[usize; 2]>::new();
|
||||
let ty_negated = ty.negate(self.db);
|
||||
|
||||
for (index, element) in self.elements.iter().enumerate() {
|
||||
if Some(*element) == bool_pair {
|
||||
to_add = KnownClass::Bool.to_instance(self.db);
|
||||
@@ -80,6 +82,17 @@ impl<'db> UnionBuilder<'db> {
|
||||
return self;
|
||||
} else if element.is_subtype_of(self.db, ty) {
|
||||
to_remove.push(index);
|
||||
} else if ty_negated.is_subtype_of(self.db, *element) {
|
||||
// We add `ty` to the union. We just checked that `~ty` is a subtype of an existing `element`.
|
||||
// This also means that `~ty | ty` is a subtype of `element | ty`, because both elements in the
|
||||
// first union are subtypes of the corresponding elements in the second union. But `~ty | ty` is
|
||||
// just `object`. Since `object` is a subtype of `element | ty`, we can only conclude that
|
||||
// `element | ty` must be `object` (object has no other supertypes). This means we can simplify
|
||||
// the whole union to just `object`, since all other potential elements would also be subtypes of
|
||||
// `object`.
|
||||
self.elements.clear();
|
||||
self.elements.push(KnownClass::Object.to_instance(self.db));
|
||||
return self;
|
||||
}
|
||||
}
|
||||
match to_remove[..] {
|
||||
@@ -321,7 +334,15 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
self.add_positive(db, *neg);
|
||||
}
|
||||
}
|
||||
ty @ (Type::Any | Type::Unknown | Type::Todo(_)) => {
|
||||
Type::Never => {
|
||||
// Adding ~Never to an intersection is a no-op.
|
||||
}
|
||||
Type::Instance(instance) if instance.class.is_known(db, KnownClass::Object) => {
|
||||
// Adding ~object to an intersection results in Never.
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::Never);
|
||||
}
|
||||
ty @ Type::Dynamic(_) => {
|
||||
// Adding any of these types to the negative side of an intersection
|
||||
// is equivalent to adding it to the positive side. We do this to
|
||||
// simplify the representation.
|
||||
@@ -386,18 +407,34 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
|
||||
use super::{IntersectionBuilder, Type, UnionBuilder, UnionType};
|
||||
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{global_symbol, todo_type, KnownClass, Truthiness, UnionBuilder};
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::{KnownClass, Truthiness};
|
||||
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use test_case::test_case;
|
||||
|
||||
#[test]
|
||||
fn build_union() {
|
||||
fn build_union_no_elements() {
|
||||
let db = setup_db();
|
||||
|
||||
let empty_union = UnionBuilder::new(&db).build();
|
||||
assert_eq!(empty_union, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_single_element() {
|
||||
let db = setup_db();
|
||||
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let union = UnionType::from_elements(&db, [t0]);
|
||||
assert_eq!(union, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_two_elements() {
|
||||
let db = setup_db();
|
||||
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
|
||||
@@ -405,605 +442,12 @@ mod tests {
|
||||
assert_eq!(union.elements(&db), &[t0, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_single() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ty = UnionType::from_elements(&db, [t0]);
|
||||
assert_eq!(ty, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_empty() {
|
||||
let db = setup_db();
|
||||
let ty = UnionBuilder::new(&db).build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_never() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ty = UnionType::from_elements(&db, [t0, Type::Never]);
|
||||
assert_eq!(ty, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_bool() {
|
||||
let db = setup_db();
|
||||
let bool_instance_ty = KnownClass::Bool.to_instance(&db);
|
||||
|
||||
let t0 = Type::BooleanLiteral(true);
|
||||
let t1 = Type::BooleanLiteral(true);
|
||||
let t2 = Type::BooleanLiteral(false);
|
||||
let t3 = Type::IntLiteral(17);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t3]).expect_union();
|
||||
assert_eq!(union.elements(&db), &[t0, t3]);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
|
||||
assert_eq!(union.elements(&db), &[bool_instance_ty, t3]);
|
||||
|
||||
let result_ty = UnionType::from_elements(&db, [bool_instance_ty, t0]);
|
||||
assert_eq!(result_ty, bool_instance_ty);
|
||||
|
||||
let result_ty = UnionType::from_elements(&db, [t0, bool_instance_ty]);
|
||||
assert_eq!(result_ty, bool_instance_ty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_flatten() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::IntLiteral(2);
|
||||
let u1 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let union = UnionType::from_elements(&db, [u1, t2]).expect_union();
|
||||
|
||||
assert_eq!(union.elements(&db), &[t0, t1, t2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_simplify_subtype() {
|
||||
let db = setup_db();
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::LiteralString;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
|
||||
assert_eq!(u0, t0);
|
||||
assert_eq!(u1, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_no_simplify_unknown() {
|
||||
let db = setup_db();
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::Unknown;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
|
||||
assert_eq!(u0.expect_union().elements(&db), &[t0, t1]);
|
||||
assert_eq!(u1.expect_union().elements(&db), &[t1, t0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_simplify_multiple_unknown() {
|
||||
let db = setup_db();
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::Unknown;
|
||||
|
||||
let u = UnionType::from_elements(&db, [t0, t1, t1]);
|
||||
|
||||
assert_eq!(u.expect_union().elements(&db), &[t0, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_subsume_multiple() {
|
||||
let db = setup_db();
|
||||
let str_ty = KnownClass::Str.to_instance(&db);
|
||||
let int_ty = KnownClass::Int.to_instance(&db);
|
||||
let object_ty = KnownClass::Object.to_instance(&db);
|
||||
let unknown_ty = Type::Unknown;
|
||||
|
||||
let u0 = UnionType::from_elements(&db, [str_ty, unknown_ty, int_ty, object_ty]);
|
||||
|
||||
assert_eq!(u0.expect_union().elements(&db), &[unknown_ty, object_ty]);
|
||||
}
|
||||
|
||||
impl<'db> IntersectionType<'db> {
|
||||
fn pos_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
|
||||
self.positive(db).into_iter().copied().collect()
|
||||
}
|
||||
|
||||
fn neg_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
|
||||
self.negative(db).into_iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ta = Type::Any;
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t0)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[ta]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[t0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_empty_intersection_equals_object() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db).build();
|
||||
|
||||
assert_eq!(ty, KnownClass::Object.to_instance(&db));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_flatten_positive() {
|
||||
let db = setup_db();
|
||||
let ta = Type::Any;
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::IntLiteral(2);
|
||||
let i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t1)
|
||||
.build();
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(t2)
|
||||
.add_positive(i0)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[t2, ta]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_flatten_negative() {
|
||||
let db = setup_db();
|
||||
let ta = Type::Any;
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = KnownClass::Int.to_instance(&db);
|
||||
// i0 = Any & ~Literal[1]
|
||||
let i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t1)
|
||||
.build();
|
||||
// ta_not_i0 = int & ~(Any & ~Literal[1])
|
||||
// -> int & (~Any | Literal[1])
|
||||
// (~Any is equivalent to Any)
|
||||
// -> (int & Any) | (int & Literal[1])
|
||||
// -> (int & Any) | Literal[1]
|
||||
let ta_not_i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(t2)
|
||||
.add_negative(i0)
|
||||
.build();
|
||||
|
||||
assert_eq!(ta_not_i0.display(&db).to_string(), "int & Any | Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_any() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Any)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Any);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Never)
|
||||
.add_negative(Type::Any)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_multiple_unknown() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_positive(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Unknown);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_negative(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Unknown);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Unknown)
|
||||
.add_negative(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Unknown);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_positive(Type::IntLiteral(0))
|
||||
.add_negative(Type::Unknown)
|
||||
.build();
|
||||
assert_eq!(
|
||||
ty,
|
||||
IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unknown)
|
||||
.add_positive(Type::IntLiteral(0))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let ta = Type::Any;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
|
||||
let union = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_positive(u0)
|
||||
.build()
|
||||
.expect_union();
|
||||
let [Type::Intersection(i0), Type::Intersection(i1)] = union.elements(&db)[..] else {
|
||||
panic!("expected a union of two intersections");
|
||||
};
|
||||
assert_eq!(i0.pos_vec(&db), &[ta, t0]);
|
||||
assert_eq!(i1.pos_vec(&db), &[ta, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_negation_distributes_over_union() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
r#"
|
||||
class A: ...
|
||||
class B: ...
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let a = global_symbol(&db, module, "A")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
let b = global_symbol(&db, module, "B")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
|
||||
// intersection: A & B
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_positive(b)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
assert_eq!(intersection.pos_vec(&db), &[a, b]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[]);
|
||||
|
||||
// ~intersection => ~A | ~B
|
||||
let negated_intersection = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Intersection(intersection))
|
||||
.build()
|
||||
.expect_union();
|
||||
|
||||
// should have as elements ~A and ~B
|
||||
let not_a = a.negate(&db);
|
||||
let not_b = b.negate(&db);
|
||||
assert_eq!(negated_intersection.elements(&db), &[not_a, not_b]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_intersection_negation_distributes_over_union() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
r#"
|
||||
class A: ...
|
||||
class B: ...
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let a = global_symbol(&db, module, "A")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
let b = global_symbol(&db, module, "B")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
let int = KnownClass::Int.to_instance(&db);
|
||||
|
||||
// a_not_b: A & ~B
|
||||
let a_not_b = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_negative(b)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
assert_eq!(a_not_b.pos_vec(&db), &[a]);
|
||||
assert_eq!(a_not_b.neg_vec(&db), &[b]);
|
||||
|
||||
// let's build
|
||||
// int & ~(A & ~B)
|
||||
// = int & ~(A & ~B)
|
||||
// = int & (~A | B)
|
||||
// = (int & ~A) | (int & B)
|
||||
let t = IntersectionBuilder::new(&db)
|
||||
.add_positive(int)
|
||||
.add_negative(Type::Intersection(a_not_b))
|
||||
.build();
|
||||
assert_eq!(t.display(&db).to_string(), "int & ~A | int & B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_self_negation() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_negative(Type::none(&db))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_never() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_negative(Type::Never)
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::none(&db));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_never() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_positive(Type::Never)
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_none() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::none(&db))
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.add_negative(Type::none(&db))
|
||||
.build();
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_negative_union_de_morgan() {
|
||||
let db = setup_db();
|
||||
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(Type::IntLiteral(1))
|
||||
.add(Type::IntLiteral(2))
|
||||
.build();
|
||||
assert_eq!(union.display(&db).to_string(), "Literal[1, 2]");
|
||||
|
||||
let ty = IntersectionBuilder::new(&db).add_negative(union).build();
|
||||
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::IntLiteral(1))
|
||||
.add_negative(Type::IntLiteral(2))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty.display(&db).to_string(), "~Literal[1] & ~Literal[2]");
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_type_and_positive_subtype() {
|
||||
let db = setup_db();
|
||||
|
||||
let t = KnownClass::Str.to_instance(&db);
|
||||
let s = Type::LiteralString;
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, s);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_positive(t)
|
||||
.build();
|
||||
assert_eq!(ty, s);
|
||||
|
||||
let literal = Type::string_literal(&db, "a");
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(literal)
|
||||
.build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t)
|
||||
.add_negative(literal)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(literal)
|
||||
.add_positive(t)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_type_and_negative_subtype() {
|
||||
let db = setup_db();
|
||||
|
||||
let t = KnownClass::Str.to_instance(&db);
|
||||
let s = Type::LiteralString;
|
||||
|
||||
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_negative(s)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(s)
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let object = KnownClass::Object.to_instance(&db);
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_positive(object)
|
||||
.build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_positive(object)
|
||||
.add_negative(s)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_type_and_multiple_negative_subtypes() {
|
||||
let db = setup_db();
|
||||
|
||||
let s1 = Type::IntLiteral(1);
|
||||
let s2 = Type::IntLiteral(2);
|
||||
let t = KnownClass::Int.to_instance(&db);
|
||||
|
||||
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(s1)
|
||||
.add_negative(s2)
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_type_and_positive_subtype() {
|
||||
let db = setup_db();
|
||||
|
||||
let t = KnownClass::Str.to_instance(&db);
|
||||
let s = Type::LiteralString;
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
// This should also work in the presence of additional contributions:
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(KnownClass::Object.to_instance(&db))
|
||||
.add_negative(t)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(Type::string_literal(&db, "a"))
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_disjoint_positive_types() {
|
||||
let db = setup_db();
|
||||
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::none(&db);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t1)
|
||||
.add_positive(t2)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
// If there are any negative contributions, they should
|
||||
// be removed too.
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(KnownClass::Str.to_instance(&db))
|
||||
.add_negative(Type::LiteralString)
|
||||
.add_positive(t2)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_disjoint_positive_and_negative_types() {
|
||||
let db = setup_db();
|
||||
|
||||
let t_p = KnownClass::Int.to_instance(&db);
|
||||
let t_n = Type::string_literal(&db, "t_n");
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_p)
|
||||
.add_negative(t_n)
|
||||
.build();
|
||||
assert_eq!(ty, t_p);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t_n)
|
||||
.add_positive(t_p)
|
||||
.build();
|
||||
assert_eq!(ty, t_p);
|
||||
|
||||
let int_literal = Type::IntLiteral(1);
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_p)
|
||||
.add_negative(int_literal)
|
||||
.build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_p)
|
||||
.add_negative(int_literal)
|
||||
.add_negative(t_n)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t_n)
|
||||
.add_negative(int_literal)
|
||||
.add_positive(t_p)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
let intersection = IntersectionBuilder::new(&db).build();
|
||||
assert_eq!(intersection, KnownClass::Object.to_instance(&db));
|
||||
}
|
||||
|
||||
#[test_case(Type::BooleanLiteral(true))]
|
||||
@@ -1048,85 +492,4 @@ mod tests {
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
}
|
||||
|
||||
#[test_case(Type::Any)]
|
||||
#[test_case(Type::Unknown)]
|
||||
#[test_case(todo_type!())]
|
||||
fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) {
|
||||
let db = setup_db();
|
||||
|
||||
let result = IntersectionBuilder::new(&db)
|
||||
.add_positive(ty)
|
||||
.add_negative(ty)
|
||||
.build();
|
||||
assert_eq!(result, ty);
|
||||
|
||||
let result = IntersectionBuilder::new(&db)
|
||||
.add_negative(ty)
|
||||
.add_positive(ty)
|
||||
.build();
|
||||
assert_eq!(result, ty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_of_two_unions_simplify() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
"
|
||||
class A: ...
|
||||
class B: ...
|
||||
a = A()
|
||||
b = B()
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
|
||||
|
||||
let a = global_symbol(&db, file, "a").expect_type();
|
||||
let b = global_symbol(&db, file, "b").expect_type();
|
||||
let union = UnionBuilder::new(&db).add(a).add(b).build();
|
||||
assert_eq!(union.display(&db).to_string(), "A | B");
|
||||
let reversed_union = UnionBuilder::new(&db).add(b).add(a).build();
|
||||
assert_eq!(reversed_union.display(&db).to_string(), "B | A");
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(union)
|
||||
.add_positive(reversed_union)
|
||||
.build();
|
||||
assert_eq!(intersection.display(&db).to_string(), "B | A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_of_two_intersections_simplify() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
"
|
||||
class A: ...
|
||||
class B: ...
|
||||
a = A()
|
||||
b = B()
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
|
||||
|
||||
let a = global_symbol(&db, file, "a").expect_type();
|
||||
let b = global_symbol(&db, file, "b").expect_type();
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_positive(b)
|
||||
.build();
|
||||
let reversed_intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(b)
|
||||
.add_positive(a)
|
||||
.build();
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(intersection)
|
||||
.add(reversed_intersection)
|
||||
.build();
|
||||
assert_eq!(union.display(&db).to_string(), "A & B");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
use super::context::InferContext;
|
||||
use super::diagnostic::CALL_NON_CALLABLE;
|
||||
use super::{Severity, Type, TypeArrayDisplay, UnionBuilder};
|
||||
use super::diagnostic::{CALL_NON_CALLABLE, TYPE_ASSERTION_FAILURE};
|
||||
use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder};
|
||||
use crate::types::diagnostic::STATIC_ASSERT_ERROR;
|
||||
use crate::Db;
|
||||
use ruff_db::diagnostic::DiagnosticId;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
mod arguments;
|
||||
mod bind;
|
||||
|
||||
pub(super) use arguments::{Argument, CallArguments};
|
||||
pub(super) use bind::{bind_call, CallBinding};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum StaticAssertionErrorKind<'db> {
|
||||
ArgumentIsFalse,
|
||||
ArgumentIsFalsy(Type<'db>),
|
||||
ArgumentTruthinessIsAmbiguous(Type<'db>),
|
||||
CustomError(&'db str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum CallOutcome<'db> {
|
||||
Callable {
|
||||
return_ty: Type<'db>,
|
||||
binding: CallBinding<'db>,
|
||||
},
|
||||
RevealType {
|
||||
return_ty: Type<'db>,
|
||||
binding: CallBinding<'db>,
|
||||
revealed_ty: Type<'db>,
|
||||
},
|
||||
NotCallable {
|
||||
@@ -25,12 +40,20 @@ pub(super) enum CallOutcome<'db> {
|
||||
called_ty: Type<'db>,
|
||||
call_outcome: Box<CallOutcome<'db>>,
|
||||
},
|
||||
StaticAssertionError {
|
||||
binding: CallBinding<'db>,
|
||||
error_kind: StaticAssertionErrorKind<'db>,
|
||||
},
|
||||
AssertType {
|
||||
binding: CallBinding<'db>,
|
||||
asserted_ty: Type<'db>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'db> CallOutcome<'db> {
|
||||
/// Create a new `CallOutcome::Callable` with given return type.
|
||||
pub(super) fn callable(return_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::Callable { return_ty }
|
||||
pub(super) fn callable(binding: CallBinding<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::Callable { binding }
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::NotCallable` with given not-callable type.
|
||||
@@ -39,9 +62,9 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::RevealType` with given revealed and return types.
|
||||
pub(super) fn revealed(return_ty: Type<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
pub(super) fn revealed(binding: CallBinding<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::RevealType {
|
||||
return_ty,
|
||||
binding,
|
||||
revealed_ty,
|
||||
}
|
||||
}
|
||||
@@ -57,14 +80,22 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::AssertType` with given revealed and return types.
|
||||
pub(super) fn asserted(binding: CallBinding<'db>, asserted_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::AssertType {
|
||||
binding,
|
||||
asserted_ty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the return type of the call, or `None` if not callable.
|
||||
pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Self::Callable { return_ty } => Some(*return_ty),
|
||||
Self::Callable { binding } => Some(binding.return_ty()),
|
||||
Self::RevealType {
|
||||
return_ty,
|
||||
binding,
|
||||
revealed_ty: _,
|
||||
} => Some(*return_ty),
|
||||
} => Some(binding.return_ty()),
|
||||
Self::NotCallable { not_callable_ty: _ } => None,
|
||||
Self::Union {
|
||||
outcomes,
|
||||
@@ -78,11 +109,16 @@ impl<'db> CallOutcome<'db> {
|
||||
match (acc, ty) {
|
||||
(None, None) => None,
|
||||
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
|
||||
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::Unknown))),
|
||||
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::unknown()))),
|
||||
}
|
||||
})
|
||||
.map(UnionBuilder::build),
|
||||
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db),
|
||||
Self::StaticAssertionError { .. } => Some(Type::none(db)),
|
||||
Self::AssertType {
|
||||
binding,
|
||||
asserted_ty: _,
|
||||
} => Some(binding.return_ty()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,23 +199,30 @@ impl<'db> CallOutcome<'db> {
|
||||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
) -> Result<Type<'db>, NotCallableError<'db>> {
|
||||
// TODO should this method emit diagnostics directly, or just return results that allow the
|
||||
// caller to decide about emitting diagnostics? Currently it emits binding diagnostics, but
|
||||
// only non-callable diagnostics in the union case, which is inconsistent.
|
||||
match self {
|
||||
Self::Callable { return_ty } => Ok(*return_ty),
|
||||
Self::Callable { binding } => {
|
||||
binding.report_diagnostics(context, node);
|
||||
Ok(binding.return_ty())
|
||||
}
|
||||
Self::RevealType {
|
||||
return_ty,
|
||||
binding,
|
||||
revealed_ty,
|
||||
} => {
|
||||
binding.report_diagnostics(context, node);
|
||||
context.report_diagnostic(
|
||||
node,
|
||||
DiagnosticId::RevealedType,
|
||||
Severity::Info,
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
|
||||
);
|
||||
Ok(*return_ty)
|
||||
Ok(binding.return_ty())
|
||||
}
|
||||
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
|
||||
not_callable_ty: *not_callable_ty,
|
||||
return_ty: Type::Unknown,
|
||||
return_ty: Type::unknown(),
|
||||
}),
|
||||
Self::PossiblyUnboundDunderCall {
|
||||
called_ty,
|
||||
@@ -188,7 +231,7 @@ impl<'db> CallOutcome<'db> {
|
||||
callable_ty: *called_ty,
|
||||
return_ty: call_outcome
|
||||
.return_ty(context.db())
|
||||
.unwrap_or(Type::Unknown),
|
||||
.unwrap_or(Type::unknown()),
|
||||
}),
|
||||
Self::Union {
|
||||
outcomes,
|
||||
@@ -201,14 +244,14 @@ impl<'db> CallOutcome<'db> {
|
||||
let return_ty = match outcome {
|
||||
Self::NotCallable { not_callable_ty } => {
|
||||
not_callable.push(*not_callable_ty);
|
||||
Type::Unknown
|
||||
Type::unknown()
|
||||
}
|
||||
Self::RevealType {
|
||||
return_ty,
|
||||
binding,
|
||||
revealed_ty: _,
|
||||
} => {
|
||||
if revealed {
|
||||
*return_ty
|
||||
binding.return_ty()
|
||||
} else {
|
||||
revealed = true;
|
||||
outcome.unwrap_with_diagnostic(context, node)
|
||||
@@ -237,6 +280,73 @@ impl<'db> CallOutcome<'db> {
|
||||
}),
|
||||
}
|
||||
}
|
||||
CallOutcome::StaticAssertionError {
|
||||
binding,
|
||||
error_kind,
|
||||
} => {
|
||||
binding.report_diagnostics(context, node);
|
||||
|
||||
match error_kind {
|
||||
StaticAssertionErrorKind::ArgumentIsFalse => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!("Static assertion error: argument evaluates to `False`"),
|
||||
);
|
||||
}
|
||||
StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty) => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!(
|
||||
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
|
||||
parameter_ty=parameter_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(parameter_ty) => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!(
|
||||
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
|
||||
parameter_ty=parameter_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
StaticAssertionErrorKind::CustomError(message) => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!("Static assertion error: {message}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Type::unknown())
|
||||
}
|
||||
CallOutcome::AssertType {
|
||||
binding,
|
||||
asserted_ty,
|
||||
} => {
|
||||
let [actual_ty, _asserted] = binding.parameter_tys() else {
|
||||
return Ok(binding.return_ty());
|
||||
};
|
||||
|
||||
if !actual_ty.is_gradual_equivalent_to(context.db(), *asserted_ty) {
|
||||
context.report_lint(
|
||||
&TYPE_ASSERTION_FAILURE,
|
||||
node,
|
||||
format_args!(
|
||||
"Actual type `{}` is not the same as asserted type `{}`",
|
||||
actual_ty.display(context.db()),
|
||||
asserted_ty.display(context.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(binding.return_ty())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
73
crates/red_knot_python_semantic/src/types/call/arguments.rs
Normal file
73
crates/red_knot_python_semantic/src/types/call/arguments.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use super::Type;
|
||||
|
||||
/// Typed arguments for a single call, in source order.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct CallArguments<'a, 'db>(Vec<Argument<'a, 'db>>);
|
||||
|
||||
impl<'a, 'db> CallArguments<'a, 'db> {
|
||||
/// Create a [`CallArguments`] from an iterator over non-variadic positional argument types.
|
||||
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
|
||||
positional_tys
|
||||
.into_iter()
|
||||
.map(Argument::Positional)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Prepend an extra positional argument.
|
||||
pub(crate) fn with_self(&self, self_ty: Type<'db>) -> Self {
|
||||
let mut arguments = Vec::with_capacity(self.0.len() + 1);
|
||||
arguments.push(Argument::Synthetic(self_ty));
|
||||
arguments.extend_from_slice(&self.0);
|
||||
Self(arguments)
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &Argument<'a, 'db>> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
// TODO this should be eliminated in favor of [`bind_call`]
|
||||
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.first().map(Argument::ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
|
||||
type Item = &'b Argument<'a, 'db>;
|
||||
type IntoIter = std::slice::Iter<'b, Argument<'a, 'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> FromIterator<Argument<'a, 'db>> for CallArguments<'a, 'db> {
|
||||
fn from_iter<T: IntoIterator<Item = Argument<'a, 'db>>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Argument<'a, 'db> {
|
||||
/// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site.
|
||||
Synthetic(Type<'db>),
|
||||
/// A positional argument.
|
||||
Positional(Type<'db>),
|
||||
/// A starred positional argument (e.g. `*args`).
|
||||
Variadic(Type<'db>),
|
||||
/// A keyword argument (e.g. `a=1`).
|
||||
Keyword { name: &'a str, ty: Type<'db> },
|
||||
/// The double-starred keywords argument (e.g. `**kwargs`).
|
||||
Keywords(Type<'db>),
|
||||
}
|
||||
|
||||
impl<'db> Argument<'_, 'db> {
|
||||
fn ty(&self) -> Type<'db> {
|
||||
match self {
|
||||
Self::Synthetic(ty) => *ty,
|
||||
Self::Positional(ty) => *ty,
|
||||
Self::Variadic(ty) => *ty,
|
||||
Self::Keyword { name: _, ty } => *ty,
|
||||
Self::Keywords(ty) => *ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
411
crates/red_knot_python_semantic/src/types/call/bind.rs
Normal file
411
crates/red_knot_python_semantic/src/types/call/bind.rs
Normal file
@@ -0,0 +1,411 @@
|
||||
use super::{Argument, CallArguments, InferContext, Signature, Type};
|
||||
use crate::db::Db;
|
||||
use crate::types::diagnostic::{
|
||||
INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED,
|
||||
TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
|
||||
};
|
||||
use crate::types::signatures::Parameter;
|
||||
use crate::types::UnionType;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
/// Bind a [`CallArguments`] against a callable [`Signature`].
|
||||
///
|
||||
/// The returned [`CallBinding`] provides the return type of the call, the bound types for all
|
||||
/// parameters, and any errors resulting from binding the call.
|
||||
pub(crate) fn bind_call<'db>(
|
||||
db: &'db dyn Db,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
signature: &Signature<'db>,
|
||||
callable_ty: Option<Type<'db>>,
|
||||
) -> CallBinding<'db> {
|
||||
let parameters = signature.parameters();
|
||||
// The type assigned to each parameter at this call site.
|
||||
let mut parameter_tys = vec![None; parameters.len()];
|
||||
let mut errors = vec![];
|
||||
let mut next_positional = 0;
|
||||
let mut first_excess_positional = None;
|
||||
let mut num_synthetic_args = 0;
|
||||
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
|
||||
if argument_index >= num_synthetic_args {
|
||||
// Adjust the argument index to skip synthetic args, which don't appear at the call
|
||||
// site and thus won't be in the Call node arguments list.
|
||||
Some(argument_index - num_synthetic_args)
|
||||
} else {
|
||||
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
|
||||
// entire Call node, since there's no argument node for this argument at the call site
|
||||
None
|
||||
}
|
||||
};
|
||||
for (argument_index, argument) in arguments.iter().enumerate() {
|
||||
let (index, parameter, argument_ty, positional) = match argument {
|
||||
Argument::Positional(ty) | Argument::Synthetic(ty) => {
|
||||
if matches!(argument, Argument::Synthetic(_)) {
|
||||
num_synthetic_args += 1;
|
||||
}
|
||||
let Some((index, parameter)) = parameters
|
||||
.get_positional(next_positional)
|
||||
.map(|param| (next_positional, param))
|
||||
.or_else(|| parameters.variadic())
|
||||
else {
|
||||
first_excess_positional.get_or_insert(argument_index);
|
||||
next_positional += 1;
|
||||
continue;
|
||||
};
|
||||
next_positional += 1;
|
||||
(index, parameter, ty, !parameter.is_variadic())
|
||||
}
|
||||
Argument::Keyword { name, ty } => {
|
||||
let Some((index, parameter)) = parameters
|
||||
.keyword_by_name(name)
|
||||
.or_else(|| parameters.keyword_variadic())
|
||||
else {
|
||||
errors.push(CallBindingError::UnknownArgument {
|
||||
argument_name: ast::name::Name::new(name),
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
});
|
||||
continue;
|
||||
};
|
||||
(index, parameter, ty, false)
|
||||
}
|
||||
|
||||
Argument::Variadic(_) | Argument::Keywords(_) => {
|
||||
// TODO
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(expected_ty) = parameter.annotated_ty() {
|
||||
if !argument_ty.is_assignable_to(db, expected_ty) {
|
||||
errors.push(CallBindingError::InvalidArgumentType {
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
expected_ty,
|
||||
provided_ty: *argument_ty,
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
|
||||
if parameter.is_variadic() {
|
||||
let union = UnionType::from_elements(db, [existing, *argument_ty]);
|
||||
parameter_tys[index].replace(union);
|
||||
} else {
|
||||
errors.push(CallBindingError::ParameterAlreadyAssigned {
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(first_excess_argument_index) = first_excess_positional {
|
||||
errors.push(CallBindingError::TooManyPositionalArguments {
|
||||
first_excess_argument_index: get_argument_index(
|
||||
first_excess_argument_index,
|
||||
num_synthetic_args,
|
||||
),
|
||||
expected_positional_count: parameters.positional().count(),
|
||||
provided_positional_count: next_positional,
|
||||
});
|
||||
}
|
||||
let mut missing = vec![];
|
||||
for (index, bound_ty) in parameter_tys.iter().enumerate() {
|
||||
if bound_ty.is_none() {
|
||||
let param = ¶meters[index];
|
||||
if param.is_variadic() || param.is_keyword_variadic() || param.default_ty().is_some() {
|
||||
// variadic/keywords and defaulted arguments are not required
|
||||
continue;
|
||||
}
|
||||
missing.push(ParameterContext::new(param, index, false));
|
||||
}
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
errors.push(CallBindingError::MissingArguments {
|
||||
parameters: ParameterContexts(missing),
|
||||
});
|
||||
}
|
||||
|
||||
CallBinding {
|
||||
callable_ty,
|
||||
return_ty: signature.return_ty.unwrap_or(Type::unknown()),
|
||||
parameter_tys: parameter_tys
|
||||
.into_iter()
|
||||
.map(|opt_ty| opt_ty.unwrap_or(Type::unknown()))
|
||||
.collect(),
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct CallBinding<'db> {
|
||||
/// Type of the callable object (function, class...)
|
||||
callable_ty: Option<Type<'db>>,
|
||||
|
||||
/// Return type of the call.
|
||||
return_ty: Type<'db>,
|
||||
|
||||
/// Bound types for parameters, in parameter source order.
|
||||
parameter_tys: Box<[Type<'db>]>,
|
||||
|
||||
/// Call binding errors, if any.
|
||||
errors: Vec<CallBindingError<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> CallBinding<'db> {
|
||||
// TODO remove this constructor and construct always from `bind_call`
|
||||
pub(crate) fn from_return_ty(return_ty: Type<'db>) -> Self {
|
||||
Self {
|
||||
callable_ty: None,
|
||||
return_ty,
|
||||
parameter_tys: Box::default(),
|
||||
errors: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_return_ty(&mut self, return_ty: Type<'db>) {
|
||||
self.return_ty = return_ty;
|
||||
}
|
||||
|
||||
pub(crate) fn return_ty(&self) -> Type<'db> {
|
||||
self.return_ty
|
||||
}
|
||||
|
||||
pub(crate) fn parameter_tys(&self) -> &[Type<'db>] {
|
||||
&self.parameter_tys
|
||||
}
|
||||
|
||||
pub(crate) fn one_parameter_ty(&self) -> Option<Type<'db>> {
|
||||
match self.parameter_tys() {
|
||||
[ty] => Some(*ty),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn two_parameter_tys(&self) -> Option<(Type<'db>, Type<'db>)> {
|
||||
match self.parameter_tys() {
|
||||
[first, second] => Some((*first, *second)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn callable_name(&self, db: &'db dyn Db) -> Option<&str> {
|
||||
match self.callable_ty {
|
||||
Some(Type::FunctionLiteral(function)) => Some(function.name(db)),
|
||||
Some(Type::ClassLiteral(class_type)) => Some(class_type.class.name(db)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
|
||||
let callable_name = self.callable_name(context.db());
|
||||
for error in &self.errors {
|
||||
error.report_diagnostic(context, node, callable_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information needed to emit a diagnostic regarding a parameter.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ParameterContext {
|
||||
name: Option<ast::name::Name>,
|
||||
index: usize,
|
||||
|
||||
/// Was the argument for this parameter passed positionally, and matched to a non-variadic
|
||||
/// positional parameter? (If so, we will provide the index in the diagnostic, not just the
|
||||
/// name.)
|
||||
positional: bool,
|
||||
}
|
||||
|
||||
impl ParameterContext {
|
||||
fn new(parameter: &Parameter, index: usize, positional: bool) -> Self {
|
||||
Self {
|
||||
name: parameter.display_name(),
|
||||
index,
|
||||
positional,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ParameterContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(name) = &self.name {
|
||||
if self.positional {
|
||||
write!(f, "{} (`{name}`)", self.index + 1)
|
||||
} else {
|
||||
write!(f, "`{name}`")
|
||||
}
|
||||
} else {
|
||||
write!(f, "{}", self.index + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ParameterContexts(Vec<ParameterContext>);
|
||||
|
||||
impl std::fmt::Display for ParameterContexts {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut iter = self.0.iter();
|
||||
if let Some(first) = iter.next() {
|
||||
write!(f, "{first}")?;
|
||||
for param in iter {
|
||||
f.write_str(", ")?;
|
||||
write!(f, "{param}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum CallBindingError<'db> {
|
||||
/// The type of an argument is not assignable to the annotated type of its corresponding
|
||||
/// parameter.
|
||||
InvalidArgumentType {
|
||||
parameter: ParameterContext,
|
||||
argument_index: Option<usize>,
|
||||
expected_ty: Type<'db>,
|
||||
provided_ty: Type<'db>,
|
||||
},
|
||||
/// One or more required parameters (that is, with no default) is not supplied by any argument.
|
||||
MissingArguments { parameters: ParameterContexts },
|
||||
/// A call argument can't be matched to any parameter.
|
||||
UnknownArgument {
|
||||
argument_name: ast::name::Name,
|
||||
argument_index: Option<usize>,
|
||||
},
|
||||
/// More positional arguments are provided in the call than can be handled by the signature.
|
||||
TooManyPositionalArguments {
|
||||
first_excess_argument_index: Option<usize>,
|
||||
expected_positional_count: usize,
|
||||
provided_positional_count: usize,
|
||||
},
|
||||
/// Multiple arguments were provided for a single parameter.
|
||||
ParameterAlreadyAssigned {
|
||||
argument_index: Option<usize>,
|
||||
parameter: ParameterContext,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'db> CallBindingError<'db> {
|
||||
pub(super) fn report_diagnostic(
|
||||
&self,
|
||||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
callable_name: Option<&str>,
|
||||
) {
|
||||
match self {
|
||||
Self::InvalidArgumentType {
|
||||
parameter,
|
||||
argument_index,
|
||||
expected_ty,
|
||||
provided_ty,
|
||||
} => {
|
||||
let provided_ty_display = provided_ty.display(context.db());
|
||||
let expected_ty_display = expected_ty.display(context.db());
|
||||
context.report_lint(
|
||||
&INVALID_ARGUMENT_TYPE,
|
||||
Self::get_node(node, *argument_index),
|
||||
format_args!(
|
||||
"Object of type `{provided_ty_display}` cannot be assigned to \
|
||||
parameter {parameter}{}; expected type `{expected_ty_display}`",
|
||||
if let Some(callable_name) = callable_name {
|
||||
format!(" of function `{callable_name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Self::TooManyPositionalArguments {
|
||||
first_excess_argument_index,
|
||||
expected_positional_count,
|
||||
provided_positional_count,
|
||||
} => {
|
||||
context.report_lint(
|
||||
&TOO_MANY_POSITIONAL_ARGUMENTS,
|
||||
Self::get_node(node, *first_excess_argument_index),
|
||||
format_args!(
|
||||
"Too many positional arguments{}: expected \
|
||||
{expected_positional_count}, got {provided_positional_count}",
|
||||
if let Some(callable_name) = callable_name {
|
||||
format!(" to function `{callable_name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Self::MissingArguments { parameters } => {
|
||||
let s = if parameters.0.len() == 1 { "" } else { "s" };
|
||||
context.report_lint(
|
||||
&MISSING_ARGUMENT,
|
||||
node,
|
||||
format_args!(
|
||||
"No argument{s} provided for required parameter{s} {parameters}{}",
|
||||
if let Some(callable_name) = callable_name {
|
||||
format!(" of function `{callable_name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Self::UnknownArgument {
|
||||
argument_name,
|
||||
argument_index,
|
||||
} => {
|
||||
context.report_lint(
|
||||
&UNKNOWN_ARGUMENT,
|
||||
Self::get_node(node, *argument_index),
|
||||
format_args!(
|
||||
"Argument `{argument_name}` does not match any known parameter{}",
|
||||
if let Some(callable_name) = callable_name {
|
||||
format!(" of function `{callable_name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Self::ParameterAlreadyAssigned {
|
||||
argument_index,
|
||||
parameter,
|
||||
} => {
|
||||
context.report_lint(
|
||||
&PARAMETER_ALREADY_ASSIGNED,
|
||||
Self::get_node(node, *argument_index),
|
||||
format_args!(
|
||||
"Multiple values provided for parameter {parameter}{}",
|
||||
if let Some(callable_name) = callable_name {
|
||||
format!(" of function `{callable_name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_node(node: ast::AnyNodeRef, argument_index: Option<usize>) -> ast::AnyNodeRef {
|
||||
// If we have a Call node and an argument index, report the diagnostic on the correct
|
||||
// argument node; otherwise, report it on the entire provided node.
|
||||
match (node, argument_index) {
|
||||
(ast::AnyNodeRef::ExprCall(call_node), Some(argument_index)) => {
|
||||
match call_node
|
||||
.arguments
|
||||
.arguments_source_order()
|
||||
.nth(argument_index)
|
||||
.expect("argument index should not be out of range")
|
||||
{
|
||||
ast::ArgOrKeyword::Arg(expr) => expr.into(),
|
||||
ast::ArgOrKeyword::Keyword(keyword) => keyword.into(),
|
||||
}
|
||||
}
|
||||
_ => node,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::types::{
|
||||
todo_type, Class, ClassLiteralType, KnownClass, KnownInstanceType, TodoType, Type,
|
||||
todo_type, Class, ClassLiteralType, DynamicType, KnownClass, KnownInstanceType, Type,
|
||||
};
|
||||
use crate::Db;
|
||||
use itertools::Either;
|
||||
@@ -8,16 +8,29 @@ use itertools::Either;
|
||||
///
|
||||
/// This is much more limited than the [`Type`] enum:
|
||||
/// all types that would be invalid to have as a class base are
|
||||
/// transformed into [`ClassBase::Unknown`]
|
||||
/// transformed into [`ClassBase::unknown`]
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
|
||||
pub enum ClassBase<'db> {
|
||||
Any,
|
||||
Unknown,
|
||||
Todo(TodoType),
|
||||
Dynamic(DynamicType),
|
||||
Class(Class<'db>),
|
||||
}
|
||||
|
||||
impl<'db> ClassBase<'db> {
|
||||
pub const fn any() -> Self {
|
||||
Self::Dynamic(DynamicType::Any)
|
||||
}
|
||||
|
||||
pub const fn unknown() -> Self {
|
||||
Self::Dynamic(DynamicType::Unknown)
|
||||
}
|
||||
|
||||
pub const fn is_dynamic(self) -> bool {
|
||||
match self {
|
||||
ClassBase::Dynamic(_) => true,
|
||||
ClassBase::Class(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
|
||||
struct Display<'db> {
|
||||
base: ClassBase<'db>,
|
||||
@@ -27,9 +40,7 @@ impl<'db> ClassBase<'db> {
|
||||
impl std::fmt::Display for Display<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.base {
|
||||
ClassBase::Any => f.write_str("Any"),
|
||||
ClassBase::Todo(todo) => todo.fmt(f),
|
||||
ClassBase::Unknown => f.write_str("Unknown"),
|
||||
ClassBase::Dynamic(dynamic) => dynamic.fmt(f),
|
||||
ClassBase::Class(class) => write!(f, "<class '{}'>", class.name(self.db)),
|
||||
}
|
||||
}
|
||||
@@ -43,7 +54,7 @@ impl<'db> ClassBase<'db> {
|
||||
KnownClass::Object
|
||||
.to_class_literal(db)
|
||||
.into_class_literal()
|
||||
.map_or(Self::Unknown, |ClassLiteralType { class }| {
|
||||
.map_or(Self::unknown(), |ClassLiteralType { class }| {
|
||||
Self::Class(class)
|
||||
})
|
||||
}
|
||||
@@ -53,9 +64,7 @@ impl<'db> ClassBase<'db> {
|
||||
/// Return `None` if `ty` is not an acceptable type for a class base.
|
||||
pub(super) fn try_from_ty(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
|
||||
match ty {
|
||||
Type::Any => Some(Self::Any),
|
||||
Type::Unknown => Some(Self::Unknown),
|
||||
Type::Todo(todo) => Some(Self::Todo(todo)),
|
||||
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
|
||||
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
|
||||
Type::Intersection(_) => None, // TODO -- probably incorrect?
|
||||
@@ -93,8 +102,12 @@ impl<'db> ClassBase<'db> {
|
||||
| KnownInstanceType::Required
|
||||
| KnownInstanceType::TypeAlias
|
||||
| KnownInstanceType::ReadOnly
|
||||
| KnownInstanceType::Optional => None,
|
||||
KnownInstanceType::Any => Some(Self::Any),
|
||||
| KnownInstanceType::Optional
|
||||
| KnownInstanceType::Not
|
||||
| KnownInstanceType::Intersection
|
||||
| KnownInstanceType::TypeOf => None,
|
||||
KnownInstanceType::Unknown => Some(Self::unknown()),
|
||||
KnownInstanceType::Any => Some(Self::any()),
|
||||
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
|
||||
KnownInstanceType::Dict => {
|
||||
Self::try_from_ty(db, KnownClass::Dict.to_class_literal(db))
|
||||
@@ -139,7 +152,7 @@ impl<'db> ClassBase<'db> {
|
||||
pub(super) fn into_class(self) -> Option<Class<'db>> {
|
||||
match self {
|
||||
Self::Class(class) => Some(class),
|
||||
_ => None,
|
||||
Self::Dynamic(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,13 +162,7 @@ impl<'db> ClassBase<'db> {
|
||||
db: &'db dyn Db,
|
||||
) -> Either<impl Iterator<Item = ClassBase<'db>>, impl Iterator<Item = ClassBase<'db>>> {
|
||||
match self {
|
||||
ClassBase::Any => Either::Left([ClassBase::Any, ClassBase::object(db)].into_iter()),
|
||||
ClassBase::Unknown => {
|
||||
Either::Left([ClassBase::Unknown, ClassBase::object(db)].into_iter())
|
||||
}
|
||||
ClassBase::Todo(todo) => {
|
||||
Either::Left([ClassBase::Todo(todo), ClassBase::object(db)].into_iter())
|
||||
}
|
||||
ClassBase::Dynamic(_) => Either::Left([self, ClassBase::object(db)].into_iter()),
|
||||
ClassBase::Class(class) => Either::Right(class.iter_mro(db)),
|
||||
}
|
||||
}
|
||||
@@ -170,9 +177,7 @@ impl<'db> From<Class<'db>> for ClassBase<'db> {
|
||||
impl<'db> From<ClassBase<'db>> for Type<'db> {
|
||||
fn from(value: ClassBase<'db>) -> Self {
|
||||
match value {
|
||||
ClassBase::Any => Type::Any,
|
||||
ClassBase::Todo(todo) => Type::Todo(todo),
|
||||
ClassBase::Unknown => Type::Unknown,
|
||||
ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic),
|
||||
ClassBase::Class(class) => Type::class_literal(class),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,11 @@ impl<'db> InferContext<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Are we currently inferring types in a stub file?
|
||||
pub(crate) fn in_stub(&self) -> bool {
|
||||
self.file.is_stub(self.db().upcast())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
|
||||
self.bomb.defuse();
|
||||
|
||||
@@ -30,27 +30,35 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&INCOMPATIBLE_SLOTS);
|
||||
registry.register_lint(&INCONSISTENT_MRO);
|
||||
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
|
||||
registry.register_lint(&INVALID_ARGUMENT_TYPE);
|
||||
registry.register_lint(&INVALID_ASSIGNMENT);
|
||||
registry.register_lint(&INVALID_BASE);
|
||||
registry.register_lint(&INVALID_CONTEXT_MANAGER);
|
||||
registry.register_lint(&INVALID_DECLARATION);
|
||||
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
|
||||
registry.register_lint(&INVALID_METACLASS);
|
||||
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
|
||||
registry.register_lint(&INVALID_RAISE);
|
||||
registry.register_lint(&INVALID_TYPE_FORM);
|
||||
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
|
||||
registry.register_lint(&MISSING_ARGUMENT);
|
||||
registry.register_lint(&NON_SUBSCRIPTABLE);
|
||||
registry.register_lint(&NOT_ITERABLE);
|
||||
registry.register_lint(&PARAMETER_ALREADY_ASSIGNED);
|
||||
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
|
||||
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
|
||||
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
|
||||
registry.register_lint(&SUBCLASS_OF_FINAL_CLASS);
|
||||
registry.register_lint(&TYPE_ASSERTION_FAILURE);
|
||||
registry.register_lint(&TOO_MANY_POSITIONAL_ARGUMENTS);
|
||||
registry.register_lint(&UNDEFINED_REVEAL);
|
||||
registry.register_lint(&UNKNOWN_ARGUMENT);
|
||||
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
|
||||
registry.register_lint(&UNRESOLVED_IMPORT);
|
||||
registry.register_lint(&UNRESOLVED_REFERENCE);
|
||||
registry.register_lint(&UNSUPPORTED_OPERATOR);
|
||||
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
|
||||
registry.register_lint(&STATIC_ASSERT_ERROR);
|
||||
|
||||
// String annotations
|
||||
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
||||
@@ -226,6 +234,27 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Detects call arguments whose type is not assignable to the corresponding typed parameter.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Passing an argument of a type the function (or callable object) does not accept violates
|
||||
/// the expectations of the function author and may cause unexpected runtime errors within the
|
||||
/// body of the function.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// def func(x: int): ...
|
||||
/// func("foo") # error: [invalid-argument-type]
|
||||
/// ```
|
||||
pub(crate) static INVALID_ARGUMENT_TYPE = {
|
||||
summary: "detects call arguments whose type is not assignable to the corresponding typed parameter",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// TODO #14889
|
||||
pub(crate) static INVALID_ASSIGNMENT = {
|
||||
@@ -263,6 +292,7 @@ declare_lint! {
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for exception handlers that catch non-exception classes.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
@@ -297,6 +327,33 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for arguments to `metaclass=` that are invalid.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python allows arbitrary expressions to be used as the argument to `metaclass=`.
|
||||
/// These expressions, however, need to be callable and accept the same arguments
|
||||
/// as `type.__new__`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def f(): ...
|
||||
///
|
||||
/// # TypeError: f() takes 0 positional arguments but 3 were given
|
||||
/// class B(metaclass=f): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
|
||||
pub(crate) static INVALID_METACLASS = {
|
||||
summary: "detects invalid `metaclass=` arguments",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for default values that can't be assigned to the parameter's annotated type.
|
||||
@@ -375,6 +432,25 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for missing required arguments in a call.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Failing to provide a required argument will raise a `TypeError` at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// def func(x: int): ...
|
||||
/// func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
/// ```
|
||||
pub(crate) static MISSING_ARGUMENT = {
|
||||
summary: "detects missing required arguments in a call",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for subscripting objects that do not support subscripting.
|
||||
@@ -413,6 +489,27 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls which provide more than one argument for a single parameter.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Providing multiple values for a single parameter will raise a `TypeError` at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```python
|
||||
/// def f(x: int) -> int: ...
|
||||
///
|
||||
/// f(1, x=2) # Error raised here
|
||||
/// ```
|
||||
pub(crate) static PARAMETER_ALREADY_ASSIGNED = {
|
||||
summary: "detects multiple arguments for the same parameter",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for possibly unbound attributes.
|
||||
@@ -479,6 +576,49 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for `assert_type()` calls where the actual type
|
||||
/// is not the same as the asserted type.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `assert_type()` allows confirming the inferred type of a certain value.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def _(x: int):
|
||||
/// assert_type(x, int) # fine
|
||||
/// assert_type(x, str) # error: Actual type does not match asserted type
|
||||
/// ```
|
||||
pub(crate) static TYPE_ASSERTION_FAILURE = {
|
||||
summary: "detects failed type assertions",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls that pass more positional arguments than the callable can accept.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Passing too many positional arguments will raise `TypeError` at runtime.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def f(): ...
|
||||
///
|
||||
/// f("foo") # Error raised here
|
||||
/// ```
|
||||
pub(crate) static TOO_MANY_POSITIONAL_ARGUMENTS = {
|
||||
summary: "detects calls passing too many positional arguments",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls to `reveal_type` without importing it.
|
||||
@@ -495,6 +635,27 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for keyword arguments in calls that don't match any parameter of the callable.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Providing an unknown keyword argument will raise `TypeError` at runtime.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def f(x: int) -> int: ...
|
||||
///
|
||||
/// f(x=1, y=2) # Error raised here
|
||||
/// ```
|
||||
pub(crate) static UNKNOWN_ARGUMENT = {
|
||||
summary: "detects unknown keyword arguments in calls",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for unresolved attributes.
|
||||
@@ -570,6 +731,25 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Makes sure that the argument of `static_assert` is statically known to be true.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// from knot_extensions import static_assert
|
||||
///
|
||||
/// static_assert(1 + 1 == 3) # error: evaluates to `False`
|
||||
///
|
||||
/// static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness
|
||||
/// ```
|
||||
pub(crate) static STATIC_ASSERT_ERROR = {
|
||||
summary: "Failed static assertion",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct TypeCheckDiagnostic {
|
||||
pub(crate) id: DiagnosticId,
|
||||
|
||||
@@ -8,8 +8,8 @@ use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::{
|
||||
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
|
||||
SubclassOfType, Type, UnionType,
|
||||
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, Type,
|
||||
UnionType,
|
||||
};
|
||||
use crate::Db;
|
||||
use rustc_hash::FxHashMap;
|
||||
@@ -65,9 +65,8 @@ struct DisplayRepresentation<'db> {
|
||||
impl Display for DisplayRepresentation<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self.ty {
|
||||
Type::Any => f.write_str("Any"),
|
||||
Type::Dynamic(dynamic) => dynamic.fmt(f),
|
||||
Type::Never => f.write_str("Never"),
|
||||
Type::Unknown => f.write_str("Unknown"),
|
||||
Type::Instance(InstanceType { class }) => {
|
||||
let representation = match class.known(self.db) {
|
||||
Some(KnownClass::NoneType) => "None",
|
||||
@@ -76,24 +75,17 @@ impl Display for DisplayRepresentation<'_> {
|
||||
};
|
||||
f.write_str(representation)
|
||||
}
|
||||
// `[Type::Todo]`'s display should be explicit that is not a valid display of
|
||||
// any other type
|
||||
Type::Todo(todo) => write!(f, "@Todo{todo}"),
|
||||
Type::ModuleLiteral(module) => {
|
||||
write!(f, "<module '{}'>", module.module(self.db).name())
|
||||
}
|
||||
// TODO functions and classes should display using a fully qualified name
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)),
|
||||
Type::SubclassOf(SubclassOfType {
|
||||
base: ClassBase::Class(class),
|
||||
}) => {
|
||||
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
|
||||
// Only show the bare class name here; ClassBase::display would render this as
|
||||
// type[<class 'Foo'>] instead of type[Foo].
|
||||
write!(f, "type[{}]", class.name(self.db))
|
||||
}
|
||||
Type::SubclassOf(SubclassOfType { base }) => {
|
||||
write!(f, "type[{}]", base.display(self.db))
|
||||
}
|
||||
ClassBase::Class(class) => write!(f, "type[{}]", class.name(self.db)),
|
||||
ClassBase::Dynamic(dynamic) => write!(f, "type[{dynamic}]"),
|
||||
},
|
||||
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
|
||||
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
@@ -177,12 +169,9 @@ impl Display for DisplayUnionType<'_> {
|
||||
|
||||
for element in elements {
|
||||
if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) {
|
||||
let Some(mut condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
|
||||
let Some(condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
|
||||
continue;
|
||||
};
|
||||
if kind == CondensedDisplayTypeKind::Int {
|
||||
condensed_kind.sort_unstable_by_key(|ty| ty.expect_int_literal());
|
||||
}
|
||||
join.entry(&DisplayLiteralGroup {
|
||||
literals: condensed_kind,
|
||||
db: self.db,
|
||||
@@ -223,17 +212,12 @@ impl Display for DisplayLiteralGroup<'_> {
|
||||
|
||||
/// Enumeration of literal types that are displayed in a "condensed way" inside `Literal` slices.
|
||||
///
|
||||
/// For example, `Literal[1] | Literal[2]` is displayed as `"Literal[1, 2]"`.
|
||||
/// Not all `Literal` types are displayed using `Literal` slices
|
||||
/// (e.g. it would be inappropriate to display `LiteralString`
|
||||
/// as `Literal[LiteralString]`).
|
||||
/// For example, `Literal[1] | Literal[2] | Literal["s"]` is displayed as `"Literal[1, 2, "s"]"`.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
enum CondensedDisplayTypeKind {
|
||||
Class,
|
||||
Function,
|
||||
Int,
|
||||
String,
|
||||
Bytes,
|
||||
LiteralExpression,
|
||||
}
|
||||
|
||||
impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
|
||||
@@ -243,9 +227,10 @@ impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
|
||||
match value {
|
||||
Type::ClassLiteral(_) => Ok(Self::Class),
|
||||
Type::FunctionLiteral(_) => Ok(Self::Function),
|
||||
Type::IntLiteral(_) => Ok(Self::Int),
|
||||
Type::StringLiteral(_) => Ok(Self::String),
|
||||
Type::BytesLiteral(_) => Ok(Self::Bytes),
|
||||
Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::BooleanLiteral(_) => Ok(Self::LiteralExpression),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
@@ -372,64 +357,8 @@ impl Display for DisplayStringLiteralType<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::{global_symbol, SliceLiteralType, StringLiteralType, Type, UnionType};
|
||||
|
||||
#[test]
|
||||
fn test_condense_literal_display_by_type() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/main.py",
|
||||
"
|
||||
def foo(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
def bar(s: str) -> str:
|
||||
return s
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
",
|
||||
)?;
|
||||
let mod_file = system_path_to_file(&db, "src/main.py").expect("file to exist");
|
||||
|
||||
let union_elements = &[
|
||||
Type::Unknown,
|
||||
Type::IntLiteral(-1),
|
||||
global_symbol(&db, mod_file, "A").expect_type(),
|
||||
Type::string_literal(&db, "A"),
|
||||
Type::bytes_literal(&db, &[0u8]),
|
||||
Type::bytes_literal(&db, &[7u8]),
|
||||
Type::IntLiteral(0),
|
||||
Type::IntLiteral(1),
|
||||
Type::string_literal(&db, "B"),
|
||||
global_symbol(&db, mod_file, "foo").expect_type(),
|
||||
global_symbol(&db, mod_file, "bar").expect_type(),
|
||||
global_symbol(&db, mod_file, "B").expect_type(),
|
||||
Type::BooleanLiteral(true),
|
||||
Type::none(&db),
|
||||
];
|
||||
let union = UnionType::from_elements(&db, union_elements).expect_union();
|
||||
let display = format!("{}", union.display(&db));
|
||||
assert_eq!(
|
||||
display,
|
||||
concat!(
|
||||
"Unknown | ",
|
||||
"Literal[-1, 0, 1] | ",
|
||||
"Literal[A, B] | ",
|
||||
"Literal[\"A\", \"B\"] | ",
|
||||
"Literal[b\"\\x00\", b\"\\x07\"] | ",
|
||||
"Literal[foo, bar] | ",
|
||||
"Literal[True] | ",
|
||||
"None"
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
use crate::types::{SliceLiteralType, StringLiteralType, Type};
|
||||
|
||||
#[test]
|
||||
fn test_slice_literal_display() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ impl<'db> Mro<'db> {
|
||||
pub(super) fn from_error(db: &'db dyn Db, class: Class<'db>) -> Self {
|
||||
Self::from([
|
||||
ClassBase::Class(class),
|
||||
ClassBase::Unknown,
|
||||
ClassBase::unknown(),
|
||||
ClassBase::object(db),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
|
||||
use crate::semantic_index::symbol_table;
|
||||
use crate::types::{
|
||||
infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass,
|
||||
KnownConstraintFunction, KnownFunction, Truthiness, Type, UnionBuilder,
|
||||
infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass, KnownFunction,
|
||||
SubclassOfType, Truthiness, Type, UnionBuilder,
|
||||
};
|
||||
use crate::Db;
|
||||
use itertools::Itertools;
|
||||
@@ -83,28 +83,39 @@ fn all_negative_narrowing_constraints_for_expression<'db>(
|
||||
NarrowingConstraintsBuilder::new(db, ConstraintNode::Expression(expression), false).finish()
|
||||
}
|
||||
|
||||
/// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`.
|
||||
///
|
||||
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
|
||||
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
|
||||
fn generate_classinfo_constraint<'db, F>(
|
||||
db: &'db dyn Db,
|
||||
classinfo: &Type<'db>,
|
||||
to_constraint: F,
|
||||
) -> Option<Type<'db>>
|
||||
where
|
||||
F: Fn(ClassLiteralType<'db>) -> Type<'db> + Copy,
|
||||
{
|
||||
match classinfo {
|
||||
Type::Tuple(tuple) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for element in tuple.elements(db) {
|
||||
builder = builder.add(generate_classinfo_constraint(db, element, to_constraint)?);
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum KnownConstraintFunction {
|
||||
/// `builtins.isinstance`
|
||||
IsInstance,
|
||||
/// `builtins.issubclass`
|
||||
IsSubclass,
|
||||
}
|
||||
|
||||
impl KnownConstraintFunction {
|
||||
/// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`.
|
||||
///
|
||||
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
|
||||
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
|
||||
fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option<Type<'db>> {
|
||||
let constraint_fn = |class| match self {
|
||||
KnownConstraintFunction::IsInstance => Type::instance(class),
|
||||
KnownConstraintFunction::IsSubclass => SubclassOfType::from(db, class),
|
||||
};
|
||||
|
||||
match classinfo {
|
||||
Type::Tuple(tuple) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for element in tuple.elements(db) {
|
||||
builder = builder.add(self.generate_constraint(db, *element)?);
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
Some(builder.build())
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => Some(constraint_fn(class)),
|
||||
Type::SubclassOf(subclass_of_ty) => {
|
||||
subclass_of_ty.subclass_of().into_class().map(constraint_fn)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
Type::ClassLiteral(class_literal_type) => Some(to_constraint(*class_literal_type)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +233,9 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
PatternConstraintKind::Singleton(singleton, _guard) => {
|
||||
self.evaluate_match_pattern_singleton(*subject, *singleton)
|
||||
}
|
||||
PatternConstraintKind::Class(cls, _guard) => {
|
||||
self.evaluate_match_pattern_class(*subject, *cls)
|
||||
}
|
||||
// TODO: support more pattern kinds
|
||||
PatternConstraintKind::Value(..) | PatternConstraintKind::Unsupported => None,
|
||||
}
|
||||
@@ -429,24 +443,13 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
let class_info_ty =
|
||||
inference.expression_ty(class_info.scoped_expression_id(self.db, scope));
|
||||
|
||||
let to_constraint = match function {
|
||||
KnownConstraintFunction::IsInstance => {
|
||||
|class_literal: ClassLiteralType<'db>| Type::instance(class_literal.class)
|
||||
}
|
||||
KnownConstraintFunction::IsSubclass => {
|
||||
|class_literal: ClassLiteralType<'db>| {
|
||||
Type::subclass_of(class_literal.class)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generate_classinfo_constraint(self.db, &class_info_ty, to_constraint).map(
|
||||
|constraint| {
|
||||
function
|
||||
.generate_constraint(self.db, class_info_ty)
|
||||
.map(|constraint| {
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
constraints.insert(symbol, constraint.negate_if(self.db, !is_positive));
|
||||
constraints
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
// for the expression `bool(E)`, we further narrow the type based on `E`
|
||||
Type::ClassLiteral(class_type)
|
||||
@@ -486,6 +489,27 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_match_pattern_class(
|
||||
&mut self,
|
||||
subject: Expression<'db>,
|
||||
cls: Expression<'db>,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
if let Some(ast::ExprName { id, .. }) = subject.node_ref(self.db).as_name_expr() {
|
||||
// SAFETY: we should always have a symbol for every Name node.
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
let 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))
|
||||
.to_instance(self.db);
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
constraints.insert(symbol, ty);
|
||||
Some(constraints)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_bool_op(
|
||||
&mut self,
|
||||
expr_bool_op: &ExprBoolOp,
|
||||
|
||||
@@ -123,14 +123,61 @@ impl Arbitrary for Ty {
|
||||
}
|
||||
|
||||
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
|
||||
// This is incredibly naive. We can do much better here by
|
||||
// trying various subsets of the elements in unions, tuples,
|
||||
// and intersections. For now, we only try to shrink by
|
||||
// reducing unions/tuples/intersections to a single element.
|
||||
match self.clone() {
|
||||
Ty::Union(types) => Box::new(types.into_iter()),
|
||||
Ty::Tuple(types) => Box::new(types.into_iter()),
|
||||
Ty::Intersection { pos, neg } => Box::new(pos.into_iter().chain(neg)),
|
||||
Ty::Union(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() {
|
||||
0 => None,
|
||||
1 => Some(elts.into_iter().next().unwrap()),
|
||||
_ => Some(Ty::Union(elts)),
|
||||
})),
|
||||
Ty::Tuple(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() {
|
||||
0 => None,
|
||||
1 => Some(elts.into_iter().next().unwrap()),
|
||||
_ => Some(Ty::Tuple(elts)),
|
||||
})),
|
||||
Ty::Intersection { pos, neg } => {
|
||||
// Shrinking on intersections is not exhaustive!
|
||||
//
|
||||
// We try to shrink the positive side or the negative side,
|
||||
// but we aren't shrinking both at the same time.
|
||||
//
|
||||
// This should remove positive or negative constraints but
|
||||
// won't shrink (A & B & ~C & ~D) to (A & ~C) in one shrink
|
||||
// iteration.
|
||||
//
|
||||
// Instead, it hopes that (A & B & ~C) or (A & ~C & ~D) fails
|
||||
// so that shrinking can happen there.
|
||||
let pos_orig = pos.clone();
|
||||
let neg_orig = neg.clone();
|
||||
Box::new(
|
||||
// we shrink negative constraints first, as
|
||||
// intersections with only negative constraints are
|
||||
// more confusing
|
||||
neg.shrink()
|
||||
.map(move |shrunk_neg| Ty::Intersection {
|
||||
pos: pos_orig.clone(),
|
||||
neg: shrunk_neg,
|
||||
})
|
||||
.chain(pos.shrink().map(move |shrunk_pos| Ty::Intersection {
|
||||
pos: shrunk_pos,
|
||||
neg: neg_orig.clone(),
|
||||
}))
|
||||
.filter_map(|ty| {
|
||||
if let Ty::Intersection { pos, neg } = &ty {
|
||||
match (pos.len(), neg.len()) {
|
||||
// an empty intersection does not mean
|
||||
// anything
|
||||
(0, 0) => None,
|
||||
// a single positive element should be
|
||||
// unwrapped
|
||||
(1, 0) => Some(pos[0].clone()),
|
||||
_ => Some(ty),
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
_ => Box::new(std::iter::empty()),
|
||||
}
|
||||
}
|
||||
@@ -173,6 +220,8 @@ macro_rules! type_property_test {
|
||||
}
|
||||
|
||||
mod stable {
|
||||
use crate::types::{KnownClass, Type};
|
||||
|
||||
// `T` is equivalent to itself.
|
||||
type_property_test!(
|
||||
equivalent_to_is_reflexive, db,
|
||||
@@ -209,12 +258,6 @@ mod stable {
|
||||
forall types s, t. s.is_subtype_of(db, t) => !s.is_disjoint_from(db, t) || s.is_never()
|
||||
);
|
||||
|
||||
// `T` can be assigned to itself.
|
||||
type_property_test!(
|
||||
assignable_to_is_reflexive, db,
|
||||
forall types t. t.is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// `S <: T` implies that `S` can be assigned to `T`.
|
||||
type_property_test!(
|
||||
subtype_of_implies_assignable_to, db,
|
||||
@@ -238,6 +281,30 @@ mod stable {
|
||||
non_fully_static_types_do_not_participate_in_subtyping, db,
|
||||
forall types s, t. !s.is_fully_static(db) => !s.is_subtype_of(db, t) && !t.is_subtype_of(db, s)
|
||||
);
|
||||
|
||||
// All types should be assignable to `object`
|
||||
type_property_test!(
|
||||
all_types_assignable_to_object, db,
|
||||
forall types t. t.is_assignable_to(db, KnownClass::Object.to_instance(db))
|
||||
);
|
||||
|
||||
// And for fully static types, they should also be subtypes of `object`
|
||||
type_property_test!(
|
||||
all_fully_static_types_subtype_of_object, db,
|
||||
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, KnownClass::Object.to_instance(db))
|
||||
);
|
||||
|
||||
// Never should be assignable to every type
|
||||
type_property_test!(
|
||||
never_assignable_to_every_type, db,
|
||||
forall types t. Type::Never.is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// And it should be a subtype of all fully static types
|
||||
type_property_test!(
|
||||
never_subtype_of_every_fully_static_type, db,
|
||||
forall types t. t.is_fully_static(db) => Type::Never.is_subtype_of(db, t)
|
||||
);
|
||||
}
|
||||
|
||||
/// This module contains property tests that currently lead to many false positives.
|
||||
@@ -248,6 +315,32 @@ mod stable {
|
||||
/// tests to the `stable` section. In the meantime, it can still be useful to run these
|
||||
/// tests (using [`types::property_tests::flaky`]), to see if there are any new obvious bugs.
|
||||
mod flaky {
|
||||
use crate::{
|
||||
db::tests::TestDb,
|
||||
types::{IntersectionBuilder, Type},
|
||||
};
|
||||
|
||||
// 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)
|
||||
);
|
||||
|
||||
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
|
||||
// An intersection of two types should be assignable to both of them
|
||||
fn intersection<'db>(db: &'db TestDb, s: Type<'db>, t: Type<'db>) -> Type<'db> {
|
||||
IntersectionBuilder::new(db)
|
||||
.add_positive(s)
|
||||
.add_positive(t)
|
||||
.build()
|
||||
}
|
||||
|
||||
type_property_test!(
|
||||
intersection_assignable_to_both, db,
|
||||
forall types s, t. intersection(db, s, t).is_assignable_to(db, s) && intersection(db, s, t).is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// `S <: T` and `T <: S` implies that `S` is equivalent to `T`.
|
||||
type_property_test!(
|
||||
subtype_of_is_antisymmetric, db,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
use super::{definition_expression_ty, Type};
|
||||
use crate::Db;
|
||||
use crate::{semantic_index::definition::Definition, types::todo_type};
|
||||
@@ -7,10 +6,18 @@ use ruff_python_ast::{self as ast, name::Name};
|
||||
/// A typed callable signature.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Signature<'db> {
|
||||
/// Parameters, in source order.
|
||||
///
|
||||
/// The ordering of parameters in a valid signature must be: first positional-only parameters,
|
||||
/// then positional-or-keyword, then optionally the variadic parameter, then keyword-only
|
||||
/// parameters, and last, optionally the variadic keywords parameter. Parameters with defaults
|
||||
/// must come after parameters without defaults.
|
||||
///
|
||||
/// We may get invalid signatures, though, and need to handle them without panicking.
|
||||
parameters: Parameters<'db>,
|
||||
|
||||
/// Annotated return type (Unknown if no annotation.)
|
||||
pub(crate) return_ty: Type<'db>,
|
||||
/// Annotated return type, if any.
|
||||
pub(crate) return_ty: Option<Type<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> Signature<'db> {
|
||||
@@ -18,7 +25,7 @@ impl<'db> Signature<'db> {
|
||||
pub(crate) fn todo() -> Self {
|
||||
Self {
|
||||
parameters: Parameters::todo(),
|
||||
return_ty: todo_type!("return type"),
|
||||
return_ty: Some(todo_type!("return type")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,17 +35,13 @@ impl<'db> Signature<'db> {
|
||||
definition: Definition<'db>,
|
||||
function_node: &'db ast::StmtFunctionDef,
|
||||
) -> Self {
|
||||
let return_ty = function_node
|
||||
.returns
|
||||
.as_ref()
|
||||
.map(|returns| {
|
||||
if function_node.is_async {
|
||||
todo_type!("generic types.CoroutineType")
|
||||
} else {
|
||||
definition_expression_ty(db, definition, returns.as_ref())
|
||||
}
|
||||
})
|
||||
.unwrap_or(Type::Unknown);
|
||||
let return_ty = function_node.returns.as_ref().map(|returns| {
|
||||
if function_node.is_async {
|
||||
todo_type!("generic types.CoroutineType")
|
||||
} else {
|
||||
definition_expression_ty(db, definition, returns.as_ref())
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
parameters: Parameters::from_parameters(
|
||||
@@ -49,45 +52,32 @@ impl<'db> Signature<'db> {
|
||||
return_ty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the parameters in this signature.
|
||||
pub(crate) fn parameters(&self) -> &Parameters<'db> {
|
||||
&self.parameters
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameters portion of a typed signature.
|
||||
///
|
||||
/// The ordering of parameters is always as given in this struct: first positional-only parameters,
|
||||
/// then positional-or-keyword, then optionally the variadic parameter, then keyword-only
|
||||
/// parameters, and last, optionally the variadic keywords parameter.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(super) struct Parameters<'db> {
|
||||
/// Parameters which may only be filled by positional arguments.
|
||||
positional_only: Box<[ParameterWithDefault<'db>]>,
|
||||
|
||||
/// Parameters which may be filled by positional or keyword arguments.
|
||||
positional_or_keyword: Box<[ParameterWithDefault<'db>]>,
|
||||
|
||||
/// The `*args` variadic parameter, if any.
|
||||
variadic: Option<Parameter<'db>>,
|
||||
|
||||
/// Parameters which may only be filled by keyword arguments.
|
||||
keyword_only: Box<[ParameterWithDefault<'db>]>,
|
||||
|
||||
/// The `**kwargs` variadic keywords parameter, if any.
|
||||
keywords: Option<Parameter<'db>>,
|
||||
}
|
||||
// TODO: use SmallVec here once invariance bug is fixed
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Parameters<'db>(Vec<Parameter<'db>>);
|
||||
|
||||
impl<'db> Parameters<'db> {
|
||||
/// Return todo parameters: (*args: Todo, **kwargs: Todo)
|
||||
fn todo() -> Self {
|
||||
Self {
|
||||
variadic: Some(Parameter {
|
||||
Self(vec![
|
||||
Parameter {
|
||||
name: Some(Name::new_static("args")),
|
||||
annotated_ty: todo_type!(),
|
||||
}),
|
||||
keywords: Some(Parameter {
|
||||
annotated_ty: Some(todo_type!("todo signature *args")),
|
||||
kind: ParameterKind::Variadic,
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("kwargs")),
|
||||
annotated_ty: todo_type!(),
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
annotated_ty: Some(todo_type!("todo signature **kwargs")),
|
||||
kind: ParameterKind::KeywordVariadic,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
fn from_parameters(
|
||||
@@ -103,94 +93,238 @@ impl<'db> Parameters<'db> {
|
||||
kwarg,
|
||||
range: _,
|
||||
} = parameters;
|
||||
let positional_only = posonlyargs
|
||||
.iter()
|
||||
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
|
||||
.collect();
|
||||
let positional_or_keyword = args
|
||||
.iter()
|
||||
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
|
||||
.collect();
|
||||
let variadic = vararg
|
||||
.as_ref()
|
||||
.map(|arg| Parameter::from_node(db, definition, arg));
|
||||
let keyword_only = kwonlyargs
|
||||
.iter()
|
||||
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
|
||||
.collect();
|
||||
let keywords = kwarg
|
||||
.as_ref()
|
||||
.map(|arg| Parameter::from_node(db, definition, arg));
|
||||
Self {
|
||||
positional_only,
|
||||
positional_or_keyword,
|
||||
variadic,
|
||||
keyword_only,
|
||||
keywords,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single parameter of a typed signature, with optional default value.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct ParameterWithDefault<'db> {
|
||||
parameter: Parameter<'db>,
|
||||
|
||||
/// Type of the default value, if any.
|
||||
default_ty: Option<Type<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> ParameterWithDefault<'db> {
|
||||
fn from_node(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
parameter_with_default: &'db ast::ParameterWithDefault,
|
||||
) -> Self {
|
||||
Self {
|
||||
default_ty: parameter_with_default
|
||||
let default_ty = |parameter_with_default: &ast::ParameterWithDefault| {
|
||||
parameter_with_default
|
||||
.default
|
||||
.as_deref()
|
||||
.map(|default| definition_expression_ty(db, definition, default)),
|
||||
parameter: Parameter::from_node(db, definition, ¶meter_with_default.parameter),
|
||||
}
|
||||
.map(|default| definition_expression_ty(db, definition, default))
|
||||
};
|
||||
let positional_only = posonlyargs.iter().map(|arg| {
|
||||
Parameter::from_node_and_kind(
|
||||
db,
|
||||
definition,
|
||||
&arg.parameter,
|
||||
ParameterKind::PositionalOnly {
|
||||
default_ty: default_ty(arg),
|
||||
},
|
||||
)
|
||||
});
|
||||
let positional_or_keyword = args.iter().map(|arg| {
|
||||
Parameter::from_node_and_kind(
|
||||
db,
|
||||
definition,
|
||||
&arg.parameter,
|
||||
ParameterKind::PositionalOrKeyword {
|
||||
default_ty: default_ty(arg),
|
||||
},
|
||||
)
|
||||
});
|
||||
let variadic = vararg
|
||||
.as_ref()
|
||||
.map(|arg| Parameter::from_node_and_kind(db, definition, arg, ParameterKind::Variadic));
|
||||
let keyword_only = kwonlyargs.iter().map(|arg| {
|
||||
Parameter::from_node_and_kind(
|
||||
db,
|
||||
definition,
|
||||
&arg.parameter,
|
||||
ParameterKind::KeywordOnly {
|
||||
default_ty: default_ty(arg),
|
||||
},
|
||||
)
|
||||
});
|
||||
let keywords = kwarg.as_ref().map(|arg| {
|
||||
Parameter::from_node_and_kind(db, definition, arg, ParameterKind::KeywordVariadic)
|
||||
});
|
||||
Self(
|
||||
positional_only
|
||||
.chain(positional_or_keyword)
|
||||
.chain(variadic)
|
||||
.chain(keyword_only)
|
||||
.chain(keywords)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> std::slice::Iter<Parameter<'db>> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Iterate initial positional parameters, not including variadic parameter, if any.
|
||||
///
|
||||
/// For a valid signature, this will be all positional parameters. In an invalid signature,
|
||||
/// there could be non-initial positional parameters; effectively, we just won't consider those
|
||||
/// to be positional, which is fine.
|
||||
pub(crate) fn positional(&self) -> impl Iterator<Item = &Parameter<'db>> {
|
||||
self.iter().take_while(|param| param.is_positional())
|
||||
}
|
||||
|
||||
/// Return parameter at given index, or `None` if index is out-of-range.
|
||||
pub(crate) fn get(&self, index: usize) -> Option<&Parameter<'db>> {
|
||||
self.0.get(index)
|
||||
}
|
||||
|
||||
/// Return positional parameter at given index, or `None` if `index` is out of range.
|
||||
///
|
||||
/// Does not return variadic parameter.
|
||||
pub(crate) fn get_positional(&self, index: usize) -> Option<&Parameter<'db>> {
|
||||
self.get(index)
|
||||
.and_then(|parameter| parameter.is_positional().then_some(parameter))
|
||||
}
|
||||
|
||||
/// Return the variadic parameter (`*args`), if any, and its index, or `None`.
|
||||
pub(crate) fn variadic(&self) -> Option<(usize, &Parameter<'db>)> {
|
||||
self.iter()
|
||||
.enumerate()
|
||||
.find(|(_, parameter)| parameter.is_variadic())
|
||||
}
|
||||
|
||||
/// Return parameter (with index) for given name, or `None` if no such parameter.
|
||||
///
|
||||
/// Does not return keywords (`**kwargs`) parameter.
|
||||
///
|
||||
/// In an invalid signature, there could be multiple parameters with the same name; we will
|
||||
/// just return the first that matches.
|
||||
pub(crate) fn keyword_by_name(&self, name: &str) -> Option<(usize, &Parameter<'db>)> {
|
||||
self.iter()
|
||||
.enumerate()
|
||||
.find(|(_, parameter)| parameter.callable_by_name(name))
|
||||
}
|
||||
|
||||
/// Return the keywords parameter (`**kwargs`), if any, and its index, or `None`.
|
||||
pub(crate) fn keyword_variadic(&self) -> Option<(usize, &Parameter<'db>)> {
|
||||
self.iter()
|
||||
.enumerate()
|
||||
.rfind(|(_, parameter)| parameter.is_keyword_variadic())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db, 'a> IntoIterator for &'a Parameters<'db> {
|
||||
type Item = &'a Parameter<'db>;
|
||||
type IntoIter = std::slice::Iter<'a, Parameter<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> std::ops::Index<usize> for Parameters<'db> {
|
||||
type Output = Parameter<'db>;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
/// A single parameter of a typed signature.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct Parameter<'db> {
|
||||
pub(crate) struct Parameter<'db> {
|
||||
/// Parameter name.
|
||||
///
|
||||
/// It is possible for signatures to be defined in ways that leave positional-only parameters
|
||||
/// nameless (e.g. via `Callable` annotations).
|
||||
name: Option<Name>,
|
||||
|
||||
/// Annotated type of the parameter (Unknown if no annotation.)
|
||||
annotated_ty: Type<'db>,
|
||||
/// Annotated type of the parameter.
|
||||
annotated_ty: Option<Type<'db>>,
|
||||
|
||||
kind: ParameterKind<'db>,
|
||||
}
|
||||
|
||||
impl<'db> Parameter<'db> {
|
||||
fn from_node(
|
||||
fn from_node_and_kind(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
parameter: &'db ast::Parameter,
|
||||
kind: ParameterKind<'db>,
|
||||
) -> Self {
|
||||
Parameter {
|
||||
Self {
|
||||
name: Some(parameter.name.id.clone()),
|
||||
annotated_ty: parameter
|
||||
.annotation
|
||||
.as_deref()
|
||||
.map(|annotation| definition_expression_ty(db, definition, annotation))
|
||||
.unwrap_or(Type::Unknown),
|
||||
.map(|annotation| definition_expression_ty(db, definition, annotation)),
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_variadic(&self) -> bool {
|
||||
matches!(self.kind, ParameterKind::Variadic)
|
||||
}
|
||||
|
||||
pub(crate) fn is_keyword_variadic(&self) -> bool {
|
||||
matches!(self.kind, ParameterKind::KeywordVariadic)
|
||||
}
|
||||
|
||||
pub(crate) fn is_positional(&self) -> bool {
|
||||
matches!(
|
||||
self.kind,
|
||||
ParameterKind::PositionalOnly { .. } | ParameterKind::PositionalOrKeyword { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn callable_by_name(&self, name: &str) -> bool {
|
||||
match self.kind {
|
||||
ParameterKind::PositionalOrKeyword { .. } | ParameterKind::KeywordOnly { .. } => self
|
||||
.name
|
||||
.as_ref()
|
||||
.is_some_and(|param_name| param_name == name),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Annotated type of the parameter, if annotated.
|
||||
pub(crate) fn annotated_ty(&self) -> Option<Type<'db>> {
|
||||
self.annotated_ty
|
||||
}
|
||||
|
||||
/// Name of the parameter (if it has one).
|
||||
pub(crate) fn name(&self) -> Option<&ast::name::Name> {
|
||||
self.name.as_ref()
|
||||
}
|
||||
|
||||
/// Display name of the parameter, if it has one.
|
||||
pub(crate) fn display_name(&self) -> Option<ast::name::Name> {
|
||||
self.name().map(|name| match self.kind {
|
||||
ParameterKind::Variadic => ast::name::Name::new(format!("*{name}")),
|
||||
ParameterKind::KeywordVariadic => ast::name::Name::new(format!("**{name}")),
|
||||
_ => name.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Default-value type of the parameter, if any.
|
||||
pub(crate) fn default_ty(&self) -> Option<Type<'db>> {
|
||||
match self.kind {
|
||||
ParameterKind::PositionalOnly { default_ty } => default_ty,
|
||||
ParameterKind::PositionalOrKeyword { default_ty } => default_ty,
|
||||
ParameterKind::Variadic => None,
|
||||
ParameterKind::KeywordOnly { default_ty } => default_ty,
|
||||
ParameterKind::KeywordVariadic => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum ParameterKind<'db> {
|
||||
/// Positional-only parameter, e.g. `def f(x, /): ...`
|
||||
PositionalOnly { default_ty: Option<Type<'db>> },
|
||||
/// Positional-or-keyword parameter, e.g. `def f(x): ...`
|
||||
PositionalOrKeyword { default_ty: Option<Type<'db>> },
|
||||
/// Variadic parameter, e.g. `def f(*args): ...`
|
||||
Variadic,
|
||||
/// Keyword-only parameter, e.g. `def f(*, x): ...`
|
||||
KeywordOnly { default_ty: Option<Type<'db>> },
|
||||
/// Variadic keywords parameter, e.g. `def f(**kwargs): ...`
|
||||
KeywordVariadic,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{global_symbol, FunctionType};
|
||||
use crate::types::{global_symbol, FunctionType, KnownClass};
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
|
||||
#[track_caller]
|
||||
@@ -202,39 +336,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_param_with_default<'db>(
|
||||
db: &'db TestDb,
|
||||
param_with_default: &ParameterWithDefault<'db>,
|
||||
expected_name: &'static str,
|
||||
expected_annotation_ty_display: &'static str,
|
||||
expected_default_ty_display: Option<&'static str>,
|
||||
) {
|
||||
assert_eq!(
|
||||
param_with_default
|
||||
.default_ty
|
||||
.map(|ty| ty.display(db).to_string()),
|
||||
expected_default_ty_display.map(ToString::to_string)
|
||||
);
|
||||
assert_param(
|
||||
db,
|
||||
¶m_with_default.parameter,
|
||||
expected_name,
|
||||
expected_annotation_ty_display,
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_param<'db>(
|
||||
db: &'db TestDb,
|
||||
param: &Parameter<'db>,
|
||||
expected_name: &'static str,
|
||||
expected_annotation_ty_display: &'static str,
|
||||
) {
|
||||
assert_eq!(param.name.as_ref().unwrap(), expected_name);
|
||||
assert_eq!(
|
||||
param.annotated_ty.display(db).to_string(),
|
||||
expected_annotation_ty_display
|
||||
);
|
||||
fn assert_params<'db>(signature: &Signature<'db>, expected: &[Parameter<'db>]) {
|
||||
assert_eq!(signature.parameters.0.as_slice(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -245,13 +348,8 @@ mod tests {
|
||||
|
||||
let sig = func.internal_signature(&db);
|
||||
|
||||
assert_eq!(sig.return_ty.display(&db).to_string(), "Unknown");
|
||||
let params = sig.parameters;
|
||||
assert!(params.positional_only.is_empty());
|
||||
assert!(params.positional_or_keyword.is_empty());
|
||||
assert!(params.variadic.is_none());
|
||||
assert!(params.keyword_only.is_empty());
|
||||
assert!(params.keywords.is_none());
|
||||
assert!(sig.return_ty.is_none());
|
||||
assert_params(&sig, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -271,34 +369,74 @@ mod tests {
|
||||
|
||||
let sig = func.internal_signature(&db);
|
||||
|
||||
assert_eq!(sig.return_ty.display(&db).to_string(), "bytes");
|
||||
let params = sig.parameters;
|
||||
let [a, b, c, d] = ¶ms.positional_only[..] else {
|
||||
panic!("expected four positional-only parameters");
|
||||
};
|
||||
let [e, f] = ¶ms.positional_or_keyword[..] else {
|
||||
panic!("expected two positional-or-keyword parameters");
|
||||
};
|
||||
let Some(args) = params.variadic else {
|
||||
panic!("expected a variadic parameter");
|
||||
};
|
||||
let [g, h] = ¶ms.keyword_only[..] else {
|
||||
panic!("expected two keyword-only parameters");
|
||||
};
|
||||
let Some(kwargs) = params.keywords else {
|
||||
panic!("expected a kwargs parameter");
|
||||
};
|
||||
|
||||
assert_param_with_default(&db, a, "a", "Unknown", None);
|
||||
assert_param_with_default(&db, b, "b", "int", None);
|
||||
assert_param_with_default(&db, c, "c", "Unknown", Some("Literal[1]"));
|
||||
assert_param_with_default(&db, d, "d", "int", Some("Literal[2]"));
|
||||
assert_param_with_default(&db, e, "e", "Unknown", Some("Literal[3]"));
|
||||
assert_param_with_default(&db, f, "f", "Literal[4]", Some("Literal[4]"));
|
||||
assert_param_with_default(&db, g, "g", "Unknown", Some("Literal[5]"));
|
||||
assert_param_with_default(&db, h, "h", "Literal[6]", Some("Literal[6]"));
|
||||
assert_param(&db, &args, "args", "object");
|
||||
assert_param(&db, &kwargs, "kwargs", "str");
|
||||
assert_eq!(sig.return_ty.unwrap().display(&db).to_string(), "bytes");
|
||||
assert_params(
|
||||
&sig,
|
||||
&[
|
||||
Parameter {
|
||||
name: Some(Name::new_static("a")),
|
||||
annotated_ty: None,
|
||||
kind: ParameterKind::PositionalOnly { default_ty: None },
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("b")),
|
||||
annotated_ty: Some(KnownClass::Int.to_instance(&db)),
|
||||
kind: ParameterKind::PositionalOnly { default_ty: None },
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("c")),
|
||||
annotated_ty: None,
|
||||
kind: ParameterKind::PositionalOnly {
|
||||
default_ty: Some(Type::IntLiteral(1)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("d")),
|
||||
annotated_ty: Some(KnownClass::Int.to_instance(&db)),
|
||||
kind: ParameterKind::PositionalOnly {
|
||||
default_ty: Some(Type::IntLiteral(2)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("e")),
|
||||
annotated_ty: None,
|
||||
kind: ParameterKind::PositionalOrKeyword {
|
||||
default_ty: Some(Type::IntLiteral(3)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("f")),
|
||||
annotated_ty: Some(Type::IntLiteral(4)),
|
||||
kind: ParameterKind::PositionalOrKeyword {
|
||||
default_ty: Some(Type::IntLiteral(4)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("args")),
|
||||
annotated_ty: Some(KnownClass::Object.to_instance(&db)),
|
||||
kind: ParameterKind::Variadic,
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("g")),
|
||||
annotated_ty: None,
|
||||
kind: ParameterKind::KeywordOnly {
|
||||
default_ty: Some(Type::IntLiteral(5)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("h")),
|
||||
annotated_ty: Some(Type::IntLiteral(6)),
|
||||
kind: ParameterKind::KeywordOnly {
|
||||
default_ty: Some(Type::IntLiteral(6)),
|
||||
},
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("kwargs")),
|
||||
annotated_ty: Some(KnownClass::Str.to_instance(&db)),
|
||||
kind: ParameterKind::KeywordVariadic,
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -322,11 +460,17 @@ mod tests {
|
||||
|
||||
let sig = func.internal_signature(&db);
|
||||
|
||||
let [a] = &sig.parameters.positional_or_keyword[..] else {
|
||||
let [Parameter {
|
||||
name: Some(name),
|
||||
annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { .. },
|
||||
}] = &sig.parameters.0[..]
|
||||
else {
|
||||
panic!("expected one positional-or-keyword parameter");
|
||||
};
|
||||
assert_eq!(name, "a");
|
||||
// Parameter resolution not deferred; we should see A not B
|
||||
assert_param_with_default(&db, a, "a", "A", None);
|
||||
assert_eq!(annotated_ty.unwrap().display(&db).to_string(), "A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -350,11 +494,17 @@ mod tests {
|
||||
|
||||
let sig = func.internal_signature(&db);
|
||||
|
||||
let [a] = &sig.parameters.positional_or_keyword[..] else {
|
||||
let [Parameter {
|
||||
name: Some(name),
|
||||
annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { .. },
|
||||
}] = &sig.parameters.0[..]
|
||||
else {
|
||||
panic!("expected one positional-or-keyword parameter");
|
||||
};
|
||||
assert_eq!(name, "a");
|
||||
// Parameter resolution deferred; we should see B
|
||||
assert_param_with_default(&db, a, "a", "B", None);
|
||||
assert_eq!(annotated_ty.unwrap().display(&db).to_string(), "B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -378,12 +528,23 @@ mod tests {
|
||||
|
||||
let sig = func.internal_signature(&db);
|
||||
|
||||
let [a, b] = &sig.parameters.positional_or_keyword[..] else {
|
||||
let [Parameter {
|
||||
name: Some(a_name),
|
||||
annotated_ty: a_annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { .. },
|
||||
}, Parameter {
|
||||
name: Some(b_name),
|
||||
annotated_ty: b_annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { .. },
|
||||
}] = &sig.parameters.0[..]
|
||||
else {
|
||||
panic!("expected two positional-or-keyword parameters");
|
||||
};
|
||||
assert_eq!(a_name, "a");
|
||||
assert_eq!(b_name, "b");
|
||||
// TODO resolution should not be deferred; we should see A not B
|
||||
assert_param_with_default(&db, a, "a", "B", None);
|
||||
assert_param_with_default(&db, b, "b", "T", None);
|
||||
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B");
|
||||
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -407,12 +568,23 @@ mod tests {
|
||||
|
||||
let sig = func.internal_signature(&db);
|
||||
|
||||
let [a, b] = &sig.parameters.positional_or_keyword[..] else {
|
||||
let [Parameter {
|
||||
name: Some(a_name),
|
||||
annotated_ty: a_annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { .. },
|
||||
}, Parameter {
|
||||
name: Some(b_name),
|
||||
annotated_ty: b_annotated_ty,
|
||||
kind: ParameterKind::PositionalOrKeyword { .. },
|
||||
}] = &sig.parameters.0[..]
|
||||
else {
|
||||
panic!("expected two positional-or-keyword parameters");
|
||||
};
|
||||
assert_eq!(a_name, "a");
|
||||
assert_eq!(b_name, "b");
|
||||
// Parameter resolution deferred; we should see B
|
||||
assert_param_with_default(&db, a, "a", "B", None);
|
||||
assert_param_with_default(&db, b, "b", "T", None);
|
||||
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B");
|
||||
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
89
crates/red_knot_python_semantic/src/types/subclass_of.rs
Normal file
89
crates/red_knot_python_semantic/src/types/subclass_of.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use super::{ClassBase, ClassLiteralType, Db, KnownClass, Symbol, Type};
|
||||
|
||||
/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub struct SubclassOfType<'db> {
|
||||
// Keep this field private, so that the only way of constructing the struct is through the `from` method.
|
||||
subclass_of: ClassBase<'db>,
|
||||
}
|
||||
|
||||
impl<'db> SubclassOfType<'db> {
|
||||
/// Construct a new [`Type`] instance representing a given class object (or a given dynamic type)
|
||||
/// and all possible subclasses of that class object/dynamic type.
|
||||
///
|
||||
/// This method does not always return a [`Type::SubclassOf`] variant.
|
||||
/// If the class object is known to be a final class,
|
||||
/// this method will return a [`Type::ClassLiteral`] variant; this is a more precise type.
|
||||
/// If the class object is `builtins.object`, `Type::Instance(<builtins.type>)` will be returned;
|
||||
/// this is no more precise, but it is exactly equivalent to `type[object]`.
|
||||
///
|
||||
/// The eager normalization here means that we do not need to worry elsewhere about distinguishing
|
||||
/// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants.
|
||||
pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into<ClassBase<'db>>) -> Type<'db> {
|
||||
let subclass_of = subclass_of.into();
|
||||
match subclass_of {
|
||||
ClassBase::Dynamic(_) => Type::SubclassOf(Self { subclass_of }),
|
||||
ClassBase::Class(class) => {
|
||||
if class.is_final(db) {
|
||||
Type::ClassLiteral(ClassLiteralType { class })
|
||||
} else if class.is_known(db, KnownClass::Object) {
|
||||
KnownClass::Type.to_instance(db)
|
||||
} else {
|
||||
Type::SubclassOf(Self { subclass_of })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a [`Type`] instance representing the type `type[Unknown]`.
|
||||
pub(crate) const fn subclass_of_unknown() -> Type<'db> {
|
||||
Type::SubclassOf(SubclassOfType {
|
||||
subclass_of: ClassBase::unknown(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return a [`Type`] instance representing the type `type[Any]`.
|
||||
pub(crate) const fn subclass_of_any() -> Type<'db> {
|
||||
Type::SubclassOf(SubclassOfType {
|
||||
subclass_of: ClassBase::any(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the inner [`ClassBase`] value wrapped by this `SubclassOfType`.
|
||||
pub(crate) const fn subclass_of(self) -> ClassBase<'db> {
|
||||
self.subclass_of
|
||||
}
|
||||
|
||||
pub const fn is_dynamic(self) -> bool {
|
||||
// Unpack `self` so that we're forced to update this method if any more fields are added in the future.
|
||||
let Self { subclass_of } = self;
|
||||
subclass_of.is_dynamic()
|
||||
}
|
||||
|
||||
pub const fn is_fully_static(self) -> bool {
|
||||
!self.is_dynamic()
|
||||
}
|
||||
|
||||
pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
Type::from(self.subclass_of).member(db, name)
|
||||
}
|
||||
|
||||
/// Return `true` if `self` is a subtype of `other`.
|
||||
///
|
||||
/// This can only return `true` if `self.subclass_of` is a [`ClassBase::Class`] variant;
|
||||
/// only fully static types participate in subtyping.
|
||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool {
|
||||
match (self.subclass_of, other.subclass_of) {
|
||||
// Non-fully-static types do not participate in subtyping
|
||||
(ClassBase::Dynamic(_), _) | (_, ClassBase::Dynamic(_)) => false,
|
||||
|
||||
// For example, `type[bool]` describes all possible runtime subclasses of the class `bool`,
|
||||
// and `type[int]` describes all possible runtime subclasses of the class `int`.
|
||||
// The first set is a subset of the second set, because `bool` is itself a subclass of `int`.
|
||||
(ClassBase::Class(self_class), ClassBase::Class(other_class)) => {
|
||||
// N.B. The subclass relation is fully static
|
||||
self_class.is_subclass_of(db, other_class)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,15 @@ 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));
|
||||
|
||||
if value.is_assign()
|
||||
&& self.context.in_stub()
|
||||
&& value
|
||||
.expression()
|
||||
.node_ref(self.db())
|
||||
.is_ellipsis_literal_expr()
|
||||
{
|
||||
value_ty = Type::unknown();
|
||||
}
|
||||
if value.is_iterable() {
|
||||
// If the value is an iterable, then the type that needs to be unpacked is the iterator
|
||||
// type.
|
||||
@@ -96,7 +105,7 @@ impl<'db> Unpacker<'db> {
|
||||
// with each individual character, instead of just an array of
|
||||
// `LiteralString`, but there would be a cost and it's not clear that
|
||||
// it's worth it.
|
||||
Type::tuple(
|
||||
TupleType::from_elements(
|
||||
self.db(),
|
||||
std::iter::repeat(Type::LiteralString)
|
||||
.take(string_literal_ty.python_len(self.db())),
|
||||
@@ -155,7 +164,7 @@ impl<'db> Unpacker<'db> {
|
||||
for (index, element) in elts.iter().enumerate() {
|
||||
// SAFETY: `target_types` is initialized with the same length as `elts`.
|
||||
let element_ty = match target_types[index].as_slice() {
|
||||
[] => Type::Unknown,
|
||||
[] => Type::unknown(),
|
||||
types => UnionType::from_elements(self.db(), types),
|
||||
};
|
||||
self.unpack_inner(element, element_ty);
|
||||
@@ -232,7 +241,7 @@ impl<'db> Unpacker<'db> {
|
||||
|
||||
// Subtract 1 to insert the starred expression type at the correct
|
||||
// index.
|
||||
element_types.resize(targets.len() - 1, Type::Unknown);
|
||||
element_types.resize(targets.len() - 1, Type::unknown());
|
||||
// TODO: This should be `list[Unknown]`
|
||||
element_types.insert(starred_index, todo_type!("starred unpacking"));
|
||||
|
||||
|
||||
@@ -76,6 +76,11 @@ impl<'db> UnpackValue<'db> {
|
||||
matches!(self, UnpackValue::Iterable(_))
|
||||
}
|
||||
|
||||
/// Returns `true` if the value is being assigned to a target.
|
||||
pub(crate) const fn is_assign(self) -> bool {
|
||||
matches!(self, UnpackValue::Assign(_))
|
||||
}
|
||||
|
||||
/// Returns the underlying [`Expression`] that is being unpacked.
|
||||
pub(crate) const fn expression(self) -> Expression<'db> {
|
||||
match self {
|
||||
|
||||
@@ -329,9 +329,9 @@ impl<'db> VisibilityConstraints<'db> {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
PatternConstraintKind::Singleton(..) | PatternConstraintKind::Unsupported => {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
PatternConstraintKind::Singleton(..)
|
||||
| PatternConstraintKind::Class(..)
|
||||
| PatternConstraintKind::Unsupported => Truthiness::Ambiguous,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ use crate::server::Server;
|
||||
mod message;
|
||||
|
||||
mod edit;
|
||||
mod logging;
|
||||
mod server;
|
||||
mod session;
|
||||
mod system;
|
||||
mod trace;
|
||||
|
||||
pub(crate) const SERVER_NAME: &str = "red-knot";
|
||||
pub(crate) const DIAGNOSTIC_NAME: &str = "Red Knot";
|
||||
|
||||
114
crates/red_knot_server/src/logging.rs
Normal file
114
crates/red_knot_server/src/logging.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
//! The logging system for `red_knot server`.
|
||||
//!
|
||||
//! Log messages are controlled by the `logLevel` setting which defaults to `"info"`. Log messages
|
||||
//! are written to `stderr` by default, which should appear in the logs for most LSP clients. A
|
||||
//! `logFile` path can also be specified in the settings, and output will be directed there
|
||||
//! instead.
|
||||
use core::str;
|
||||
use serde::Deserialize;
|
||||
use std::{path::PathBuf, str::FromStr, sync::Arc};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{
|
||||
fmt::{time::Uptime, writer::BoxMakeWriter},
|
||||
layer::SubscriberExt,
|
||||
Layer,
|
||||
};
|
||||
|
||||
pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&std::path::Path>) {
|
||||
let log_file = log_file
|
||||
.map(|path| {
|
||||
// this expands `logFile` so that tildes and environment variables
|
||||
// are replaced with their values, if possible.
|
||||
if let Some(expanded) = shellexpand::full(&path.to_string_lossy())
|
||||
.ok()
|
||||
.and_then(|path| PathBuf::from_str(&path).ok())
|
||||
{
|
||||
expanded
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
})
|
||||
.and_then(|path| {
|
||||
std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.map_err(|err| {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!(
|
||||
"Failed to open file at {} for logging: {err}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
let logger = match log_file {
|
||||
Some(file) => BoxMakeWriter::new(Arc::new(file)),
|
||||
None => BoxMakeWriter::new(std::io::stderr),
|
||||
};
|
||||
let subscriber = tracing_subscriber::Registry::default().with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_timer(Uptime::default())
|
||||
.with_thread_names(true)
|
||||
.with_ansi(false)
|
||||
.with_writer(logger)
|
||||
.with_filter(LogLevelFilter { filter: log_level }),
|
||||
);
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.expect("should be able to set global default subscriber");
|
||||
}
|
||||
|
||||
/// The log level for the server as provided by the client during initialization.
|
||||
///
|
||||
/// The default log level is `info`.
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
#[default]
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
fn trace_level(self) -> tracing::Level {
|
||||
match self {
|
||||
Self::Error => tracing::Level::ERROR,
|
||||
Self::Warn => tracing::Level::WARN,
|
||||
Self::Info => tracing::Level::INFO,
|
||||
Self::Debug => tracing::Level::DEBUG,
|
||||
Self::Trace => tracing::Level::TRACE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Filters out traces which have a log level lower than the `logLevel` set by the client.
|
||||
struct LogLevelFilter {
|
||||
filter: LogLevel,
|
||||
}
|
||||
|
||||
impl<S> tracing_subscriber::layer::Filter<S> for LogLevelFilter {
|
||||
fn enabled(
|
||||
&self,
|
||||
meta: &tracing::Metadata<'_>,
|
||||
_: &tracing_subscriber::layer::Context<'_, S>,
|
||||
) -> bool {
|
||||
let filter = if meta.target().starts_with("red_knot") {
|
||||
self.filter.trace_level()
|
||||
} else {
|
||||
tracing::Level::INFO
|
||||
};
|
||||
|
||||
meta.level() <= &filter
|
||||
}
|
||||
|
||||
fn max_level_hint(&self) -> Option<tracing::level_filters::LevelFilter> {
|
||||
Some(LevelFilter::from_level(self.filter.trace_level()))
|
||||
}
|
||||
}
|
||||
@@ -51,10 +51,6 @@ impl Server {
|
||||
crate::version(),
|
||||
)?;
|
||||
|
||||
if let Some(trace) = init_params.trace {
|
||||
crate::trace::set_trace_value(trace);
|
||||
}
|
||||
|
||||
crate::message::init_messenger(connection.make_sender());
|
||||
|
||||
let AllSettings {
|
||||
@@ -66,14 +62,9 @@ impl Server {
|
||||
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())),
|
||||
);
|
||||
|
||||
crate::trace::init_tracing(
|
||||
connection.make_sender(),
|
||||
global_settings
|
||||
.tracing
|
||||
.log_level
|
||||
.unwrap_or(crate::trace::LogLevel::Info),
|
||||
crate::logging::init_logging(
|
||||
global_settings.tracing.log_level.unwrap_or_default(),
|
||||
global_settings.tracing.log_file.as_deref(),
|
||||
init_params.client_info.as_ref(),
|
||||
);
|
||||
|
||||
let mut workspace_for_url = |url: Url| {
|
||||
|
||||
@@ -52,9 +52,6 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
|
||||
notification::DidCloseNotebookHandler::METHOD => {
|
||||
local_notification_task::<notification::DidCloseNotebookHandler>(notif)
|
||||
}
|
||||
notification::SetTraceHandler::METHOD => {
|
||||
local_notification_task::<notification::SetTraceHandler>(notif)
|
||||
}
|
||||
method => {
|
||||
tracing::warn!("Received notification {method} which does not have a handler.");
|
||||
return Task::nothing();
|
||||
|
||||
@@ -3,11 +3,9 @@ mod did_close;
|
||||
mod did_close_notebook;
|
||||
mod did_open;
|
||||
mod did_open_notebook;
|
||||
mod set_trace;
|
||||
|
||||
pub(super) use did_change::DidChangeTextDocumentHandler;
|
||||
pub(super) use did_close::DidCloseTextDocumentHandler;
|
||||
pub(super) use did_close_notebook::DidCloseNotebookHandler;
|
||||
pub(super) use did_open::DidOpenTextDocumentHandler;
|
||||
pub(super) use did_open_notebook::DidOpenNotebookHandler;
|
||||
pub(super) use set_trace::SetTraceHandler;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
use lsp_types::notification::SetTrace;
|
||||
use lsp_types::SetTraceParams;
|
||||
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
use crate::server::client::{Notifier, Requester};
|
||||
use crate::server::Result;
|
||||
use crate::session::Session;
|
||||
|
||||
pub(crate) struct SetTraceHandler;
|
||||
|
||||
impl NotificationHandler for SetTraceHandler {
|
||||
type NotificationType = SetTrace;
|
||||
}
|
||||
|
||||
impl SyncNotificationHandler for SetTraceHandler {
|
||||
fn run(
|
||||
_session: &mut Session,
|
||||
_notifier: Notifier,
|
||||
_requester: &mut Requester,
|
||||
params: SetTraceParams,
|
||||
) -> Result<()> {
|
||||
crate::trace::set_trace_value(params.value);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ impl Index {
|
||||
DocumentKey::NotebookCell(url)
|
||||
} else if Path::new(url.path())
|
||||
.extension()
|
||||
.map_or(false, |ext| ext.eq_ignore_ascii_case("ipynb"))
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("ipynb"))
|
||||
{
|
||||
DocumentKey::Notebook(url)
|
||||
} else {
|
||||
|
||||
@@ -24,7 +24,7 @@ pub struct ClientSettings {
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct TracingSettings {
|
||||
pub(crate) log_level: Option<crate::trace::LogLevel>,
|
||||
pub(crate) log_level: Option<crate::logging::LogLevel>,
|
||||
/// Path to the log file - tildes and environment variables are supported.
|
||||
pub(crate) log_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
//! The tracing system for `ruff server`.
|
||||
//!
|
||||
//! Traces are controlled by the `logLevel` setting, along with the
|
||||
//! trace level set through the LSP. On VS Code, the trace level can
|
||||
//! also be set with `ruff.trace.server`. A trace level of `messages` or
|
||||
//! `verbose` will enable tracing - otherwise, no traces will be shown.
|
||||
//!
|
||||
//! `logLevel` can be used to configure the level of tracing that is shown.
|
||||
//! By default, `logLevel` is set to `"info"`.
|
||||
//!
|
||||
//! The server also supports the `RUFF_TRACE` environment variable, which will
|
||||
//! override the trace value provided by the LSP client. Use this if there's no good way
|
||||
//! to set the trace value through your editor's configuration.
|
||||
//!
|
||||
//! Tracing will write to `stderr` by default, which should appear in the logs for most LSP clients.
|
||||
//! A `logFile` path can also be specified in the settings, and output will be directed there instead.
|
||||
use core::str;
|
||||
use lsp_server::{Message, Notification};
|
||||
use lsp_types::{
|
||||
notification::{LogTrace, Notification as _},
|
||||
ClientInfo, TraceValue,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
io::{Error as IoError, ErrorKind, Write},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{
|
||||
fmt::{time::Uptime, writer::BoxMakeWriter, MakeWriter},
|
||||
layer::SubscriberExt,
|
||||
Layer,
|
||||
};
|
||||
|
||||
use crate::server::ClientSender;
|
||||
|
||||
const TRACE_ENV_KEY: &str = "RUFF_TRACE";
|
||||
|
||||
static LOGGING_SENDER: OnceLock<ClientSender> = OnceLock::new();
|
||||
|
||||
static TRACE_VALUE: Mutex<lsp_types::TraceValue> = Mutex::new(lsp_types::TraceValue::Off);
|
||||
|
||||
pub(crate) fn set_trace_value(trace_value: TraceValue) {
|
||||
let mut global_trace_value = TRACE_VALUE
|
||||
.lock()
|
||||
.expect("trace value mutex should be available");
|
||||
*global_trace_value = trace_value;
|
||||
}
|
||||
|
||||
// A tracing writer that uses LSPs logTrace method.
|
||||
struct TraceLogWriter;
|
||||
|
||||
impl Write for TraceLogWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let message = str::from_utf8(buf).map_err(|e| IoError::new(ErrorKind::InvalidData, e))?;
|
||||
LOGGING_SENDER
|
||||
.get()
|
||||
.expect("logging sender should be initialized at this point")
|
||||
.send(Message::Notification(Notification {
|
||||
method: LogTrace::METHOD.to_owned(),
|
||||
params: serde_json::json!({
|
||||
"message": message
|
||||
}),
|
||||
}))
|
||||
.map_err(|e| IoError::new(ErrorKind::Other, e))?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MakeWriter<'a> for TraceLogWriter {
|
||||
type Writer = Self;
|
||||
|
||||
fn make_writer(&'a self) -> Self::Writer {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init_tracing(
|
||||
sender: ClientSender,
|
||||
log_level: LogLevel,
|
||||
log_file: Option<&std::path::Path>,
|
||||
client: Option<&ClientInfo>,
|
||||
) {
|
||||
LOGGING_SENDER
|
||||
.set(sender)
|
||||
.expect("logging sender should only be initialized once");
|
||||
|
||||
let log_file = log_file
|
||||
.map(|path| {
|
||||
// this expands `logFile` so that tildes and environment variables
|
||||
// are replaced with their values, if possible.
|
||||
if let Some(expanded) = shellexpand::full(&path.to_string_lossy())
|
||||
.ok()
|
||||
.and_then(|path| PathBuf::from_str(&path).ok())
|
||||
{
|
||||
expanded
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
})
|
||||
.and_then(|path| {
|
||||
std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.map_err(|err| {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!(
|
||||
"Failed to open file at {} for logging: {err}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
let logger = match log_file {
|
||||
Some(file) => BoxMakeWriter::new(Arc::new(file)),
|
||||
None => {
|
||||
if client.is_some_and(|client| {
|
||||
client.name.starts_with("Zed") || client.name.starts_with("Visual Studio Code")
|
||||
}) {
|
||||
BoxMakeWriter::new(TraceLogWriter)
|
||||
} else {
|
||||
BoxMakeWriter::new(std::io::stderr)
|
||||
}
|
||||
}
|
||||
};
|
||||
let subscriber = tracing_subscriber::Registry::default().with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_timer(Uptime::default())
|
||||
.with_thread_names(true)
|
||||
.with_ansi(false)
|
||||
.with_writer(logger)
|
||||
.with_filter(TraceLevelFilter)
|
||||
.with_filter(LogLevelFilter { filter: log_level }),
|
||||
);
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.expect("should be able to set global default subscriber");
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum LogLevel {
|
||||
#[default]
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
fn trace_level(self) -> tracing::Level {
|
||||
match self {
|
||||
Self::Error => tracing::Level::ERROR,
|
||||
Self::Warn => tracing::Level::WARN,
|
||||
Self::Info => tracing::Level::INFO,
|
||||
Self::Debug => tracing::Level::DEBUG,
|
||||
Self::Trace => tracing::Level::TRACE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Filters out traces which have a log level lower than the `logLevel` set by the client.
|
||||
struct LogLevelFilter {
|
||||
filter: LogLevel,
|
||||
}
|
||||
|
||||
/// Filters out traces if the trace value set by the client is `off`.
|
||||
struct TraceLevelFilter;
|
||||
|
||||
impl<S> tracing_subscriber::layer::Filter<S> for LogLevelFilter {
|
||||
fn enabled(
|
||||
&self,
|
||||
meta: &tracing::Metadata<'_>,
|
||||
_: &tracing_subscriber::layer::Context<'_, S>,
|
||||
) -> bool {
|
||||
let filter = if meta.target().starts_with("ruff") {
|
||||
self.filter.trace_level()
|
||||
} else {
|
||||
tracing::Level::INFO
|
||||
};
|
||||
|
||||
meta.level() <= &filter
|
||||
}
|
||||
|
||||
fn max_level_hint(&self) -> Option<tracing::level_filters::LevelFilter> {
|
||||
Some(LevelFilter::from_level(self.filter.trace_level()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> tracing_subscriber::layer::Filter<S> for TraceLevelFilter {
|
||||
fn enabled(
|
||||
&self,
|
||||
_: &tracing::Metadata<'_>,
|
||||
_: &tracing_subscriber::layer::Context<'_, S>,
|
||||
) -> bool {
|
||||
trace_value() != lsp_types::TraceValue::Off
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn trace_value() -> lsp_types::TraceValue {
|
||||
std::env::var(TRACE_ENV_KEY)
|
||||
.ok()
|
||||
.and_then(|trace| serde_json::from_value(serde_json::Value::String(trace)).ok())
|
||||
.unwrap_or_else(|| {
|
||||
*TRACE_VALUE
|
||||
.lock()
|
||||
.expect("trace value mutex should be available")
|
||||
})
|
||||
}
|
||||
@@ -6,10 +6,11 @@ use red_knot_python_semantic::types::check_types;
|
||||
use red_knot_python_semantic::Program;
|
||||
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic};
|
||||
use ruff_db::files::{system_path_to_file, File, Files};
|
||||
use ruff_db::panic::catch_unwind;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
|
||||
use ruff_source_file::LineIndex;
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::TextSize;
|
||||
use salsa::Setter;
|
||||
|
||||
@@ -136,7 +137,33 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
})
|
||||
.collect();
|
||||
|
||||
let type_diagnostics = check_types(db, test_file.file);
|
||||
let type_diagnostics = match catch_unwind(|| check_types(db, test_file.file)) {
|
||||
Ok(type_diagnostics) => type_diagnostics,
|
||||
Err(info) => {
|
||||
let mut by_line = matcher::FailuresByLine::default();
|
||||
let mut messages = vec![];
|
||||
match info.location {
|
||||
Some(location) => messages.push(format!("panicked at {location}")),
|
||||
None => messages.push("panicked at unknown location".to_string()),
|
||||
};
|
||||
match info.payload {
|
||||
Some(payload) => messages.push(payload),
|
||||
// Mimic the default panic hook's rendering of the panic payload if it's
|
||||
// not a string.
|
||||
None => messages.push("Box<dyn Any>".to_string()),
|
||||
};
|
||||
if let Some(backtrace) = info.backtrace {
|
||||
if std::env::var("RUST_BACKTRACE").is_ok() {
|
||||
messages.extend(backtrace.to_string().split('\n').map(String::from));
|
||||
}
|
||||
}
|
||||
by_line.push(OneIndexed::from_zero_indexed(0), messages);
|
||||
return Some(FileFailures {
|
||||
backtick_offset: test_file.backtick_offset,
|
||||
by_line,
|
||||
});
|
||||
}
|
||||
};
|
||||
diagnostics.extend(type_diagnostics.into_iter().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new((*diagnostic).clone());
|
||||
diagnostic
|
||||
|
||||
@@ -27,7 +27,7 @@ impl FailuresByLine {
|
||||
})
|
||||
}
|
||||
|
||||
fn push(&mut self, line_number: OneIndexed, messages: Vec<String>) {
|
||||
pub(super) fn push(&mut self, line_number: OneIndexed, messages: Vec<String>) {
|
||||
let start = self.failures.len();
|
||||
self.failures.extend(messages);
|
||||
self.lines.push(LineFailures {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! in `crates/red_knot_vendored/vendor/typeshed` change.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use path_slash::PathExt;
|
||||
@@ -14,13 +15,15 @@ use zip::write::{FileOptions, ZipWriter};
|
||||
use zip::CompressionMethod;
|
||||
|
||||
const TYPESHED_SOURCE_DIR: &str = "vendor/typeshed";
|
||||
const KNOT_EXTENSIONS_STUBS: &str = "knot_extensions/knot_extensions.pyi";
|
||||
const TYPESHED_ZIP_LOCATION: &str = "/zipped_typeshed.zip";
|
||||
|
||||
/// Recursively zip the contents of an entire directory.
|
||||
/// Recursively zip the contents of the entire typeshed directory and patch typeshed
|
||||
/// on the fly to include the `knot_extensions` module.
|
||||
///
|
||||
/// This routine is adapted from a recipe at
|
||||
/// <https://github.com/zip-rs/zip-old/blob/5d0f198124946b7be4e5969719a7f29f363118cd/examples/write_dir.rs>
|
||||
fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
|
||||
fn write_zipped_typeshed_to(writer: File) -> ZipResult<File> {
|
||||
let mut zip = ZipWriter::new(writer);
|
||||
|
||||
// Use deflated compression for WASM builds because compiling `zstd-sys` requires clang
|
||||
@@ -42,11 +45,11 @@ fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
|
||||
.compression_method(method)
|
||||
.unix_permissions(0o644);
|
||||
|
||||
for entry in walkdir::WalkDir::new(directory_path) {
|
||||
for entry in walkdir::WalkDir::new(TYPESHED_SOURCE_DIR) {
|
||||
let dir_entry = entry.unwrap();
|
||||
let absolute_path = dir_entry.path();
|
||||
let normalized_relative_path = absolute_path
|
||||
.strip_prefix(Path::new(directory_path))
|
||||
.strip_prefix(Path::new(TYPESHED_SOURCE_DIR))
|
||||
.unwrap()
|
||||
.to_slash()
|
||||
.expect("Unexpected non-utf8 typeshed path!");
|
||||
@@ -55,9 +58,14 @@ fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
|
||||
// Some unzip tools unzip files with directory paths correctly, some do not!
|
||||
if absolute_path.is_file() {
|
||||
println!("adding file {absolute_path:?} as {normalized_relative_path:?} ...");
|
||||
zip.start_file(normalized_relative_path, options)?;
|
||||
zip.start_file(&*normalized_relative_path, options)?;
|
||||
let mut f = File::open(absolute_path)?;
|
||||
std::io::copy(&mut f, &mut zip).unwrap();
|
||||
|
||||
// Patch the VERSIONS file to make `knot_extensions` available
|
||||
if normalized_relative_path == "stdlib/VERSIONS" {
|
||||
writeln!(&mut zip, "knot_extensions: 3.0-")?;
|
||||
}
|
||||
} else if !normalized_relative_path.is_empty() {
|
||||
// Only if not root! Avoids path spec / warning
|
||||
// and mapname conversion failed error on unzip
|
||||
@@ -65,11 +73,17 @@ fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
|
||||
zip.add_directory(normalized_relative_path, options)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Patch typeshed and add the stubs for the `knot_extensions` module
|
||||
println!("adding file {KNOT_EXTENSIONS_STUBS} as stdlib/knot_extensions.pyi ...");
|
||||
zip.start_file("stdlib/knot_extensions.pyi", options)?;
|
||||
let mut f = File::open(KNOT_EXTENSIONS_STUBS)?;
|
||||
std::io::copy(&mut f, &mut zip).unwrap();
|
||||
|
||||
zip.finish()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-changed={TYPESHED_SOURCE_DIR}");
|
||||
assert!(
|
||||
Path::new(TYPESHED_SOURCE_DIR).is_dir(),
|
||||
"Where is typeshed?"
|
||||
@@ -84,6 +98,6 @@ fn main() {
|
||||
// which can't be done at compile time.)
|
||||
let zipped_typeshed_location = format!("{out_dir}{TYPESHED_ZIP_LOCATION}");
|
||||
|
||||
let zipped_typeshed = File::create(zipped_typeshed_location).unwrap();
|
||||
zip_dir(TYPESHED_SOURCE_DIR, zipped_typeshed).unwrap();
|
||||
let zipped_typeshed_file = File::create(zipped_typeshed_location).unwrap();
|
||||
write_zipped_typeshed_to(zipped_typeshed_file).unwrap();
|
||||
}
|
||||
|
||||
3
crates/red_knot_vendored/knot_extensions/README.md
Normal file
3
crates/red_knot_vendored/knot_extensions/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
The `knot_extensions.pyi` file in this directory will be symlinked into
|
||||
the `vendor/typeshed/stdlib` directory every time we sync our `typeshed`
|
||||
stubs (see `.github/workflows/sync_typeshed.yaml`).
|
||||
24
crates/red_knot_vendored/knot_extensions/knot_extensions.pyi
Normal file
24
crates/red_knot_vendored/knot_extensions/knot_extensions.pyi
Normal file
@@ -0,0 +1,24 @@
|
||||
from typing import _SpecialForm, Any, LiteralString
|
||||
|
||||
# Special operations
|
||||
def static_assert(condition: object, msg: LiteralString | None = None) -> None: ...
|
||||
|
||||
# Types
|
||||
Unknown = object()
|
||||
|
||||
# Special forms
|
||||
Not: _SpecialForm
|
||||
Intersection: _SpecialForm
|
||||
TypeOf: _SpecialForm
|
||||
|
||||
# Predicates on types
|
||||
#
|
||||
# Ideally, these would be annotated using `TypeForm`, but that has not been
|
||||
# standardized yet (https://peps.python.org/pep-0747).
|
||||
def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
|
||||
def is_subtype_of(type_derived: Any, typ_ebase: Any) -> bool: ...
|
||||
def is_assignable_to(type_target: Any, type_source: Any) -> bool: ...
|
||||
def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ...
|
||||
def is_fully_static(type: Any) -> bool: ...
|
||||
def is_singleton(type: Any) -> bool: ...
|
||||
def is_single_valued(type: Any) -> bool: ...
|
||||
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Regression test that makes sure we do not short-circuit here after
|
||||
determining that the overall type will be `Never` and still infer
|
||||
a type for the second tuple element `2`.
|
||||
|
||||
Relevant discussion:
|
||||
https://github.com/astral-sh/ruff/pull/15218#discussion_r1900811073
|
||||
"""
|
||||
|
||||
from typing_extensions import Never
|
||||
|
||||
|
||||
def never() -> Never:
|
||||
return never()
|
||||
|
||||
|
||||
(never(), 2)
|
||||
@@ -285,7 +285,7 @@ impl Workspace {
|
||||
open_files.contains(&file)
|
||||
} else if let Some(system_path) = file.path(db).as_system_path() {
|
||||
self.package(db, system_path)
|
||||
.map_or(false, |package| package.contains_file(db, file))
|
||||
.is_some_and(|package| package.contains_file(db, file))
|
||||
} else {
|
||||
file.path(db).is_system_virtual_path()
|
||||
}
|
||||
|
||||
@@ -272,7 +272,8 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
||||
#[rustfmt::skip]
|
||||
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
|
||||
// related to circular references in class definitions
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, false),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F811_19.py", true, false),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyupgrade/UP039.py", true, false),
|
||||
// related to circular references in type aliases (salsa cycle panic):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.8.5"
|
||||
version = "0.9.1"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -9,6 +9,7 @@ use anyhow::bail;
|
||||
use clap::builder::{TypedValueParser, ValueParserFactory};
|
||||
use clap::{command, Parser, Subcommand};
|
||||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use path_absolutize::path_dedot;
|
||||
use regex::Regex;
|
||||
use ruff_graph::Direction;
|
||||
@@ -24,6 +25,7 @@ use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||
use ruff_workspace::options::{Options, PycodestyleOptions};
|
||||
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
|
||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
use rustc_hash::FxHashMap;
|
||||
use toml;
|
||||
@@ -957,7 +959,7 @@ A `--config` flag must either be a path to a `.toml` configuration file
|
||||
// We want to display the most helpful error to the user as possible.
|
||||
if Path::new(value)
|
||||
.extension()
|
||||
.map_or(false, |ext| ext.eq_ignore_ascii_case("toml"))
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("toml"))
|
||||
{
|
||||
if !value.contains('=') {
|
||||
tip.push_str(&format!(
|
||||
@@ -967,11 +969,36 @@ It looks like you were trying to pass a path to a configuration file.
|
||||
The path `{value}` does not point to a configuration file"
|
||||
));
|
||||
}
|
||||
} else if value.contains('=') {
|
||||
tip.push_str(&format!(
|
||||
"\n\n{}:\n\n{underlying_error}",
|
||||
config_parse_error.description()
|
||||
));
|
||||
} else if let Some((key, value)) = value.split_once('=') {
|
||||
let key = key.trim_ascii();
|
||||
let value = value.trim_ascii_start();
|
||||
|
||||
match Options::metadata().find(key) {
|
||||
Some(OptionEntry::Set(set)) if !value.starts_with('{') => {
|
||||
let prefixed_subfields = set
|
||||
.collect_fields()
|
||||
.iter()
|
||||
.map(|(name, _)| format!("- `{key}.{name}`"))
|
||||
.join("\n");
|
||||
|
||||
tip.push_str(&format!(
|
||||
"
|
||||
|
||||
`{key}` is a table of configuration options.
|
||||
Did you want to override one of the table's subkeys?
|
||||
|
||||
Possible choices:
|
||||
|
||||
{prefixed_subfields}"
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
tip.push_str(&format!(
|
||||
"\n\n{}:\n\n{underlying_error}",
|
||||
config_parse_error.description()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let tip = tip.trim_end().to_owned().into();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use log::{debug, error, warn};
|
||||
use rayon::prelude::*;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::panic::catch_unwind;
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_linter::message::Message;
|
||||
use ruff_linter::package::PackageRoot;
|
||||
@@ -27,7 +28,6 @@ use ruff_workspace::resolver::{
|
||||
use crate::args::ConfigArguments;
|
||||
use crate::cache::{Cache, PackageCacheMap, PackageCaches};
|
||||
use crate::diagnostics::Diagnostics;
|
||||
use crate::panic::catch_unwind;
|
||||
|
||||
/// Run the linter over a collection of files.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -15,6 +15,7 @@ use rustc_hash::FxHashSet;
|
||||
use thiserror::Error;
|
||||
use tracing::debug;
|
||||
|
||||
use ruff_db::panic::{catch_unwind, PanicError};
|
||||
use ruff_diagnostics::SourceMap;
|
||||
use ruff_linter::fs;
|
||||
use ruff_linter::logging::{DisplayParseError, LogLevel};
|
||||
@@ -32,7 +33,6 @@ use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{ConfigArguments, FormatArguments, FormatRange};
|
||||
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
|
||||
use crate::panic::{catch_unwind, PanicError};
|
||||
use crate::resolve::resolve;
|
||||
use crate::{resolve_default_files, ExitStatus};
|
||||
|
||||
@@ -233,7 +233,7 @@ pub(crate) fn format(
|
||||
}
|
||||
|
||||
/// Format the file at the given [`Path`].
|
||||
#[tracing::instrument(level="debug", skip_all, fields(path = %path.display()))]
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(path = %path.display()))]
|
||||
pub(crate) fn format_path(
|
||||
path: &Path,
|
||||
settings: &FormatterSettings,
|
||||
@@ -788,8 +788,6 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
|
||||
let mut incompatible_rules = FxHashSet::default();
|
||||
for setting in resolver.settings() {
|
||||
for rule in [
|
||||
// The formatter might collapse implicit string concatenation on a single line.
|
||||
Rule::SingleLineImplicitStringConcatenation,
|
||||
// Flags missing trailing commas when all arguments are on its own line:
|
||||
// ```python
|
||||
// def args(
|
||||
@@ -829,6 +827,19 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
|
||||
warn_user_once!("The `format.indent-style=\"tab\"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `\"space\"`.");
|
||||
}
|
||||
|
||||
if !setting
|
||||
.linter
|
||||
.rules
|
||||
.enabled(Rule::SingleLineImplicitStringConcatenation)
|
||||
&& setting
|
||||
.linter
|
||||
.rules
|
||||
.enabled(Rule::MultiLineImplicitStringConcatenation)
|
||||
&& !setting.linter.flake8_implicit_str_concat.allow_multiline
|
||||
{
|
||||
warn_user_once!("The `lint.flake8-implicit-str-concat.allow-multiline = false` option is incompatible with the formatter unless `ISC001` is enabled. We recommend enabling `ISC001` or setting `allow-multiline=true`.");
|
||||
}
|
||||
|
||||
// Validate all rules that rely on tab styles.
|
||||
if setting.linter.rules.enabled(Rule::DocstringTabIndentation)
|
||||
&& setting.formatter.indent_style.is_tab()
|
||||
|
||||
@@ -29,7 +29,6 @@ pub mod args;
|
||||
mod cache;
|
||||
mod commands;
|
||||
mod diagnostics;
|
||||
mod panic;
|
||||
mod printer;
|
||||
pub mod resolve;
|
||||
mod stdin;
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct PanicError {
|
||||
pub(crate) info: String,
|
||||
pub(crate) backtrace: Option<std::backtrace::Backtrace>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PanicError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "{}", self.info)?;
|
||||
if let Some(backtrace) = &self.backtrace {
|
||||
writeln!(f, "Backtrace: {backtrace}")
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static LAST_PANIC: std::cell::Cell<Option<PanicError>> = const { std::cell::Cell::new(None) };
|
||||
}
|
||||
|
||||
/// [`catch_unwind`](std::panic::catch_unwind) wrapper that sets a custom [`set_hook`](std::panic::set_hook)
|
||||
/// to extract the backtrace. The original panic-hook gets restored before returning.
|
||||
pub(crate) fn catch_unwind<F, R>(f: F) -> Result<R, PanicError>
|
||||
where
|
||||
F: FnOnce() -> R + std::panic::UnwindSafe,
|
||||
{
|
||||
let prev = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let info = info.to_string();
|
||||
let backtrace = std::backtrace::Backtrace::force_capture();
|
||||
LAST_PANIC.with(|cell| {
|
||||
cell.set(Some(PanicError {
|
||||
info,
|
||||
backtrace: Some(backtrace),
|
||||
}));
|
||||
});
|
||||
}));
|
||||
|
||||
let result = std::panic::catch_unwind(f)
|
||||
.map_err(|_| LAST_PANIC.with(std::cell::Cell::take).unwrap_or_default());
|
||||
|
||||
std::panic::set_hook(prev);
|
||||
|
||||
result
|
||||
}
|
||||
@@ -877,7 +877,7 @@ indent-width = 2
|
||||
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
ignore = ["D203", "D212"]
|
||||
ignore = ["D203", "D212", "ISC001"]
|
||||
|
||||
[lint.isort]
|
||||
lines-after-imports = 3
|
||||
@@ -891,6 +891,9 @@ inline-quotes = "single"
|
||||
docstring-quotes = "single"
|
||||
multiline-quotes = "single"
|
||||
|
||||
[lint.flake8-implicit-str-concat]
|
||||
allow-multiline = false
|
||||
|
||||
[format]
|
||||
skip-magic-trailing-comma = true
|
||||
indent-style = "tab"
|
||||
@@ -915,8 +918,9 @@ def say_hy(name: str):
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
|
||||
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
|
||||
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
|
||||
warning: The `lint.flake8-implicit-str-concat.allow-multiline = false` option is incompatible with the formatter unless `ISC001` is enabled. We recommend enabling `ISC001` or setting `allow-multiline=true`.
|
||||
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
|
||||
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
|
||||
@@ -974,7 +978,7 @@ def say_hy(name: str):
|
||||
print(f"Hy {name}")
|
||||
|
||||
----- stderr -----
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
|
||||
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
|
||||
warning: The `format.indent-style="tab"` option is incompatible with `W191`, which lints against all uses of tabs. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
|
||||
warning: The `format.indent-style="tab"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `"space"`.
|
||||
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
|
||||
@@ -1114,7 +1118,7 @@ def say_hy(name: str):
|
||||
----- stderr -----
|
||||
warning: `incorrect-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `incorrect-blank-line-before-class`.
|
||||
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
|
||||
warning: The following rule may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling this rule, either by removing it from the `select` or `extend-select` configuration, or adding it to the `ignore` configuration.
|
||||
");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -780,6 +780,90 @@ fn each_toml_option_requires_a_new_flag_2() {
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_given_to_table_key_is_not_inline_table_1() {
|
||||
// https://github.com/astral-sh/ruff/issues/13995
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args([".", "--config", r#"lint.flake8-pytest-style="csv""#]),
|
||||
@r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value 'lint.flake8-pytest-style="csv"' for '--config <CONFIG_OPTION>'
|
||||
|
||||
tip: A `--config` flag must either be a path to a `.toml` configuration file
|
||||
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
|
||||
option
|
||||
|
||||
`lint.flake8-pytest-style` is a table of configuration options.
|
||||
Did you want to override one of the table's subkeys?
|
||||
|
||||
Possible choices:
|
||||
|
||||
- `lint.flake8-pytest-style.fixture-parentheses`
|
||||
- `lint.flake8-pytest-style.parametrize-names-type`
|
||||
- `lint.flake8-pytest-style.parametrize-values-type`
|
||||
- `lint.flake8-pytest-style.parametrize-values-row-type`
|
||||
- `lint.flake8-pytest-style.raises-require-match-for`
|
||||
- `lint.flake8-pytest-style.raises-extend-require-match-for`
|
||||
- `lint.flake8-pytest-style.mark-parentheses`
|
||||
|
||||
For more information, try '--help'.
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_given_to_table_key_is_not_inline_table_2() {
|
||||
// https://github.com/astral-sh/ruff/issues/13995
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args([".", "--config", r#"lint=123"#]),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value 'lint=123' for '--config <CONFIG_OPTION>'
|
||||
|
||||
tip: A `--config` flag must either be a path to a `.toml` configuration file
|
||||
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
|
||||
option
|
||||
|
||||
`lint` is a table of configuration options.
|
||||
Did you want to override one of the table's subkeys?
|
||||
|
||||
Possible choices:
|
||||
|
||||
- `lint.allowed-confusables`
|
||||
- `lint.dummy-variable-rgx`
|
||||
- `lint.extend-ignore`
|
||||
- `lint.extend-select`
|
||||
- `lint.extend-fixable`
|
||||
- `lint.external`
|
||||
- `lint.fixable`
|
||||
- `lint.ignore`
|
||||
- `lint.extend-safe-fixes`
|
||||
- `lint.extend-unsafe-fixes`
|
||||
- `lint.ignore-init-module-imports`
|
||||
- `lint.logger-objects`
|
||||
- `lint.select`
|
||||
- `lint.explicit-preview-rules`
|
||||
- `lint.task-tags`
|
||||
- `lint.typing-modules`
|
||||
- `lint.unfixable`
|
||||
- `lint.per-file-ignores`
|
||||
- `lint.extend-per-file-ignores`
|
||||
- `lint.exclude`
|
||||
- `lint.preview`
|
||||
|
||||
For more information, try '--help'.
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_doubly_overridden_via_cli() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user