Compare commits
71 Commits
0.7.0
...
micha/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca4c006f7d | ||
|
|
6aaf1d9446 | ||
|
|
5eb87aa56e | ||
|
|
085a43a262 | ||
|
|
32b57b2ee4 | ||
|
|
337af836d3 | ||
|
|
113ce840a6 | ||
|
|
7272f83868 | ||
|
|
3eb454699a | ||
|
|
77ae0ccf0f | ||
|
|
73ee72b665 | ||
|
|
e402e27a09 | ||
|
|
de4181d7dd | ||
|
|
2c57c2dc8a | ||
|
|
72c18c8225 | ||
|
|
00b078268b | ||
|
|
4d109514d6 | ||
|
|
387076d212 | ||
|
|
2f88f84972 | ||
|
|
f335fe4d4a | ||
|
|
c6ce52c29e | ||
|
|
5d4edd61bf | ||
|
|
7dbd8f0f8e | ||
|
|
46c0961b0b | ||
|
|
cd6c937194 | ||
|
|
9d102799f9 | ||
|
|
e39110e18b | ||
|
|
155be88373 | ||
|
|
a77512df68 | ||
|
|
e9dd92107c | ||
|
|
9e3cf14dde | ||
|
|
fa7626160b | ||
|
|
d9ef83bfef | ||
|
|
f3612c2717 | ||
|
|
c2dc502f3b | ||
|
|
a3a83635f7 | ||
|
|
28e995023d | ||
|
|
a80d515be3 | ||
|
|
9477058790 | ||
|
|
19a2fd1c82 | ||
|
|
cf26676c60 | ||
|
|
7060bf87c0 | ||
|
|
ca8f10862e | ||
|
|
c9b74eda4a | ||
|
|
a5d52b00ca | ||
|
|
efa798b4c4 | ||
|
|
02bca9a8d0 | ||
|
|
72adb09bf3 | ||
|
|
7fd8e30eed | ||
|
|
7ca3571194 | ||
|
|
27c50bebec | ||
|
|
075e378b0f | ||
|
|
0f0fff4d5a | ||
|
|
2d2baeca23 | ||
|
|
fb66f715f3 | ||
|
|
55bccf6680 | ||
|
|
f4b5e70fae | ||
|
|
36cb1199cc | ||
|
|
2ff36530c3 | ||
|
|
bd33b4972d | ||
|
|
6964eef369 | ||
|
|
c93a7c7878 | ||
|
|
6d7da7bdbe | ||
|
|
ff72055558 | ||
|
|
4ecfe95295 | ||
|
|
c2f7c39987 | ||
|
|
f80528fbf2 | ||
|
|
040a591cad | ||
|
|
3d0bdb426a | ||
|
|
e2a30b71f4 | ||
|
|
5c537b6dbb |
2
.github/workflows/build-binaries.yml
vendored
2
.github/workflows/build-binaries.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
macos-x86_64:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
|
||||
runs-on: macos-12
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
250
.github/workflows/build-docker.yml
vendored
250
.github/workflows/build-docker.yml
vendored
@@ -17,12 +17,21 @@ on:
|
||||
paths:
|
||||
- .github/workflows/build-docker.yml
|
||||
|
||||
env:
|
||||
RUFF_BASE_IMG: ghcr.io/${{ github.repository_owner }}/ruff
|
||||
|
||||
jobs:
|
||||
docker-publish:
|
||||
name: Build Docker image (ghcr.io/astral-sh/ruff)
|
||||
docker-build:
|
||||
name: Build Docker image (ghcr.io/astral-sh/ruff) for ${{ matrix.platform }}
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -36,12 +45,6 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/astral-sh/ruff
|
||||
|
||||
- name: Check tag consistency
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
run: |
|
||||
@@ -55,14 +58,233 @@ jobs:
|
||||
echo "Releasing ${version}"
|
||||
fi
|
||||
|
||||
- name: "Build and push Docker image"
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.RUFF_BASE_IMG }}
|
||||
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
|
||||
tags: |
|
||||
type=raw,value=dry-run,enable=${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
type=pep440,pattern={{ version }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
|
||||
- name: Normalize Platform Pair (replace / with -)
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_TUPLE=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
cache-from: type=gha,scope=ruff-${{ env.PLATFORM_TUPLE }}
|
||||
cache-to: type=gha,mode=min,scope=ruff-${{ env.PLATFORM_TUPLE }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.RUFF_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
|
||||
- name: Export digests
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_TUPLE }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
docker-publish:
|
||||
name: Publish Docker image (ghcr.io/astral-sh/ruff)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
needs:
|
||||
- docker-build
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.RUFF_BASE_IMG }}
|
||||
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
|
||||
tags: |
|
||||
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
# The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array
|
||||
# The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
|
||||
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
|
||||
|
||||
docker-publish-extra:
|
||||
name: Publish additional Docker image based on ${{ matrix.image-mapping }}
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
needs:
|
||||
- docker-publish
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Mapping of base image followed by a comma followed by one or more base tags (comma separated)
|
||||
# Note, org.opencontainers.image.version label will use the first base tag (use the most specific tag first)
|
||||
image-mapping:
|
||||
- alpine:3.20,alpine3.20,alpine
|
||||
- debian:bookworm-slim,bookworm-slim,debian-slim
|
||||
- buildpack-deps:bookworm,bookworm,debian
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate Dynamic Dockerfile Tags
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Extract the image and tags from the matrix variable
|
||||
IFS=',' read -r BASE_IMAGE BASE_TAGS <<< "${{ matrix.image-mapping }}"
|
||||
|
||||
# Generate Dockerfile content
|
||||
cat <<EOF > Dockerfile
|
||||
FROM ${BASE_IMAGE}
|
||||
COPY --from=${{ env.RUFF_BASE_IMG }}:latest /ruff /usr/local/bin/ruff
|
||||
ENTRYPOINT []
|
||||
CMD ["/usr/local/bin/ruff"]
|
||||
EOF
|
||||
|
||||
# Initialize a variable to store all tag docker metadata patterns
|
||||
TAG_PATTERNS=""
|
||||
|
||||
# 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=raw,value=${TAG}\n"
|
||||
done
|
||||
|
||||
# Remove the trailing newline from the pattern list
|
||||
TAG_PATTERNS="${TAG_PATTERNS%\\n}"
|
||||
|
||||
# Export image cache name
|
||||
echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> $GITHUB_ENV
|
||||
|
||||
# Export tag patterns using the multiline env var syntax
|
||||
{
|
||||
echo "TAG_PATTERNS<<EOF"
|
||||
echo -e "${TAG_PATTERNS}"
|
||||
echo EOF
|
||||
} >> $GITHUB_ENV
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
# ghcr.io prefers index level annotations
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
|
||||
with:
|
||||
images: ${{ env.RUFF_BASE_IMG }}
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
${{ env.TAG_PATTERNS }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
# Reuse the builder
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
push: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
tags: ghcr.io/astral-sh/ruff:latest,ghcr.io/astral-sh/ruff:${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || 'dry-run' }}
|
||||
# We do not really need to cache here as the Dockerfile is tiny
|
||||
#cache-from: type=gha,scope=ruff-${{ env.IMAGE_REF }}
|
||||
#cache-to: type=gha,mode=min,scope=ruff-${{ env.IMAGE_REF }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
# This is effectively a duplicate of `docker-publish` to make https://github.com/astral-sh/ruff/pkgs/container/ruff
|
||||
# show the ruff base image first since GitHub always shows the last updated image digests
|
||||
# This works by annotating the original digests (previously non-annotated) which triggers an update to ghcr.io
|
||||
docker-republish:
|
||||
name: Annotate Docker image (ghcr.io/astral-sh/ruff)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
needs:
|
||||
- docker-publish-extra
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
|
||||
with:
|
||||
images: ${{ env.RUFF_BASE_IMG }}
|
||||
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
|
||||
tags: |
|
||||
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
# The readarray part is used to make sure the quoting and special characters are preserved on expansion (e.g. spaces)
|
||||
# The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array
|
||||
# The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
|
||||
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
|
||||
run: |
|
||||
readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done
|
||||
docker buildx imagetools create \
|
||||
"${annotations[@]}" \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -193,7 +193,7 @@ jobs:
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: playground/package-lock.json
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
|
||||
4
.github/workflows/publish-playground.yml
vendored
4
.github/workflows/publish-playground.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: playground/package-lock.json
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.7.0
|
||||
uses: cloudflare/wrangler-action@v3.9.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
2
.github/workflows/publish-wasm.yml
vendored
2
.github/workflows/publish-wasm.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: "Publish (dry-run)"
|
||||
if: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
fail_fast: true
|
||||
fail_fast: false
|
||||
|
||||
exclude: |
|
||||
(?x)^(
|
||||
@@ -17,12 +17,12 @@ exclude: |
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.20.2
|
||||
rev: v0.21
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/executablebooks/mdformat
|
||||
rev: 0.7.17
|
||||
rev: 0.7.18
|
||||
hooks:
|
||||
- id: mdformat
|
||||
additional_dependencies:
|
||||
@@ -45,8 +45,17 @@ repos:
|
||||
| docs/\w+\.md
|
||||
)$
|
||||
|
||||
- repo: https://github.com/adamchainz/blacken-docs
|
||||
rev: 1.19.1
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
args: ["--pyi", "--line-length", "130"]
|
||||
files: '^crates/.*/resources/mdtest/.*\.md'
|
||||
additional_dependencies:
|
||||
- black==24.10.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.25.0
|
||||
rev: v1.26.0
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -60,7 +69,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.9
|
||||
rev: v0.7.0
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments
|
||||
([#12838](https://github.com/astral-sh/ruff/pull/12838), [#13292](https://github.com/astral-sh/ruff/pull/13292)).
|
||||
This was a change that we attempted to make in Ruff v0.6.0, but only partially made due to an error on our part.
|
||||
See the [blog post](https://astral.sh/blog/ruff-v0.7.0) for more details.
|
||||
- The `useless-try-except` rule (in our `tryceratops` category) has been recoded from `TRY302` to
|
||||
`TRY203` ([#13502](https://github.com/astral-sh/ruff/pull/13502)). This ensures Ruff's code is consistent with
|
||||
the same rule in the [`tryceratops`](https://github.com/guilatrova/tryceratops) linter.
|
||||
- The `lint.allow-unused-imports` setting has been removed ([#13677](https://github.com/astral-sh/ruff/pull/13677)). Use
|
||||
[`lint.pyflakes.allow-unused-imports`](https://docs.astral.sh/ruff/settings/#lint_pyflakes_allowed-unused-imports)
|
||||
instead.
|
||||
|
||||
## 0.6.0
|
||||
|
||||
- Detect imports in `src` layouts by default for `isort` rules ([#12848](https://github.com/astral-sh/ruff/pull/12848))
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,5 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Preview features
|
||||
|
||||
- Fix `E221` and `E222` to flag missing or extra whitespace around `==` operator ([#13890](https://github.com/astral-sh/ruff/pull/13890))
|
||||
- Formatter: Alternate quotes for strings inside f-strings in preview ([#13860](https://github.com/astral-sh/ruff/pull/13860))
|
||||
- Formatter: Join implicit concatenated strings when they fit on a line ([#13663](https://github.com/astral-sh/ruff/pull/13663))
|
||||
- \[`pylint`\] Restrict `iteration-over-set` to only work on sets of literals (`PLC0208`) ([#13731](https://github.com/astral-sh/ruff/pull/13731))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-type-checking`\] Support auto-quoting when annotations contain quotes ([#11811](https://github.com/astral-sh/ruff/pull/11811))
|
||||
|
||||
### Server
|
||||
|
||||
- Avoid indexing the workspace for single-file mode ([#13770](https://github.com/astral-sh/ruff/pull/13770))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Make `ARG002` compatible with `EM101` when raising `NotImplementedError` ([#13714](https://github.com/astral-sh/ruff/pull/13714))
|
||||
|
||||
### Other changes
|
||||
|
||||
- Introduce more Docker tags for Ruff (similar to uv) ([#13274](https://github.com/astral-sh/ruff/pull/13274))
|
||||
|
||||
## 0.7.0
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.7.0) for a migration guide and overview of the changes!
|
||||
|
||||
221
Cargo.lock
generated
221
Cargo.lock
generated
@@ -69,7 +69,7 @@ version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
"yansi-term",
|
||||
]
|
||||
|
||||
@@ -123,9 +123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.89"
|
||||
version = "1.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
|
||||
checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95"
|
||||
|
||||
[[package]]
|
||||
name = "append-only-vec"
|
||||
@@ -407,7 +407,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -491,7 +491,7 @@ dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -687,7 +687,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -698,7 +698,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -750,6 +750,27 @@ dependencies = [
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dir-test"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c44bdf9319ad5223afb7eb15a7110452b0adf0373ea6756561b2c708eef0dd1"
|
||||
dependencies = [
|
||||
"dir-test-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dir-test-macros"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "644f96047137dfaa7a09e34d4623f9e52a1926ecc25ba32ad2ba3fc422536b25"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "4.0.0"
|
||||
@@ -879,9 +900,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
|
||||
[[package]]
|
||||
name = "fern"
|
||||
version = "0.6.2"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee"
|
||||
checksum = "69ff9c9d5fb3e6da8ac2f77ab76fe7e8087d512ce095200f8f29ac5b656cf6dc"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
@@ -957,7 +978,7 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1160,7 +1181,7 @@ dependencies = [
|
||||
"instant",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
"vt100",
|
||||
]
|
||||
|
||||
@@ -1246,7 +1267,7 @@ dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1346,9 +1367,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.159"
|
||||
version = "0.2.161"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -1372,7 +1393,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1760,18 +1781,17 @@ dependencies = [
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pep440_rs"
|
||||
version = "0.6.6"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "466eada3179c2e069ca897b99006cbb33f816290eaeec62464eea907e22ae385"
|
||||
checksum = "7c8ee724d21f351f9d47276614ac9710975db827ba9fe2ca5a517ba648193307"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"serde",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.0",
|
||||
"unscanny",
|
||||
]
|
||||
|
||||
@@ -1787,7 +1807,7 @@ dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -1828,7 +1848,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1943,9 +1963,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.87"
|
||||
version = "1.0.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
|
||||
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2080,6 +2100,7 @@ dependencies = [
|
||||
"camino",
|
||||
"compact_str",
|
||||
"countme",
|
||||
"dir-test",
|
||||
"hashbrown 0.15.0",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
@@ -2087,7 +2108,6 @@ dependencies = [
|
||||
"ordermap",
|
||||
"red_knot_test",
|
||||
"red_knot_vendored",
|
||||
"rstest",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_python_ast",
|
||||
@@ -2136,7 +2156,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"colored",
|
||||
"once_cell",
|
||||
"memchr",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_vendored",
|
||||
"regex",
|
||||
@@ -2154,7 +2174,6 @@ dependencies = [
|
||||
name = "red_knot_vendored"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"path-slash",
|
||||
"ruff_db",
|
||||
"walkdir",
|
||||
@@ -2270,12 +2289,6 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "relative-path"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
@@ -2291,36 +2304,9 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936"
|
||||
dependencies = [
|
||||
"rstest_macros",
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest_macros"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"relative-path",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2382,7 +2368,6 @@ dependencies = [
|
||||
"codspeed-criterion-compat",
|
||||
"criterion",
|
||||
"mimalloc",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_workspace",
|
||||
@@ -2506,7 +2491,7 @@ dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"tracing",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2515,7 +2500,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"once_cell",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
@@ -2539,7 +2523,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2560,10 +2544,9 @@ dependencies = [
|
||||
"log",
|
||||
"memchr",
|
||||
"natord",
|
||||
"once_cell",
|
||||
"path-absolutize",
|
||||
"pathdiff",
|
||||
"pep440_rs 0.6.6",
|
||||
"pep440_rs 0.7.1",
|
||||
"pyproject-toml",
|
||||
"quick-junit",
|
||||
"regex",
|
||||
@@ -2594,7 +2577,7 @@ dependencies = [
|
||||
"toml",
|
||||
"typed-arena",
|
||||
"unicode-normalization",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
"unicode_names2",
|
||||
"url",
|
||||
]
|
||||
@@ -2607,7 +2590,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2616,7 +2599,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"ruff_diagnostics",
|
||||
"ruff_source_file",
|
||||
@@ -2638,7 +2620,6 @@ dependencies = [
|
||||
"compact_str",
|
||||
"is-macro",
|
||||
"itertools 0.13.0",
|
||||
"once_cell",
|
||||
"ruff_cache",
|
||||
"ruff_macros",
|
||||
"ruff_python_trivia",
|
||||
@@ -2664,7 +2645,6 @@ dependencies = [
|
||||
name = "ruff_python_codegen"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_literal",
|
||||
"ruff_python_parser",
|
||||
@@ -2682,7 +2662,6 @@ dependencies = [
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
"ruff_formatter",
|
||||
@@ -2833,6 +2812,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
@@ -2842,7 +2822,6 @@ name = "ruff_source_file"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"ruff_text_size",
|
||||
"serde",
|
||||
]
|
||||
@@ -2859,7 +2838,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -2898,7 +2877,7 @@ dependencies = [
|
||||
"matchit",
|
||||
"path-absolutize",
|
||||
"path-slash",
|
||||
"pep440_rs 0.6.6",
|
||||
"pep440_rs 0.7.1",
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
"ruff_formatter",
|
||||
@@ -2940,15 +2919,6 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.37"
|
||||
@@ -3009,7 +2979,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
@@ -3029,17 +2999,17 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -3073,7 +3043,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3094,12 +3064,6 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
@@ -3128,7 +3092,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3139,14 +3103,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.128"
|
||||
version = "1.0.132"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3162,7 +3126,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3203,7 +3167,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3305,7 +3269,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3316,9 +3280,20 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.79"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3333,7 +3308,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3396,7 +3371,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3407,7 +3382,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
@@ -3428,7 +3403,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3540,7 +3515,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3704,6 +3679,12 @@ version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode_names2"
|
||||
version = "1.3.0"
|
||||
@@ -3774,9 +3755,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
@@ -3786,13 +3767,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid-macro-internal"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee1cd046f83ea2c4e920d6ee9f7c3537ef928d75dce5d84a87c2c5d6b3999a3a"
|
||||
checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3815,7 +3796,7 @@ checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"log",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
"vte",
|
||||
]
|
||||
|
||||
@@ -3878,7 +3859,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -3912,7 +3893,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -3946,7 +3927,7 @@ checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4234,7 +4215,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.82",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -4,7 +4,7 @@ resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
rust-version = "1.80"
|
||||
homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
@@ -65,10 +65,11 @@ compact_str = "0.8.0"
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
crossbeam = { version = "0.8.4" }
|
||||
dashmap = { version = "6.0.1" }
|
||||
dir-test = { version = "0.3.0" }
|
||||
drop_bomb = { version = "0.1.5" }
|
||||
env_logger = { version = "0.11.0" }
|
||||
etcetera = { version = "0.8.0" }
|
||||
fern = { version = "0.6.1" }
|
||||
fern = { version = "0.7.0" }
|
||||
filetime = { version = "0.2.23" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
@@ -101,12 +102,11 @@ memchr = { version = "2.7.1" }
|
||||
mimalloc = { version = "0.1.39" }
|
||||
natord = { version = "1.0.9" }
|
||||
notify = { version = "6.1.1" }
|
||||
once_cell = { version = "1.19.0" }
|
||||
ordermap = { version = "0.5.0" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
path-slash = { version = "0.2.1" }
|
||||
pathdiff = { version = "0.2.1" }
|
||||
pep440_rs = { version = "0.6.0", features = ["serde"] }
|
||||
pep440_rs = { version = "0.7.1" }
|
||||
pretty_assertions = "1.3.0"
|
||||
proc-macro2 = { version = "1.0.79" }
|
||||
pyproject-toml = { version = "0.9.0" }
|
||||
@@ -115,9 +115,8 @@ quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rstest = { version = "0.22.0", default-features = false }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "b14be5c0392f4c55eca60b92e457a35549372382" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "254c749b02cde2fd29852a7463a33e800b771758" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -203,6 +202,10 @@ get_unwrap = "warn"
|
||||
rc_buffer = "warn"
|
||||
rc_mutex = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
# nursery rules
|
||||
redundant_clone = "warn"
|
||||
debug_assert_with_mut_call = "warn"
|
||||
unused_peekable = "warn"
|
||||
|
||||
[profile.release]
|
||||
# Note that we set these explicitly, and these values
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM ubuntu as build
|
||||
FROM --platform=$BUILDPLATFORM ubuntu AS build
|
||||
ENV HOME="/root"
|
||||
WORKDIR $HOME
|
||||
|
||||
|
||||
@@ -136,8 +136,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.7.0/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.7.0/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.7.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.7.1/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -170,7 +170,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.7.0
|
||||
rev: v0.7.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -144,7 +144,7 @@ pub fn main() -> ExitStatus {
|
||||
}
|
||||
|
||||
fn run() -> anyhow::Result<ExitStatus> {
|
||||
let args = Args::parse_from(std::env::args().collect::<Vec<_>>());
|
||||
let args = Args::parse_from(std::env::args());
|
||||
|
||||
if matches!(args.command, Some(Command::Server)) {
|
||||
return run_server().map(|()| ExitStatus::Success);
|
||||
|
||||
@@ -43,8 +43,8 @@ red_knot_test = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
dir-test = {workspace = true}
|
||||
insta = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Rebuild the crate if a test file is added or removed from
|
||||
pub fn main() {
|
||||
println!("cargo:rerun-if-changed=resources/mdtest");
|
||||
println!("cargo::rerun-if-changed=resources/mdtest");
|
||||
}
|
||||
|
||||
@@ -13,13 +13,22 @@ reveal_type(y) # revealed: Literal[1]
|
||||
## Violates own annotation
|
||||
|
||||
```py
|
||||
x: int = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||
|
||||
x: int = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## Violates previous annotation
|
||||
|
||||
```py
|
||||
x: int
|
||||
x = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## PEP-604 annotations not yet supported
|
||||
|
||||
```py
|
||||
def f() -> str | None:
|
||||
return None
|
||||
|
||||
# TODO: should be `str | None` (but Todo is better than `Unknown`)
|
||||
reveal_type(f()) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
## Unbound
|
||||
|
||||
```py
|
||||
x = foo
|
||||
x = foo # error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
foo = 1
|
||||
reveal_type(x) # revealed: Unbound
|
||||
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## Unbound class variable
|
||||
@@ -13,12 +16,17 @@ reveal_type(x) # revealed: Unbound
|
||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
|
||||
class C:
|
||||
y = x
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
reveal_type(C.x) # revealed: Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
reveal_type(C.x) # revealed: Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
## Union of attributes
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
class C:
|
||||
x = 1
|
||||
|
||||
else:
|
||||
class C:
|
||||
x = 2
|
||||
|
||||
y = C.x
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
reveal_type(C.x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
## Binary operations on booleans
|
||||
|
||||
## Basic Arithmetic
|
||||
|
||||
We try to be precise and all operations except for division will result in Literal type.
|
||||
|
||||
```py
|
||||
a = True
|
||||
b = False
|
||||
|
||||
reveal_type(a + a) # revealed: Literal[2]
|
||||
reveal_type(a + b) # revealed: Literal[1]
|
||||
reveal_type(b + a) # revealed: Literal[1]
|
||||
reveal_type(b + b) # revealed: Literal[0]
|
||||
|
||||
reveal_type(a - a) # revealed: Literal[0]
|
||||
reveal_type(a - b) # revealed: Literal[1]
|
||||
reveal_type(b - a) # revealed: Literal[-1]
|
||||
reveal_type(b - b) # revealed: Literal[0]
|
||||
|
||||
reveal_type(a * a) # revealed: Literal[1]
|
||||
reveal_type(a * b) # revealed: Literal[0]
|
||||
reveal_type(b * a) # revealed: Literal[0]
|
||||
reveal_type(b * b) # revealed: Literal[0]
|
||||
|
||||
reveal_type(a % a) # revealed: Literal[0]
|
||||
reveal_type(b % a) # revealed: Literal[0]
|
||||
|
||||
reveal_type(a // a) # revealed: Literal[1]
|
||||
reveal_type(b // a) # revealed: Literal[0]
|
||||
|
||||
reveal_type(a**a) # revealed: Literal[1]
|
||||
reveal_type(a**b) # revealed: Literal[1]
|
||||
reveal_type(b**a) # revealed: Literal[0]
|
||||
reveal_type(b**b) # revealed: Literal[1]
|
||||
|
||||
# Division
|
||||
reveal_type(a / a) # revealed: float
|
||||
reveal_type(b / a) # revealed: float
|
||||
b / b # error: [division-by-zero] "Cannot divide object of type `Literal[False]` by zero"
|
||||
a / b # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
|
||||
|
||||
# bitwise OR
|
||||
reveal_type(a | a) # revealed: Literal[True]
|
||||
reveal_type(a | b) # revealed: Literal[True]
|
||||
reveal_type(b | a) # revealed: Literal[True]
|
||||
reveal_type(b | b) # revealed: Literal[False]
|
||||
```
|
||||
@@ -0,0 +1,447 @@
|
||||
# Binary operations on instances
|
||||
|
||||
Binary operations in Python are implemented by means of magic double-underscore methods.
|
||||
|
||||
For references, see:
|
||||
|
||||
- <https://snarky.ca/unravelling-binary-arithmetic-operations-in-python/>
|
||||
- <https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types>
|
||||
|
||||
## Operations
|
||||
|
||||
We support inference for all Python's binary operators:
|
||||
`+`, `-`, `*`, `@`, `/`, `//`, `%`, `**`, `<<`, `>>`, `&`, `^`, and `|`.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __sub__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __mul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __matmul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __truediv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __floordiv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __mod__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __pow__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __lshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __and__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __xor__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __or__(self, other) -> A:
|
||||
return self
|
||||
|
||||
class B: ...
|
||||
|
||||
reveal_type(A() + B()) # revealed: A
|
||||
reveal_type(A() - B()) # revealed: A
|
||||
reveal_type(A() * B()) # revealed: A
|
||||
reveal_type(A() @ B()) # revealed: A
|
||||
reveal_type(A() / B()) # revealed: A
|
||||
reveal_type(A() // B()) # revealed: A
|
||||
reveal_type(A() % B()) # revealed: A
|
||||
reveal_type(A() ** B()) # revealed: A
|
||||
reveal_type(A() << B()) # revealed: A
|
||||
reveal_type(A() >> B()) # revealed: A
|
||||
reveal_type(A() & B()) # revealed: A
|
||||
reveal_type(A() ^ B()) # revealed: A
|
||||
reveal_type(A() | B()) # revealed: A
|
||||
```
|
||||
|
||||
## Reflected
|
||||
|
||||
We also support inference for reflected operations:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __radd__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rsub__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rmul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rmatmul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rtruediv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rfloordiv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rmod__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rpow__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rlshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rrshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rand__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rxor__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __ror__(self, other) -> A:
|
||||
return self
|
||||
|
||||
class B: ...
|
||||
|
||||
reveal_type(B() + A()) # revealed: A
|
||||
reveal_type(B() - A()) # revealed: A
|
||||
reveal_type(B() * A()) # revealed: A
|
||||
reveal_type(B() @ A()) # revealed: A
|
||||
reveal_type(B() / A()) # revealed: A
|
||||
reveal_type(B() // A()) # revealed: A
|
||||
reveal_type(B() % A()) # revealed: A
|
||||
reveal_type(B() ** A()) # revealed: A
|
||||
reveal_type(B() << A()) # revealed: A
|
||||
reveal_type(B() >> A()) # revealed: A
|
||||
reveal_type(B() & A()) # revealed: A
|
||||
reveal_type(B() ^ A()) # revealed: A
|
||||
reveal_type(B() | A()) # revealed: A
|
||||
```
|
||||
|
||||
## Returning a different type
|
||||
|
||||
The magic methods aren't required to return the type of `self`:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> int:
|
||||
return 1
|
||||
|
||||
def __rsub__(self, other) -> int:
|
||||
return 1
|
||||
|
||||
class B: ...
|
||||
|
||||
reveal_type(A() + B()) # revealed: int
|
||||
reveal_type(B() - A()) # revealed: int
|
||||
```
|
||||
|
||||
## Non-reflected precedence in general
|
||||
|
||||
In general, if the left-hand side defines `__add__` and the right-hand side
|
||||
defines `__radd__` and the right-hand side is not a subtype of the left-hand
|
||||
side, `lhs.__add__` will take precedence:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other: B) -> int:
|
||||
return 42
|
||||
|
||||
class B:
|
||||
def __radd__(self, other: A) -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(A() + B()) # revealed: int
|
||||
|
||||
# Edge case: C is a subtype of C, *but* if the two sides are of *equal* types,
|
||||
# the lhs *still* takes precedence
|
||||
class C:
|
||||
def __add__(self, other: C) -> int:
|
||||
return 42
|
||||
|
||||
def __radd__(self, other: C) -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(C() + C()) # revealed: int
|
||||
```
|
||||
|
||||
## Reflected precedence for subtypes (in some cases)
|
||||
|
||||
If the right-hand operand is a subtype of the left-hand operand and has a
|
||||
different implementation of the reflected method, the reflected method on the
|
||||
right-hand operand takes precedence.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> str:
|
||||
return "foo"
|
||||
|
||||
def __radd__(self, other) -> str:
|
||||
return "foo"
|
||||
|
||||
class MyString(str): ...
|
||||
|
||||
class B(A):
|
||||
def __radd__(self, other) -> MyString:
|
||||
return MyString()
|
||||
|
||||
reveal_type(A() + B()) # revealed: MyString
|
||||
|
||||
# N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__`
|
||||
class C(B): ...
|
||||
|
||||
# TODO: we currently only understand direct subclasses as subtypes of the superclass.
|
||||
# We need to iterate through the full MRO rather than just the class's bases;
|
||||
# if we do, we'll understand `C` as a subtype of `A`, and correctly understand this as being
|
||||
# `MyString` rather than `str`
|
||||
reveal_type(A() + C()) # revealed: str
|
||||
```
|
||||
|
||||
## Reflected precedence 2
|
||||
|
||||
If the right-hand operand is a subtype of the left-hand operand, but does not
|
||||
override the reflected method, the left-hand operand's non-reflected method
|
||||
still takes precedence:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> str:
|
||||
return "foo"
|
||||
|
||||
def __radd__(self, other) -> int:
|
||||
return 42
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(A() + B()) # revealed: str
|
||||
```
|
||||
|
||||
## Only reflected supported
|
||||
|
||||
For example, at runtime, `(1).__add__(1.2)` is `NotImplemented`, but
|
||||
`(1.2).__radd__(1) == 2.2`, meaning that `1 + 1.2` succeeds at runtime
|
||||
(producing `2.2`). The runtime tries the second one only if the first one
|
||||
returns `NotImplemented` to signal failure.
|
||||
|
||||
Typeshed and other stubs annotate dunder-method calls that would return
|
||||
`NotImplemented` as being "illegal" calls. `int.__add__` is annotated as only
|
||||
"accepting" `int`s, even though it strictly-speaking "accepts" any other object
|
||||
without raising an exception -- it will simply return `NotImplemented`,
|
||||
allowing the runtime to try the `__radd__` method of the right-hand operand
|
||||
as well.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __sub__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
class B:
|
||||
def __rsub__(self, other: A) -> B:
|
||||
return B()
|
||||
|
||||
# TODO: this should be `B` (the return annotation of `B.__rsub__`),
|
||||
# because `A.__sub__` is annotated as only accepting `A`,
|
||||
# but `B.__rsub__` will accept `A`.
|
||||
reveal_type(A() - B()) # revealed: A
|
||||
```
|
||||
|
||||
## Callable instances as dunders
|
||||
|
||||
Believe it or not, this is supported at runtime:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __call__(self, other) -> int:
|
||||
return 42
|
||||
|
||||
class B:
|
||||
__add__ = A()
|
||||
|
||||
reveal_type(B() + B()) # revealed: int
|
||||
```
|
||||
|
||||
## Integration test: numbers from typeshed
|
||||
|
||||
```py
|
||||
reveal_type(3j + 3.14) # revealed: complex
|
||||
reveal_type(4.2 + 42) # revealed: float
|
||||
reveal_type(3j + 3) # revealed: complex
|
||||
|
||||
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3.14 + 3j) # revealed: float
|
||||
|
||||
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(42 + 4.2) # revealed: int
|
||||
|
||||
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3 + 3j) # revealed: int
|
||||
|
||||
def returns_int() -> int:
|
||||
return 42
|
||||
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
x = returns_bool()
|
||||
y = returns_int()
|
||||
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(4.2 + x) # revealed: float
|
||||
|
||||
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(y + 4.12) # revealed: int
|
||||
```
|
||||
|
||||
## With literal types
|
||||
|
||||
When we have a literal type for one operand, we're able to fall back to the
|
||||
instance handling for its instance super-type.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __radd__(self, other) -> A:
|
||||
return self
|
||||
|
||||
reveal_type(A() + 1) # revealed: A
|
||||
# TODO should be `A` since `int.__add__` doesn't support `A` instances
|
||||
reveal_type(1 + A()) # revealed: int
|
||||
|
||||
reveal_type(A() + "foo") # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type("foo" + A()) # revealed: @Todo
|
||||
|
||||
reveal_type(A() + b"foo") # revealed: A
|
||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||
reveal_type(b"foo" + A()) # revealed: bytes
|
||||
|
||||
reveal_type(A() + ()) # revealed: A
|
||||
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
||||
reveal_type(() + A()) # revealed: @Todo
|
||||
|
||||
literal_string_instance = "foo" * 1_000_000_000
|
||||
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
||||
reveal_type(literal_string_instance) # revealed: LiteralString
|
||||
|
||||
reveal_type(A() + literal_string_instance) # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
|
||||
`Any` and `Unknown` represent a set of possible runtime objects, wherein the
|
||||
bounds of the set are unknown. Whether the left-hand operand's dunder or the
|
||||
right-hand operand's reflected dunder depends on whether the right-hand operand
|
||||
is an instance of a class that is a subclass of the left-hand operand's class
|
||||
and overrides the reflected dunder. In the following example, because of the
|
||||
unknowable nature of `Any`/`Unknown`, we must consider both possibilities:
|
||||
`Any`/`Unknown` might resolve to an unknown third class that inherits from `X`
|
||||
and overrides `__radd__`; but it also might not. Thus, the correct answer here
|
||||
for the `reveal_type` is `int | Unknown`.
|
||||
|
||||
```py
|
||||
from does_not_exist import Foo # error: [unresolved-import]
|
||||
|
||||
reveal_type(Foo) # revealed: Unknown
|
||||
|
||||
class X:
|
||||
def __add__(self, other: object) -> int:
|
||||
return 42
|
||||
|
||||
class Y(Foo): ...
|
||||
|
||||
# TODO: Should be `int | Unknown`; see above discussion.
|
||||
reveal_type(X() + Y()) # revealed: int
|
||||
```
|
||||
|
||||
## Unsupported
|
||||
|
||||
### Dunder as instance attribute
|
||||
|
||||
The magic method must exist on the class, not just on the instance:
|
||||
|
||||
```py
|
||||
def add_impl(self, other) -> int:
|
||||
return 1
|
||||
|
||||
class A:
|
||||
def __init__(self):
|
||||
self.__add__ = add_impl
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `A` and `A`"
|
||||
# revealed: Unknown
|
||||
reveal_type(A() + A())
|
||||
```
|
||||
|
||||
### Missing dunder
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
# error: [unsupported-operator]
|
||||
# revealed: Unknown
|
||||
reveal_type(A() + A())
|
||||
```
|
||||
|
||||
### Wrong position
|
||||
|
||||
A left-hand dunder method doesn't apply for the right-hand operand, or vice versa:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> int: ...
|
||||
|
||||
class B:
|
||||
def __radd__(self, other) -> int: ...
|
||||
|
||||
class C: ...
|
||||
|
||||
# error: [unsupported-operator]
|
||||
# revealed: Unknown
|
||||
reveal_type(C() + A())
|
||||
|
||||
# error: [unsupported-operator]
|
||||
# revealed: Unknown
|
||||
reveal_type(B() + C())
|
||||
```
|
||||
|
||||
### Reflected dunder is not tried between two objects of the same type
|
||||
|
||||
For the specific case where the left-hand operand is the exact same type as the
|
||||
right-hand operand, the reflected dunder of the right-hand operand is not
|
||||
tried; the runtime short-circuits after trying the unreflected dunder of the
|
||||
left-hand operand. For context, see
|
||||
[this mailing list discussion](https://mail.python.org/archives/list/python-dev@python.org/thread/7NZUCODEAPQFMRFXYRMGJXDSIS3WJYIV/).
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __radd__(self, other: Foo) -> Foo:
|
||||
return self
|
||||
|
||||
# error: [unsupported-operator]
|
||||
# revealed: Unknown
|
||||
reveal_type(Foo() + Foo())
|
||||
```
|
||||
|
||||
### Wrong type
|
||||
|
||||
TODO: check signature and error if `other` is the wrong type
|
||||
@@ -1,36 +1,70 @@
|
||||
## Binary operations on integers
|
||||
# Binary operations on integers
|
||||
|
||||
## Basic Arithmetic
|
||||
|
||||
```py
|
||||
a = 2 + 1
|
||||
b = a - 4
|
||||
c = a * b
|
||||
d = c // 3
|
||||
e = c / 3
|
||||
f = 5 % 3
|
||||
reveal_type(2 + 1) # revealed: Literal[3]
|
||||
reveal_type(3 - 4) # revealed: Literal[-1]
|
||||
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]
|
||||
```
|
||||
|
||||
reveal_type(a) # revealed: Literal[3]
|
||||
reveal_type(b) # revealed: Literal[-1]
|
||||
reveal_type(c) # revealed: Literal[-3]
|
||||
reveal_type(d) # revealed: Literal[-1]
|
||||
reveal_type(e) # revealed: float
|
||||
reveal_type(f) # revealed: Literal[2]
|
||||
## Power
|
||||
|
||||
For power if the result fits in the int literal type it will be a Literal type. Otherwise the
|
||||
outcome is int.
|
||||
|
||||
```py
|
||||
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
|
||||
```
|
||||
|
||||
## Division by Zero
|
||||
|
||||
```py
|
||||
# TODO: `a` should be `int` and `e` should be `float` once we support inference.
|
||||
a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero"
|
||||
b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero"
|
||||
c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
|
||||
d = int() / 0 # error: "Cannot divide object of type `int` by zero"
|
||||
e = 1.0 / 0 # error: "Cannot divide object of type `float` by zero"
|
||||
This error is really outside the current Python type system, because e.g. `int.__truediv__` and
|
||||
friends are not annotated to indicate that it's an error, and we don't even have a facility to
|
||||
permit such an annotation. So arguably divide-by-zero should be a lint error rather than a type
|
||||
checker error. But we choose to go ahead and error in the cases that are very likely to be an error:
|
||||
dividing something typed as `int` or `float` by something known to be `Literal[0]`.
|
||||
|
||||
This isn't _definitely_ an error, because the object typed as `int` or `float` could be an instance
|
||||
of a custom subclass which overrides division behavior to handle zero without error. But if this
|
||||
unusual case occurs, the error can be avoided by explicitly typing the dividend as that safe custom
|
||||
subclass; we only emit the error if the LHS type is exactly `int` or `float`, not if its a subclass.
|
||||
|
||||
```py
|
||||
a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero"
|
||||
reveal_type(a) # revealed: float
|
||||
|
||||
b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero"
|
||||
reveal_type(b) # revealed: int
|
||||
|
||||
c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
|
||||
reveal_type(c) # revealed: int
|
||||
reveal_type(d) # revealed: @Todo
|
||||
reveal_type(e) # revealed: @Todo
|
||||
|
||||
# error: "Cannot divide object of type `int` by zero"
|
||||
# revealed: float
|
||||
reveal_type(int() / 0)
|
||||
|
||||
# error: "Cannot divide object of type `Literal[1]` by zero"
|
||||
# revealed: float
|
||||
reveal_type(1 / False)
|
||||
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
|
||||
True / False
|
||||
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
|
||||
bool(1) / False
|
||||
|
||||
# error: "Cannot divide object of type `float` by zero"
|
||||
# revealed: float
|
||||
reveal_type(1.0 / 0)
|
||||
|
||||
class MyInt(int): ...
|
||||
|
||||
# No error for a subclass of int
|
||||
# revealed: float
|
||||
reveal_type(MyInt(3) / 0)
|
||||
```
|
||||
|
||||
@@ -11,11 +11,10 @@ class Multiplier:
|
||||
return number * self.factor
|
||||
|
||||
a = Multiplier(2.0)(3.0)
|
||||
reveal_type(a) # revealed: float
|
||||
|
||||
class Unit: ...
|
||||
|
||||
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
|
||||
|
||||
reveal_type(a) # revealed: float
|
||||
reveal_type(b) # revealed: Unknown
|
||||
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
```py
|
||||
class Foo: ...
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
def get_int() -> int:
|
||||
return 42
|
||||
|
||||
x = get_int()
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(get_int()) # revealed: int
|
||||
```
|
||||
|
||||
## Async
|
||||
@@ -16,10 +15,8 @@ reveal_type(x) # revealed: int
|
||||
async def get_int_async() -> int:
|
||||
return 42
|
||||
|
||||
x = get_int_async()
|
||||
|
||||
# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]`
|
||||
reveal_type(x) # revealed: @Todo
|
||||
reveal_type(get_int_async()) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Decorated
|
||||
@@ -35,12 +32,10 @@ def decorator(func) -> Callable[[], int]:
|
||||
|
||||
@decorator
|
||||
def bar() -> str:
|
||||
return 'bar'
|
||||
|
||||
x = bar()
|
||||
return "bar"
|
||||
|
||||
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
||||
reveal_type(x) # revealed: @Todo
|
||||
reveal_type(bar()) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Invalid callable
|
||||
|
||||
@@ -3,28 +3,40 @@
|
||||
## Union of return types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
else:
|
||||
def f() -> str:
|
||||
return 'foo'
|
||||
|
||||
x = f()
|
||||
reveal_type(x) # revealed: int | str
|
||||
else:
|
||||
|
||||
def f() -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(f()) # revealed: int | str
|
||||
```
|
||||
|
||||
## Calling with an unknown union
|
||||
|
||||
```py
|
||||
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
|
||||
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
x = f()
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
reveal_type(f()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Non-callable elements in a union
|
||||
@@ -32,9 +44,15 @@ reveal_type(x) # revealed: Unknown | int
|
||||
Calling a union with a non-callable element should emit a diagnostic.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
@@ -47,16 +65,23 @@ reveal_type(x) # revealed: Unknown | int
|
||||
Calling a union with multiple non-callable elements should mention all of them in the diagnostic.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
elif flag2:
|
||||
f = 'foo'
|
||||
f = "foo"
|
||||
else:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
# revealed: Unknown | int
|
||||
reveal_type(f())
|
||||
```
|
||||
|
||||
## All non-callable union elements
|
||||
@@ -64,10 +89,15 @@ reveal_type(x) # revealed: Unknown | int
|
||||
Calling a union with no callable elements can emit a simpler diagnostic.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
f = 'foo'
|
||||
f = "foo"
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
### Comparison: Byte literals
|
||||
# Comparison: Byte literals
|
||||
|
||||
These tests assert that we infer precise `Literal` types for comparisons between objects
|
||||
inferred as having `Literal` bytes types:
|
||||
|
||||
```py
|
||||
reveal_type(b"abc" == b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" == b"ab") # revealed: Literal[False]
|
||||
reveal_type(b"abc" == b"ab") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" != b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"abc" != b"ab") # revealed: Literal[True]
|
||||
reveal_type(b"abc" != b"ab") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" < b"abd") # revealed: Literal[True]
|
||||
reveal_type(b"abc" < b"abb") # revealed: Literal[False]
|
||||
reveal_type(b"abc" < b"abd") # revealed: Literal[True]
|
||||
reveal_type(b"abc" < b"abb") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" <= b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" <= b"abb") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" > b"abd") # revealed: Literal[False]
|
||||
reveal_type(b"abc" > b"abb") # revealed: Literal[True]
|
||||
reveal_type(b"abc" > b"abd") # revealed: Literal[False]
|
||||
reveal_type(b"abc" > b"abb") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" >= b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" >= b"abd") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"" in b"") # revealed: Literal[True]
|
||||
reveal_type(b"" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"") # revealed: Literal[False]
|
||||
reveal_type(b"ab" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"d" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"" in b"") # revealed: Literal[True]
|
||||
reveal_type(b"" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"") # revealed: Literal[False]
|
||||
reveal_type(b"ab" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"d" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"\x81\x82" in b"\x80\x81\x82") # revealed: Literal[True]
|
||||
reveal_type(b"\x82\x83" in b"\x80\x81\x82") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"ab" not in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" not in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"ab" not in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" not in b"abc") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" is b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is b"ab") # revealed: Literal[False]
|
||||
reveal_type(b"abc" is b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is b"ab") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" is not b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is not b"ab") # revealed: Literal[True]
|
||||
reveal_type(b"abc" is not b"ab") # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
# Comparing integers
|
||||
# Comparison: Integers
|
||||
|
||||
## Integer literals
|
||||
|
||||
```py
|
||||
a = 1 == 1 == True
|
||||
b = 1 == 1 == 2 == 4
|
||||
c = False < True <= 2 < 3 != 6
|
||||
d = 1 < 1
|
||||
e = 1 > 1
|
||||
f = 1 is 1
|
||||
g = 1 is not 1
|
||||
h = 1 is 2
|
||||
i = 1 is not 7
|
||||
j = 1 <= "" and 0 < 1
|
||||
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(c) # revealed: Literal[True]
|
||||
reveal_type(d) # revealed: Literal[False]
|
||||
reveal_type(e) # revealed: Literal[False]
|
||||
reveal_type(f) # revealed: bool
|
||||
reveal_type(g) # revealed: bool
|
||||
reveal_type(h) # revealed: Literal[False]
|
||||
reveal_type(i) # revealed: Literal[True]
|
||||
reveal_type(j) # revealed: @Todo | Literal[True]
|
||||
reveal_type(1 == 1 == True) # revealed: Literal[True]
|
||||
reveal_type(1 == 1 == 2 == 4) # revealed: Literal[False]
|
||||
reveal_type(False < True <= 2 < 3 != 6) # revealed: Literal[True]
|
||||
reveal_type(1 < 1) # revealed: Literal[False]
|
||||
reveal_type(1 > 1) # revealed: Literal[False]
|
||||
reveal_type(1 is 1) # revealed: bool
|
||||
reveal_type(1 is not 1) # revealed: bool
|
||||
reveal_type(1 is 2) # revealed: Literal[False]
|
||||
reveal_type(1 is not 7) # revealed: Literal[True]
|
||||
reveal_type(1 <= "" and 0 < 1) # revealed: @Todo | Literal[True]
|
||||
```
|
||||
|
||||
## Integer instance
|
||||
@@ -31,11 +20,8 @@ reveal_type(j) # revealed: @Todo | Literal[True]
|
||||
```py
|
||||
# TODO: implement lookup of `__eq__` on typeshed `int` stub.
|
||||
def int_instance() -> int: ...
|
||||
a = 1 == int_instance()
|
||||
b = 9 < int_instance()
|
||||
c = int_instance() < int_instance()
|
||||
|
||||
reveal_type(a) # revealed: @Todo
|
||||
reveal_type(b) # revealed: bool
|
||||
reveal_type(c) # revealed: bool
|
||||
reveal_type(1 == int_instance()) # revealed: @Todo
|
||||
reveal_type(9 < int_instance()) # revealed: bool
|
||||
reveal_type(int_instance() < int_instance()) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Non boolean returns
|
||||
# Comparison: Non boolean returns
|
||||
|
||||
Walking through examples:
|
||||
|
||||
@@ -20,18 +20,22 @@ Walking through examples:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __lt__(self, other) -> A: ...
|
||||
|
||||
class B:
|
||||
def __lt__(self, other) -> B: ...
|
||||
|
||||
class C:
|
||||
def __lt__(self, other) -> C: ...
|
||||
|
||||
a = A() < B() < C()
|
||||
b = 0 < 1 < A() < 3
|
||||
c = 10 < 0 < A() < B() < C()
|
||||
x = A() < B() < C()
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
reveal_type(a) # revealed: A | B
|
||||
reveal_type(b) # revealed: bool | A
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
y = 0 < 1 < A() < 3
|
||||
reveal_type(y) # revealed: bool | A
|
||||
|
||||
z = 10 < 0 < A() < B() < C()
|
||||
reveal_type(z) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
# Comparing strings
|
||||
# Comparison: Strings
|
||||
|
||||
## String literals
|
||||
|
||||
```py
|
||||
def str_instance() -> str: ...
|
||||
a = "abc" == "abc"
|
||||
b = "ab_cd" <= "ab_ce"
|
||||
c = "abc" in "ab cd"
|
||||
d = "" not in "hello"
|
||||
e = "--" is "--"
|
||||
f = "A" is "B"
|
||||
g = "--" is not "--"
|
||||
h = "A" is not "B"
|
||||
i = str_instance() < "..."
|
||||
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
|
||||
j = "ab" < "ab_cd"
|
||||
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal[True]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[False]
|
||||
reveal_type(e) # revealed: bool
|
||||
reveal_type(f) # revealed: Literal[False]
|
||||
reveal_type(g) # revealed: bool
|
||||
reveal_type(h) # revealed: Literal[True]
|
||||
reveal_type(i) # revealed: bool
|
||||
reveal_type(j) # revealed: Literal[True]
|
||||
reveal_type("abc" == "abc") # revealed: Literal[True]
|
||||
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
|
||||
reveal_type("abc" in "ab cd") # revealed: Literal[False]
|
||||
reveal_type("" not in "hello") # revealed: Literal[False]
|
||||
reveal_type("--" is "--") # revealed: bool
|
||||
reveal_type("A" is "B") # revealed: Literal[False]
|
||||
reveal_type("--" is not "--") # revealed: bool
|
||||
reveal_type("A" is not "B") # revealed: Literal[True]
|
||||
reveal_type(str_instance() < "...") # revealed: bool
|
||||
|
||||
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
|
||||
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Comparison - Tuples
|
||||
# Comparison: Tuples
|
||||
|
||||
## Heterogeneous
|
||||
|
||||
@@ -18,16 +18,16 @@ b = (1, "test", (3, 14), False)
|
||||
|
||||
reveal_type(a == a) # revealed: Literal[True]
|
||||
reveal_type(a != a) # revealed: Literal[False]
|
||||
reveal_type(a < a) # revealed: Literal[False]
|
||||
reveal_type(a < a) # revealed: Literal[False]
|
||||
reveal_type(a <= a) # revealed: Literal[True]
|
||||
reveal_type(a > a) # revealed: Literal[False]
|
||||
reveal_type(a > a) # revealed: Literal[False]
|
||||
reveal_type(a >= a) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
@@ -39,9 +39,9 @@ b = (1, 2, 3, 4)
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
|
||||
c = ("a", "b", "c", "d")
|
||||
@@ -49,9 +49,9 @@ d = ("a", "b", "c")
|
||||
|
||||
reveal_type(c == d) # revealed: Literal[False]
|
||||
reveal_type(c != d) # revealed: Literal[True]
|
||||
reveal_type(c < d) # revealed: Literal[False]
|
||||
reveal_type(c < d) # revealed: Literal[False]
|
||||
reveal_type(c <= d) # revealed: Literal[False]
|
||||
reveal_type(c > d) # revealed: Literal[True]
|
||||
reveal_type(c > d) # revealed: Literal[True]
|
||||
reveal_type(c >= d) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -67,16 +67,16 @@ b = (int_instance(),)
|
||||
# TODO: All @Todo should be `bool`
|
||||
reveal_type(a == a) # revealed: @Todo
|
||||
reveal_type(a != a) # revealed: @Todo
|
||||
reveal_type(a < a) # revealed: @Todo
|
||||
reveal_type(a < a) # revealed: @Todo
|
||||
reveal_type(a <= a) # revealed: @Todo
|
||||
reveal_type(a > a) # revealed: @Todo
|
||||
reveal_type(a > a) # revealed: @Todo
|
||||
reveal_type(a >= a) # revealed: @Todo
|
||||
|
||||
reveal_type(a == b) # revealed: @Todo
|
||||
reveal_type(a != b) # revealed: @Todo
|
||||
reveal_type(a < b) # revealed: @Todo
|
||||
reveal_type(a < b) # revealed: @Todo
|
||||
reveal_type(a <= b) # revealed: @Todo
|
||||
reveal_type(a > b) # revealed: @Todo
|
||||
reveal_type(a > b) # revealed: @Todo
|
||||
reveal_type(a >= b) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -96,9 +96,9 @@ reveal_type(a == b) # revealed: @Todo
|
||||
reveal_type(a != b) # revealed: @Todo
|
||||
|
||||
# TODO: should be Unknown and add more informative diagnostics
|
||||
reveal_type(a < b) # revealed: @Todo
|
||||
reveal_type(a < b) # revealed: @Todo
|
||||
reveal_type(a <= b) # revealed: @Todo
|
||||
reveal_type(a > b) # revealed: @Todo
|
||||
reveal_type(a > b) # revealed: @Todo
|
||||
reveal_type(a >= b) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -111,9 +111,9 @@ b = (999999, "hello")
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
@@ -126,16 +126,16 @@ c = (b, b, b)
|
||||
|
||||
reveal_type(c == c) # revealed: Literal[True]
|
||||
reveal_type(c != c) # revealed: Literal[False]
|
||||
reveal_type(c < c) # revealed: Literal[False]
|
||||
reveal_type(c < c) # revealed: Literal[False]
|
||||
reveal_type(c <= c) # revealed: Literal[True]
|
||||
reveal_type(c > c) # revealed: Literal[False]
|
||||
reveal_type(c > c) # revealed: Literal[False]
|
||||
reveal_type(c >= c) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
#### Non Boolean Rich Comparisons
|
||||
|
||||
```py
|
||||
class A():
|
||||
class A:
|
||||
def __eq__(self, o) -> str: ...
|
||||
def __ne__(self, o) -> int: ...
|
||||
def __lt__(self, o) -> float: ...
|
||||
@@ -148,9 +148,9 @@ a = (A(), A())
|
||||
# TODO: All @Todo should be bool
|
||||
reveal_type(a == a) # revealed: @Todo
|
||||
reveal_type(a != a) # revealed: @Todo
|
||||
reveal_type(a < a) # revealed: @Todo
|
||||
reveal_type(a < a) # revealed: @Todo
|
||||
reveal_type(a <= a) # revealed: @Todo
|
||||
reveal_type(a > a) # revealed: @Todo
|
||||
reveal_type(a > a) # revealed: @Todo
|
||||
reveal_type(a >= a) # revealed: @Todo
|
||||
```
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
Comparisons on union types need to consider all possible cases:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
one_or_two = 1 if flag else 2
|
||||
|
||||
reveal_type(one_or_two <= 2) # revealed: Literal[True]
|
||||
@@ -52,14 +56,18 @@ With unions on both sides, we need to consider the full cross product of
|
||||
options when building the resulting (union) type:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag_s, flag_l = bool_instance(), bool_instance()
|
||||
small = 1 if flag_s else 2
|
||||
large = 2 if flag_l else 3
|
||||
|
||||
reveal_type(small <= large) # revealed: Literal[True]
|
||||
reveal_type(small >= large) # revealed: bool
|
||||
|
||||
reveal_type(small < large) # revealed: bool
|
||||
reveal_type(small > large) # revealed: Literal[False]
|
||||
reveal_type(small < large) # revealed: bool
|
||||
reveal_type(small > large) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Unsupported operations
|
||||
@@ -69,6 +77,10 @@ unsupported. For now, we fall back to `bool` for the result type instead of
|
||||
trying to infer something more precise from the other (supported) variants:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = [1, 2] if flag else 1
|
||||
|
||||
result = 1 in x # error: "Operator `in` is not supported"
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
# Unsupported operators
|
||||
# Comparison: Unsupported operators
|
||||
|
||||
```py
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||
c = object() < 5 # error: "Operator `<` is not supported for types `object` and `Literal[5]`"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
reveal_type(a) # revealed: bool
|
||||
|
||||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||
reveal_type(b) # revealed: bool
|
||||
|
||||
c = object() < 5 # error: "Operator `<` is not supported for types `object` and `int`"
|
||||
reveal_type(c) # revealed: Unknown
|
||||
|
||||
# TODO should error, need to check if __lt__ signature is valid for right operand
|
||||
d = 5 < object()
|
||||
|
||||
reveal_type(a) # revealed: bool
|
||||
reveal_type(b) # revealed: bool
|
||||
reveal_type(c) # revealed: Unknown
|
||||
# TODO: should be `Unknown`
|
||||
reveal_type(d) # revealed: bool
|
||||
|
||||
flag = bool_instance()
|
||||
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"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
reveal_type(e) # revealed: bool
|
||||
|
||||
# TODO: should error, need to check if __lt__ signature is valid for right operand
|
||||
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
|
||||
f = (1, 2) < (1, "hello")
|
||||
reveal_type(f) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
## Simple if-expression
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else 2
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
@@ -10,19 +14,25 @@ reveal_type(x) # revealed: Literal[1, 2]
|
||||
## If-expression with walrus operator
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
y = 0
|
||||
z = 0
|
||||
x = (y := 1) if flag else (z := 2)
|
||||
a = y
|
||||
b = z
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
reveal_type(a) # revealed: Literal[0, 1]
|
||||
reveal_type(b) # revealed: Literal[0, 2]
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
reveal_type(z) # revealed: Literal[0, 2]
|
||||
```
|
||||
|
||||
## Nested if-expression
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
x = 1 if flag else 2 if flag2 else 3
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
@@ -30,6 +40,10 @@ reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
## None
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else None
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
|
||||
@@ -3,20 +3,26 @@
|
||||
## Simple if
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
if flag:
|
||||
y = 3
|
||||
|
||||
x = y
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
## Simple if-elif-else
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
@@ -30,13 +36,24 @@ else:
|
||||
x = y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
reveal_type(r) # revealed: Unbound | Literal[2]
|
||||
reveal_type(s) # revealed: Unbound | Literal[5]
|
||||
|
||||
# revealed: Unbound | Literal[2]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(r)
|
||||
|
||||
# revealed: Unbound | Literal[5]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(s)
|
||||
```
|
||||
|
||||
## Single symbol across if-elif-else
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
@@ -49,6 +66,10 @@ reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
## if-elif-else without else assignment
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
y = 1
|
||||
@@ -62,6 +83,10 @@ reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
## if-elif-else with intervening assignment
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
y = 1
|
||||
@@ -76,6 +101,10 @@ reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
## Nested if statement
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
if flag2:
|
||||
@@ -86,13 +115,16 @@ reveal_type(y) # revealed: Literal[0, 1]
|
||||
## if-elif without else
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
x = y
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3, 4]
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
@@ -21,7 +21,9 @@ match 0:
|
||||
case 2:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Unbound | Literal[2, 3]
|
||||
# revealed: Unbound | Literal[2, 3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
## Basic match
|
||||
|
||||
@@ -10,6 +10,10 @@ x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred t
|
||||
## Incompatible declarations
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
@@ -20,6 +24,10 @@ x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`:
|
||||
## Partial declarations
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: int
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
|
||||
@@ -28,6 +36,10 @@ x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`:
|
||||
## Incompatible declarations with bad assignment
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
@@ -35,5 +47,5 @@ else:
|
||||
|
||||
# error: [conflicting-declarations]
|
||||
# error: [invalid-assignment]
|
||||
x = b'foo'
|
||||
x = b"foo"
|
||||
```
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
```py
|
||||
import re
|
||||
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except NameError as e:
|
||||
reveal_type(e) # revealed: NameError
|
||||
except re.error as f:
|
||||
@@ -15,10 +16,10 @@ except re.error as f:
|
||||
## Unknown type in except handler does not cause spurious diagnostic
|
||||
|
||||
```py
|
||||
from nonexistent_module import foo # error: [unresolved-import]
|
||||
from nonexistent_module import foo # error: [unresolved-import]
|
||||
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except foo as e:
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
reveal_type(e) # revealed: Unknown
|
||||
@@ -30,7 +31,7 @@ except foo as e:
|
||||
EXCEPTIONS = (AttributeError, TypeError)
|
||||
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except (RuntimeError, OSError) as e:
|
||||
reveal_type(e) # revealed: RuntimeError | OSError
|
||||
except EXCEPTIONS as f:
|
||||
@@ -42,7 +43,7 @@ except EXCEPTIONS as f:
|
||||
```py
|
||||
def foo(x: type[AttributeError], y: tuple[type[OSError], type[RuntimeError]], z: tuple[type[BaseException], ...]):
|
||||
try:
|
||||
w
|
||||
help()
|
||||
except x as e:
|
||||
# TODO: should be `AttributeError`
|
||||
reveal_type(e) # revealed: @Todo
|
||||
|
||||
@@ -38,7 +38,7 @@ to the `except` suite *after* that redefinition.
|
||||
|
||||
```py path=union_type_inferred.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -60,7 +60,7 @@ unify and `x` is not inferred as having a union type following the
|
||||
|
||||
```py path=branches_unify_to_non_union_type.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -84,7 +84,7 @@ possibility when it comes to control-flow analysis.
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -114,7 +114,7 @@ the three suites:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -147,7 +147,7 @@ the end of the `except` suite:
|
||||
|
||||
```py path=single_except.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -175,7 +175,7 @@ in their entireties:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -208,7 +208,7 @@ therefore `Literal[2]`:
|
||||
|
||||
```py path=redef_in_finally.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -235,7 +235,7 @@ suites, however; this is still a TODO item for us.)
|
||||
|
||||
```py path=no_redef_in_finally.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -267,10 +267,10 @@ following possibilities inside `finally` suites:
|
||||
|
||||
```py path=redef_in_finally.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b'foo'
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
@@ -305,10 +305,10 @@ conclusion of the `finally` suite.)
|
||||
|
||||
```py path=no_redef_in_finally.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b'foo'
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
@@ -336,10 +336,10 @@ An example with multiple `except` branches and a `finally` branch:
|
||||
|
||||
```py path=multiple_except_branches.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b'foo'
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
@@ -383,10 +383,10 @@ partway through the `else` suite due to an exception raised *there*.
|
||||
|
||||
```py path=single_except_branch.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b'foo'
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
@@ -426,10 +426,10 @@ The same again, this time with multiple `except` branches:
|
||||
|
||||
```py path=multiple_except_branches.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b'foo'
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
@@ -491,10 +491,10 @@ prior to the suite running to completion.
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b'foo'
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
@@ -585,10 +585,10 @@ variable by that name in the outer scope:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b'foo'
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_range() -> range:
|
||||
return range(42)
|
||||
@@ -602,6 +602,7 @@ def could_raise_returns_float() -> float:
|
||||
x = 1
|
||||
|
||||
try:
|
||||
|
||||
def foo(param=could_raise_returns_str()):
|
||||
x = could_raise_returns_str()
|
||||
|
||||
@@ -619,7 +620,6 @@ try:
|
||||
# TODO: should be `str | bytes | bytearray | float`
|
||||
reveal_type(x) # revealed: bytes | float
|
||||
reveal_type(x) # revealed: bytes | float
|
||||
|
||||
x = foo
|
||||
reveal_type(x) # revealed: Literal[foo]
|
||||
except:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
```py
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except* BaseException as e:
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
@@ -13,7 +13,7 @@ except* BaseException as e:
|
||||
|
||||
```py
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except* OSError as e:
|
||||
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
@@ -23,8 +23,8 @@ except* OSError as e:
|
||||
|
||||
```py
|
||||
try:
|
||||
x
|
||||
help()
|
||||
except* (TypeError, AttributeError) as e:
|
||||
#TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
|
||||
# TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
@@ -6,23 +6,14 @@
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
a = True or False
|
||||
b = 'x' or 'y' or 'z'
|
||||
c = '' or 'y' or 'z'
|
||||
d = False or 'z'
|
||||
e = False or True
|
||||
f = False or False
|
||||
g = foo() or False
|
||||
h = foo() or True
|
||||
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal["x"]
|
||||
reveal_type(c) # revealed: Literal["y"]
|
||||
reveal_type(d) # revealed: Literal["z"]
|
||||
reveal_type(e) # revealed: Literal[True]
|
||||
reveal_type(f) # revealed: Literal[False]
|
||||
reveal_type(g) # revealed: str | Literal[False]
|
||||
reveal_type(h) # revealed: str | Literal[True]
|
||||
reveal_type(True or False) # revealed: Literal[True]
|
||||
reveal_type("x" or "y" or "z") # revealed: Literal["x"]
|
||||
reveal_type("" or "y" or "z") # revealed: Literal["y"]
|
||||
reveal_type(False or "z") # revealed: Literal["z"]
|
||||
reveal_type(False or True) # revealed: Literal[True]
|
||||
reveal_type(False or False) # revealed: Literal[False]
|
||||
reveal_type(foo() or False) # revealed: str | Literal[False]
|
||||
reveal_type(foo() or True) # revealed: str | Literal[True]
|
||||
```
|
||||
|
||||
## AND
|
||||
@@ -31,21 +22,13 @@ reveal_type(h) # revealed: str | Literal[True]
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
a = True and False
|
||||
b = False and True
|
||||
c = foo() and False
|
||||
d = foo() and True
|
||||
e = 'x' and 'y' and 'z'
|
||||
f = 'x' and 'y' and ''
|
||||
g = '' and 'y'
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(c) # revealed: str | Literal[False]
|
||||
reveal_type(d) # revealed: str | Literal[True]
|
||||
reveal_type(e) # revealed: Literal["z"]
|
||||
reveal_type(f) # revealed: Literal[""]
|
||||
reveal_type(g) # revealed: Literal[""]
|
||||
reveal_type(True and False) # revealed: Literal[False]
|
||||
reveal_type(False and True) # revealed: Literal[False]
|
||||
reveal_type(foo() and False) # revealed: str | Literal[False]
|
||||
reveal_type(foo() and True) # revealed: str | Literal[True]
|
||||
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
|
||||
reveal_type("x" and "y" and "") # revealed: Literal[""]
|
||||
reveal_type("" and "y") # revealed: Literal[""]
|
||||
```
|
||||
|
||||
## Simple function calls to bool
|
||||
@@ -68,19 +51,12 @@ reveal_type(x) # revealed: bool
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
a = "x" and "y" or "z"
|
||||
b = "x" or "y" and "z"
|
||||
c = "" and "y" or "z"
|
||||
d = "" or "y" and "z"
|
||||
e = "x" and "y" or ""
|
||||
f = "x" or "y" and ""
|
||||
|
||||
reveal_type(a) # revealed: Literal["y"]
|
||||
reveal_type(b) # revealed: Literal["x"]
|
||||
reveal_type(c) # revealed: Literal["z"]
|
||||
reveal_type(d) # revealed: Literal["z"]
|
||||
reveal_type(e) # revealed: Literal["y"]
|
||||
reveal_type(f) # revealed: Literal["x"]
|
||||
reveal_type("x" and "y" or "z") # revealed: Literal["y"]
|
||||
reveal_type("x" or "y" and "z") # revealed: Literal["x"]
|
||||
reveal_type("" and "y" or "z") # revealed: Literal["z"]
|
||||
reveal_type("" or "y" and "z") # revealed: Literal["z"]
|
||||
reveal_type("x" and "y" or "") # revealed: Literal["y"]
|
||||
reveal_type("x" or "y" and "") # revealed: Literal["x"]
|
||||
```
|
||||
|
||||
## `bool()` function
|
||||
@@ -90,62 +66,45 @@ reveal_type(f) # revealed: Literal["x"]
|
||||
```py path=a.py
|
||||
redefined_builtin_bool = bool
|
||||
|
||||
def my_bool(x)-> bool: pass
|
||||
def my_bool(x) -> bool:
|
||||
return True
|
||||
```
|
||||
|
||||
```py
|
||||
from a import redefined_builtin_bool, my_bool
|
||||
a = redefined_builtin_bool(0)
|
||||
b = my_bool(0)
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: bool
|
||||
reveal_type(redefined_builtin_bool(0)) # revealed: Literal[False]
|
||||
reveal_type(my_bool(0)) # revealed: bool
|
||||
```
|
||||
|
||||
## Truthy values
|
||||
|
||||
```py
|
||||
a = bool(1)
|
||||
b = bool((0,))
|
||||
c = bool("NON EMPTY")
|
||||
d = bool(True)
|
||||
reveal_type(bool(1)) # revealed: Literal[True]
|
||||
reveal_type(bool((0,))) # revealed: Literal[True]
|
||||
reveal_type(bool("NON EMPTY")) # revealed: Literal[True]
|
||||
reveal_type(bool(True)) # revealed: Literal[True]
|
||||
|
||||
def foo(): pass
|
||||
e = bool(foo)
|
||||
def foo(): ...
|
||||
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal[True]
|
||||
reveal_type(c) # revealed: Literal[True]
|
||||
reveal_type(d) # revealed: Literal[True]
|
||||
reveal_type(e) # revealed: Literal[True]
|
||||
reveal_type(bool(foo)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Falsy values
|
||||
|
||||
```py
|
||||
a = bool(0)
|
||||
b = bool(())
|
||||
c = bool(None)
|
||||
d = bool("")
|
||||
e = bool(False)
|
||||
f = bool()
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[False]
|
||||
reveal_type(e) # revealed: Literal[False]
|
||||
reveal_type(f) # revealed: Literal[False]
|
||||
reveal_type(bool(0)) # revealed: Literal[False]
|
||||
reveal_type(bool(())) # revealed: Literal[False]
|
||||
reveal_type(bool(None)) # revealed: Literal[False]
|
||||
reveal_type(bool("")) # revealed: Literal[False]
|
||||
reveal_type(bool(False)) # revealed: Literal[False]
|
||||
reveal_type(bool()) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Ambiguous values
|
||||
|
||||
```py
|
||||
a = bool([])
|
||||
b = bool({})
|
||||
c = bool(set())
|
||||
|
||||
reveal_type(a) # revealed: bool
|
||||
reveal_type(b) # revealed: bool
|
||||
reveal_type(c) # revealed: bool
|
||||
reveal_type(bool([])) # revealed: bool
|
||||
reveal_type(bool({})) # revealed: bool
|
||||
reveal_type(bool(set())) # revealed: bool
|
||||
```
|
||||
|
||||
66
crates/red_knot_python_semantic/resources/mdtest/generics.md
Normal file
66
crates/red_knot_python_semantic/resources/mdtest/generics.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# PEP 695 Generics
|
||||
|
||||
## Class Declarations
|
||||
|
||||
Basic PEP 695 generics
|
||||
|
||||
```py
|
||||
class MyBox[T]:
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
data: T
|
||||
box_model_number = 695
|
||||
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
def __init__(self, data: T):
|
||||
self.data = data
|
||||
|
||||
# TODO not error (should be subscriptable)
|
||||
box: MyBox[int] = MyBox(5) # error: [non-subscriptable]
|
||||
# TODO error differently (str and int don't unify)
|
||||
wrong_innards: MyBox[int] = MyBox("five") # error: [non-subscriptable]
|
||||
# TODO reveal int
|
||||
reveal_type(box.data) # revealed: @Todo
|
||||
|
||||
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||
```
|
||||
|
||||
## Subclassing
|
||||
|
||||
```py
|
||||
class MyBox[T]:
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
data: T
|
||||
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
def __init__(self, data: T):
|
||||
self.data = data
|
||||
|
||||
# TODO not error on the subscripting or the use of type param
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
# error: [non-subscriptable]
|
||||
class MySecureBox[T](MyBox[T]): ...
|
||||
|
||||
secure_box: MySecureBox[int] = MySecureBox(5)
|
||||
reveal_type(secure_box) # revealed: MySecureBox
|
||||
# TODO reveal int
|
||||
reveal_type(secure_box.data) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Cyclical class definition
|
||||
|
||||
In type stubs, classes can reference themselves in their base class definitions. For example, in `typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
|
||||
This should hold true even with generics at play.
|
||||
|
||||
```py path=a.pyi
|
||||
class Seq[T]: ...
|
||||
|
||||
# TODO not error on the subscripting
|
||||
class S[T](Seq[S]): ... # error: [non-subscriptable]
|
||||
|
||||
reveal_type(S) # revealed: Literal[S]
|
||||
```
|
||||
@@ -3,21 +3,25 @@
|
||||
## Class import following
|
||||
|
||||
```py
|
||||
from b import C as D; E = D
|
||||
reveal_type(E) # revealed: Literal[C]
|
||||
from b import C as D
|
||||
|
||||
E = D
|
||||
reveal_type(E) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
class C: pass
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Module member resolution
|
||||
|
||||
```py
|
||||
import b; D = b.C
|
||||
reveal_type(D) # revealed: Literal[C]
|
||||
import b
|
||||
|
||||
D = b.C
|
||||
reveal_type(D) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
class C: pass
|
||||
class C: ...
|
||||
```
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Importing builtin module
|
||||
|
||||
```py
|
||||
import builtins; x = builtins.copyright
|
||||
reveal_type(x) # revealed: Literal[copyright]
|
||||
import builtins
|
||||
|
||||
x = builtins.copyright
|
||||
reveal_type(x) # revealed: Literal[copyright]
|
||||
```
|
||||
|
||||
@@ -3,15 +3,27 @@
|
||||
## Maybe unbound
|
||||
|
||||
```py path=maybe_unbound.py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
y = 3
|
||||
x = y
|
||||
reveal_type(x) # revealed: Unbound | Literal[3]
|
||||
reveal_type(y) # revealed: Unbound | Literal[3]
|
||||
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
```py
|
||||
from maybe_unbound import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(y) # revealed: Literal[3]
|
||||
```
|
||||
@@ -19,17 +31,29 @@ reveal_type(y) # revealed: Literal[3]
|
||||
## Maybe unbound annotated
|
||||
|
||||
```py path=maybe_unbound_annotated.py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
y: int = 3
|
||||
x = y
|
||||
reveal_type(x) # revealed: Unbound | Literal[3]
|
||||
reveal_type(y) # revealed: Unbound | Literal[3]
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
Importing an annotated name prefers the declared type over the inferred type:
|
||||
|
||||
```py
|
||||
from maybe_unbound_annotated import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
@@ -41,14 +65,20 @@ def f(): ...
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
from c import f
|
||||
else:
|
||||
|
||||
def f(): ...
|
||||
```
|
||||
|
||||
```py
|
||||
from b import f
|
||||
|
||||
# TODO: We should disambiguate in such cases, showing `Literal[b.f, c.f]`.
|
||||
reveal_type(f) # revealed: Literal[f, f]
|
||||
```
|
||||
@@ -63,6 +93,10 @@ x: int
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
from c import x
|
||||
else:
|
||||
@@ -71,5 +105,6 @@ else:
|
||||
|
||||
```py
|
||||
from b import x
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
## Unresolved import statement
|
||||
|
||||
```py
|
||||
import bar # error: "Cannot resolve import `bar`"
|
||||
import bar # error: "Cannot resolve import `bar`"
|
||||
|
||||
reveal_type(bar) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unresolved import from statement
|
||||
|
||||
```py
|
||||
from bar import baz # error: "Cannot resolve import `bar`"
|
||||
from bar import baz # error: "Cannot resolve import `bar`"
|
||||
|
||||
reveal_type(baz) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -20,14 +22,16 @@ reveal_type(baz) # revealed: Unknown
|
||||
```
|
||||
|
||||
```py
|
||||
from a import thing # error: "Module `a` has no member `thing`"
|
||||
from a import thing # error: "Module `a` has no member `thing`"
|
||||
|
||||
reveal_type(thing) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Resolved import of symbol from unresolved import
|
||||
|
||||
```py path=a.py
|
||||
import foo as foo # error: "Cannot resolve import `foo`"
|
||||
import foo as foo # error: "Cannot resolve import `foo`"
|
||||
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -36,6 +40,7 @@ import" violation:
|
||||
|
||||
```py
|
||||
from a import foo
|
||||
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -48,5 +53,5 @@ x: int
|
||||
```py
|
||||
from b import x
|
||||
|
||||
x = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]"
|
||||
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
|
||||
```
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from .foo import X # error: [unresolved-import]
|
||||
from .foo import X # error: [unresolved-import]
|
||||
|
||||
reveal_type(X) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -21,6 +22,7 @@ X = 42
|
||||
|
||||
```py path=package/bar.py
|
||||
from .foo import X
|
||||
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
@@ -35,6 +37,7 @@ X = 42
|
||||
|
||||
```py path=package/bar.py
|
||||
from .foo.bar.baz import X
|
||||
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
@@ -46,13 +49,15 @@ X = 42
|
||||
|
||||
```py path=package/bar.py
|
||||
from . import X
|
||||
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
## Non-existent + bare to package
|
||||
|
||||
```py path=package/bar.py
|
||||
from . import X # error: [unresolved-import]
|
||||
from . import X # error: [unresolved-import]
|
||||
|
||||
reveal_type(X) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -60,6 +65,7 @@ reveal_type(X) # revealed: Unknown
|
||||
|
||||
```py path=package/__init__.py
|
||||
from .foo import X
|
||||
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
@@ -70,8 +76,9 @@ X = 42
|
||||
## Non-existent + dunder init
|
||||
|
||||
```py path=package/__init__.py
|
||||
from .foo import X # error: [unresolved-import]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
from .foo import X # error: [unresolved-import]
|
||||
|
||||
reveal_type(X) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Long relative import
|
||||
@@ -85,6 +92,7 @@ X = 42
|
||||
|
||||
```py path=package/subpackage/subsubpackage/bar.py
|
||||
from ...foo import X
|
||||
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
@@ -94,12 +102,13 @@ reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
x
|
||||
x # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from .foo import x # error: [unresolved-import]
|
||||
reveal_type(x) # revealed: Unknown
|
||||
from .foo import x # error: [unresolved-import]
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Bare to module
|
||||
@@ -114,10 +123,11 @@ X = 42
|
||||
```py path=package/bar.py
|
||||
# TODO: support submodule imports
|
||||
from . import foo # error: [unresolved-import]
|
||||
|
||||
y = foo.X
|
||||
|
||||
# TODO: should be `Literal[42]`
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Non-existent + bare to module
|
||||
@@ -129,5 +139,5 @@ reveal_type(y) # revealed: Unknown
|
||||
# TODO: support submodule imports
|
||||
from . import foo # error: [unresolved-import]
|
||||
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
```py
|
||||
from b import x
|
||||
|
||||
y = x
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
@@ -16,6 +17,7 @@ x: int
|
||||
|
||||
```py
|
||||
from b import x
|
||||
|
||||
y = x
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# Boolean literals
|
||||
|
||||
```py
|
||||
x = True
|
||||
y = False
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
reveal_type(y) # revealed: Literal[False]
|
||||
reveal_type(True) # revealed: Literal[True]
|
||||
reveal_type(False) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Bytes literals
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
reveal_type(b"red" b"knot") # revealed: Literal[b"redknot"]
|
||||
reveal_type(b"hello") # revealed: Literal[b"hello"]
|
||||
reveal_type(b"world" + b"!") # revealed: Literal[b"world!"]
|
||||
reveal_type(b"\xff\x00") # revealed: Literal[b"\xff\x00"]
|
||||
```
|
||||
@@ -3,6 +3,5 @@
|
||||
## Empty dictionary
|
||||
|
||||
```py
|
||||
x = {}
|
||||
reveal_type(x) # revealed: dict
|
||||
reveal_type({}) # revealed: dict
|
||||
```
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
## Empty list
|
||||
|
||||
```py
|
||||
x = []
|
||||
reveal_type(x) # revealed: list
|
||||
reveal_type([]) # revealed: list
|
||||
```
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
## Basic set
|
||||
|
||||
```py
|
||||
x = {1, 2}
|
||||
reveal_type(x) # revealed: set
|
||||
reveal_type({1, 2}) # revealed: set
|
||||
```
|
||||
|
||||
@@ -3,18 +3,15 @@
|
||||
## Empty tuple
|
||||
|
||||
```py
|
||||
x = ()
|
||||
reveal_type(x) # revealed: tuple[()]
|
||||
reveal_type(()) # revealed: tuple[()]
|
||||
```
|
||||
|
||||
## Heterogeneous tuple
|
||||
|
||||
```py
|
||||
x = (1, 'a')
|
||||
y = (1, (2, 3))
|
||||
z = (x, 2)
|
||||
reveal_type((1, "a")) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
|
||||
reveal_type(x) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(y) # revealed: tuple[Literal[1], tuple[Literal[2], Literal[3]]]
|
||||
reveal_type(z) # revealed: tuple[tuple[Literal[1], Literal["a"]], Literal[2]]
|
||||
reveal_type((1, (2, 3))) # revealed: tuple[Literal[1], tuple[Literal[2], Literal[3]]]
|
||||
|
||||
reveal_type(((1, "a"), 2)) # revealed: tuple[tuple[Literal[1], Literal["a"]], Literal[2]]
|
||||
```
|
||||
|
||||
@@ -7,38 +7,27 @@ x = 0
|
||||
y = str()
|
||||
z = False
|
||||
|
||||
a = f'hello'
|
||||
b = f'h {x}'
|
||||
c = 'one ' f'single ' f'literal'
|
||||
d = 'first ' f'second({b})' f' third'
|
||||
e = f'-{y}-'
|
||||
f = f'-{y}-' f'--' '--'
|
||||
g = f'{z} == {False} is {True}'
|
||||
|
||||
reveal_type(a) # revealed: Literal["hello"]
|
||||
reveal_type(b) # revealed: Literal["h 0"]
|
||||
reveal_type(c) # revealed: Literal["one single literal"]
|
||||
reveal_type(d) # revealed: Literal["first second(h 0) third"]
|
||||
reveal_type(e) # revealed: str
|
||||
reveal_type(f) # revealed: str
|
||||
reveal_type(g) # revealed: Literal["False == False is True"]
|
||||
reveal_type(f"hello") # revealed: Literal["hello"]
|
||||
reveal_type(f"h {x}") # revealed: Literal["h 0"]
|
||||
reveal_type("one " f"single " f"literal") # revealed: Literal["one single literal"]
|
||||
reveal_type("first " f"second({x})" f" third") # revealed: Literal["first second(0) third"]
|
||||
reveal_type(f"-{y}-") # revealed: str
|
||||
reveal_type(f"-{y}-" f"--" "--") # revealed: str
|
||||
reveal_type(f"{z} == {False} is {True}") # revealed: Literal["False == False is True"]
|
||||
```
|
||||
|
||||
## Conversion Flags
|
||||
|
||||
```py
|
||||
string = 'hello'
|
||||
a = f'{string!r}'
|
||||
string = "hello"
|
||||
|
||||
# TODO: should be `Literal["'hello'"]`
|
||||
reveal_type(a) # revealed: str
|
||||
reveal_type(f"{string!r}") # revealed: str
|
||||
```
|
||||
|
||||
## Format Specifiers
|
||||
|
||||
```py
|
||||
a = f'{1:02}'
|
||||
|
||||
# TODO: should be `Literal["01"]`
|
||||
reveal_type(a) # revealed: str
|
||||
reveal_type(f"{1:02}") # revealed: str
|
||||
```
|
||||
|
||||
@@ -3,24 +3,19 @@
|
||||
## Simple
|
||||
|
||||
```py
|
||||
w = "Hello"
|
||||
x = 'world'
|
||||
y = "Guten " + 'tag'
|
||||
z = 'bon ' + "jour"
|
||||
|
||||
reveal_type(w) # revealed: Literal["Hello"]
|
||||
reveal_type(x) # revealed: Literal["world"]
|
||||
reveal_type(y) # revealed: Literal["Guten tag"]
|
||||
reveal_type(z) # revealed: Literal["bon jour"]
|
||||
reveal_type("Hello") # revealed: Literal["Hello"]
|
||||
reveal_type("world") # revealed: Literal["world"]
|
||||
reveal_type("Guten " + "Tag") # revealed: Literal["Guten Tag"]
|
||||
reveal_type("bon " + "jour") # revealed: Literal["bon jour"]
|
||||
```
|
||||
|
||||
## Nested Quotes
|
||||
|
||||
```py
|
||||
x = 'I say "hello" to you'
|
||||
y = "You say \"hey\" back"
|
||||
z = 'No "closure here'
|
||||
reveal_type(x) # revealed: Literal["I say \"hello\" to you"]
|
||||
reveal_type(y) # revealed: Literal["You say \"hey\" back"]
|
||||
reveal_type(z) # revealed: Literal["No \"closure here"]
|
||||
reveal_type('I say "hello" to you') # revealed: Literal["I say \"hello\" to you"]
|
||||
|
||||
# revealed: Literal["You say \"hey\" back"]
|
||||
reveal_type("You say \"hey\" back") # fmt: skip
|
||||
|
||||
reveal_type('No "closure here') # revealed: Literal["No \"closure here"]
|
||||
```
|
||||
|
||||
@@ -17,8 +17,10 @@ async def foo():
|
||||
async for x in Iterator():
|
||||
pass
|
||||
|
||||
# TODO
|
||||
reveal_type(x) # revealed: Unbound | @Todo
|
||||
# TODO: should reveal `Unbound | Unknown` because `__aiter__` is not defined
|
||||
# revealed: Unbound | @Todo
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## Basic async for loop
|
||||
@@ -33,9 +35,11 @@ async def foo():
|
||||
def __aiter__(self) -> IntAsyncIterator:
|
||||
return IntAsyncIterator()
|
||||
|
||||
#TODO(Alex): async iterables/iterators!
|
||||
# TODO(Alex): async iterables/iterators!
|
||||
async for x in IntAsyncIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | @Todo
|
||||
# error: [possibly-unresolved-reference]
|
||||
# revealed: Unbound | @Todo
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
@@ -14,7 +14,9 @@ class IntIterable:
|
||||
for x in IntIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | int
|
||||
# revealed: Unbound | int
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With previous definition
|
||||
@@ -28,7 +30,7 @@ class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
x = 'foo'
|
||||
x = "foo"
|
||||
|
||||
for x in IntIterable():
|
||||
pass
|
||||
@@ -50,7 +52,7 @@ class IntIterable:
|
||||
for x in IntIterable():
|
||||
pass
|
||||
else:
|
||||
x = 'foo'
|
||||
x = "foo"
|
||||
|
||||
reveal_type(x) # revealed: Literal["foo"]
|
||||
```
|
||||
@@ -70,7 +72,7 @@ for x in IntIterable():
|
||||
if x > 5:
|
||||
break
|
||||
else:
|
||||
x = 'foo'
|
||||
x = "foo"
|
||||
|
||||
reveal_type(x) # revealed: int | Literal["foo"]
|
||||
```
|
||||
@@ -85,38 +87,49 @@ class OldStyleIterable:
|
||||
for x in OldStyleIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | int
|
||||
# revealed: Unbound | int
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With heterogeneous tuple
|
||||
|
||||
```py
|
||||
for x in (1, 'a', b'foo'):
|
||||
for x in (1, "a", b"foo"):
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
# revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## With non-callable iterator
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
else:
|
||||
__iter__ = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | Unknown
|
||||
# revealed: Unbound | Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## Invalid iterable
|
||||
|
||||
```py
|
||||
nonsense = 123
|
||||
for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
|
||||
for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -126,9 +139,8 @@ for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
|
||||
class NotIterable:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
|
||||
__iter__ = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Yield must be iterable
|
||||
|
||||
```py
|
||||
class NotIterable: pass
|
||||
class NotIterable: ...
|
||||
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
@@ -14,5 +14,5 @@ class Iterable:
|
||||
|
||||
def generator_function():
|
||||
yield from Iterable()
|
||||
yield from NotIterable() # error: "Object of type `NotIterable` is not iterable"
|
||||
yield from NotIterable() # error: "Object of type `NotIterable` is not iterable"
|
||||
```
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
## Basic While Loop
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
@@ -13,20 +17,27 @@ reveal_type(x) # revealed: Literal[1, 2]
|
||||
## While with else (no break)
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
else:
|
||||
y = x
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## While with Else (may break)
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
x = 1
|
||||
y = 0
|
||||
while flag:
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
## `is None`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
|
||||
if x is None:
|
||||
# TODO the following should be simplified to 'None'
|
||||
reveal_type(x) # revealed: None | Literal[1] & None
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
@@ -15,15 +18,36 @@ reveal_type(x) # revealed: None | Literal[1]
|
||||
## `is` for other types
|
||||
|
||||
```py
|
||||
class A:
|
||||
...
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class A: ...
|
||||
|
||||
x = A()
|
||||
y = x if flag else None
|
||||
|
||||
if y is x:
|
||||
# TODO the following should be simplified to 'A'
|
||||
reveal_type(y) # revealed: A | None & A
|
||||
reveal_type(y) # revealed: A
|
||||
|
||||
reveal_type(y) # revealed: A | None
|
||||
```
|
||||
|
||||
## `is` in chained comparisons
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x_flag, y_flag = bool_instance(), bool_instance()
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
|
||||
if y is x is False: # Interpreted as `(y is x) and (x is False)`
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(y) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
The type guard removes `None` from the union type:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
|
||||
if x is not None:
|
||||
@@ -16,12 +20,15 @@ reveal_type(x) # revealed: None | Literal[1]
|
||||
## `is not` for other singleton types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = True if flag else False
|
||||
reveal_type(x) # revealed: bool
|
||||
|
||||
if x is not False:
|
||||
# TODO the following should be `Literal[True]`
|
||||
reveal_type(x) # revealed: bool & ~Literal[False]
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## `is not` for non-singleton types
|
||||
@@ -37,3 +44,23 @@ y = 345
|
||||
if x is not y:
|
||||
reveal_type(x) # revealed: Literal[345]
|
||||
```
|
||||
|
||||
## `is not` in chained comparisons
|
||||
|
||||
The type guard removes `False` from the union type of the tested value only.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x_flag, y_flag = bool_instance(), bool_instance()
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
|
||||
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
reveal_type(y) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Narrowing for nested conditionals
|
||||
|
||||
## Multiple negative contributions
|
||||
|
||||
```py
|
||||
def int_instance() -> int: ...
|
||||
|
||||
x = int_instance()
|
||||
|
||||
if x != 1:
|
||||
if x != 2:
|
||||
if x != 3:
|
||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||
```
|
||||
|
||||
## Multiple negative contributions with simplification
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag1, flag2 = bool_instance(), bool_instance()
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
if x != 2:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# Narrowing for `!=` conditionals
|
||||
|
||||
## `x != None`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
|
||||
if x != None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## `!=` for other singleton types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = True if flag else False
|
||||
|
||||
if x != False:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## `x != y` where `y` is of literal type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else 2
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## `x != y` where `y` is a single-valued type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
C = A if flag else B
|
||||
|
||||
if C != A:
|
||||
reveal_type(C) # revealed: Literal[B]
|
||||
```
|
||||
|
||||
## `!=` for non-single-valued types
|
||||
|
||||
Only single-valued types should narrow the type:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
flag = bool_instance()
|
||||
x = int_instance() if flag else None
|
||||
y = int_instance()
|
||||
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: int | None
|
||||
```
|
||||
@@ -0,0 +1,192 @@
|
||||
# Narrowing for `isinstance` checks
|
||||
|
||||
Narrowing for `isinstance(object, classinfo)` expressions.
|
||||
|
||||
## `classinfo` is a single type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if isinstance(x, str):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
```
|
||||
|
||||
## `classinfo` is a tuple of types
|
||||
|
||||
Note: `isinstance(x, (int, str))` should not be confused with
|
||||
`isinstance(x, tuple[(int, str)])`. The former is equivalent to
|
||||
`isinstance(x, int | str)`:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag1, flag2 = bool_instance(), bool_instance(), bool_instance()
|
||||
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, (int, str)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
|
||||
if isinstance(x, (int, bytes)):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if isinstance(x, (bytes, str)):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
|
||||
# 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"]
|
||||
|
||||
y = 1 if flag1 else "a" if flag2 else b"b"
|
||||
if isinstance(y, (int, str)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal["a"]
|
||||
|
||||
if isinstance(y, (int, bytes)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
|
||||
|
||||
if isinstance(y, (str, bytes)):
|
||||
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
|
||||
```
|
||||
|
||||
## `classinfo` is a nested tuple of types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, (bool, (bytes, int))):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Class types
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
if isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & B
|
||||
```
|
||||
|
||||
## No narrowing for instances of `builtins.type`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
t = type("t", (), {})
|
||||
|
||||
# This isn't testing what we want it to test if we infer anything more precise here:
|
||||
reveal_type(t) # revealed: type
|
||||
x = 1 if flag else "foo"
|
||||
|
||||
if isinstance(x, t):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
|
||||
```
|
||||
|
||||
## Do not use custom `isinstance` for narrowing
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
def isinstance(x, t):
|
||||
return True
|
||||
|
||||
x = 1 if flag else "a"
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
```
|
||||
|
||||
## Do support narrowing if `isinstance` is aliased
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
isinstance_alias = isinstance
|
||||
|
||||
x = 1 if flag else "a"
|
||||
if isinstance_alias(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Do support narrowing if `isinstance` is imported
|
||||
|
||||
```py
|
||||
from builtins import isinstance as imported_isinstance
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
if imported_isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Do not narrow if second argument is not a type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "a"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["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"]
|
||||
```
|
||||
|
||||
## Do not narrow if there are keyword arguments
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`isinstance` has no `foo` parameter)
|
||||
if isinstance(x, int, foo="bar"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
```
|
||||
@@ -3,6 +3,11 @@
|
||||
## Single `match` pattern
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
x = None if flag else 1
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
|
||||
@@ -12,6 +17,5 @@ match x:
|
||||
case None:
|
||||
y = x
|
||||
|
||||
# TODO intersection simplification: should be just Literal[0] | None
|
||||
reveal_type(y) # revealed: Literal[0] | None | Literal[1] & None
|
||||
reveal_type(y) # revealed: Literal[0] | None
|
||||
```
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# `is not None` narrowing
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
if x is not None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
@@ -3,8 +3,9 @@
|
||||
## Implicit error
|
||||
|
||||
```py
|
||||
class C: pass
|
||||
C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional"
|
||||
class C: ...
|
||||
|
||||
C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional"
|
||||
```
|
||||
|
||||
## Explicit
|
||||
@@ -12,6 +13,7 @@ C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if
|
||||
No diagnostic is raised in the case of explicit shadowing:
|
||||
|
||||
```py
|
||||
class C: pass
|
||||
class C: ...
|
||||
|
||||
C: int = 1
|
||||
```
|
||||
|
||||
@@ -12,13 +12,15 @@ def f(x: str):
|
||||
## Implicit error
|
||||
|
||||
```py path=a.py
|
||||
def f(): pass
|
||||
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
|
||||
def f(): ...
|
||||
|
||||
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
|
||||
```
|
||||
|
||||
## Explicit shadowing
|
||||
|
||||
```py path=a.py
|
||||
def f(): pass
|
||||
def f(): ...
|
||||
|
||||
f: int = 1
|
||||
```
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
## Shadow after incompatible declarations is OK
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
x: bytes = b'foo'
|
||||
x: bytes = b"foo"
|
||||
```
|
||||
|
||||
@@ -6,5 +6,6 @@ In type stubs, classes can reference themselves in their base class definitions.
|
||||
|
||||
```py path=a.pyi
|
||||
class C(C): ...
|
||||
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
@@ -3,13 +3,32 @@
|
||||
## Simple
|
||||
|
||||
```py
|
||||
w = b'red' b'knot'
|
||||
x = b'hello'
|
||||
y = b'world' + b'!'
|
||||
z = b'\xff\x00'
|
||||
b = b"\x00abc\xff"
|
||||
|
||||
reveal_type(w) # revealed: Literal[b"redknot"]
|
||||
reveal_type(x) # revealed: Literal[b"hello"]
|
||||
reveal_type(y) # revealed: Literal[b"world!"]
|
||||
reveal_type(z) # revealed: Literal[b"\xff\x00"]
|
||||
reveal_type(b[0]) # revealed: Literal[b"\x00"]
|
||||
reveal_type(b[1]) # revealed: Literal[b"a"]
|
||||
reveal_type(b[4]) # revealed: Literal[b"\xff"]
|
||||
|
||||
reveal_type(b[-1]) # revealed: Literal[b"\xff"]
|
||||
reveal_type(b[-2]) # revealed: Literal[b"c"]
|
||||
reveal_type(b[-5]) # revealed: Literal[b"\x00"]
|
||||
|
||||
reveal_type(b[False]) # revealed: Literal[b"\x00"]
|
||||
reveal_type(b[True]) # revealed: Literal[b"a"]
|
||||
|
||||
x = b[5] # error: [index-out-of-bounds] "Index 5 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5"
|
||||
reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Function return
|
||||
|
||||
```py
|
||||
def int_instance() -> int: ...
|
||||
|
||||
a = b"abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
## Class getitem unbound
|
||||
|
||||
```py
|
||||
class NotSubscriptable: pass
|
||||
class NotSubscriptable: ...
|
||||
|
||||
a = NotSubscriptable[0] # error: "Cannot subscript object of type `Literal[NotSubscriptable]` with no `__class_getitem__` method"
|
||||
```
|
||||
|
||||
@@ -14,8 +15,7 @@ class Identity:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
|
||||
a = Identity[0]
|
||||
reveal_type(a) # revealed: str
|
||||
reveal_type(Identity[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Class getitem union
|
||||
@@ -23,16 +23,17 @@ reveal_type(a) # revealed: str
|
||||
```py
|
||||
flag = True
|
||||
|
||||
class Identity:
|
||||
class UnionClassGetItem:
|
||||
if flag:
|
||||
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
else:
|
||||
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
a = Identity[0]
|
||||
reveal_type(a) # revealed: str | int
|
||||
reveal_type(UnionClassGetItem[0]) # revealed: str | int
|
||||
```
|
||||
|
||||
## Class getitem with class union
|
||||
@@ -40,22 +41,18 @@ reveal_type(a) # revealed: str | int
|
||||
```py
|
||||
flag = True
|
||||
|
||||
class Identity1:
|
||||
class A:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
|
||||
class Identity2:
|
||||
class B:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
if flag:
|
||||
a = Identity1
|
||||
else:
|
||||
a = Identity2
|
||||
x = A if flag else B
|
||||
|
||||
b = a[0]
|
||||
reveal_type(a) # revealed: Literal[Identity1, Identity2]
|
||||
reveal_type(b) # revealed: str | int
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(x[0]) # revealed: str | int
|
||||
```
|
||||
|
||||
## Class getitem with unbound method union
|
||||
@@ -64,14 +61,16 @@ reveal_type(b) # revealed: str | int
|
||||
flag = True
|
||||
|
||||
if flag:
|
||||
class Identity:
|
||||
class Spam:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
pass
|
||||
else:
|
||||
class Identity: pass
|
||||
return "foo"
|
||||
|
||||
a = Identity[42] # error: [call-non-callable] "Method `__class_getitem__` of type `Literal[__class_getitem__] | Unbound` is not callable on object of type `Literal[Identity, Identity]`"
|
||||
reveal_type(a) # revealed: str | Unknown
|
||||
else:
|
||||
class Spam: ...
|
||||
|
||||
# error: [call-non-callable] "Method `__class_getitem__` of type `Literal[__class_getitem__] | Unbound` is not callable on object of type `Literal[Spam, Spam]`"
|
||||
# revealed: str | Unknown
|
||||
reveal_type(Spam[42])
|
||||
```
|
||||
|
||||
## TODO: Class getitem non-class union
|
||||
@@ -80,13 +79,15 @@ reveal_type(a) # revealed: str | Unknown
|
||||
flag = True
|
||||
|
||||
if flag:
|
||||
class Identity:
|
||||
class Eggs:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
pass
|
||||
else:
|
||||
Identity = 1
|
||||
return "foo"
|
||||
|
||||
a = Identity[42] # error: "Cannot subscript object of type `Literal[Identity] | Literal[1]` with no `__getitem__` method"
|
||||
# TODO: should _probably_ emit `str | Unknown`
|
||||
reveal_type(a) # revealed: Unknown
|
||||
else:
|
||||
Eggs = 1
|
||||
|
||||
a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method"
|
||||
|
||||
# TODO: should _probably_ emit `str | Unknown`
|
||||
reveal_type(a) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
## Getitem unbound
|
||||
|
||||
```py
|
||||
class NotSubscriptable: pass
|
||||
class NotSubscriptable: ...
|
||||
|
||||
a = NotSubscriptable()[0] # error: "Cannot subscript object of type `NotSubscriptable` with no `__getitem__` method"
|
||||
```
|
||||
|
||||
@@ -23,8 +24,7 @@ class Identity:
|
||||
def __getitem__(self, index: int) -> int:
|
||||
return index
|
||||
|
||||
a = Identity()[0]
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(Identity()[0]) # revealed: int
|
||||
```
|
||||
|
||||
## Getitem union
|
||||
@@ -34,12 +34,13 @@ flag = True
|
||||
|
||||
class Identity:
|
||||
if flag:
|
||||
|
||||
def __getitem__(self, index: int) -> int:
|
||||
return index
|
||||
else:
|
||||
|
||||
def __getitem__(self, index: int) -> str:
|
||||
return str(index)
|
||||
|
||||
a = Identity()[0]
|
||||
reveal_type(a) # revealed: int | str
|
||||
reveal_type(Identity()[0]) # revealed: int | str
|
||||
```
|
||||
|
||||
@@ -10,10 +10,13 @@ A list can be indexed into with:
|
||||
```py
|
||||
x = [1, 2, 3]
|
||||
reveal_type(x) # revealed: list
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(x[0]) # revealed: @Todo
|
||||
|
||||
# TODO reveal list
|
||||
reveal_type(x[0:1]) # revealed: @Todo
|
||||
|
||||
# TODO error
|
||||
reveal_type(x["a"]) # revealed: @Todo
|
||||
```
|
||||
@@ -26,8 +29,10 @@ This should also get type checked.
|
||||
```py
|
||||
x = [1, 2, 3]
|
||||
x[0 if (y := 2) else 1] = 5
|
||||
|
||||
# TODO error? (indeterminite index type)
|
||||
x["a" if (y := 2) else 1] = 6
|
||||
|
||||
# TODO error (can't index via string)
|
||||
x["a" if (y := 2) else "b"] = 6
|
||||
```
|
||||
|
||||
@@ -3,30 +3,29 @@
|
||||
## Simple
|
||||
|
||||
```py
|
||||
s = 'abcde'
|
||||
s = "abcde"
|
||||
|
||||
a = s[0]
|
||||
b = s[1]
|
||||
c = s[-1]
|
||||
d = s[-2]
|
||||
e = s[8] # error: [index-out-of-bounds] "Index 8 is out of bounds for string `Literal["abcde"]` with length 5"
|
||||
f = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5"
|
||||
reveal_type(s[0]) # revealed: Literal["a"]
|
||||
reveal_type(s[1]) # revealed: Literal["b"]
|
||||
reveal_type(s[-1]) # revealed: Literal["e"]
|
||||
reveal_type(s[-2]) # revealed: Literal["d"]
|
||||
|
||||
reveal_type(a) # revealed: Literal["a"]
|
||||
reveal_type(b) # revealed: Literal["b"]
|
||||
reveal_type(c) # revealed: Literal["e"]
|
||||
reveal_type(d) # revealed: Literal["d"]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
reveal_type(s[False]) # revealed: Literal["a"]
|
||||
reveal_type(s[True]) # revealed: Literal["b"]
|
||||
|
||||
a = s[8] # error: [index-out-of-bounds] "Index 8 is out of bounds for string `Literal["abcde"]` with length 5"
|
||||
reveal_type(a) # revealed: Unknown
|
||||
|
||||
b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5"
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Function return
|
||||
|
||||
```py
|
||||
def add(x: int, y: int) -> int:
|
||||
return x + y
|
||||
def int_instance() -> int: ...
|
||||
|
||||
a = 'abcde'[add(0, 1)]
|
||||
a = "abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -3,19 +3,16 @@
|
||||
## Basic
|
||||
|
||||
```py
|
||||
t = (1, 'a', 'b')
|
||||
t = (1, "a", "b")
|
||||
|
||||
a = t[0]
|
||||
b = t[1]
|
||||
c = t[-1]
|
||||
d = t[-2]
|
||||
e = t[4] # error: [index-out-of-bounds]
|
||||
f = t[-4] # error: [index-out-of-bounds]
|
||||
reveal_type(t[0]) # revealed: Literal[1]
|
||||
reveal_type(t[1]) # revealed: Literal["a"]
|
||||
reveal_type(t[-1]) # revealed: Literal["b"]
|
||||
reveal_type(t[-2]) # revealed: Literal["a"]
|
||||
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal["a"]
|
||||
reveal_type(c) # revealed: Literal["b"]
|
||||
reveal_type(d) # revealed: Literal["a"]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
a = t[4] # error: [index-out-of-bounds]
|
||||
reveal_type(a) # revealed: Unknown
|
||||
|
||||
b = t[-4] # error: [index-out-of-bounds]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Unary Operations
|
||||
|
||||
```py
|
||||
class Number:
|
||||
def __init__(self, value: int):
|
||||
self.value = 1
|
||||
|
||||
def __pos__(self) -> int:
|
||||
return +self.value
|
||||
|
||||
def __neg__(self) -> int:
|
||||
return -self.value
|
||||
|
||||
def __invert__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
a = Number()
|
||||
|
||||
reveal_type(+a) # revealed: int
|
||||
reveal_type(-a) # revealed: int
|
||||
reveal_type(~a) # revealed: @Todo
|
||||
|
||||
class NoDunder: ...
|
||||
|
||||
b = NoDunder()
|
||||
+b # error: [unsupported-operator] "Unary operator `+` is unsupported for type `NoDunder`"
|
||||
-b # error: [unsupported-operator] "Unary operator `-` is unsupported for type `NoDunder`"
|
||||
~b # error: [unsupported-operator] "Unary operator `~` is unsupported for type `NoDunder`"
|
||||
```
|
||||
@@ -3,35 +3,23 @@
|
||||
## Unary Addition
|
||||
|
||||
```py
|
||||
a = +0
|
||||
b = +1
|
||||
c = +True
|
||||
|
||||
reveal_type(a) # revealed: Literal[0]
|
||||
reveal_type(b) # revealed: Literal[1]
|
||||
reveal_type(c) # revealed: Literal[1]
|
||||
reveal_type(+0) # revealed: Literal[0]
|
||||
reveal_type(+1) # revealed: Literal[1]
|
||||
reveal_type(+True) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Unary Subtraction
|
||||
|
||||
```py
|
||||
a = -0
|
||||
b = -1
|
||||
c = -True
|
||||
|
||||
reveal_type(a) # revealed: Literal[0]
|
||||
reveal_type(b) # revealed: Literal[-1]
|
||||
reveal_type(c) # revealed: Literal[-1]
|
||||
reveal_type(-0) # revealed: Literal[0]
|
||||
reveal_type(-1) # revealed: Literal[-1]
|
||||
reveal_type(-True) # revealed: Literal[-1]
|
||||
```
|
||||
|
||||
## Unary Bitwise Inversion
|
||||
|
||||
```py
|
||||
a = ~0
|
||||
b = ~1
|
||||
c = ~True
|
||||
|
||||
reveal_type(a) # revealed: Literal[-1]
|
||||
reveal_type(b) # revealed: Literal[-2]
|
||||
reveal_type(c) # revealed: Literal[-2]
|
||||
reveal_type(~0) # revealed: Literal[-1]
|
||||
reveal_type(~1) # revealed: Literal[-2]
|
||||
reveal_type(~True) # revealed: Literal[-2]
|
||||
```
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
## None
|
||||
|
||||
```py
|
||||
a = not None
|
||||
b = not not None
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(not None) # revealed: Literal[True]
|
||||
reveal_type(not not None) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Function
|
||||
@@ -17,24 +15,19 @@ from typing import reveal_type
|
||||
def f():
|
||||
return 1
|
||||
|
||||
a = not f
|
||||
b = not reveal_type
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(not f) # revealed: Literal[False]
|
||||
# TODO Unknown should not be part of the type of typing.reveal_type
|
||||
# reveal_type(b) revealed: Literal[False]
|
||||
# reveal_type(not reveal_type) revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Module
|
||||
|
||||
```py
|
||||
import b; import warnings
|
||||
import b
|
||||
import warnings
|
||||
|
||||
x = not b
|
||||
z = not warnings
|
||||
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(z) # revealed: Literal[False]
|
||||
reveal_type(not b) # revealed: Literal[False]
|
||||
reveal_type(not warnings) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
@@ -44,6 +37,11 @@ y = 1
|
||||
## Union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
p = 1
|
||||
q = 3.3
|
||||
@@ -57,93 +55,63 @@ else:
|
||||
s = 0
|
||||
t = ""
|
||||
|
||||
a = not p
|
||||
b = not q
|
||||
c = not r
|
||||
d = not s
|
||||
e = not t
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: bool
|
||||
reveal_type(c) # revealed: bool
|
||||
reveal_type(d) # revealed: bool
|
||||
reveal_type(e) # revealed: Literal[True]
|
||||
reveal_type(not p) # revealed: Literal[False]
|
||||
reveal_type(not q) # revealed: bool
|
||||
reveal_type(not r) # revealed: bool
|
||||
reveal_type(not s) # revealed: bool
|
||||
reveal_type(not t) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Integer literal
|
||||
|
||||
```py
|
||||
a = not 1
|
||||
b = not 1234567890987654321
|
||||
e = not 0
|
||||
x = not -1
|
||||
y = not -1234567890987654321
|
||||
z = not --987
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(e) # revealed: Literal[True]
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(y) # revealed: Literal[False]
|
||||
reveal_type(z) # revealed: Literal[False]
|
||||
reveal_type(not 1) # revealed: Literal[False]
|
||||
reveal_type(not 1234567890987654321) # revealed: Literal[False]
|
||||
reveal_type(not 0) # revealed: Literal[True]
|
||||
reveal_type(not -1) # revealed: Literal[False]
|
||||
reveal_type(not -1234567890987654321) # revealed: Literal[False]
|
||||
reveal_type(not --987) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Boolean literal
|
||||
|
||||
```py
|
||||
w = True
|
||||
x = False
|
||||
y = not w
|
||||
z = not x
|
||||
|
||||
reveal_type(w) # revealed: Literal[True]
|
||||
|
||||
x = False
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(y) # revealed: Literal[False]
|
||||
reveal_type(z) # revealed: Literal[True]
|
||||
|
||||
reveal_type(not w) # revealed: Literal[False]
|
||||
|
||||
reveal_type(not x) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## String literal
|
||||
|
||||
```py
|
||||
a = not "hello"
|
||||
b = not ""
|
||||
c = not "0"
|
||||
d = not "hello" + "world"
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[True]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[False]
|
||||
reveal_type(not "hello") # revealed: Literal[False]
|
||||
reveal_type(not "") # revealed: Literal[True]
|
||||
reveal_type(not "0") # revealed: Literal[False]
|
||||
reveal_type(not "hello" + "world") # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Bytes literal
|
||||
|
||||
```py
|
||||
a = not b"hello"
|
||||
b = not b""
|
||||
c = not b"0"
|
||||
d = not b"hello" + b"world"
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[True]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[False]
|
||||
reveal_type(not b"hello") # revealed: Literal[False]
|
||||
reveal_type(not b"") # revealed: Literal[True]
|
||||
reveal_type(not b"0") # revealed: Literal[False]
|
||||
reveal_type(not b"hello" + b"world") # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Tuple
|
||||
|
||||
```py
|
||||
a = not (1,)
|
||||
b = not (1, 2)
|
||||
c = not (1, 2, 3)
|
||||
d = not ()
|
||||
e = not ("hello",)
|
||||
f = not (1, "hello")
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[True]
|
||||
reveal_type(e) # revealed: Literal[False]
|
||||
reveal_type(f) # revealed: Literal[False]
|
||||
reveal_type(not (1,)) # revealed: Literal[False]
|
||||
reveal_type(not (1, 2)) # revealed: Literal[False]
|
||||
reveal_type(not (1, 2, 3)) # revealed: Literal[False]
|
||||
reveal_type(not ()) # revealed: Literal[True]
|
||||
reveal_type(not ("hello",)) # revealed: Literal[False]
|
||||
reveal_type(not (1, "hello")) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
@@ -155,12 +155,10 @@ class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
|
||||
(a, b) = Iterable()
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int
|
||||
@@ -173,12 +171,10 @@ class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
|
||||
(a, (b, c), d) = (1, Iterable(), 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: int
|
||||
@@ -191,7 +187,7 @@ reveal_type(d) # revealed: Literal[2]
|
||||
### Simple unpacking
|
||||
|
||||
```py
|
||||
a, b = 'ab'
|
||||
a, b = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
```
|
||||
@@ -200,7 +196,7 @@ reveal_type(b) # revealed: LiteralString
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (there aren't enough values to unpack)
|
||||
a, b, c = 'ab'
|
||||
a, b, c = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
reveal_type(c) # revealed: Unknown
|
||||
@@ -210,7 +206,7 @@ reveal_type(c) # revealed: Unknown
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (too many values to unpack)
|
||||
a, b = 'abc'
|
||||
a, b = "abc"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
```
|
||||
|
||||
@@ -132,7 +132,7 @@ mod tests {
|
||||
#[test]
|
||||
fn inequality() {
|
||||
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
|
||||
let parsed = ParsedModule::new(parsed_raw.clone());
|
||||
let parsed = ParsedModule::new(parsed_raw);
|
||||
|
||||
let stmt = &parsed.syntax().body[0];
|
||||
let node = unsafe { AstNodeRef::new(parsed.clone(), stmt) };
|
||||
@@ -150,7 +150,7 @@ mod tests {
|
||||
#[allow(unsafe_code)]
|
||||
fn debug() {
|
||||
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
|
||||
let parsed = ParsedModule::new(parsed_raw.clone());
|
||||
let parsed = ParsedModule::new(parsed_raw);
|
||||
|
||||
let stmt = &parsed.syntax().body[0];
|
||||
|
||||
|
||||
@@ -21,5 +21,6 @@ mod semantic_model;
|
||||
pub(crate) mod site_packages;
|
||||
mod stdlib;
|
||||
pub mod types;
|
||||
mod util;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
@@ -1294,7 +1294,7 @@ mod tests {
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
custom_typeshed: Some(custom_typeshed.clone()),
|
||||
custom_typeshed: Some(custom_typeshed),
|
||||
site_packages: SitePackages::Known(vec![site_packages]),
|
||||
},
|
||||
},
|
||||
@@ -1445,7 +1445,7 @@ mod tests {
|
||||
assert_function_query_was_not_run(
|
||||
&db,
|
||||
resolve_module_query,
|
||||
ModuleNameIngredient::new(&db, functools_module_name.clone()),
|
||||
ModuleNameIngredient::new(&db, functools_module_name),
|
||||
&events,
|
||||
);
|
||||
assert_eq!(functools_module.search_path(), &stdlib);
|
||||
|
||||
@@ -34,7 +34,10 @@ pub(crate) struct AstIds {
|
||||
|
||||
impl AstIds {
|
||||
fn expression_id(&self, key: impl Into<ExpressionNodeKey>) -> ScopedExpressionId {
|
||||
self.expressions_map[&key.into()]
|
||||
let key = &key.into();
|
||||
*self.expressions_map.get(key).unwrap_or_else(|| {
|
||||
panic!("Could not find expression ID for {key:?}");
|
||||
})
|
||||
}
|
||||
|
||||
fn use_id(&self, key: impl Into<ExpressionNodeKey>) -> ScopedUseId {
|
||||
|
||||
@@ -1012,7 +1012,7 @@ where
|
||||
|
||||
// Add symbols and definitions for the parameters to the lambda scope.
|
||||
if let Some(parameters) = &lambda.parameters {
|
||||
for parameter in &**parameters {
|
||||
for parameter in parameters {
|
||||
self.declare_parameter(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,13 @@ impl<'db> Definition<'db> {
|
||||
self.kind(db).category().is_binding()
|
||||
}
|
||||
|
||||
/// Return true if this is a symbol was defined in the `typing` or `typing_extensions` modules
|
||||
pub(crate) fn is_builtin_definition(self, db: &'db dyn Db) -> bool {
|
||||
file_to_module(db, self.file(db)).is_some_and(|module| {
|
||||
module.search_path().is_standard_library() && matches!(&**module.name(), "builtins")
|
||||
})
|
||||
}
|
||||
|
||||
/// Return true if this symbol was defined in the `typing` or `typing_extensions` modules
|
||||
pub(crate) fn is_typing_definition(self, db: &'db dyn Db) -> bool {
|
||||
file_to_module(db, self.file(db)).is_some_and(|module| {
|
||||
module.search_path().is_standard_library()
|
||||
@@ -290,7 +296,7 @@ impl DefinitionNodeRef<'_> {
|
||||
handler,
|
||||
is_star,
|
||||
}) => DefinitionKind::ExceptHandler(ExceptHandlerDefinitionKind {
|
||||
handler: AstNodeRef::new(parsed.clone(), handler),
|
||||
handler: AstNodeRef::new(parsed, handler),
|
||||
is_star,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -175,7 +175,6 @@ mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::types::Type;
|
||||
use crate::{HasTy, ProgramSettings, SemanticModel};
|
||||
|
||||
fn setup_db<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<TestDb> {
|
||||
@@ -205,7 +204,7 @@ mod tests {
|
||||
let model = SemanticModel::new(&db, foo);
|
||||
let ty = function.ty(&model);
|
||||
|
||||
assert!(matches!(ty, Type::Function(_)));
|
||||
assert!(ty.is_function_literal());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -222,7 +221,7 @@ mod tests {
|
||||
let model = SemanticModel::new(&db, foo);
|
||||
let ty = class.ty(&model);
|
||||
|
||||
assert!(matches!(ty, Type::Class(_)));
|
||||
assert!(ty.is_class_literal());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -243,7 +242,7 @@ mod tests {
|
||||
let model = SemanticModel::new(&db, bar);
|
||||
let ty = alias.ty(&model);
|
||||
|
||||
assert!(matches!(ty, Type::Class(_)));
|
||||
assert!(ty.is_class_literal());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::stdlib::{
|
||||
builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty, typing_extensions_symbol_ty,
|
||||
};
|
||||
use crate::types::narrow::narrowing_constraint;
|
||||
use crate::{Db, FxOrderSet, Module};
|
||||
use crate::{Db, FxOrderSet, HasTy, Module, SemanticModel};
|
||||
|
||||
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
||||
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
@@ -148,18 +148,18 @@ fn bindings_ty<'db>(
|
||||
binding,
|
||||
constraints,
|
||||
}| {
|
||||
let mut constraint_tys =
|
||||
constraints.filter_map(|constraint| narrowing_constraint(db, constraint, binding));
|
||||
let mut constraint_tys = constraints
|
||||
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
|
||||
.peekable();
|
||||
|
||||
let binding_ty = binding_ty(db, binding);
|
||||
if let Some(first_constraint_ty) = constraint_tys.next() {
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
builder = builder
|
||||
.add_positive(binding_ty)
|
||||
.add_positive(first_constraint_ty);
|
||||
for constraint_ty in constraint_tys {
|
||||
builder = builder.add_positive(constraint_ty);
|
||||
}
|
||||
builder.build()
|
||||
if constraint_tys.peek().is_some() {
|
||||
constraint_tys
|
||||
.fold(
|
||||
IntersectionBuilder::new(db).add_positive(binding_ty),
|
||||
IntersectionBuilder::add_positive,
|
||||
)
|
||||
.build()
|
||||
} else {
|
||||
binding_ty
|
||||
}
|
||||
@@ -211,7 +211,13 @@ fn declarations_ty<'db>(
|
||||
let declared_ty = if let Some(second) = all_types.next() {
|
||||
let mut builder = UnionBuilder::new(db).add(first);
|
||||
for other in [second].into_iter().chain(all_types) {
|
||||
if !first.is_equivalent_to(db, other) {
|
||||
// Make sure not to emit spurious errors relating to `Type::Todo`,
|
||||
// since we only infer this type due to a limitation in our current model.
|
||||
//
|
||||
// `Unknown` is different here, since we might infer `Unknown`
|
||||
// for one of these due to a variable being defined in one possible
|
||||
// control-flow branch but not another one.
|
||||
if !first.is_equivalent_to(db, other) && !first.is_todo() && !other.is_todo() {
|
||||
conflicting.push(other);
|
||||
}
|
||||
builder = builder.add(other);
|
||||
@@ -255,11 +261,11 @@ pub enum Type<'db> {
|
||||
/// `Todo` would change the output type.
|
||||
Todo,
|
||||
/// A specific function object
|
||||
Function(FunctionType<'db>),
|
||||
FunctionLiteral(FunctionType<'db>),
|
||||
/// A specific module object
|
||||
Module(File),
|
||||
ModuleLiteral(File),
|
||||
/// A specific class object
|
||||
Class(ClassType<'db>),
|
||||
ClassLiteral(ClassType<'db>),
|
||||
/// The set of Python objects with the given class in their __class__'s method resolution order
|
||||
Instance(ClassType<'db>),
|
||||
/// The set of objects in any of the types in the union
|
||||
@@ -292,28 +298,38 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::Never)
|
||||
}
|
||||
|
||||
pub const fn into_class_type(self) -> Option<ClassType<'db>> {
|
||||
pub const fn is_todo(&self) -> bool {
|
||||
matches!(self, Type::Todo)
|
||||
}
|
||||
|
||||
pub const fn into_class_literal_type(self) -> Option<ClassType<'db>> {
|
||||
match self {
|
||||
Type::Class(class_type) => Some(class_type),
|
||||
Type::ClassLiteral(class_type) => Some(class_type),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_class(self) -> ClassType<'db> {
|
||||
self.into_class_type()
|
||||
.expect("Expected a Type::Class variant")
|
||||
#[track_caller]
|
||||
pub fn expect_class_literal(self) -> ClassType<'db> {
|
||||
self.into_class_literal_type()
|
||||
.expect("Expected a Type::ClassLiteral variant")
|
||||
}
|
||||
|
||||
pub const fn into_module_type(self) -> Option<File> {
|
||||
pub const fn is_class_literal(&self) -> bool {
|
||||
matches!(self, Type::ClassLiteral(..))
|
||||
}
|
||||
|
||||
pub const fn into_module_literal_type(self) -> Option<File> {
|
||||
match self {
|
||||
Type::Module(file) => Some(file),
|
||||
Type::ModuleLiteral(file) => Some(file),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_module(self) -> File {
|
||||
self.into_module_type()
|
||||
.expect("Expected a Type::Module variant")
|
||||
#[track_caller]
|
||||
pub fn expect_module_literal(self) -> File {
|
||||
self.into_module_literal_type()
|
||||
.expect("Expected a Type::ModuleLiteral variant")
|
||||
}
|
||||
|
||||
pub const fn into_union_type(self) -> Option<UnionType<'db>> {
|
||||
@@ -323,11 +339,16 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn expect_union(self) -> UnionType<'db> {
|
||||
self.into_union_type()
|
||||
.expect("Expected a Type::Union variant")
|
||||
}
|
||||
|
||||
pub const fn is_union(&self) -> bool {
|
||||
matches!(self, Type::Union(..))
|
||||
}
|
||||
|
||||
pub const fn into_intersection_type(self) -> Option<IntersectionType<'db>> {
|
||||
match self {
|
||||
Type::Intersection(intersection_type) => Some(intersection_type),
|
||||
@@ -335,21 +356,27 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn expect_intersection(self) -> IntersectionType<'db> {
|
||||
self.into_intersection_type()
|
||||
.expect("Expected a Type::Intersection variant")
|
||||
}
|
||||
|
||||
pub const fn into_function_type(self) -> Option<FunctionType<'db>> {
|
||||
pub const fn into_function_literal_type(self) -> Option<FunctionType<'db>> {
|
||||
match self {
|
||||
Type::Function(function_type) => Some(function_type),
|
||||
Type::FunctionLiteral(function_type) => Some(function_type),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_function(self) -> FunctionType<'db> {
|
||||
self.into_function_type()
|
||||
.expect("Expected a Type::Function variant")
|
||||
#[track_caller]
|
||||
pub fn expect_function_literal(self) -> FunctionType<'db> {
|
||||
self.into_function_literal_type()
|
||||
.expect("Expected a Type::FunctionLiteral variant")
|
||||
}
|
||||
|
||||
pub const fn is_function_literal(&self) -> bool {
|
||||
matches!(self, Type::FunctionLiteral(..))
|
||||
}
|
||||
|
||||
pub const fn into_int_literal_type(self) -> Option<i64> {
|
||||
@@ -359,11 +386,20 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn expect_int_literal(self) -> i64 {
|
||||
self.into_int_literal_type()
|
||||
.expect("Expected a Type::IntLiteral variant")
|
||||
}
|
||||
|
||||
pub const fn is_boolean_literal(&self) -> bool {
|
||||
matches!(self, Type::BooleanLiteral(..))
|
||||
}
|
||||
|
||||
pub const fn is_literal_string(&self) -> bool {
|
||||
matches!(self, Type::LiteralString)
|
||||
}
|
||||
|
||||
pub fn may_be_unbound(&self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Type::Unbound => true,
|
||||
@@ -387,18 +423,8 @@ impl<'db> Type<'db> {
|
||||
|
||||
pub fn is_stdlib_symbol(&self, db: &'db dyn Db, module_name: &str, name: &str) -> bool {
|
||||
match self {
|
||||
Type::Class(class) => class.is_stdlib_symbol(db, module_name, name),
|
||||
Type::Function(function) => function.is_stdlib_symbol(db, module_name, name),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if the type is a class or a union of classes.
|
||||
pub fn is_class(&self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Type::Union(union) => union.elements(db).iter().all(|ty| ty.is_class(db)),
|
||||
Type::Class(_) => true,
|
||||
// / TODO include type[X], once we add that type
|
||||
Type::ClassLiteral(class) => class.is_stdlib_symbol(db, module_name, name),
|
||||
Type::FunctionLiteral(function) => function.is_stdlib_symbol(db, module_name, name),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -415,6 +441,8 @@ impl<'db> Type<'db> {
|
||||
(_, Type::Unknown | Type::Any | Type::Todo) => false,
|
||||
(Type::Never, _) => true,
|
||||
(_, Type::Never) => false,
|
||||
(_, Type::Instance(class)) if class.is_known(db, KnownClass::Object) => true,
|
||||
(Type::Instance(class), _) if class.is_known(db, KnownClass::Object) => false,
|
||||
(Type::BooleanLiteral(_), Type::Instance(class))
|
||||
if class.is_known(db, KnownClass::Bool) =>
|
||||
{
|
||||
@@ -434,12 +462,22 @@ impl<'db> Type<'db> {
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::ClassLiteral(..), Type::Instance(class))
|
||||
if class.is_known(db, KnownClass::Type) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::Union(union), ty) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|&elem_ty| elem_ty.is_subtype_of(db, ty)),
|
||||
(ty, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| ty.is_subtype_of(db, elem_ty)),
|
||||
(_, Type::Instance(class)) if class.is_known(db, KnownClass::Object) => true,
|
||||
(Type::Instance(class), _) if class.is_known(db, KnownClass::Object) => false,
|
||||
(Type::Instance(self_class), Type::Instance(target_class)) => {
|
||||
self_class.is_subclass_of(db, target_class)
|
||||
}
|
||||
// TODO
|
||||
_ => false,
|
||||
}
|
||||
@@ -471,6 +509,150 @@ impl<'db> Type<'db> {
|
||||
self == other
|
||||
}
|
||||
|
||||
/// Return true if this type and `other` have no common elements.
|
||||
///
|
||||
/// Note: This function aims to have no false positives, but might return
|
||||
/// wrong `false` answers in some cases.
|
||||
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||
match (self, other) {
|
||||
(Type::Never, _) | (_, Type::Never) => true,
|
||||
|
||||
(Type::Any, _) | (_, Type::Any) => false,
|
||||
(Type::Unknown, _) | (_, Type::Unknown) => false,
|
||||
(Type::Unbound, _) | (_, Type::Unbound) => false,
|
||||
(Type::Todo, _) | (_, Type::Todo) => false,
|
||||
|
||||
(Type::Union(union), other) | (other, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|e| e.is_disjoint_from(db, other)),
|
||||
|
||||
(Type::Intersection(intersection), other)
|
||||
| (other, Type::Intersection(intersection)) => {
|
||||
if intersection
|
||||
.positive(db)
|
||||
.iter()
|
||||
.any(|p| p.is_disjoint_from(db, other))
|
||||
{
|
||||
true
|
||||
} else {
|
||||
// TODO we can do better here. For example:
|
||||
// X & ~Literal[1] is disjoint from Literal[1]
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
left @ (Type::None
|
||||
| Type::BooleanLiteral(..)
|
||||
| Type::IntLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::ModuleLiteral(..)
|
||||
| Type::ClassLiteral(..)),
|
||||
right @ (Type::None
|
||||
| Type::BooleanLiteral(..)
|
||||
| Type::IntLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::ModuleLiteral(..)
|
||||
| Type::ClassLiteral(..)),
|
||||
) => left != right,
|
||||
|
||||
(Type::None, Type::Instance(class_type)) | (Type::Instance(class_type), Type::None) => {
|
||||
!matches!(
|
||||
class_type.known(db),
|
||||
Some(KnownClass::NoneType | KnownClass::Object)
|
||||
)
|
||||
}
|
||||
(Type::None, _) | (_, Type::None) => true,
|
||||
|
||||
(Type::BooleanLiteral(..), Type::Instance(class_type))
|
||||
| (Type::Instance(class_type), Type::BooleanLiteral(..)) => !matches!(
|
||||
class_type.known(db),
|
||||
Some(KnownClass::Bool | KnownClass::Int | KnownClass::Object)
|
||||
),
|
||||
(Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true,
|
||||
|
||||
(Type::IntLiteral(..), Type::Instance(class_type))
|
||||
| (Type::Instance(class_type), Type::IntLiteral(..)) => !matches!(
|
||||
class_type.known(db),
|
||||
Some(KnownClass::Int | KnownClass::Object)
|
||||
),
|
||||
(Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true,
|
||||
|
||||
(Type::StringLiteral(..), Type::LiteralString)
|
||||
| (Type::LiteralString, Type::StringLiteral(..)) => false,
|
||||
(Type::StringLiteral(..), Type::Instance(class_type))
|
||||
| (Type::Instance(class_type), Type::StringLiteral(..)) => !matches!(
|
||||
class_type.known(db),
|
||||
Some(KnownClass::Str | KnownClass::Object)
|
||||
),
|
||||
(Type::StringLiteral(..), _) | (_, Type::StringLiteral(..)) => true,
|
||||
|
||||
(Type::LiteralString, Type::LiteralString) => false,
|
||||
(Type::LiteralString, Type::Instance(class_type))
|
||||
| (Type::Instance(class_type), Type::LiteralString) => !matches!(
|
||||
class_type.known(db),
|
||||
Some(KnownClass::Str | KnownClass::Object)
|
||||
),
|
||||
(Type::LiteralString, _) | (_, Type::LiteralString) => true,
|
||||
|
||||
(Type::BytesLiteral(..), Type::Instance(class_type))
|
||||
| (Type::Instance(class_type), Type::BytesLiteral(..)) => !matches!(
|
||||
class_type.known(db),
|
||||
Some(KnownClass::Bytes | KnownClass::Object)
|
||||
),
|
||||
(Type::BytesLiteral(..), _) | (_, Type::BytesLiteral(..)) => true,
|
||||
|
||||
(
|
||||
Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..),
|
||||
Type::Instance(class_type),
|
||||
)
|
||||
| (
|
||||
Type::Instance(class_type),
|
||||
Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..),
|
||||
) => !class_type.is_known(db, KnownClass::Object),
|
||||
|
||||
(Type::Instance(..), Type::Instance(..)) => {
|
||||
// TODO: once we have support for `final`, there might be some cases where
|
||||
// we can determine that two types are disjoint. For non-final classes, we
|
||||
// return false (multiple inheritance).
|
||||
|
||||
// TODO: is there anything specific to do for instances of KnownClass::Type?
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
(Type::Tuple(tuple), other) | (other, Type::Tuple(tuple)) => {
|
||||
if let Type::Tuple(other_tuple) = other {
|
||||
if tuple.len(db) == other_tuple.len(db) {
|
||||
tuple
|
||||
.elements(db)
|
||||
.iter()
|
||||
.zip(other_tuple.elements(db))
|
||||
.any(|(e1, e2)| e1.is_disjoint_from(db, *e2))
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// We can not be sure if the tuple is disjoint from 'other' because:
|
||||
// - 'other' might be the homogeneous arbitrary-length tuple type
|
||||
// tuple[T, ...] (which we don't have support for yet); if all of
|
||||
// our element types are not disjoint with T, this is not disjoint
|
||||
// - 'other' might be a user subtype of tuple, which, if generic
|
||||
// over the same or compatible *Ts, would overlap with tuple.
|
||||
//
|
||||
// TODO: add checks for the above cases once we support them
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if there is just a single inhabitant for this type.
|
||||
///
|
||||
/// Note: This function aims to have no false positives, but might return `false`
|
||||
@@ -492,7 +674,7 @@ impl<'db> Type<'db> {
|
||||
// are both of type Literal[345], for example.
|
||||
false
|
||||
}
|
||||
Type::None | Type::BooleanLiteral(_) | Type::Function(..) | Type::Class(..) | Type::Module(..) => true,
|
||||
Type::None | Type::BooleanLiteral(_) | Type::FunctionLiteral(..) | Type::ClassLiteral(..) | Type::ModuleLiteral(..) => true,
|
||||
Type::Tuple(..) => {
|
||||
// The empty tuple is a singleton on CPython and PyPy, but not on other Python
|
||||
// implementations such as GraalPy. Its *use* as a singleton is discouraged and
|
||||
@@ -520,6 +702,55 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is non-empty and all inhabitants of this type compare equal.
|
||||
pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Type::None
|
||||
| Type::FunctionLiteral(..)
|
||||
| Type::ModuleLiteral(..)
|
||||
| Type::ClassLiteral(..)
|
||||
| Type::IntLiteral(..)
|
||||
| Type::BooleanLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..) => true,
|
||||
|
||||
Type::Tuple(tuple) => tuple
|
||||
.elements(db)
|
||||
.iter()
|
||||
.all(|elem| elem.is_single_valued(db)),
|
||||
|
||||
Type::Instance(class_type) => match class_type.known(db) {
|
||||
Some(KnownClass::NoneType) => true,
|
||||
Some(
|
||||
KnownClass::Bool
|
||||
| KnownClass::Object
|
||||
| KnownClass::Bytes
|
||||
| KnownClass::Type
|
||||
| KnownClass::Int
|
||||
| KnownClass::Float
|
||||
| KnownClass::Str
|
||||
| KnownClass::List
|
||||
| KnownClass::Tuple
|
||||
| KnownClass::Set
|
||||
| KnownClass::Dict
|
||||
| KnownClass::GenericAlias
|
||||
| KnownClass::ModuleType
|
||||
| KnownClass::FunctionType,
|
||||
) => false,
|
||||
None => false,
|
||||
},
|
||||
|
||||
Type::Any
|
||||
| Type::Never
|
||||
| Type::Unknown
|
||||
| Type::Unbound
|
||||
| Type::Todo
|
||||
| Type::Union(..)
|
||||
| Type::Intersection(..)
|
||||
| Type::LiteralString => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a member access of a type.
|
||||
///
|
||||
/// For example, if `foo` is `Type::Instance(<Bar>)`,
|
||||
@@ -547,12 +778,12 @@ impl<'db> Type<'db> {
|
||||
// TODO: attribute lookup on None type
|
||||
Type::Todo
|
||||
}
|
||||
Type::Function(_) => {
|
||||
Type::FunctionLiteral(_) => {
|
||||
// TODO: attribute lookup on function type
|
||||
Type::Todo
|
||||
}
|
||||
Type::Module(file) => global_symbol_ty(db, *file, name),
|
||||
Type::Class(class) => class.class_member(db, name),
|
||||
Type::ModuleLiteral(file) => global_symbol_ty(db, *file, name),
|
||||
Type::ClassLiteral(class) => class.class_member(db, name),
|
||||
Type::Instance(_) => {
|
||||
// TODO MRO? get_own_instance_member, get_instance_member
|
||||
Type::Todo
|
||||
@@ -600,9 +831,9 @@ impl<'db> Type<'db> {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
Type::None => Truthiness::AlwaysFalse,
|
||||
Type::Function(_) => Truthiness::AlwaysTrue,
|
||||
Type::Module(_) => Truthiness::AlwaysTrue,
|
||||
Type::Class(_) => {
|
||||
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
|
||||
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
|
||||
Type::ClassLiteral(_) => {
|
||||
// TODO: lookup `__bool__` and `__len__` methods on the class's metaclass
|
||||
// More info in https://docs.python.org/3/library/stdtypes.html#truth-value-testing
|
||||
Truthiness::Ambiguous
|
||||
@@ -647,16 +878,19 @@ impl<'db> Type<'db> {
|
||||
fn call(self, db: &'db dyn Db, arg_types: &[Type<'db>]) -> CallOutcome<'db> {
|
||||
match self {
|
||||
// TODO validate typed call arguments vs callable signature
|
||||
Type::Function(function_type) => match function_type.known(db) {
|
||||
None => CallOutcome::callable(function_type.return_type(db)),
|
||||
Some(KnownFunction::RevealType) => CallOutcome::revealed(
|
||||
function_type.return_type(db),
|
||||
*arg_types.first().unwrap_or(&Type::Unknown),
|
||||
),
|
||||
},
|
||||
Type::FunctionLiteral(function_type) => {
|
||||
if function_type.is_known(db, KnownFunction::RevealType) {
|
||||
CallOutcome::revealed(
|
||||
function_type.return_type(db),
|
||||
*arg_types.first().unwrap_or(&Type::Unknown),
|
||||
)
|
||||
} else {
|
||||
CallOutcome::callable(function_type.return_type(db))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
||||
Type::Class(class) => {
|
||||
Type::ClassLiteral(class) => {
|
||||
CallOutcome::callable(match class.known(db) {
|
||||
// If the class is the builtin-bool class (for example `bool(1)`), we try to
|
||||
// return the specific truthiness value of the input arg, `Literal[True]` for
|
||||
@@ -716,7 +950,7 @@ impl<'db> Type<'db> {
|
||||
fn iterate(self, db: &'db dyn Db) -> IterationOutcome<'db> {
|
||||
if let Type::Tuple(tuple_type) = self {
|
||||
return IterationOutcome::Iterable {
|
||||
element_ty: UnionType::from_elements(db, &**tuple_type.elements(db)),
|
||||
element_ty: UnionType::from_elements(db, tuple_type.elements(db)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -776,7 +1010,7 @@ impl<'db> Type<'db> {
|
||||
Type::Unknown => Type::Unknown,
|
||||
Type::Unbound => Type::Unknown,
|
||||
Type::Never => Type::Never,
|
||||
Type::Class(class) => Type::Instance(*class),
|
||||
Type::ClassLiteral(class) => Type::Instance(*class),
|
||||
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
|
||||
// TODO: we can probably do better here: --Alex
|
||||
Type::Intersection(_) => Type::Todo,
|
||||
@@ -784,9 +1018,9 @@ impl<'db> Type<'db> {
|
||||
// since they already indicate that the object is an instance of some kind:
|
||||
Type::BooleanLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::Function(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::Instance(_)
|
||||
| Type::Module(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
@@ -802,17 +1036,17 @@ impl<'db> Type<'db> {
|
||||
match self {
|
||||
Type::Unbound => Type::Unbound,
|
||||
Type::Never => Type::Never,
|
||||
Type::Instance(class) => Type::Class(*class),
|
||||
Type::Instance(class) => Type::ClassLiteral(*class),
|
||||
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
|
||||
Type::BooleanLiteral(_) => KnownClass::Bool.to_class(db),
|
||||
Type::BytesLiteral(_) => KnownClass::Bytes.to_class(db),
|
||||
Type::IntLiteral(_) => KnownClass::Int.to_class(db),
|
||||
Type::Function(_) => KnownClass::FunctionType.to_class(db),
|
||||
Type::Module(_) => KnownClass::ModuleType.to_class(db),
|
||||
Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class(db),
|
||||
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class(db),
|
||||
Type::Tuple(_) => KnownClass::Tuple.to_class(db),
|
||||
Type::None => KnownClass::NoneType.to_class(db),
|
||||
// TODO not accurate if there's a custom metaclass...
|
||||
Type::Class(_) => KnownClass::Type.to_class(db),
|
||||
Type::ClassLiteral(_) => KnownClass::Type.to_class(db),
|
||||
// TODO can we do better here? `type[LiteralString]`?
|
||||
Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class(db),
|
||||
// TODO: `type[Any]`?
|
||||
@@ -1163,7 +1397,7 @@ impl<'db> CallOutcome<'db> {
|
||||
let mut not_callable = vec![];
|
||||
let mut union_builder = UnionBuilder::new(db);
|
||||
let mut revealed = false;
|
||||
for outcome in &**outcomes {
|
||||
for outcome in outcomes {
|
||||
let return_ty = match outcome {
|
||||
Self::NotCallable { not_callable_ty } => {
|
||||
not_callable.push(*not_callable_ty);
|
||||
@@ -1374,6 +1608,10 @@ impl<'db> FunctionType<'db> {
|
||||
})
|
||||
.unwrap_or(Type::Unknown)
|
||||
}
|
||||
|
||||
pub fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool {
|
||||
self.known(db) == Some(known_function)
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might
|
||||
@@ -1382,6 +1620,8 @@ impl<'db> FunctionType<'db> {
|
||||
pub enum KnownFunction {
|
||||
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
|
||||
RevealType,
|
||||
/// `builtins.isinstance`
|
||||
IsInstance,
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
@@ -1425,7 +1665,29 @@ impl<'db> ClassType<'db> {
|
||||
class_stmt_node
|
||||
.bases()
|
||||
.iter()
|
||||
.map(move |base_expr| definition_expression_ty(db, definition, base_expr))
|
||||
.map(move |base_expr: &ast::Expr| {
|
||||
if class_stmt_node.type_params.is_some() {
|
||||
// when we have a specialized scope, we'll look up the inference
|
||||
// within that scope
|
||||
let model: SemanticModel<'db> = SemanticModel::new(db, definition.file(db));
|
||||
base_expr.ty(&model)
|
||||
} else {
|
||||
// Otherwise, we can do the lookup based on the definition scope
|
||||
definition_expression_ty(db, definition, base_expr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_subclass_of(self, db: &'db dyn Db, other: ClassType) -> bool {
|
||||
// TODO: we need to iterate over the *MRO* here, not the bases
|
||||
(other == self)
|
||||
|| self.bases(db).any(|base| match base {
|
||||
Type::ClassLiteral(base_class) => base_class == other,
|
||||
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
|
||||
// participate, so we should not return `True` if we find `Any/Unknown` in the
|
||||
// bases.
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the class member of this class named `name`.
|
||||
@@ -1548,8 +1810,8 @@ impl<'db> TupleType<'db> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
builtins_symbol_ty, BytesLiteralType, StringLiteralType, Truthiness, TupleType, Type,
|
||||
UnionType,
|
||||
builtins_symbol_ty, BytesLiteralType, IntersectionBuilder, StringLiteralType, Truthiness,
|
||||
TupleType, Type, UnionType,
|
||||
};
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
@@ -1587,12 +1849,13 @@ mod tests {
|
||||
None,
|
||||
Any,
|
||||
IntLiteral(i64),
|
||||
BoolLiteral(bool),
|
||||
BooleanLiteral(bool),
|
||||
StringLiteral(&'static str),
|
||||
LiteralString,
|
||||
BytesLiteral(&'static str),
|
||||
BuiltinInstance(&'static str),
|
||||
Union(Vec<Ty>),
|
||||
Intersection { pos: Vec<Ty>, neg: Vec<Ty> },
|
||||
Tuple(Vec<Ty>),
|
||||
}
|
||||
|
||||
@@ -1605,13 +1868,23 @@ mod tests {
|
||||
Ty::Any => Type::Any,
|
||||
Ty::IntLiteral(n) => Type::IntLiteral(n),
|
||||
Ty::StringLiteral(s) => Type::StringLiteral(StringLiteralType::new(db, s)),
|
||||
Ty::BoolLiteral(b) => Type::BooleanLiteral(b),
|
||||
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
|
||||
Ty::LiteralString => Type::LiteralString,
|
||||
Ty::BytesLiteral(s) => Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes())),
|
||||
Ty::BuiltinInstance(s) => builtins_symbol_ty(db, s).to_instance(db),
|
||||
Ty::Union(tys) => {
|
||||
UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db)))
|
||||
}
|
||||
Ty::Intersection { pos, neg } => {
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
for p in pos {
|
||||
builder = builder.add_positive(p.into_type(db));
|
||||
}
|
||||
for n in neg {
|
||||
builder = builder.add_negative(n.into_type(db));
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
Ty::Tuple(tys) => {
|
||||
let elements: Box<_> = tys.into_iter().map(|ty| ty.into_type(db)).collect();
|
||||
Type::Tuple(TupleType::new(db, elements))
|
||||
@@ -1651,13 +1924,24 @@ mod tests {
|
||||
|
||||
#[test_case(Ty::BuiltinInstance("str"), Ty::BuiltinInstance("object"))]
|
||||
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("object"))]
|
||||
#[test_case(Ty::BuiltinInstance("bool"), Ty::BuiltinInstance("object"))]
|
||||
#[test_case(Ty::BuiltinInstance("bool"), Ty::BuiltinInstance("int"))]
|
||||
#[test_case(Ty::Never, Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("int"))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("object"))]
|
||||
#[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("bool"))]
|
||||
#[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("object"))]
|
||||
#[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("object"))]
|
||||
#[test_case(Ty::StringLiteral("foo"), Ty::LiteralString)]
|
||||
#[test_case(Ty::LiteralString, Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::LiteralString, Ty::BuiltinInstance("object"))]
|
||||
#[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("bytes"))]
|
||||
#[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("object"))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
|
||||
#[test_case(Ty::Union(vec![Ty::BuiltinInstance("str"), Ty::BuiltinInstance("int")]), Ty::BuiltinInstance("object"))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2), Ty::IntLiteral(3)]))]
|
||||
#[test_case(Ty::BuiltinInstance("TypeError"), Ty::BuiltinInstance("Exception"))]
|
||||
fn is_subtype_of(from: Ty, to: Ty) {
|
||||
let db = setup_db();
|
||||
assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
|
||||
@@ -1670,6 +1954,8 @@ mod tests {
|
||||
#[test_case(Ty::IntLiteral(1), Ty::Any)]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::Unknown, Ty::BuiltinInstance("str")]))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(3)]))]
|
||||
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::BuiltinInstance("int"), Ty::IntLiteral(1))]
|
||||
fn is_not_subtype_of(from: Ty, to: Ty) {
|
||||
@@ -1677,6 +1963,32 @@ mod tests {
|
||||
assert!(!from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_subtype_of_class_literals() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
"
|
||||
class A: ...
|
||||
class B: ...
|
||||
U = A if flag else B
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let type_a = super::global_symbol_ty(&db, module, "A");
|
||||
let type_u = super::global_symbol_ty(&db, module, "U");
|
||||
|
||||
assert!(type_a.is_class_literal());
|
||||
assert!(type_a.is_subtype_of(&db, Ty::BuiltinInstance("type").into_type(&db)));
|
||||
assert!(type_a.is_subtype_of(&db, Ty::BuiltinInstance("object").into_type(&db)));
|
||||
|
||||
assert!(type_u.is_union());
|
||||
assert!(type_u.is_subtype_of(&db, Ty::BuiltinInstance("type").into_type(&db)));
|
||||
assert!(type_u.is_subtype_of(&db, Ty::BuiltinInstance("object").into_type(&db)));
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]),
|
||||
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)])
|
||||
@@ -1687,22 +1999,134 @@ mod tests {
|
||||
assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
|
||||
}
|
||||
|
||||
#[test_case(Ty::Never, Ty::Never)]
|
||||
#[test_case(Ty::Never, Ty::None)]
|
||||
#[test_case(Ty::Never, Ty::BuiltinInstance("int"))]
|
||||
#[test_case(Ty::None, Ty::BooleanLiteral(true))]
|
||||
#[test_case(Ty::None, Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::None, Ty::StringLiteral("test"))]
|
||||
#[test_case(Ty::None, Ty::BytesLiteral("test"))]
|
||||
#[test_case(Ty::None, Ty::LiteralString)]
|
||||
#[test_case(Ty::None, Ty::BuiltinInstance("int"))]
|
||||
#[test_case(Ty::None, Ty::Tuple(vec![Ty::None]))]
|
||||
#[test_case(Ty::BooleanLiteral(true), Ty::BooleanLiteral(false))]
|
||||
#[test_case(Ty::BooleanLiteral(true), Ty::Tuple(vec![Ty::None]))]
|
||||
#[test_case(Ty::BooleanLiteral(true), Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::BooleanLiteral(false), Ty::IntLiteral(0))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::IntLiteral(2))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::Tuple(vec![Ty::None]))]
|
||||
#[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("b"))]
|
||||
#[test_case(Ty::StringLiteral("a"), Ty::Tuple(vec![Ty::None]))]
|
||||
#[test_case(Ty::LiteralString, Ty::BytesLiteral("a"))]
|
||||
#[test_case(Ty::BytesLiteral("a"), Ty::BytesLiteral("b"))]
|
||||
#[test_case(Ty::BytesLiteral("a"), Ty::Tuple(vec![Ty::None]))]
|
||||
#[test_case(Ty::BytesLiteral("a"), Ty::StringLiteral("a"))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(3))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(3), Ty::IntLiteral(4)]))]
|
||||
#[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], neg: vec![]}, Ty::IntLiteral(2))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(1)]), Ty::Tuple(vec![Ty::IntLiteral(2)]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1)]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(3)]))]
|
||||
fn is_disjoint_from(a: Ty, b: Ty) {
|
||||
let db = setup_db();
|
||||
let a = a.into_type(&db);
|
||||
let b = b.into_type(&db);
|
||||
|
||||
assert!(a.is_disjoint_from(&db, b));
|
||||
assert!(b.is_disjoint_from(&db, a));
|
||||
}
|
||||
|
||||
#[test_case(Ty::Any, Ty::BuiltinInstance("int"))]
|
||||
#[test_case(Ty::None, Ty::None)]
|
||||
#[test_case(Ty::None, Ty::BuiltinInstance("object"))]
|
||||
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("int"))]
|
||||
#[test_case(Ty::BuiltinInstance("str"), Ty::LiteralString)]
|
||||
#[test_case(Ty::BooleanLiteral(true), Ty::BooleanLiteral(true))]
|
||||
#[test_case(Ty::BooleanLiteral(false), Ty::BooleanLiteral(false))]
|
||||
#[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("bool"))]
|
||||
#[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("int"))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("a"))]
|
||||
#[test_case(Ty::StringLiteral("a"), Ty::LiteralString)]
|
||||
#[test_case(Ty::StringLiteral("a"), Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::LiteralString, Ty::LiteralString)]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(2))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(2), Ty::IntLiteral(3)]))]
|
||||
#[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(2)], neg: vec![]}, Ty::IntLiteral(2))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1), Ty::BuiltinInstance("int")]))]
|
||||
fn is_not_disjoint_from(a: Ty, b: Ty) {
|
||||
let db = setup_db();
|
||||
let a = a.into_type(&db);
|
||||
let b = b.into_type(&db);
|
||||
|
||||
assert!(!a.is_disjoint_from(&db, b));
|
||||
assert!(!b.is_disjoint_from(&db, a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_disjoint_from_union_of_class_types() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
"
|
||||
class A: ...
|
||||
class B: ...
|
||||
U = A if flag else B
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let type_a = super::global_symbol_ty(&db, module, "A");
|
||||
let type_u = super::global_symbol_ty(&db, module, "U");
|
||||
|
||||
assert!(type_a.is_class_literal());
|
||||
assert!(type_u.is_union());
|
||||
|
||||
assert!(!type_a.is_disjoint_from(&db, type_u));
|
||||
}
|
||||
|
||||
#[test_case(Ty::None)]
|
||||
#[test_case(Ty::BoolLiteral(true))]
|
||||
#[test_case(Ty::BoolLiteral(false))]
|
||||
#[test_case(Ty::BooleanLiteral(true))]
|
||||
#[test_case(Ty::BooleanLiteral(false))]
|
||||
fn is_singleton(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(from.into_type(&db).is_singleton());
|
||||
}
|
||||
|
||||
#[test_case(Ty::None)]
|
||||
#[test_case(Ty::BooleanLiteral(true))]
|
||||
#[test_case(Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::StringLiteral("abc"))]
|
||||
#[test_case(Ty::BytesLiteral("abc"))]
|
||||
#[test_case(Ty::Tuple(vec![]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::BooleanLiteral(true), Ty::IntLiteral(1)]))]
|
||||
fn is_single_valued(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(from.into_type(&db).is_single_valued(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::Never)]
|
||||
#[test_case(Ty::Any)]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::None, Ty::BuiltinInstance("int")]))]
|
||||
#[test_case(Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::LiteralString)]
|
||||
fn is_not_single_valued(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(!from.into_type(&db).is_single_valued(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::Never)]
|
||||
#[test_case(Ty::IntLiteral(345))]
|
||||
#[test_case(Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
|
||||
#[test_case(Ty::Tuple(vec![]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::None]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::None, Ty::BoolLiteral(true)]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::None, Ty::BooleanLiteral(true)]))]
|
||||
fn is_not_singleton(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
@@ -1738,8 +2162,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Ty::IntLiteral(1), Ty::StringLiteral("1"))]
|
||||
#[test_case(Ty::BoolLiteral(true), Ty::StringLiteral("True"))]
|
||||
#[test_case(Ty::BoolLiteral(false), Ty::StringLiteral("False"))]
|
||||
#[test_case(Ty::BooleanLiteral(true), Ty::StringLiteral("True"))]
|
||||
#[test_case(Ty::BooleanLiteral(false), Ty::StringLiteral("False"))]
|
||||
#[test_case(Ty::StringLiteral("ab'cd"), Ty::StringLiteral("ab'cd"))] // no quotes
|
||||
#[test_case(Ty::LiteralString, Ty::LiteralString)]
|
||||
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]
|
||||
@@ -1750,8 +2174,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Ty::IntLiteral(1), Ty::StringLiteral("1"))]
|
||||
#[test_case(Ty::BoolLiteral(true), Ty::StringLiteral("True"))]
|
||||
#[test_case(Ty::BoolLiteral(false), Ty::StringLiteral("False"))]
|
||||
#[test_case(Ty::BooleanLiteral(true), Ty::StringLiteral("True"))]
|
||||
#[test_case(Ty::BooleanLiteral(false), Ty::StringLiteral("False"))]
|
||||
#[test_case(Ty::StringLiteral("ab'cd"), Ty::StringLiteral("'ab\\'cd'"))] // single quotes
|
||||
#[test_case(Ty::LiteralString, Ty::LiteralString)]
|
||||
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]
|
||||
|
||||
@@ -216,76 +216,145 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
}
|
||||
|
||||
/// Adds a positive type to this intersection.
|
||||
fn add_positive(&mut self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
// TODO `Any`/`Unknown`/`Todo` actually should not self-cancel
|
||||
match ty {
|
||||
Type::Intersection(inter) => {
|
||||
let pos = inter.positive(db);
|
||||
let neg = inter.negative(db);
|
||||
self.positive.extend(pos.difference(&self.negative));
|
||||
self.negative.extend(neg.difference(&self.positive));
|
||||
self.positive.retain(|elem| !neg.contains(elem));
|
||||
self.negative.retain(|elem| !pos.contains(elem));
|
||||
fn add_positive(&mut self, db: &'db dyn Db, new_positive: Type<'db>) {
|
||||
if let Type::Intersection(other) = new_positive {
|
||||
for pos in other.positive(db) {
|
||||
self.add_positive(db, *pos);
|
||||
}
|
||||
_ => {
|
||||
if !self.negative.remove(&ty) {
|
||||
self.positive.insert(ty);
|
||||
};
|
||||
for neg in other.negative(db) {
|
||||
self.add_negative(db, *neg);
|
||||
}
|
||||
} else {
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
if let Type::Instance(class_type) = new_positive {
|
||||
if class_type.is_known(db, KnownClass::Bool) {
|
||||
if let Some(&Type::BooleanLiteral(value)) = self
|
||||
.negative
|
||||
.iter()
|
||||
.find(|element| element.is_boolean_literal())
|
||||
{
|
||||
*self = Self::new();
|
||||
self.positive.insert(Type::BooleanLiteral(!value));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_positive) in self.positive.iter().enumerate() {
|
||||
// S & T = S if S <: T
|
||||
if existing_positive.is_subtype_of(db, new_positive) {
|
||||
return;
|
||||
}
|
||||
// same rule, reverse order
|
||||
if new_positive.is_subtype_of(db, *existing_positive) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
// A & B = Never if A and B are disjoint
|
||||
if new_positive.is_disjoint_from(db, *existing_positive) {
|
||||
*self = Self::new();
|
||||
self.positive.insert(Type::Never);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for index in to_remove.iter().rev() {
|
||||
self.positive.swap_remove_index(*index);
|
||||
}
|
||||
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_negative) in self.negative.iter().enumerate() {
|
||||
// S & ~T = Never if S <: T
|
||||
if new_positive.is_subtype_of(db, *existing_negative) {
|
||||
*self = Self::new();
|
||||
self.positive.insert(Type::Never);
|
||||
return;
|
||||
}
|
||||
// A & ~B = A if A and B are disjoint
|
||||
if existing_negative.is_disjoint_from(db, new_positive) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
}
|
||||
for index in to_remove.iter().rev() {
|
||||
self.negative.swap_remove_index(*index);
|
||||
}
|
||||
|
||||
self.positive.insert(new_positive);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a negative type to this intersection.
|
||||
fn add_negative(&mut self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
// TODO `Any`/`Unknown`/`Todo` actually should not self-cancel
|
||||
match ty {
|
||||
Type::Intersection(intersection) => {
|
||||
let pos = intersection.negative(db);
|
||||
let neg = intersection.positive(db);
|
||||
self.positive.extend(pos.difference(&self.negative));
|
||||
self.negative.extend(neg.difference(&self.positive));
|
||||
self.positive.retain(|elem| !neg.contains(elem));
|
||||
self.negative.retain(|elem| !pos.contains(elem));
|
||||
fn add_negative(&mut self, db: &'db dyn Db, new_negative: Type<'db>) {
|
||||
match new_negative {
|
||||
Type::Intersection(inter) => {
|
||||
for pos in inter.positive(db) {
|
||||
self.add_negative(db, *pos);
|
||||
}
|
||||
for neg in inter.negative(db) {
|
||||
self.add_positive(db, *neg);
|
||||
}
|
||||
}
|
||||
Type::Never => {}
|
||||
Type::Unbound => {}
|
||||
ty @ (Type::Any | Type::Unknown | Type::Todo) => {
|
||||
// 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.
|
||||
self.positive.insert(ty);
|
||||
}
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
Type::BooleanLiteral(bool)
|
||||
if self
|
||||
.positive
|
||||
.iter()
|
||||
.any(|pos| *pos == KnownClass::Bool.to_instance(db)) =>
|
||||
{
|
||||
*self = Self::new();
|
||||
self.positive.insert(Type::BooleanLiteral(!bool));
|
||||
}
|
||||
_ => {
|
||||
if !self.positive.remove(&ty) {
|
||||
self.negative.insert(ty);
|
||||
};
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_negative) in self.negative.iter().enumerate() {
|
||||
// ~S & ~T = ~T if S <: T
|
||||
if existing_negative.is_subtype_of(db, new_negative) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
// same rule, reverse order
|
||||
if new_negative.is_subtype_of(db, *existing_negative) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for index in to_remove.iter().rev() {
|
||||
self.negative.swap_remove_index(*index);
|
||||
}
|
||||
|
||||
for existing_positive in &self.positive {
|
||||
// S & ~T = Never if S <: T
|
||||
if existing_positive.is_subtype_of(db, new_negative) {
|
||||
*self = Self::new();
|
||||
self.positive.insert(Type::Never);
|
||||
return;
|
||||
}
|
||||
// A & ~B = A if A and B are disjoint
|
||||
if existing_positive.is_disjoint_from(db, new_negative) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.negative.insert(new_negative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn simplify(&mut self) {
|
||||
// TODO this should be generalized based on subtyping, for now we just handle a few cases
|
||||
|
||||
// Never is a subtype of all types
|
||||
if self.positive.contains(&Type::Never) {
|
||||
self.positive.retain(Type::is_never);
|
||||
self.negative.clear();
|
||||
}
|
||||
|
||||
fn simplify_unbound(&mut self) {
|
||||
if self.positive.contains(&Type::Unbound) {
|
||||
self.positive.retain(Type::is_unbound);
|
||||
self.negative.clear();
|
||||
}
|
||||
|
||||
// None intersects only with object
|
||||
for pos in &self.positive {
|
||||
if let Type::Instance(_) = pos {
|
||||
// could be `object` type
|
||||
} else {
|
||||
self.negative.remove(&Type::None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.simplify();
|
||||
self.simplify_unbound();
|
||||
match (self.positive.len(), self.negative.len()) {
|
||||
(0, 0) => Type::Never,
|
||||
(0, 0) => KnownClass::Object.to_instance(db),
|
||||
(1, 0) => self.positive[0],
|
||||
_ => {
|
||||
self.positive.shrink_to_fit();
|
||||
@@ -302,9 +371,10 @@ mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::types::{KnownClass, UnionBuilder};
|
||||
use crate::types::{KnownClass, StringLiteralType, UnionBuilder};
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use test_case::test_case;
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
@@ -456,6 +526,15 @@ mod tests {
|
||||
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();
|
||||
@@ -473,7 +552,7 @@ mod tests {
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[t2, ta]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[t1]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -481,7 +560,7 @@ mod tests {
|
||||
let db = setup_db();
|
||||
let ta = Type::Any;
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::IntLiteral(2);
|
||||
let t2 = KnownClass::Int.to_instance(&db);
|
||||
let i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t1)
|
||||
@@ -492,8 +571,8 @@ mod tests {
|
||||
.build()
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[t2, t1]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[ta]);
|
||||
assert_eq!(intersection.pos_vec(&db), &[ta, t1]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -574,11 +653,269 @@ mod tests {
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_none() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::None)
|
||||
.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)
|
||||
.build();
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
}
|
||||
|
||||
#[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::StringLiteral(StringLiteralType::new(&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::StringLiteral(StringLiteralType::new(&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;
|
||||
|
||||
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::StringLiteral(StringLiteralType::new(&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);
|
||||
}
|
||||
|
||||
#[test_case(true)]
|
||||
#[test_case(false)]
|
||||
fn build_intersection_simplify_split_bool(bool_value: bool) {
|
||||
let db = setup_db();
|
||||
|
||||
let t_bool = KnownClass::Bool.to_instance(&db);
|
||||
let t_boolean_literal = Type::BooleanLiteral(bool_value);
|
||||
|
||||
// We add t_object in various orders (in first or second position) in
|
||||
// the tests below to ensure that the boolean simplification eliminates
|
||||
// everything from the intersection, not just `bool`.
|
||||
let t_object = KnownClass::Object.to_instance(&db);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_object)
|
||||
.add_positive(t_bool)
|
||||
.add_negative(t_boolean_literal)
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_bool)
|
||||
.add_positive(t_object)
|
||||
.add_negative(t_boolean_literal)
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_object)
|
||||
.add_negative(t_boolean_literal)
|
||||
.add_positive(t_bool)
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t_boolean_literal)
|
||||
.add_positive(t_object)
|
||||
.add_positive(t_bool)
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
}
|
||||
|
||||
#[test_case(Type::Any)]
|
||||
#[test_case(Type::Unknown)]
|
||||
#[test_case(Type::Todo)]
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ impl Display for DisplayType<'_> {
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::Class(_)
|
||||
| Type::Function(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
) {
|
||||
write!(f, "Literal[{representation}]")
|
||||
} else {
|
||||
@@ -69,13 +69,13 @@ impl Display for DisplayRepresentation<'_> {
|
||||
// `[Type::Todo]`'s display should be explicit that is not a valid display of
|
||||
// any other type
|
||||
Type::Todo => f.write_str("@Todo"),
|
||||
Type::Module(file) => {
|
||||
Type::ModuleLiteral(file) => {
|
||||
write!(f, "<module '{:?}'>", file.path(self.db))
|
||||
}
|
||||
// TODO functions and classes should display using a fully qualified name
|
||||
Type::Class(class) => f.write_str(class.name(self.db)),
|
||||
Type::ClassLiteral(class) => f.write_str(class.name(self.db)),
|
||||
Type::Instance(class) => f.write_str(class.name(self.db)),
|
||||
Type::Function(function) => f.write_str(function.name(self.db)),
|
||||
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
|
||||
Type::IntLiteral(n) => n.fmt(f),
|
||||
@@ -119,13 +119,13 @@ impl Display for DisplayUnionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let elements = self.ty.elements(self.db);
|
||||
|
||||
// Group literal types by kind.
|
||||
let mut grouped_literals = FxHashMap::default();
|
||||
// Group condensed-display types by kind.
|
||||
let mut grouped_condensed_kinds = FxHashMap::default();
|
||||
|
||||
for element in elements {
|
||||
if let Ok(literal_kind) = LiteralTypeKind::try_from(*element) {
|
||||
grouped_literals
|
||||
.entry(literal_kind)
|
||||
if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) {
|
||||
grouped_condensed_kinds
|
||||
.entry(kind)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(*element);
|
||||
}
|
||||
@@ -134,15 +134,15 @@ impl Display for DisplayUnionType<'_> {
|
||||
let mut join = f.join(" | ");
|
||||
|
||||
for element in elements {
|
||||
if let Ok(literal_kind) = LiteralTypeKind::try_from(*element) {
|
||||
let Some(mut literals) = grouped_literals.remove(&literal_kind) else {
|
||||
if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) {
|
||||
let Some(mut condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
|
||||
continue;
|
||||
};
|
||||
if literal_kind == LiteralTypeKind::IntLiteral {
|
||||
literals.sort_unstable_by_key(|ty| ty.expect_int_literal());
|
||||
if kind == CondensedDisplayTypeKind::Int {
|
||||
condensed_kind.sort_unstable_by_key(|ty| ty.expect_int_literal());
|
||||
}
|
||||
join.entry(&DisplayLiteralGroup {
|
||||
literals,
|
||||
literals: condensed_kind,
|
||||
db: self.db,
|
||||
});
|
||||
} else {
|
||||
@@ -152,7 +152,7 @@ impl Display for DisplayUnionType<'_> {
|
||||
|
||||
join.finish()?;
|
||||
|
||||
debug_assert!(grouped_literals.is_empty());
|
||||
debug_assert!(grouped_condensed_kinds.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -179,25 +179,31 @@ 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]`).
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
enum LiteralTypeKind {
|
||||
enum CondensedDisplayTypeKind {
|
||||
Class,
|
||||
Function,
|
||||
IntLiteral,
|
||||
StringLiteral,
|
||||
BytesLiteral,
|
||||
Int,
|
||||
String,
|
||||
Bytes,
|
||||
}
|
||||
|
||||
impl TryFrom<Type<'_>> for LiteralTypeKind {
|
||||
impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Type<'_>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Type::Class(_) => Ok(Self::Class),
|
||||
Type::Function(_) => Ok(Self::Function),
|
||||
Type::IntLiteral(_) => Ok(Self::IntLiteral),
|
||||
Type::StringLiteral(_) => Ok(Self::StringLiteral),
|
||||
Type::BytesLiteral(_) => Ok(Self::BytesLiteral),
|
||||
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),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,11 @@ use crate::semantic_index::definition::Definition;
|
||||
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, IntersectionBuilder, Type};
|
||||
use crate::types::{
|
||||
infer_expression_types, IntersectionBuilder, KnownFunction, Type, UnionBuilder,
|
||||
};
|
||||
use crate::Db;
|
||||
use itertools::Itertools;
|
||||
use ruff_python_ast as ast;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -59,6 +62,28 @@ fn all_narrowing_constraints_for_expression<'db>(
|
||||
NarrowingConstraintsBuilder::new(db, Constraint::Expression(expression)).finish()
|
||||
}
|
||||
|
||||
/// Generate a constraint from the *type* of the second argument of an `isinstance` call.
|
||||
///
|
||||
/// Example: for `isinstance(…, str)`, we would infer `Type::ClassLiteral(str)` from the
|
||||
/// second argument, but we need to generate a `Type::Instance(str)` constraint that can
|
||||
/// be used to narrow down the type of the first argument.
|
||||
fn generate_isinstance_constraint<'db>(
|
||||
db: &'db dyn Db,
|
||||
classinfo: &Type<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
match classinfo {
|
||||
Type::ClassLiteral(class) => Some(Type::Instance(*class)),
|
||||
Type::Tuple(tuple) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for element in tuple.elements(db) {
|
||||
builder = builder.add(generate_isinstance_constraint(db, element)?);
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
type NarrowingConstraints<'db> = FxHashMap<ScopedSymbolId, Type<'db>>;
|
||||
|
||||
struct NarrowingConstraintsBuilder<'db> {
|
||||
@@ -87,10 +112,15 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
|
||||
fn evaluate_expression_constraint(&mut self, expression: Expression<'db>) {
|
||||
if let ast::Expr::Compare(expr_compare) = expression.node_ref(self.db).node() {
|
||||
self.add_expr_compare(expr_compare, expression);
|
||||
match expression.node_ref(self.db).node() {
|
||||
ast::Expr::Compare(expr_compare) => {
|
||||
self.add_expr_compare(expr_compare, expression);
|
||||
}
|
||||
ast::Expr::Call(expr_call) => {
|
||||
self.add_expr_call(expr_call, expression);
|
||||
}
|
||||
_ => {} // TODO other test expression kinds
|
||||
}
|
||||
// TODO other test expression kinds
|
||||
}
|
||||
|
||||
fn evaluate_pattern_constraint(&mut self, pattern: PatternConstraint<'db>) {
|
||||
@@ -142,19 +172,27 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
ops,
|
||||
comparators,
|
||||
} = expr_compare;
|
||||
if !left.is_name_expr() && comparators.iter().all(|c| !c.is_name_expr()) {
|
||||
// If none of the comparators are name expressions,
|
||||
// we have no symbol to narrow down the type of.
|
||||
return;
|
||||
}
|
||||
let scope = self.scope();
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
|
||||
if let ast::Expr::Name(ast::ExprName {
|
||||
range: _,
|
||||
id,
|
||||
ctx: _,
|
||||
}) = left.as_ref()
|
||||
{
|
||||
// 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, expression);
|
||||
for (op, comparator) in std::iter::zip(&**ops, &**comparators) {
|
||||
let comp_ty = inference.expression_ty(comparator.scoped_ast_id(self.db, scope));
|
||||
let comparator_tuples = std::iter::once(&**left)
|
||||
.chain(comparators)
|
||||
.tuple_windows::<(&ruff_python_ast::Expr, &ruff_python_ast::Expr)>();
|
||||
for (op, (left, right)) in std::iter::zip(&**ops, comparator_tuples) {
|
||||
if let ast::Expr::Name(ast::ExprName {
|
||||
range: _,
|
||||
id,
|
||||
ctx: _,
|
||||
}) = left
|
||||
{
|
||||
// SAFETY: we should always have a symbol for every Name node.
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
let comp_ty = inference.expression_ty(right.scoped_ast_id(self.db, scope));
|
||||
match op {
|
||||
ast::CmpOp::IsNot => {
|
||||
if comp_ty.is_singleton() {
|
||||
@@ -169,6 +207,14 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
ast::CmpOp::Is => {
|
||||
self.constraints.insert(symbol, comp_ty);
|
||||
}
|
||||
ast::CmpOp::NotEq => {
|
||||
if comp_ty.is_single_valued(self.db) {
|
||||
let ty = IntersectionBuilder::new(self.db)
|
||||
.add_negative(comp_ty)
|
||||
.build();
|
||||
self.constraints.insert(symbol, ty);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// TODO other comparison types
|
||||
}
|
||||
@@ -177,6 +223,33 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_expr_call(&mut self, expr_call: &ast::ExprCall, expression: Expression<'db>) {
|
||||
let scope = self.scope();
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
|
||||
if let Some(func_type) = inference
|
||||
.expression_ty(expr_call.func.scoped_ast_id(self.db, scope))
|
||||
.into_function_literal_type()
|
||||
{
|
||||
if func_type.is_known(self.db, KnownFunction::IsInstance)
|
||||
&& expr_call.arguments.keywords.is_empty()
|
||||
{
|
||||
if let [ast::Expr::Name(ast::ExprName { id, .. }), rhs] = &*expr_call.arguments.args
|
||||
{
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
|
||||
let rhs_type = inference.expression_ty(rhs.scoped_ast_id(self.db, scope));
|
||||
|
||||
// TODO: add support for PEP 604 union types on the right hand side:
|
||||
// isinstance(x, str | (int | float))
|
||||
if let Some(constraint) = generate_isinstance_constraint(self.db, &rhs_type) {
|
||||
self.constraints.insert(symbol, constraint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_match_pattern_singleton(
|
||||
&mut self,
|
||||
subject: &ast::Expr,
|
||||
|
||||
1
crates/red_knot_python_semantic/src/util/mod.rs
Normal file
1
crates/red_knot_python_semantic/src/util/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub(crate) mod subscript;
|
||||
83
crates/red_knot_python_semantic/src/util/subscript.rs
Normal file
83
crates/red_knot_python_semantic/src/util/subscript.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
pub(crate) trait PythonSubscript {
|
||||
type Item;
|
||||
|
||||
fn python_subscript(&mut self, index: i64) -> Option<Self::Item>;
|
||||
}
|
||||
|
||||
impl<I, T: DoubleEndedIterator<Item = I>> PythonSubscript for T {
|
||||
type Item = I;
|
||||
|
||||
fn python_subscript(&mut self, index: i64) -> Option<I> {
|
||||
if index >= 0 {
|
||||
self.nth(usize::try_from(index).ok()?)
|
||||
} else {
|
||||
let nth_rev = usize::try_from(index.checked_neg()?).ok()?.checked_sub(1)?;
|
||||
self.rev().nth(nth_rev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::redundant_clone)]
|
||||
mod tests {
|
||||
use super::PythonSubscript;
|
||||
|
||||
#[test]
|
||||
fn python_subscript_basic() {
|
||||
let iter = 'a'..='e';
|
||||
|
||||
assert_eq!(iter.clone().python_subscript(0), Some('a'));
|
||||
assert_eq!(iter.clone().python_subscript(1), Some('b'));
|
||||
assert_eq!(iter.clone().python_subscript(4), Some('e'));
|
||||
assert_eq!(iter.clone().python_subscript(5), None);
|
||||
|
||||
assert_eq!(iter.clone().python_subscript(-1), Some('e'));
|
||||
assert_eq!(iter.clone().python_subscript(-2), Some('d'));
|
||||
assert_eq!(iter.clone().python_subscript(-5), Some('a'));
|
||||
assert_eq!(iter.clone().python_subscript(-6), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_subscript_empty() {
|
||||
let iter = 'a'..'a';
|
||||
|
||||
assert_eq!(iter.clone().python_subscript(0), None);
|
||||
assert_eq!(iter.clone().python_subscript(1), None);
|
||||
assert_eq!(iter.clone().python_subscript(-1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_subscript_single_element() {
|
||||
let iter = 'a'..='a';
|
||||
|
||||
assert_eq!(iter.clone().python_subscript(0), Some('a'));
|
||||
assert_eq!(iter.clone().python_subscript(1), None);
|
||||
assert_eq!(iter.clone().python_subscript(-1), Some('a'));
|
||||
assert_eq!(iter.clone().python_subscript(-2), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_subscript_uses_full_index_range() {
|
||||
let iter = 0..=u64::MAX;
|
||||
|
||||
assert_eq!(iter.clone().python_subscript(0), Some(0));
|
||||
assert_eq!(iter.clone().python_subscript(1), Some(1));
|
||||
assert_eq!(
|
||||
iter.clone().python_subscript(i64::MAX),
|
||||
Some(i64::MAX as u64)
|
||||
);
|
||||
|
||||
assert_eq!(iter.clone().python_subscript(-1), Some(u64::MAX));
|
||||
assert_eq!(iter.clone().python_subscript(-2), Some(u64::MAX - 1));
|
||||
|
||||
// i64::MIN is not representable as a positive number, so it is not
|
||||
// a valid index:
|
||||
assert_eq!(iter.clone().python_subscript(i64::MIN), None);
|
||||
|
||||
// but i64::MIN +1 is:
|
||||
assert_eq!(
|
||||
iter.clone().python_subscript(i64::MIN + 1),
|
||||
Some(2u64.pow(63) + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
use dir_test::{dir_test, Fixture};
|
||||
use red_knot_test::run;
|
||||
use std::path::PathBuf;
|
||||
use std::path::Path;
|
||||
|
||||
/// See `crates/red_knot_test/README.md` for documentation on these tests.
|
||||
#[rstest::rstest]
|
||||
fn mdtest(#[files("resources/mdtest/**/*.md")] path: PathBuf) {
|
||||
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources")
|
||||
.join("mdtest")
|
||||
#[dir_test(
|
||||
dir: "$CARGO_MANIFEST_DIR/resources/mdtest",
|
||||
glob: "**/*.md"
|
||||
)]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn mdtest(fixture: Fixture<&str>) {
|
||||
let path = fixture.path();
|
||||
|
||||
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources/mdtest")
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
let title = path.strip_prefix(crate_dir).unwrap();
|
||||
run(&path, title.as_os_str().to_str().unwrap());
|
||||
|
||||
let relative_path = path
|
||||
.strip_prefix(crate_dir.to_str().unwrap())
|
||||
.unwrap_or(path);
|
||||
|
||||
run(Path::new(path), relative_path);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Scheduling, I/O, and API endpoints.
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
// The new PanicInfoHook name requires MSRV >= 1.82
|
||||
#[allow(deprecated)]
|
||||
use std::panic::PanicInfo;
|
||||
|
||||
use lsp_server::Message;
|
||||
@@ -119,6 +121,8 @@ impl Server {
|
||||
}
|
||||
|
||||
pub(crate) fn run(self) -> crate::Result<()> {
|
||||
// The new PanicInfoHook name requires MSRV >= 1.82
|
||||
#[allow(deprecated)]
|
||||
type PanicHook = Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send>;
|
||||
struct RestorePanicHook {
|
||||
hook: Option<PanicHook>,
|
||||
|
||||
@@ -37,7 +37,7 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
|
||||
params.cell_text_documents,
|
||||
)
|
||||
.with_failure_code(ErrorCode::InternalError)?;
|
||||
session.open_notebook_document(params.notebook_document.uri.clone(), notebook);
|
||||
session.open_notebook_document(params.notebook_document.uri, notebook);
|
||||
|
||||
match path {
|
||||
AnySystemPath::System(path) => {
|
||||
|
||||
@@ -21,7 +21,7 @@ ruff_text_size = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
|
||||
@@ -34,14 +34,14 @@ syntax, it's just how this README embeds an example mdtest Markdown document.)
|
||||
See actual example mdtest suites in
|
||||
[`crates/red_knot_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/resources/mdtest).
|
||||
|
||||
> ℹ️ Note: If you use `rstest` to generate a separate test for all Markdown files in a certain directory,
|
||||
> ℹ️ Note: If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory,
|
||||
> as with the example in `crates/red_knot_python_semantic/tests/mdtest.rs`,
|
||||
> you will likely want to also make sure that the crate the tests are in is rebuilt every time a
|
||||
> Markdown file is added or removed from the directory. See
|
||||
> [`crates/red_knot_python_semantic/build.rs`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/build.rs)
|
||||
> for an example of how to do this.
|
||||
>
|
||||
> This is because `rstest` generates its tests at build time rather than at runtime.
|
||||
> This is because these macros generate their tests at build time rather than at runtime.
|
||||
> Without the `build.rs` file to force a rebuild when a Markdown file is added or removed,
|
||||
> a new Markdown test suite might not be run unless some other change in the crate caused a rebuild
|
||||
> following the addition of the new test file.
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
//! ```
|
||||
|
||||
use crate::db::Db;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
@@ -45,6 +44,7 @@ use ruff_source_file::{LineIndex, Locator, OneIndexed};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use smallvec::SmallVec;
|
||||
use std::ops::Deref;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Diagnostic assertion comments in a single embedded file.
|
||||
#[derive(Debug)]
|
||||
@@ -76,7 +76,7 @@ impl InlineFileAssertions {
|
||||
}
|
||||
|
||||
fn is_own_line_comment(&self, ranged_assertion: &AssertionWithRange) -> bool {
|
||||
CommentRanges::is_own_line(ranged_assertion.start(), &self.locator())
|
||||
CommentRanges::is_own_line(ranged_assertion.start(), self.source.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,10 +239,10 @@ impl<'a> Deref for LineAssertions<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
static TYPE_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^#\s*revealed:\s*(?<ty_display>.+?)\s*$").unwrap());
|
||||
static TYPE_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^#\s*revealed:\s*(?<ty_display>.+?)\s*$").unwrap());
|
||||
|
||||
static ERROR_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
static ERROR_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r#"^#\s*error:(\s*(?<column>\d+))?(\s*\[(?<rule>.+?)\])?(\s*"(?<message>.+?)")?\s*$"#,
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user