Compare commits

..

1 Commits

Author SHA1 Message Date
Dhruv Manilawala
2d0468137c Extract creating workspaces in the server 2024-10-16 14:51:23 +05:30
414 changed files with 5243 additions and 14629 deletions

View File

@@ -63,7 +63,7 @@ jobs:
macos-x86_64:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-14
runs-on: macos-12
steps:
- uses: actions/checkout@v4
with:

View File

@@ -17,21 +17,12 @@ on:
paths:
- .github/workflows/build-docker.yml
env:
RUFF_BASE_IMG: ghcr.io/${{ github.repository_owner }}/ruff
jobs:
docker-build:
name: Build Docker image (ghcr.io/astral-sh/ruff) for ${{ matrix.platform }}
docker-publish:
name: Build Docker image (ghcr.io/astral-sh/ruff)
runs-on: ubuntu-latest
environment:
name: release
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- uses: actions/checkout@v4
with:
@@ -45,6 +36,12 @@ 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: |
@@ -58,233 +55,14 @@ jobs:
echo "Releasing ${version}"
fi
- 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
- name: "Build and push Docker image"
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
# 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 }}
# 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' }}
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 ' *)

View File

@@ -193,7 +193,7 @@ jobs:
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 18
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-pack-action@v0.4.0

View File

@@ -29,7 +29,7 @@ jobs:
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 18
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.9.0
uses: cloudflare/wrangler-action@v3.7.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -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: 20
node-version: 18
registry-url: "https://registry.npmjs.org"
- name: "Publish (dry-run)"
if: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}

View File

@@ -1,4 +1,4 @@
fail_fast: false
fail_fast: true
exclude: |
(?x)^(
@@ -17,12 +17,12 @@ exclude: |
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.21
rev: v0.20.2
hooks:
- id: validate-pyproject
- repo: https://github.com/executablebooks/mdformat
rev: 0.7.18
rev: 0.7.17
hooks:
- id: mdformat
additional_dependencies:
@@ -45,17 +45,8 @@ 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.26.0
rev: v1.25.0
hooks:
- id: typos
@@ -69,7 +60,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.0
rev: v0.6.9
hooks:
- id: ruff-format
- id: ruff

View File

@@ -1,18 +1,5 @@
# 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))

View File

@@ -1,77 +1,5 @@
# 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!
### Breaking changes
- 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.
### Formatter preview style
- Normalize implicit concatenated f-string quotes per part ([#13539](https://github.com/astral-sh/ruff/pull/13539))
### Preview linter features
- \[`refurb`\] implement `hardcoded-string-charset` (FURB156) ([#13530](https://github.com/astral-sh/ruff/pull/13530))
- \[`refurb`\] Count codepoints not bytes for `slice-to-remove-prefix-or-suffix (FURB188)` ([#13631](https://github.com/astral-sh/ruff/pull/13631))
### Rule changes
- \[`pylint`\] Mark `PLE1141` fix as unsafe ([#13629](https://github.com/astral-sh/ruff/pull/13629))
- \[`flake8-async`\] Consider async generators to be "checkpoints" for `cancel-scope-no-checkpoint` (`ASYNC100`) ([#13639](https://github.com/astral-sh/ruff/pull/13639))
- \[`flake8-bugbear`\] Do not suggest setting parameter `strict=` to `False` in `B905` diagnostic message ([#13656](https://github.com/astral-sh/ruff/pull/13656))
- \[`flake8-todos`\] Only flag the word "TODO", not words starting with "todo" (`TD006`) ([#13640](https://github.com/astral-sh/ruff/pull/13640))
- \[`pycodestyle`\] Fix whitespace-related false positives and false negatives inside type-parameter lists (`E231`, `E251`) ([#13704](https://github.com/astral-sh/ruff/pull/13704))
- \[`flake8-simplify`\] Stabilize preview behavior for `SIM115` so that the rule can detect files
being opened from a wider range of standard-library functions ([#12959](https://github.com/astral-sh/ruff/pull/12959)).
### CLI
- Add explanation of fixable in `--statistics` command ([#13774](https://github.com/astral-sh/ruff/pull/13774))
### Bug fixes
- \[`pyflakes`\] Allow `ipytest` cell magic (`F401`) ([#13745](https://github.com/astral-sh/ruff/pull/13745))
- \[`flake8-use-pathlib`\] Fix `PTH123` false positive when `open` is passed a file descriptor ([#13616](https://github.com/astral-sh/ruff/pull/13616))
- \[`flake8-bandit`\] Detect patterns from multi line SQL statements (`S608`) ([#13574](https://github.com/astral-sh/ruff/pull/13574))
- \[`flake8-pyi`\] - Fix dropped expressions in `PYI030` autofix ([#13727](https://github.com/astral-sh/ruff/pull/13727))
## 0.6.9
### Preview features

220
Cargo.lock generated
View File

@@ -69,7 +69,7 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e"
dependencies = [
"unicode-width 0.1.13",
"unicode-width",
"yansi-term",
]
@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.90"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
[[package]]
name = "append-only-vec"
@@ -407,7 +407,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -491,7 +491,7 @@ dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"unicode-width 0.1.13",
"unicode-width",
"windows-sys 0.52.0",
]
@@ -687,7 +687,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -698,7 +698,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -750,27 +750,6 @@ 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"
@@ -900,9 +879,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "fern"
version = "0.7.0"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ff9c9d5fb3e6da8ac2f77ab76fe7e8087d512ce095200f8f29ac5b656cf6dc"
checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee"
dependencies = [
"log",
]
@@ -978,7 +957,7 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width 0.1.13",
"unicode-width",
]
[[package]]
@@ -1181,7 +1160,7 @@ dependencies = [
"instant",
"number_prefix",
"portable-atomic",
"unicode-width 0.1.13",
"unicode-width",
"vt100",
]
@@ -1267,7 +1246,7 @@ dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -1367,9 +1346,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.161"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "libcst"
@@ -1393,7 +1372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
dependencies = [
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -1781,17 +1760,18 @@ dependencies = [
"once_cell",
"regex",
"serde",
"unicode-width 0.1.13",
"unicode-width",
]
[[package]]
name = "pep440_rs"
version = "0.7.1"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c8ee724d21f351f9d47276614ac9710975db827ba9fe2ca5a517ba648193307"
checksum = "466eada3179c2e069ca897b99006cbb33f816290eaeec62464eea907e22ae385"
dependencies = [
"once_cell",
"serde",
"unicode-width 0.2.0",
"unicode-width",
"unscanny",
]
@@ -1807,7 +1787,7 @@ dependencies = [
"serde",
"thiserror",
"tracing",
"unicode-width 0.1.13",
"unicode-width",
"url",
]
@@ -1848,7 +1828,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -1963,9 +1943,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.88"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
dependencies = [
"unicode-ident",
]
@@ -2100,7 +2080,6 @@ dependencies = [
"camino",
"compact_str",
"countme",
"dir-test",
"hashbrown 0.15.0",
"insta",
"itertools 0.13.0",
@@ -2108,6 +2087,7 @@ dependencies = [
"ordermap",
"red_knot_test",
"red_knot_vendored",
"rstest",
"ruff_db",
"ruff_index",
"ruff_python_ast",
@@ -2156,7 +2136,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"colored",
"memchr",
"once_cell",
"red_knot_python_semantic",
"red_knot_vendored",
"regex",
@@ -2174,6 +2154,7 @@ dependencies = [
name = "red_knot_vendored"
version = "0.0.0"
dependencies = [
"once_cell",
"path-slash",
"ruff_db",
"walkdir",
@@ -2289,6 +2270,12 @@ 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"
@@ -2304,9 +2291,36 @@ 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.1"
version = "0.6.9"
dependencies = [
"anyhow",
"argfile",
@@ -2368,6 +2382,7 @@ dependencies = [
"codspeed-criterion-compat",
"criterion",
"mimalloc",
"once_cell",
"rayon",
"red_knot_python_semantic",
"red_knot_workspace",
@@ -2491,7 +2506,7 @@ dependencies = [
"serde",
"static_assertions",
"tracing",
"unicode-width 0.1.13",
"unicode-width",
]
[[package]]
@@ -2500,6 +2515,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"once_cell",
"red_knot_python_semantic",
"ruff_cache",
"ruff_db",
@@ -2523,7 +2539,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.7.1"
version = "0.6.9"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2544,9 +2560,10 @@ dependencies = [
"log",
"memchr",
"natord",
"once_cell",
"path-absolutize",
"pathdiff",
"pep440_rs 0.7.1",
"pep440_rs 0.6.6",
"pyproject-toml",
"quick-junit",
"regex",
@@ -2577,7 +2594,7 @@ dependencies = [
"toml",
"typed-arena",
"unicode-normalization",
"unicode-width 0.1.13",
"unicode-width",
"unicode_names2",
"url",
]
@@ -2590,7 +2607,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -2599,6 +2616,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"itertools 0.13.0",
"once_cell",
"rand",
"ruff_diagnostics",
"ruff_source_file",
@@ -2620,6 +2638,7 @@ dependencies = [
"compact_str",
"is-macro",
"itertools 0.13.0",
"once_cell",
"ruff_cache",
"ruff_macros",
"ruff_python_trivia",
@@ -2645,6 +2664,7 @@ dependencies = [
name = "ruff_python_codegen"
version = "0.0.0"
dependencies = [
"once_cell",
"ruff_python_ast",
"ruff_python_literal",
"ruff_python_parser",
@@ -2662,6 +2682,7 @@ dependencies = [
"insta",
"itertools 0.13.0",
"memchr",
"once_cell",
"regex",
"ruff_cache",
"ruff_formatter",
@@ -2822,6 +2843,7 @@ name = "ruff_source_file"
version = "0.0.0"
dependencies = [
"memchr",
"once_cell",
"ruff_text_size",
"serde",
]
@@ -2838,7 +2860,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.7.1"
version = "0.6.9"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -2877,7 +2899,7 @@ dependencies = [
"matchit",
"path-absolutize",
"path-slash",
"pep440_rs 0.7.1",
"pep440_rs 0.6.6",
"regex",
"ruff_cache",
"ruff_formatter",
@@ -2919,6 +2941,15 @@ 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"
@@ -2979,7 +3010,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "salsa"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
dependencies = [
"append-only-vec",
"arc-swap",
@@ -2999,17 +3030,17 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
[[package]]
name = "salsa-macros"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
"synstructure",
]
@@ -3043,7 +3074,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -3064,6 +3095,12 @@ 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"
@@ -3092,7 +3129,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -3103,14 +3140,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.132"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
@@ -3126,7 +3163,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -3167,7 +3204,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -3269,7 +3306,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -3280,20 +3317,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "1.0.109"
version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
@@ -3308,7 +3334,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -3371,7 +3397,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -3382,7 +3408,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
"test-case-core",
]
@@ -3403,7 +3429,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -3515,7 +3541,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -3679,12 +3705,6 @@ 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"
@@ -3755,9 +3775,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [
"getrandom",
"rand",
@@ -3767,13 +3787,13 @@ dependencies = [
[[package]]
name = "uuid-macro-internal"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
checksum = "ee1cd046f83ea2c4e920d6ee9f7c3537ef928d75dce5d84a87c2c5d6b3999a3a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -3796,7 +3816,7 @@ checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de"
dependencies = [
"itoa",
"log",
"unicode-width 0.1.13",
"unicode-width",
"vte",
]
@@ -3859,7 +3879,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
"wasm-bindgen-shared",
]
@@ -3893,7 +3913,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3927,7 +3947,7 @@ checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]
@@ -4215,7 +4235,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"syn",
]
[[package]]

View File

@@ -4,7 +4,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
rust-version = "1.80"
rust-version = "1.76"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
@@ -65,11 +65,10 @@ 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.7.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.23" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
@@ -102,11 +101,12 @@ 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.7.1" }
pep440_rs = { version = "0.6.0", features = ["serde"] }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.79" }
pyproject-toml = { version = "0.9.0" }
@@ -115,8 +115,9 @@ 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 = "254c749b02cde2fd29852a7463a33e800b771758" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "b14be5c0392f4c55eca60b92e457a35549372382" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -202,10 +203,6 @@ 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

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM ubuntu AS build
FROM --platform=$BUILDPLATFORM ubuntu as build
ENV HOME="/root"
WORKDIR $HOME

View File

@@ -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.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.7.1/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.6.9/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.6.9/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.1
rev: v0.6.9
hooks:
# Run the linter.
- id: ruff

View File

@@ -144,7 +144,7 @@ pub fn main() -> ExitStatus {
}
fn run() -> anyhow::Result<ExitStatus> {
let args = Args::parse_from(std::env::args());
let args = Args::parse_from(std::env::args().collect::<Vec<_>>());
if matches!(args.command, Some(Command::Server)) {
return run_server().map(|()| ExitStatus::Success);

View File

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

View File

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

View File

@@ -13,22 +13,13 @@ 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`"
```
## 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
x = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
```

View File

@@ -1,32 +1,32 @@
# Unbound
## Maybe unbound
```py
if flag:
y = 3
x = y
reveal_type(x) # revealed: Unbound | Literal[3]
```
## Unbound
```py
x = foo # error: [unresolved-reference] "Name `foo` used when not defined"
foo = 1
# error: [unresolved-reference]
# revealed: Unbound
reveal_type(x)
x = foo; foo = 1
reveal_type(x) # revealed: Unbound
```
## Unbound class variable
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
Class variables can reference global variables unless overridden within the class scope.
```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: Unbound | Literal[2]
reveal_type(C.y) # revealed: Literal[1]
```

View File

@@ -3,18 +3,13 @@
## Union of attributes
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
class C:
x = 1
else:
class C:
x = 2
reveal_type(C.x) # revealed: Literal[1, 2]
y = C.x
reveal_type(y) # revealed: Literal[1, 2]
```

View File

@@ -1,48 +0,0 @@
## 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]
```

View File

@@ -1,447 +0,0 @@
# 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

View File

@@ -1,70 +1,36 @@
# Binary operations on integers
## Binary operations on integers
## Basic Arithmetic
```py
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]
```
a = 2 + 1
b = a - 4
c = a * b
d = c // 3
e = c / 3
f = 5 % 3
## 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
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]
```
## Division 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
# 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"
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"
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"
reveal_type(a) # revealed: float
reveal_type(b) # revealed: int
reveal_type(c) # revealed: int
# 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)
reveal_type(d) # revealed: @Todo
reveal_type(e) # revealed: @Todo
```

View File

@@ -11,10 +11,11 @@ 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(b) # revealed: Unknown
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
reveal_type(a) # revealed: float
reveal_type(b) # revealed: Unknown
```

View File

@@ -3,5 +3,6 @@
```py
class Foo: ...
reveal_type(Foo()) # revealed: Foo
x = Foo()
reveal_type(x) # revealed: Foo
```

View File

@@ -6,7 +6,8 @@
def get_int() -> int:
return 42
reveal_type(get_int()) # revealed: int
x = get_int()
reveal_type(x) # revealed: int
```
## Async
@@ -15,8 +16,10 @@ reveal_type(get_int()) # 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(get_int_async()) # revealed: @Todo
reveal_type(x) # revealed: @Todo
```
## Decorated
@@ -32,10 +35,12 @@ def decorator(func) -> Callable[[], int]:
@decorator
def bar() -> str:
return "bar"
return 'bar'
x = bar()
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
reveal_type(bar()) # revealed: @Todo
reveal_type(x) # revealed: @Todo
```
## Invalid callable

View File

@@ -3,40 +3,28 @@
## 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"
return 'foo'
reveal_type(f()) # revealed: int | str
x = f()
reveal_type(x) # revealed: int | str
```
## Calling with an unknown union
```py
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
def bool_instance() -> bool:
return True
flag = bool_instance()
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
if flag:
def f() -> int:
return 1
reveal_type(f()) # revealed: Unknown | int
x = f()
reveal_type(x) # revealed: Unknown | int
```
## Non-callable elements in a union
@@ -44,15 +32,9 @@ reveal_type(f()) # 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
@@ -65,23 +47,16 @@ 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
# 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())
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
```
## All non-callable union elements
@@ -89,15 +64,10 @@ reveal_type(f())
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

View File

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

View File

@@ -1,18 +1,29 @@
# Comparison: Integers
# Comparing integers
## Integer literals
```py
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]
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]
```
## Integer instance
@@ -20,8 +31,11 @@ reveal_type(1 <= "" and 0 < 1) # 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(1 == int_instance()) # revealed: @Todo
reveal_type(9 < int_instance()) # revealed: bool
reveal_type(int_instance() < int_instance()) # revealed: bool
reveal_type(a) # revealed: @Todo
reveal_type(b) # revealed: bool
reveal_type(c) # revealed: bool
```

View File

@@ -1,4 +1,4 @@
# Comparison: Non boolean returns
# Non boolean returns
Walking through examples:
@@ -20,22 +20,18 @@ 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: ...
x = A() < B() < C()
reveal_type(x) # revealed: A | B
a = A() < B() < C()
b = 0 < 1 < A() < 3
c = 10 < 0 < A() < B() < C()
y = 0 < 1 < A() < 3
reveal_type(y) # revealed: bool | A
z = 10 < 0 < A() < B() < C()
reveal_type(z) # revealed: Literal[False]
reveal_type(a) # revealed: A | B
reveal_type(b) # revealed: bool | A
reveal_type(c) # revealed: Literal[False]
```

View File

@@ -1,20 +1,29 @@
# Comparison: Strings
# Comparing strings
## String literals
```py
def str_instance() -> str: ...
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
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.
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
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]
```

View File

@@ -1,205 +0,0 @@
# Comparison: Tuples
## Heterogeneous
For tuples like `tuple[int, str, Literal[1]]`
### Value Comparisons
"Value Comparisons" refers to the operators: `==`, `!=`, `<`, `<=`, `>`, `>=`
#### Results without Ambiguity
Cases where the result can be definitively inferred as a `BooleanLiteral`.
```py
a = (1, "test", (3, 13), True)
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[True]
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[False]
reveal_type(a >= b) # revealed: Literal[False]
```
Even when tuples have different lengths, comparisons should be handled appropriately.
```py path=different_length.py
a = (1, 2, 3)
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[False]
reveal_type(a >= b) # revealed: Literal[False]
c = ("a", "b", "c", "d")
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[True]
reveal_type(c >= d) # revealed: Literal[True]
```
#### Results with Ambiguity
```py
def bool_instance() -> bool: ...
def int_instance() -> int: ...
a = (bool_instance(),)
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 == 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
```
#### Comparison Unsupported
If two tuples contain types that do not support comparison, the result may be `Unknown`.
However, `==` and `!=` are exceptions and can still provide definite results.
```py
a = (1, 2)
b = (1, "hello")
# TODO: should be Literal[False]
reveal_type(a == b) # revealed: @Todo
# TODO: should be Literal[True]
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
```
However, if the lexicographic comparison completes without reaching a point where str and int are compared,
Python will still produce a result based on the prior elements.
```py path=short_circuit.py
a = (1, 2)
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[False]
reveal_type(a >= b) # revealed: Literal[False]
```
#### Matryoshka Tuples
```py
a = (1, True, "Hello")
b = (a, a, a)
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[True]
reveal_type(c > c) # revealed: Literal[False]
reveal_type(c >= c) # revealed: Literal[True]
```
#### Non Boolean Rich Comparisons
```py
class A:
def __eq__(self, o) -> str: ...
def __ne__(self, o) -> int: ...
def __lt__(self, o) -> float: ...
def __le__(self, o) -> object: ...
def __gt__(self, o) -> tuple: ...
def __ge__(self, o) -> list: ...
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
```
### Membership Test Comparisons
"Membership Test Comparisons" refers to the operators `in` and `not in`.
```py
def int_instance() -> int: ...
a = (1, 2)
b = ((3, 4), (1, 2))
c = ((1, 2, 3), (4, 5, 6))
d = ((int_instance(), int_instance()), (int_instance(), int_instance()))
reveal_type(a in b) # revealed: Literal[True]
reveal_type(a not in b) # revealed: Literal[False]
reveal_type(a in c) # revealed: Literal[False]
reveal_type(a not in c) # revealed: Literal[True]
# TODO: All @Todo should be bool
reveal_type(a in d) # revealed: @Todo
reveal_type(a not in d) # revealed: @Todo
```
### Identity Comparisons
"Identity Comparisons" refers to `is` and `is not`.
```py
a = (1, 2)
b = ("a", "b")
c = (1, 2, 3)
reveal_type(a is (1, 2)) # revealed: bool
reveal_type(a is not (1, 2)) # revealed: bool
# TODO: Update to Literal[False] once str == int comparison is implemented
reveal_type(a is b) # revealed: @Todo
# TODO: Update to Literal[True] once str == int comparison is implemented
reveal_type(a is not b) # revealed: @Todo
reveal_type(a is c) # revealed: Literal[False]
reveal_type(a is not c) # revealed: Literal[True]
```
## Homogeneous
For tuples like `tuple[int, ...]`, `tuple[Any, ...]`
// TODO

View File

@@ -1,88 +0,0 @@
# Comparison: Unions
## Union on one side of the comparison
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]
reveal_type(one_or_two <= 1) # revealed: bool
reveal_type(one_or_two <= 0) # revealed: Literal[False]
reveal_type(2 >= one_or_two) # revealed: Literal[True]
reveal_type(1 >= one_or_two) # revealed: bool
reveal_type(0 >= one_or_two) # revealed: Literal[False]
reveal_type(one_or_two < 1) # revealed: Literal[False]
reveal_type(one_or_two < 2) # revealed: bool
reveal_type(one_or_two < 3) # revealed: Literal[True]
reveal_type(one_or_two > 0) # revealed: Literal[True]
reveal_type(one_or_two > 1) # revealed: bool
reveal_type(one_or_two > 2) # revealed: Literal[False]
reveal_type(one_or_two == 3) # revealed: Literal[False]
reveal_type(one_or_two == 1) # revealed: bool
reveal_type(one_or_two != 3) # revealed: Literal[True]
reveal_type(one_or_two != 1) # revealed: bool
a_or_ab = "a" if flag else "ab"
reveal_type(a_or_ab in "ab") # revealed: Literal[True]
reveal_type("a" in a_or_ab) # revealed: Literal[True]
reveal_type("c" not in a_or_ab) # revealed: Literal[True]
reveal_type("a" not in a_or_ab) # revealed: Literal[False]
reveal_type("b" in a_or_ab) # revealed: bool
reveal_type("b" not in a_or_ab) # revealed: bool
one_or_none = 1 if flag else None
reveal_type(one_or_none is None) # revealed: bool
reveal_type(one_or_none is not None) # revealed: bool
```
## Union on both sides of the comparison
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]
```
## Unsupported operations
Make sure we emit a diagnostic if *any* of the possible comparisons is
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"
reveal_type(result) # revealed: bool
```

View File

@@ -1,31 +1,15 @@
# Comparison: Unsupported operators
# Unsupported operators
```py
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
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]`"
# 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
```

View File

@@ -3,10 +3,6 @@
## 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]
```
@@ -14,25 +10,19 @@ 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(y) # revealed: Literal[0, 1]
reveal_type(z) # revealed: Literal[0, 2]
reveal_type(a) # revealed: Literal[0, 1]
reveal_type(b) # 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]
```
@@ -40,10 +30,6 @@ 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
```

View File

@@ -3,26 +3,20 @@
## Simple if
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
y = 1
y = 2
if flag:
y = 3
reveal_type(y) # revealed: Literal[2, 3]
x = y
reveal_type(x) # 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:
@@ -34,26 +28,14 @@ else:
y = 5
s = y
x = y
reveal_type(x) # revealed: Literal[3, 4, 5]
# revealed: Unbound | Literal[2]
# error: [possibly-unresolved-reference]
reveal_type(r)
# revealed: Unbound | Literal[5]
# error: [possibly-unresolved-reference]
reveal_type(s)
reveal_type(r) # revealed: Unbound | Literal[2]
reveal_type(s) # revealed: Unbound | Literal[5]
```
## 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:
@@ -66,10 +48,6 @@ 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
@@ -83,10 +61,6 @@ 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
@@ -101,10 +75,6 @@ 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:
@@ -115,16 +85,13 @@ 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(y) # revealed: Literal[2, 3, 4]
reveal_type(x) # revealed: Literal[2, 3, 4]
```

View File

@@ -21,9 +21,7 @@ match 0:
case 2:
y = 3
# revealed: Unbound | Literal[2, 3]
# error: [possibly-unresolved-reference]
reveal_type(y)
reveal_type(y) # revealed: Unbound | Literal[2, 3]
```
## Basic match

View File

@@ -10,10 +10,6 @@ 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:
@@ -24,10 +20,6 @@ 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"
@@ -36,10 +28,6 @@ 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:
@@ -47,5 +35,5 @@ else:
# error: [conflicting-declarations]
# error: [invalid-assignment]
x = b"foo"
x = b'foo'
```

View File

@@ -4,9 +4,8 @@
```py
import re
try:
help()
x
except NameError as e:
reveal_type(e) # revealed: NameError
except re.error as f:
@@ -16,10 +15,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:
help()
x
except foo as e:
reveal_type(foo) # revealed: Unknown
reveal_type(e) # revealed: Unknown
@@ -31,7 +30,7 @@ except foo as e:
EXCEPTIONS = (AttributeError, TypeError)
try:
help()
x
except (RuntimeError, OSError) as e:
reveal_type(e) # revealed: RuntimeError | OSError
except EXCEPTIONS as f:
@@ -43,7 +42,7 @@ except EXCEPTIONS as f:
```py
def foo(x: type[AttributeError], y: tuple[type[OSError], type[RuntimeError]], z: tuple[type[BaseException], ...]):
try:
help()
w
except x as e:
# TODO: should be `AttributeError`
reveal_type(e) # revealed: @Todo

View File

@@ -1,641 +0,0 @@
# Control flow for exception handlers
These tests assert that we understand the possible "definition states" (which
symbols might or might not be defined) in the various branches of a
`try`/`except`/`else`/`finally` block.
For a full writeup on the semantics of exception handlers,
see [this document][1].
The tests throughout this Markdown document use functions with names starting
with `could_raise_*` to mark definitions that might or might not succeed
(as the function could raise an exception). A type checker must assume that any
arbitrary function call could raise an exception in Python; this is just a
naming convention used in these tests for clarity, and to future-proof the
tests against possible future improvements whereby certain statements or
expressions could potentially be inferred as being incapable of causing an
exception to be raised.
## A single bare `except`
Consider the following `try`/`except` block, with a single bare `except:`.
There are different types for the variable `x` in the two branches of this
block, and we can't determine which branch might have been taken from the
perspective of code following this block. The inferred type after the block's
conclusion is therefore the union of the type at the end of the `try` suite
(`str`) and the type at the end of the `except` suite (`Literal[2]`).
*Within* the `except` suite, we must infer a union of all possible "definition
states" we could have been in at any point during the `try` suite. This is
because control flow could have jumped to the `except` suite without any of the
`try`-suite definitions successfully completing, with only *some* of the
`try`-suite definitions successfully completing, or indeed with *all* of them
successfully completing. The type of `x` at the beginning of the `except` suite
in this example is therefore `Literal[1] | str`, taking into account that we
might have jumped to the `except` suite before the
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped
to the `except` suite *after* that redefinition.
```py path=union_type_inferred.py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except:
reveal_type(x) # revealed: Literal[1] | str
x = 2
reveal_type(x) # revealed: Literal[2]
reveal_type(x) # revealed: str | Literal[2]
```
If `x` has the same type at the end of both branches, however, the branches
unify and `x` is not inferred as having a union type following the
`try`/`except` block:
```py path=branches_unify_to_non_union_type.py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
x = could_raise_returns_str()
except:
x = could_raise_returns_str()
reveal_type(x) # revealed: str
```
## A non-bare `except`
For simple `try`/`except` blocks, an `except TypeError:` handler has the same
control flow semantics as an `except:` handler. An `except TypeError:` handler
will not catch *all* exceptions: if this is the only handler, it opens up the
possibility that an exception might occur that would not be handled. However,
as described in [the document on exception-handling semantics][1], that would
lead to termination of the scope. It's therefore irrelevant to consider this
possibility when it comes to control-flow analysis.
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = 2
reveal_type(x) # revealed: Literal[2]
reveal_type(x) # revealed: str | Literal[2]
```
## Multiple `except` branches
If the scope reaches the final `reveal_type` call in this example,
either the `try`-block suite of statements was executed in its entirety,
or exactly one `except` suite was executed in its entirety.
The inferred type of `x` at this point is the union of the types at the end of
the three suites:
- At the end of `try`, `type(x) == str`
- At the end of `except TypeError`, `x == 2`
- At the end of `except ValueError`, `x == 3`
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = 2
reveal_type(x) # revealed: Literal[2]
except ValueError:
reveal_type(x) # revealed: Literal[1] | str
x = 3
reveal_type(x) # revealed: Literal[3]
reveal_type(x) # revealed: str | Literal[2, 3]
```
## Exception handlers with `else` branches (but no `finally`)
If we reach the `reveal_type` call at the end of this scope,
either the `try` and `else` suites were both executed in their entireties,
or the `except` suite was executed in its entirety. The type of `x` at this
point is the union of the type at the end of the `else` suite and the type at
the end of the `except` suite:
- At the end of `else`, `x == 3`
- At the end of `except`, `x == 2`
```py path=single_except.py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = 2
reveal_type(x) # revealed: Literal[2]
else:
reveal_type(x) # revealed: str
x = 3
reveal_type(x) # revealed: Literal[3]
reveal_type(x) # revealed: Literal[2, 3]
```
For a block that has multiple `except` branches and an `else` branch, the same
principle applies. In order to reach the final `reveal_type` call,
either exactly one of the `except` suites must have been executed in its
entirety, or the `try` suite and the `else` suite must both have been executed
in their entireties:
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = 2
reveal_type(x) # revealed: Literal[2]
except ValueError:
reveal_type(x) # revealed: Literal[1] | str
x = 3
reveal_type(x) # revealed: Literal[3]
else:
reveal_type(x) # revealed: str
x = 4
reveal_type(x) # revealed: Literal[4]
reveal_type(x) # revealed: Literal[2, 3, 4]
```
## Exception handlers with `finally` branches (but no `except` branches)
A `finally` suite is *always* executed. As such, if we reach the `reveal_type`
call at the end of this example, we know that `x` *must* have been reassigned
to `2` during the `finally` suite. The type of `x` at the end of the example is
therefore `Literal[2]`:
```py path=redef_in_finally.py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
finally:
x = 2
reveal_type(x) # revealed: Literal[2]
reveal_type(x) # revealed: Literal[2]
```
If `x` was *not* redefined in the `finally` suite, however, things are somewhat
more complicated. If we reach the final `reveal_type` call,
unlike the state when we're visiting the `finally` suite,
we know that the `try`-block suite ran to completion.
This means that there are fewer possible states at this point than there were
when we were inside the `finally` block.
(Our current model does *not* correctly infer the types *inside* `finally`
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"
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
finally:
# TODO: should be Literal[1] | str
reveal_type(x) # revealed: str
reveal_type(x) # revealed: str
```
## Combining an `except` branch with a `finally` branch
As previously stated, we do not yet have accurate inference for types *inside*
`finally` suites. When we do, however, we will have to take account of the
following possibilities inside `finally` suites:
- The `try` suite could have run to completion
- Or we could have jumped from halfway through the `try` suite to an `except`
suite, and the `except` suite ran to completion
- Or we could have jumped from halfway through the `try` suite straight to the
`finally` suite due to an unhandled exception
- Or we could have jumped from halfway through the `try` suite to an
`except` suite, only for an exception raised in the `except` suite to cause
us to jump to the `finally` suite before the `except` suite ran to completion
```py path=redef_in_finally.py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
finally:
# TODO: should be `Literal[1] | str | bytes | bool`
reveal_type(x) # revealed: str | bool
x = 2
reveal_type(x) # revealed: Literal[2]
reveal_type(x) # revealed: Literal[2]
```
Now for an example without a redefinition in the `finally` suite.
As before, there *should* be fewer possibilities after completion of the
`finally` suite than there were during the `finally` suite itself.
(In some control-flow possibilities, some exceptions were merely *suspended*
during the `finally` suite; these lead to the scope's termination following the
conclusion of the `finally` suite.)
```py path=no_redef_in_finally.py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
finally:
# TODO: should be `Literal[1] | str | bytes | bool`
reveal_type(x) # revealed: str | bool
reveal_type(x) # revealed: str | bool
```
An example with multiple `except` branches and a `finally` branch:
```py path=multiple_except_branches.py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_float() -> float:
return 3.14
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
except ValueError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float`
reveal_type(x) # revealed: str | bool | float
reveal_type(x) # revealed: str | bool | float
```
## Combining `except`, `else` and `finally` branches
If the exception handler has an `else` branch, we must also take into account
the possibility that control flow could have jumped to the `finally` suite from
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"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_float() -> float:
return 3.14
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
else:
reveal_type(x) # revealed: str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float`
reveal_type(x) # revealed: bool | float
reveal_type(x) # revealed: bool | float
```
The same again, this time with multiple `except` branches:
```py path=multiple_except_branches.py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_range() -> range:
return range(42)
def could_raise_returns_slice() -> slice:
return slice(None)
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
except ValueError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
else:
reveal_type(x) # revealed: str
x = could_raise_returns_range()
reveal_type(x) # revealed: range
x = could_raise_returns_slice()
reveal_type(x) # revealed: slice
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice`
reveal_type(x) # revealed: bool | float | slice
reveal_type(x) # revealed: bool | float | slice
```
## Nested `try`/`except` blocks
It would take advanced analysis, which we are not yet capable of, to be able
to determine that an exception handler always suppresses all exceptions. This
is partly because it is possible for statements in `except`, `else` and
`finally` suites to raise exceptions as well as statements in `try` suites.
This means that if an exception handler is nested inside the `try` statement of
an enclosing exception handler, it should (at least for now) be treated the
same as any other node: as a suite containing statements that could possibly
raise exceptions, which would lead to control flow jumping out of that suite
prior to the suite running to completion.
```py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_range() -> range:
return range(42)
def could_raise_returns_slice() -> slice:
return slice(None)
def could_raise_returns_complex() -> complex:
return 3j
def could_raise_returns_bytearray() -> bytearray:
return bytearray()
class Foo: ...
class Bar: ...
def could_raise_returns_Foo() -> Foo:
return Foo()
def could_raise_returns_Bar() -> Bar:
return Bar()
x = 1
try:
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
except ValueError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
else:
reveal_type(x) # revealed: str
x = could_raise_returns_range()
reveal_type(x) # revealed: range
x = could_raise_returns_slice()
reveal_type(x) # revealed: slice
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice`
reveal_type(x) # revealed: bool | float | slice
x = 2
reveal_type(x) # revealed: Literal[2]
reveal_type(x) # revealed: Literal[2]
except:
reveal_type(x) # revealed: Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice
x = could_raise_returns_complex()
reveal_type(x) # revealed: complex
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
else:
reveal_type(x) # revealed: Literal[2]
x = could_raise_returns_Foo()
reveal_type(x) # revealed: Foo
x = could_raise_returns_Bar()
reveal_type(x) # revealed: Bar
finally:
# TODO: should be `Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice | complex | bytearray | Foo | Bar`
reveal_type(x) # revealed: bytearray | Bar
# Either one `except` branch or the `else`
# must have been taken and completed to get here:
reveal_type(x) # revealed: bytearray | Bar
```
## Nested scopes inside `try` blocks
Shadowing a variable in an inner scope has no effect on type inference of the
variable by that name in the outer scope:
```py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_range() -> range:
return range(42)
def could_raise_returns_bytearray() -> bytearray:
return bytearray()
def could_raise_returns_float() -> float:
return 3.14
x = 1
try:
def foo(param=could_raise_returns_str()):
x = could_raise_returns_str()
try:
reveal_type(x) # revealed: str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
except:
reveal_type(x) # revealed: str | bytes
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
x = could_raise_returns_float()
reveal_type(x) # revealed: float
finally:
# 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:
reveal_type(x) # revealed: Literal[1] | Literal[foo]
class Bar:
x = could_raise_returns_range()
reveal_type(x) # revealed: range
x = Bar
reveal_type(x) # revealed: Literal[Bar]
finally:
# TODO: should be `Literal[1] | Literal[foo] | Literal[Bar]`
reveal_type(x) # revealed: Literal[foo] | Literal[Bar]
reveal_type(x) # revealed: Literal[foo] | Literal[Bar]
```
[1]: https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d

View File

@@ -1,30 +1,32 @@
# Except star
TODO(Alex): Once we support `sys.version_info` branches, we can set `--target-version=py311` in these tests and the inferred type will just be `BaseExceptionGroup`
## Except\* with BaseException
```py
try:
help()
x
except* BaseException as e:
reveal_type(e) # revealed: BaseExceptionGroup
reveal_type(e) # revealed: Unknown | BaseExceptionGroup
```
## Except\* with specific exception
```py
try:
help()
x
except* OSError as e:
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
reveal_type(e) # revealed: BaseExceptionGroup
reveal_type(e) # revealed: Unknown | BaseExceptionGroup
```
## Except\* with multiple exceptions
```py
try:
help()
x
except* (TypeError, AttributeError) as e:
# TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
reveal_type(e) # revealed: BaseExceptionGroup
#TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
reveal_type(e) # revealed: Unknown | BaseExceptionGroup
```

View File

@@ -6,14 +6,23 @@
def foo() -> str:
pass
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]
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]
```
## AND
@@ -22,13 +31,21 @@ reveal_type(foo() or True) # revealed: str | Literal[True]
def foo() -> str:
pass
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[""]
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[""]
```
## Simple function calls to bool
@@ -51,12 +68,19 @@ reveal_type(x) # revealed: bool
def foo() -> str:
pass
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"]
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"]
```
## `bool()` function
@@ -66,45 +90,62 @@ reveal_type("x" or "y" and "") # revealed: Literal["x"]
```py path=a.py
redefined_builtin_bool = bool
def my_bool(x) -> bool:
return True
def my_bool(x)-> bool: pass
```
```py
from a import redefined_builtin_bool, my_bool
a = redefined_builtin_bool(0)
b = my_bool(0)
reveal_type(redefined_builtin_bool(0)) # revealed: Literal[False]
reveal_type(my_bool(0)) # revealed: bool
reveal_type(a) # revealed: Literal[False]
reveal_type(b) # revealed: bool
```
## Truthy values
```py
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]
a = bool(1)
b = bool((0,))
c = bool("NON EMPTY")
d = bool(True)
def foo(): ...
def foo(): pass
e = bool(foo)
reveal_type(bool(foo)) # revealed: Literal[True]
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]
```
## Falsy values
```py
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]
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]
```
## Ambiguous values
```py
reveal_type(bool([])) # revealed: bool
reveal_type(bool({})) # revealed: bool
reveal_type(bool(set())) # revealed: bool
a = bool([])
b = bool({})
c = bool(set())
reveal_type(a) # revealed: bool
reveal_type(b) # revealed: bool
reveal_type(c) # revealed: bool
```

View File

@@ -1,66 +0,0 @@
# 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]
```

View File

@@ -3,25 +3,21 @@
## 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: ...
class C: pass
```
## 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: ...
class C: pass
```

View File

@@ -1,8 +1,6 @@
# 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]
```

View File

@@ -1,63 +1,5 @@
# Conditional imports
## Maybe unbound
```py path=maybe_unbound.py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
y = 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]
```
## 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 # 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
```
## Reimport
```py path=c.py
@@ -65,20 +7,15 @@ 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 not emit this error
from b import f # error: [invalid-assignment] "Object of type `Literal[f, f]` is not assignable to `Literal[f, f]`"
# TODO: We should disambiguate in such cases, showing `Literal[b.f, c.f]`.
reveal_type(f) # revealed: Literal[f, f]
```
@@ -93,10 +30,6 @@ x: int
```
```py path=b.py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
from c import x
else:
@@ -105,6 +38,5 @@ else:
```py
from b import x
reveal_type(x) # revealed: int
```

View File

@@ -3,17 +3,13 @@
## Unresolved import statement
```py
import bar # error: "Cannot resolve import `bar`"
reveal_type(bar) # revealed: Unknown
import bar # error: "Cannot resolve import `bar`"
```
## Unresolved import from statement
```py
from bar import baz # error: "Cannot resolve import `bar`"
reveal_type(baz) # revealed: Unknown
from bar import baz # error: "Cannot resolve import `bar`"
```
## Unresolved import from resolved module
@@ -22,17 +18,13 @@ reveal_type(baz) # revealed: Unknown
```
```py
from a import thing # error: "Module `a` has no member `thing`"
reveal_type(thing) # revealed: Unknown
from a import thing # error: "Module `a` has no member `thing`"
```
## Resolved import of symbol from unresolved import
```py path=a.py
import foo as foo # error: "Cannot resolve import `foo`"
reveal_type(foo) # revealed: Unknown
import foo as foo # error: "Cannot resolve import `foo`"
```
Importing the unresolved import into a second file should not trigger an additional "unresolved
@@ -40,11 +32,9 @@ import" violation:
```py
from a import foo
reveal_type(foo) # revealed: Unknown
```
## No implicit shadowing
## No implicit shadowing error
```py path=b.py
x: int
@@ -53,5 +43,5 @@ x: int
```py
from b import x
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
x = 'foo' # error: "Object of type `Literal["foo"]"
```

View File

@@ -6,8 +6,7 @@
```
```py path=package/bar.py
from .foo import X # error: [unresolved-import]
from .foo import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
```
@@ -22,7 +21,6 @@ X = 42
```py path=package/bar.py
from .foo import X
reveal_type(X) # revealed: Literal[42]
```
@@ -37,7 +35,6 @@ X = 42
```py path=package/bar.py
from .foo.bar.baz import X
reveal_type(X) # revealed: Literal[42]
```
@@ -49,15 +46,13 @@ 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
```
@@ -65,7 +60,6 @@ reveal_type(X) # revealed: Unknown
```py path=package/__init__.py
from .foo import X
reveal_type(X) # revealed: Literal[42]
```
@@ -76,9 +70,8 @@ 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
@@ -92,7 +85,6 @@ X = 42
```py path=package/subpackage/subsubpackage/bar.py
from ...foo import X
reveal_type(X) # revealed: Literal[42]
```
@@ -102,13 +94,12 @@ reveal_type(X) # revealed: Literal[42]
```
```py path=package/foo.py
x # error: [unresolved-reference]
x
```
```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
@@ -123,11 +114,10 @@ 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
@@ -136,8 +126,8 @@ reveal_type(y) # revealed: Unknown
```
```py path=package/bar.py
# TODO: support submodule imports
# TODO: submodule imports possibly not supported right now?
from . import foo # error: [unresolved-import]
reveal_type(foo) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
```

View File

@@ -4,7 +4,6 @@
```py
from b import x
y = x
reveal_type(y) # revealed: int
```
@@ -17,7 +16,6 @@ x: int
```py
from b import x
y = x
reveal_type(y) # revealed: int
```

View File

@@ -1,6 +1,8 @@
# Boolean literals
```py
reveal_type(True) # revealed: Literal[True]
reveal_type(False) # revealed: Literal[False]
x = True
y = False
reveal_type(x) # revealed: Literal[True]
reveal_type(y) # revealed: Literal[False]
```

View File

@@ -1,10 +0,0 @@
# 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"]
```

View File

@@ -3,5 +3,6 @@
## Empty dictionary
```py
reveal_type({}) # revealed: dict
x = {}
reveal_type(x) # revealed: dict
```

View File

@@ -3,5 +3,6 @@
## Empty list
```py
reveal_type([]) # revealed: list
x = []
reveal_type(x) # revealed: list
```

View File

@@ -3,5 +3,6 @@
## Basic set
```py
reveal_type({1, 2}) # revealed: set
x = {1, 2}
reveal_type(x) # revealed: set
```

View File

@@ -3,15 +3,18 @@
## Empty tuple
```py
reveal_type(()) # revealed: tuple[()]
x = ()
reveal_type(x) # revealed: tuple[()]
```
## Heterogeneous tuple
```py
reveal_type((1, "a")) # revealed: tuple[Literal[1], Literal["a"]]
x = (1, 'a')
y = (1, (2, 3))
z = (x, 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]]
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]]
```

View File

@@ -7,27 +7,38 @@ x = 0
y = str()
z = False
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"]
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"]
```
## Conversion Flags
```py
string = "hello"
string = 'hello'
a = f'{string!r}'
# TODO: should be `Literal["'hello'"]`
reveal_type(f"{string!r}") # revealed: str
reveal_type(a) # revealed: str
```
## Format Specifiers
```py
a = f'{1:02}'
# TODO: should be `Literal["01"]`
reveal_type(f"{1:02}") # revealed: str
reveal_type(a) # revealed: str
```

View File

@@ -3,19 +3,24 @@
## Simple
```py
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"]
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"]
```
## Nested Quotes
```py
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"]
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"]
```

View File

@@ -17,10 +17,8 @@ async def foo():
async for x in Iterator():
pass
# TODO: should reveal `Unbound | Unknown` because `__aiter__` is not defined
# revealed: Unbound | @Todo
# error: [possibly-unresolved-reference]
reveal_type(x)
# TODO
reveal_type(x) # revealed: Unbound | @Todo
```
## Basic async for loop
@@ -35,11 +33,9 @@ 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
# error: [possibly-unresolved-reference]
# revealed: Unbound | @Todo
reveal_type(x)
reveal_type(x) # revealed: Unbound | @Todo
```

View File

@@ -14,9 +14,7 @@ class IntIterable:
for x in IntIterable():
pass
# revealed: Unbound | int
# error: [possibly-unresolved-reference]
reveal_type(x)
reveal_type(x) # revealed: Unbound | int
```
## With previous definition
@@ -30,7 +28,7 @@ class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
x = "foo"
x = 'foo'
for x in IntIterable():
pass
@@ -52,7 +50,7 @@ class IntIterable:
for x in IntIterable():
pass
else:
x = "foo"
x = 'foo'
reveal_type(x) # revealed: Literal["foo"]
```
@@ -72,7 +70,7 @@ for x in IntIterable():
if x > 5:
break
else:
x = "foo"
x = 'foo'
reveal_type(x) # revealed: int | Literal["foo"]
```
@@ -87,49 +85,38 @@ class OldStyleIterable:
for x in OldStyleIterable():
pass
# revealed: Unbound | int
# error: [possibly-unresolved-reference]
reveal_type(x)
reveal_type(x) # revealed: Unbound | int
```
## With heterogeneous tuple
```py
for x in (1, "a", b"foo"):
for x in (1, 'a', b'foo'):
pass
# revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
# error: [possibly-unresolved-reference]
reveal_type(x)
reveal_type(x) # revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
```
## 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
# revealed: Unbound | Unknown
# error: [possibly-unresolved-reference]
reveal_type(x)
reveal_type(x) # revealed: Unbound | Unknown
```
## 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
```
@@ -139,8 +126,9 @@ 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
```

View File

@@ -3,7 +3,7 @@
## Yield must be iterable
```py
class NotIterable: ...
class NotIterable: pass
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"
```

View File

@@ -3,10 +3,6 @@
## Basic While Loop
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1
while flag:
x = 2
@@ -17,27 +13,20 @@ 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:
reveal_type(x) # revealed: Literal[1, 2]
y = x
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:

View File

@@ -3,14 +3,11 @@
## `is None`
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = None if flag else 1
if x is None:
reveal_type(x) # revealed: None
# TODO the following should be simplified to 'None'
reveal_type(x) # revealed: None | Literal[1] & None
reveal_type(x) # revealed: None | Literal[1]
```
@@ -18,36 +15,15 @@ reveal_type(x) # revealed: None | Literal[1]
## `is` for other types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
class A: ...
class A:
...
x = A()
y = x if flag else None
if y is x:
reveal_type(y) # revealed: A
# TODO the following should be simplified to 'A'
reveal_type(y) # revealed: A | None & 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
```

View File

@@ -5,10 +5,6 @@
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:
@@ -20,15 +16,12 @@ 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:
reveal_type(x) # revealed: Literal[True]
# TODO the following should be `Literal[True]`
reveal_type(x) # revealed: bool & ~Literal[False]
```
## `is not` for non-singleton types
@@ -38,29 +31,10 @@ non-singleton class may occupy different addresses in memory even if
they compare equal.
```py
x = 345
y = 345
x = [1]
y = [1]
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
# TODO: should include type parameter: list[int]
reveal_type(x) # revealed: list
```

View File

@@ -1,29 +0,0 @@
# 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]
```

View File

@@ -1,76 +0,0 @@
# 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
```

View File

@@ -1,192 +0,0 @@
# 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"]
```

View File

@@ -3,11 +3,6 @@
## 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]
@@ -17,5 +12,6 @@ match x:
case None:
y = x
reveal_type(y) # revealed: Literal[0] | None
# TODO intersection simplification: should be just Literal[0] | None
reveal_type(y) # revealed: Literal[0] | None | Literal[1] & None
```

View File

@@ -0,0 +1,9 @@
# `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]
```

View File

@@ -1,32 +0,0 @@
# Builtin scope
## Conditionally global or builtin
If a builtin name is conditionally defined as a global, a name lookup should union the builtin type
with the conditionally-defined type:
```py
def returns_bool() -> bool:
return True
if returns_bool():
copyright = 1
def f():
reveal_type(copyright) # revealed: Literal[copyright] | Literal[1]
```
## Conditionally global or builtin, with annotation
Same is true if the name is annotated:
```py
def returns_bool() -> bool:
return True
if returns_bool():
copyright: int = 1
def f():
reveal_type(copyright) # revealed: Literal[copyright] | int
```

View File

@@ -3,9 +3,8 @@
## Implicit error
```py
class C: ...
C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional"
class C: pass
C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional"
```
## Explicit
@@ -13,7 +12,6 @@ C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit i
No diagnostic is raised in the case of explicit shadowing:
```py
class C: ...
class C: pass
C: int = 1
```

View File

@@ -12,15 +12,13 @@ def f(x: str):
## Implicit error
```py path=a.py
def f(): ...
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
def f(): pass
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(): ...
def f(): pass
f: int = 1
```

View File

@@ -3,14 +3,9 @@
## 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'
```

View File

@@ -6,6 +6,5 @@ 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]
```

View File

@@ -3,32 +3,13 @@
## Simple
```py
b = b"\x00abc\xff"
w = b'red' b'knot'
x = b'hello'
y = b'world' + b'!'
z = 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
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"]
```

View File

@@ -3,8 +3,7 @@
## Class getitem unbound
```py
class NotSubscriptable: ...
class NotSubscriptable: pass
a = NotSubscriptable[0] # error: "Cannot subscript object of type `Literal[NotSubscriptable]` with no `__class_getitem__` method"
```
@@ -15,7 +14,8 @@ class Identity:
def __class_getitem__(cls, item: int) -> str:
return item
reveal_type(Identity[0]) # revealed: str
a = Identity[0]
reveal_type(a) # revealed: str
```
## Class getitem union
@@ -23,17 +23,16 @@ reveal_type(Identity[0]) # revealed: str
```py
flag = True
class UnionClassGetItem:
class Identity:
if flag:
def __class_getitem__(cls, item: int) -> str:
return item
else:
def __class_getitem__(cls, item: int) -> int:
return item
reveal_type(UnionClassGetItem[0]) # revealed: str | int
a = Identity[0]
reveal_type(a) # revealed: str | int
```
## Class getitem with class union
@@ -41,18 +40,22 @@ reveal_type(UnionClassGetItem[0]) # revealed: str | int
```py
flag = True
class A:
class Identity1:
def __class_getitem__(cls, item: int) -> str:
return item
class B:
class Identity2:
def __class_getitem__(cls, item: int) -> int:
return item
x = A if flag else B
if flag:
a = Identity1
else:
a = Identity2
reveal_type(x) # revealed: Literal[A, B]
reveal_type(x[0]) # revealed: str | int
b = a[0]
reveal_type(a) # revealed: Literal[Identity1, Identity2]
reveal_type(b) # revealed: str | int
```
## Class getitem with unbound method union
@@ -61,16 +64,14 @@ reveal_type(x[0]) # revealed: str | int
flag = True
if flag:
class Spam:
class Identity:
def __class_getitem__(self, x: int) -> str:
return "foo"
pass
else:
class Spam: ...
class Identity: pass
# 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])
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
```
## TODO: Class getitem non-class union
@@ -79,15 +80,13 @@ reveal_type(Spam[42])
flag = True
if flag:
class Eggs:
class Identity:
def __class_getitem__(self, x: int) -> str:
return "foo"
pass
else:
Eggs = 1
Identity = 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
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
```

View File

@@ -3,8 +3,7 @@
## Getitem unbound
```py
class NotSubscriptable: ...
class NotSubscriptable: pass
a = NotSubscriptable()[0] # error: "Cannot subscript object of type `NotSubscriptable` with no `__getitem__` method"
```
@@ -24,7 +23,8 @@ class Identity:
def __getitem__(self, index: int) -> int:
return index
reveal_type(Identity()[0]) # revealed: int
a = Identity()[0]
reveal_type(a) # revealed: int
```
## Getitem union
@@ -34,13 +34,12 @@ flag = True
class Identity:
if flag:
def __getitem__(self, index: int) -> int:
return index
else:
def __getitem__(self, index: int) -> str:
return str(index)
reveal_type(Identity()[0]) # revealed: int | str
a = Identity()[0]
reveal_type(a) # revealed: int | str
```

View File

@@ -1,38 +0,0 @@
# List subscripts
## Indexing into lists
A list can be indexed into with:
- numbers
- slices
```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
```
## Assignments within list assignment
In assignment, we might also have a named assignment.
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
```

View File

@@ -3,29 +3,30 @@
## Simple
```py
s = "abcde"
s = 'abcde'
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"]
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[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
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
```
## Function return
```py
def int_instance() -> int: ...
def add(x: int, y: int) -> int:
return x + y
a = "abcde"[int_instance()]
a = 'abcde'[add(0, 1)]
# TODO: Support overloads... Should be `str`
reveal_type(a) # revealed: @Todo
```

View File

@@ -3,16 +3,19 @@
## Basic
```py
t = (1, "a", "b")
t = (1, 'a', 'b')
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"]
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]
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
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
```

View File

@@ -1,29 +0,0 @@
# 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`"
```

View File

@@ -3,23 +3,35 @@
## Unary Addition
```py
reveal_type(+0) # revealed: Literal[0]
reveal_type(+1) # revealed: Literal[1]
reveal_type(+True) # revealed: Literal[1]
a = +0
b = +1
c = +True
reveal_type(a) # revealed: Literal[0]
reveal_type(b) # revealed: Literal[1]
reveal_type(c) # revealed: Literal[1]
```
## Unary Subtraction
```py
reveal_type(-0) # revealed: Literal[0]
reveal_type(-1) # revealed: Literal[-1]
reveal_type(-True) # revealed: Literal[-1]
a = -0
b = -1
c = -True
reveal_type(a) # revealed: Literal[0]
reveal_type(b) # revealed: Literal[-1]
reveal_type(c) # revealed: Literal[-1]
```
## Unary Bitwise Inversion
```py
reveal_type(~0) # revealed: Literal[-1]
reveal_type(~1) # revealed: Literal[-2]
reveal_type(~True) # revealed: Literal[-2]
a = ~0
b = ~1
c = ~True
reveal_type(a) # revealed: Literal[-1]
reveal_type(b) # revealed: Literal[-2]
reveal_type(c) # revealed: Literal[-2]
```

View File

@@ -3,8 +3,10 @@
## None
```py
reveal_type(not None) # revealed: Literal[True]
reveal_type(not not None) # revealed: Literal[False]
a = not None
b = not not None
reveal_type(a) # revealed: Literal[True]
reveal_type(b) # revealed: Literal[False]
```
## Function
@@ -15,19 +17,24 @@ from typing import reveal_type
def f():
return 1
reveal_type(not f) # revealed: Literal[False]
a = not f
b = not reveal_type
reveal_type(a) # revealed: Literal[False]
# TODO Unknown should not be part of the type of typing.reveal_type
# reveal_type(not reveal_type) revealed: Literal[False]
# reveal_type(b) revealed: Literal[False]
```
## Module
```py
import b
import warnings
import b; import warnings
reveal_type(not b) # revealed: Literal[False]
reveal_type(not warnings) # revealed: Literal[False]
x = not b
z = not warnings
reveal_type(x) # revealed: Literal[False]
reveal_type(z) # revealed: Literal[False]
```
```py path=b.py
@@ -37,11 +44,6 @@ y = 1
## Union
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
p = 1
q = 3.3
@@ -55,63 +57,93 @@ else:
s = 0
t = ""
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]
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]
```
## Integer literal
```py
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]
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]
```
## Boolean literal
```py
w = True
reveal_type(w) # revealed: Literal[True]
x = False
y = not w
z = not x
reveal_type(w) # revealed: Literal[True]
reveal_type(x) # revealed: Literal[False]
reveal_type(not w) # revealed: Literal[False]
reveal_type(not x) # revealed: Literal[True]
reveal_type(y) # revealed: Literal[False]
reveal_type(z) # revealed: Literal[True]
```
## String literal
```py
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]
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]
```
## Bytes literal
```py
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]
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]
```
## Tuple
```py
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]
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]
```

View File

@@ -155,10 +155,12 @@ 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
@@ -171,10 +173,12 @@ 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
@@ -187,7 +191,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
```
@@ -196,7 +200,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
@@ -206,7 +210,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
```

View File

@@ -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);
let parsed = ParsedModule::new(parsed_raw.clone());
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);
let parsed = ParsedModule::new(parsed_raw.clone());
let stmt = &parsed.syntax().body[0];

View File

@@ -21,6 +21,5 @@ mod semantic_model;
pub(crate) mod site_packages;
mod stdlib;
pub mod types;
mod util;
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;

View File

@@ -1294,7 +1294,7 @@ mod tests {
search_paths: SearchPathSettings {
extra_paths: vec![],
src_root: src.clone(),
custom_typeshed: Some(custom_typeshed),
custom_typeshed: Some(custom_typeshed.clone()),
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),
ModuleNameIngredient::new(&db, functools_module_name.clone()),
&events,
);
assert_eq!(functools_module.search_path(), &stdlib);

View File

@@ -34,10 +34,7 @@ pub(crate) struct AstIds {
impl AstIds {
fn expression_id(&self, key: impl Into<ExpressionNodeKey>) -> ScopedExpressionId {
let key = &key.into();
*self.expressions_map.get(key).unwrap_or_else(|| {
panic!("Could not find expression ID for {key:?}");
})
self.expressions_map[&key.into()]
}
fn use_id(&self, key: impl Into<ExpressionNodeKey>) -> ScopedUseId {

View File

@@ -1,6 +1,5 @@
use std::sync::Arc;
use except_handlers::TryNodeContextStackManager;
use rustc_hash::FxHashMap;
use ruff_db::files::File;
@@ -33,23 +32,18 @@ use super::definition::{
MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
};
mod except_handlers;
pub(super) struct SemanticIndexBuilder<'db> {
// Builder state
db: &'db dyn Db,
file: File,
module: &'db ParsedModule,
scope_stack: Vec<FileScopeId>,
/// The assignments we're currently visiting, with
/// the most recent visit at the end of the Vec
current_assignments: Vec<CurrentAssignment<'db>>,
/// The assignment we're currently visiting.
current_assignment: Option<CurrentAssignment<'db>>,
/// The match case we're currently visiting.
current_match_case: Option<CurrentMatchCase<'db>>,
/// Flow states at each `break` in the current loop.
loop_break_states: Vec<FlowSnapshot>,
/// Per-scope contexts regarding nested `try`/`except` statements
try_node_context_stack_manager: TryNodeContextStackManager,
/// Flags about the file's global scope
has_future_annotations: bool,
@@ -73,10 +67,9 @@ impl<'db> SemanticIndexBuilder<'db> {
file,
module: parsed,
scope_stack: Vec::new(),
current_assignments: vec![],
current_assignment: None,
current_match_case: None,
loop_break_states: vec![],
try_node_context_stack_manager: TryNodeContextStackManager::default(),
has_future_annotations: false,
@@ -117,7 +110,6 @@ impl<'db> SemanticIndexBuilder<'db> {
kind: node.scope_kind(),
descendents: children_start..children_start,
};
self.try_node_context_stack_manager.enter_nested_scope();
let file_scope_id = self.scopes.push(scope);
self.symbol_tables.push(SymbolTableBuilder::new());
@@ -147,7 +139,6 @@ impl<'db> SemanticIndexBuilder<'db> {
let children_end = self.scopes.next_index();
let scope = &mut self.scopes[id];
scope.descendents = scope.descendents.start..children_end;
self.try_node_context_stack_manager.exit_scope();
id
}
@@ -236,10 +227,6 @@ impl<'db> SemanticIndexBuilder<'db> {
DefinitionCategory::Binding => use_def.record_binding(symbol, definition),
}
let mut try_node_stack_manager = std::mem::take(&mut self.try_node_context_stack_manager);
try_node_stack_manager.record_definition(self);
self.try_node_context_stack_manager = try_node_stack_manager;
definition
}
@@ -251,19 +238,6 @@ impl<'db> SemanticIndexBuilder<'db> {
expression
}
fn push_assignment(&mut self, assignment: CurrentAssignment<'db>) {
self.current_assignments.push(assignment);
}
fn pop_assignment(&mut self) {
let popped_assignment = self.current_assignments.pop();
debug_assert!(popped_assignment.is_some());
}
fn current_assignment(&self) -> Option<&CurrentAssignment<'db>> {
self.current_assignments.last()
}
fn add_pattern_constraint(
&mut self,
subject: &ast::Expr,
@@ -385,12 +359,12 @@ impl<'db> SemanticIndexBuilder<'db> {
self.visit_expr(&generator.iter);
self.push_scope(scope);
self.push_assignment(CurrentAssignment::Comprehension {
self.current_assignment = Some(CurrentAssignment::Comprehension {
node: generator,
first: true,
});
self.visit_expr(&generator.target);
self.pop_assignment();
self.current_assignment = None;
for expr in &generator.ifs {
self.visit_expr(expr);
@@ -400,12 +374,12 @@ impl<'db> SemanticIndexBuilder<'db> {
self.add_standalone_expression(&generator.iter);
self.visit_expr(&generator.iter);
self.push_assignment(CurrentAssignment::Comprehension {
self.current_assignment = Some(CurrentAssignment::Comprehension {
node: generator,
first: false,
});
self.visit_expr(&generator.target);
self.pop_assignment();
self.current_assignment = None;
for expr in &generator.ifs {
self.visit_expr(expr);
@@ -441,7 +415,7 @@ impl<'db> SemanticIndexBuilder<'db> {
self.pop_scope();
assert!(self.scope_stack.is_empty());
assert_eq!(&self.current_assignments, &[]);
assert!(self.current_assignment.is_none());
let mut symbol_tables: IndexVec<_, _> = self
.symbol_tables
@@ -589,7 +563,7 @@ where
}
}
ast::Stmt::Assign(node) => {
debug_assert_eq!(&self.current_assignments, &[]);
debug_assert!(self.current_assignment.is_none());
self.visit_expr(&node.value);
self.add_standalone_expression(&node.value);
for (target_index, target) in node.targets.iter().enumerate() {
@@ -599,28 +573,25 @@ where
_ => None,
};
if let Some(kind) = kind {
self.push_assignment(CurrentAssignment::Assign {
self.current_assignment = Some(CurrentAssignment::Assign {
assignment: node,
target_index,
kind,
});
}
self.visit_expr(target);
if kind.is_some() {
// only need to pop in the case where we pushed something
self.pop_assignment();
}
self.current_assignment = None;
}
}
ast::Stmt::AnnAssign(node) => {
debug_assert_eq!(&self.current_assignments, &[]);
debug_assert!(self.current_assignment.is_none());
self.visit_expr(&node.annotation);
if let Some(value) = &node.value {
self.visit_expr(value);
}
self.push_assignment(node.into());
self.current_assignment = Some(node.into());
self.visit_expr(&node.target);
self.pop_assignment();
self.current_assignment = None;
}
ast::Stmt::AugAssign(
aug_assign @ ast::StmtAugAssign {
@@ -630,11 +601,11 @@ where
value,
},
) => {
debug_assert_eq!(&self.current_assignments, &[]);
debug_assert!(self.current_assignment.is_none());
self.visit_expr(value);
self.push_assignment(aug_assign.into());
self.current_assignment = Some(aug_assign.into());
self.visit_expr(target);
self.pop_assignment();
self.current_assignment = None;
}
ast::Stmt::If(node) => {
self.visit_expr(&node.test);
@@ -702,9 +673,9 @@ where
self.visit_expr(&item.context_expr);
if let Some(optional_vars) = item.optional_vars.as_deref() {
self.add_standalone_expression(&item.context_expr);
self.push_assignment(item.into());
self.current_assignment = Some(item.into());
self.visit_expr(optional_vars);
self.pop_assignment();
self.current_assignment = None;
}
}
self.visit_body(body);
@@ -729,10 +700,10 @@ where
let pre_loop = self.flow_snapshot();
let saved_break_states = std::mem::take(&mut self.loop_break_states);
debug_assert_eq!(&self.current_assignments, &[]);
self.push_assignment(for_stmt.into());
debug_assert!(self.current_assignment.is_none());
self.current_assignment = Some(for_stmt.into());
self.visit_expr(target);
self.pop_assignment();
self.current_assignment = None;
// TODO: Definitions created by loop variables
// (and definitions created inside the body)
@@ -793,104 +764,40 @@ where
is_star,
range: _,
}) => {
// Save the state prior to visiting any of the `try` block.
//
// Potentially none of the `try` block could have been executed prior to executing
// the `except` block(s) and/or the `finally` block.
// We will merge this state with all of the intermediate
// states during the `try` block before visiting those suites.
let pre_try_block_state = self.flow_snapshot();
self.try_node_context_stack_manager.push_context();
// Visit the `try` block!
self.visit_body(body);
let mut post_except_states = vec![];
for except_handler in handlers {
let ast::ExceptHandler::ExceptHandler(except_handler) = except_handler;
let ast::ExceptHandlerExceptHandler {
name: symbol_name,
type_: handled_exceptions,
body: handler_body,
range: _,
} = except_handler;
// Take a record also of all the intermediate states we encountered
// while visiting the `try` block
let try_block_snapshots = self.try_node_context_stack_manager.pop_context();
if !handlers.is_empty() {
// Save the state immediately *after* visiting the `try` block
// but *before* we prepare for visiting the `except` block(s).
//
// We will revert to this state prior to visiting the the `else` block,
// as there necessarily must have been 0 `except` blocks executed
// if we hit the `else` block.
let post_try_block_state = self.flow_snapshot();
// Prepare for visiting the `except` block(s)
self.flow_restore(pre_try_block_state);
for state in try_block_snapshots {
self.flow_merge(state);
if let Some(handled_exceptions) = handled_exceptions {
self.visit_expr(handled_exceptions);
}
let pre_except_state = self.flow_snapshot();
let num_handlers = handlers.len();
// If `handled_exceptions` above was `None`, it's something like `except as e:`,
// which is invalid syntax. However, it's still pretty obvious here that the user
// *wanted* `e` to be bound, so we should still create a definition here nonetheless.
if let Some(symbol_name) = symbol_name {
let symbol = self.add_symbol(symbol_name.id.clone());
for (i, except_handler) in handlers.iter().enumerate() {
let ast::ExceptHandler::ExceptHandler(except_handler) = except_handler;
let ast::ExceptHandlerExceptHandler {
name: symbol_name,
type_: handled_exceptions,
body: handler_body,
range: _,
} = except_handler;
if let Some(handled_exceptions) = handled_exceptions {
self.visit_expr(handled_exceptions);
}
// If `handled_exceptions` above was `None`, it's something like `except as e:`,
// which is invalid syntax. However, it's still pretty obvious here that the user
// *wanted* `e` to be bound, so we should still create a definition here nonetheless.
if let Some(symbol_name) = symbol_name {
let symbol = self.add_symbol(symbol_name.id.clone());
self.add_definition(
symbol,
DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef {
handler: except_handler,
is_star: *is_star,
}),
);
}
self.visit_body(handler_body);
// Each `except` block is mutually exclusive with all other `except` blocks.
post_except_states.push(self.flow_snapshot());
// It's unnecessary to do the `self.flow_restore()` call for the final except handler,
// as we'll immediately call `self.flow_restore()` to a different state
// as soon as this loop over the handlers terminates.
if i < (num_handlers - 1) {
self.flow_restore(pre_except_state.clone());
}
self.add_definition(
symbol,
DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef {
handler: except_handler,
is_star: *is_star,
}),
);
}
// If we get to the `else` block, we know that 0 of the `except` blocks can have been executed,
// and the entire `try` block must have been executed:
self.flow_restore(post_try_block_state);
self.visit_body(handler_body);
}
self.visit_body(orelse);
for post_except_state in post_except_states {
self.flow_merge(post_except_state);
}
// TODO: there's lots of complexity here that isn't yet handled by our model.
// In order to accurately model the semantics of `finally` suites, we in fact need to visit
// the suite twice: once under the (current) assumption that either the `try + else` suite
// ran to completion or exactly one `except` branch ran to completion, and then again under
// the assumption that potentially none of the branches ran to completion and we in fact
// jumped from a `try`, `else` or `except` branch straight into the `finally` branch.
// This requires rethinking some fundamental assumptions semantic indexing makes.
// For more details, see:
// - https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d
// - https://github.com/astral-sh/ruff/pull/13633#discussion_r1788626702
self.visit_body(finalbody);
}
_ => {
@@ -906,7 +813,7 @@ where
match expr {
ast::Expr::Name(name_node @ ast::ExprName { id, ctx, .. }) => {
let (is_use, is_definition) = match (ctx, self.current_assignment()) {
let (is_use, is_definition) = match (ctx, self.current_assignment) {
(ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => {
// For augmented assignment, the target expression is also used.
(true, true)
@@ -917,9 +824,8 @@ where
(ast::ExprContext::Invalid, _) => (false, false),
};
let symbol = self.add_symbol(id.clone());
if is_definition {
match self.current_assignment().copied() {
match self.current_assignment {
Some(CurrentAssignment::Assign {
assignment,
target_index,
@@ -990,11 +896,12 @@ where
walk_expr(self, expr);
}
ast::Expr::Named(node) => {
debug_assert!(self.current_assignment.is_none());
// TODO walrus in comprehensions is implicitly nonlocal
self.visit_expr(&node.value);
self.push_assignment(node.into());
self.current_assignment = Some(node.into());
self.visit_expr(&node.target);
self.pop_assignment();
self.current_assignment = None;
}
ast::Expr::Lambda(lambda) => {
if let Some(parameters) = &lambda.parameters {
@@ -1012,7 +919,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);
}
}
@@ -1153,7 +1060,7 @@ where
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
#[derive(Copy, Clone, Debug)]
enum CurrentAssignment<'a> {
Assign {
assignment: &'a ast::StmtAssign,

View File

@@ -1,102 +0,0 @@
use crate::semantic_index::use_def::FlowSnapshot;
use super::SemanticIndexBuilder;
/// An abstraction over the fact that each scope should have its own [`TryNodeContextStack`]
#[derive(Debug, Default)]
pub(super) struct TryNodeContextStackManager(Vec<TryNodeContextStack>);
impl TryNodeContextStackManager {
/// Push a new [`TryNodeContextStack`] onto the stack of stacks.
///
/// Each [`TryNodeContextStack`] is only valid for a single scope
pub(super) fn enter_nested_scope(&mut self) {
self.0.push(TryNodeContextStack::default());
}
/// Pop a new [`TryNodeContextStack`] off the stack of stacks.
///
/// Each [`TryNodeContextStack`] is only valid for a single scope
pub(super) fn exit_scope(&mut self) {
let popped_context = self.0.pop();
debug_assert!(
popped_context.is_some(),
"exit_scope() should never be called on an empty stack \
(this indicates an unbalanced `enter_nested_scope()`/`exit_scope()` pair of calls)"
);
}
/// Push a [`TryNodeContext`] onto the [`TryNodeContextStack`]
/// at the top of our stack of stacks
pub(super) fn push_context(&mut self) {
self.current_try_context_stack().push_context();
}
/// Pop a [`TryNodeContext`] off the [`TryNodeContextStack`]
/// at the top of our stack of stacks. Return the Vec of [`FlowSnapshot`]s
/// recorded while we were visiting the `try` suite.
pub(super) fn pop_context(&mut self) -> Vec<FlowSnapshot> {
self.current_try_context_stack().pop_context()
}
/// Retrieve the stack that is at the top of our stack of stacks.
/// For each `try` block on that stack, push the snapshot onto the `try` block
pub(super) fn record_definition(&mut self, builder: &SemanticIndexBuilder) {
self.current_try_context_stack().record_definition(builder);
}
/// Retrieve the [`TryNodeContextStack`] that is relevant for the current scope.
fn current_try_context_stack(&mut self) -> &mut TryNodeContextStack {
self.0
.last_mut()
.expect("There should always be at least one `TryBlockContexts` on the stack")
}
}
/// The contexts of nested `try`/`except` blocks for a single scope
#[derive(Debug, Default)]
struct TryNodeContextStack(Vec<TryNodeContext>);
impl TryNodeContextStack {
/// Push a new [`TryNodeContext`] for recording intermediate states
/// while visiting a [`ruff_python_ast::StmtTry`] node that has a `finally` branch.
fn push_context(&mut self) {
self.0.push(TryNodeContext::default());
}
/// Pop a [`TryNodeContext`] off the stack. Return the Vec of [`FlowSnapshot`]s
/// recorded while we were visiting the `try` suite.
fn pop_context(&mut self) -> Vec<FlowSnapshot> {
let TryNodeContext {
try_suite_snapshots,
} = self
.0
.pop()
.expect("Cannot pop a `try` block off an empty `TryBlockContexts` stack");
try_suite_snapshots
}
/// For each `try` block on the stack, push the snapshot onto the `try` block
fn record_definition(&mut self, builder: &SemanticIndexBuilder) {
for context in &mut self.0 {
context.record_definition(builder.flow_snapshot());
}
}
}
/// Context for tracking definitions over the course of a single
/// [`ruff_python_ast::StmtTry`] node
///
/// It will likely be necessary to add more fields to this struct in the future
/// when we add more advanced handling of `finally` branches.
#[derive(Debug, Default)]
struct TryNodeContext {
try_suite_snapshots: Vec<FlowSnapshot>,
}
impl TryNodeContext {
/// Take a record of what the internal state looked like after a definition
fn record_definition(&mut self, snapshot: FlowSnapshot) {
self.try_suite_snapshots.push(snapshot);
}
}

View File

@@ -47,13 +47,7 @@ impl<'db> Definition<'db> {
self.kind(db).category().is_binding()
}
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
/// Return true if this is a 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()
@@ -296,7 +290,7 @@ impl DefinitionNodeRef<'_> {
handler,
is_star,
}) => DefinitionKind::ExceptHandler(ExceptHandlerDefinitionKind {
handler: AstNodeRef::new(parsed, handler),
handler: AstNodeRef::new(parsed.clone(), handler),
is_star,
}),
}

View File

@@ -175,6 +175,7 @@ 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> {
@@ -204,7 +205,7 @@ mod tests {
let model = SemanticModel::new(&db, foo);
let ty = function.ty(&model);
assert!(ty.is_function_literal());
assert!(matches!(ty, Type::Function(_)));
Ok(())
}
@@ -221,7 +222,7 @@ mod tests {
let model = SemanticModel::new(&db, foo);
let ty = class.ty(&model);
assert!(ty.is_class_literal());
assert!(matches!(ty, Type::Class(_)));
Ok(())
}
@@ -242,7 +243,7 @@ mod tests {
let model = SemanticModel::new(&db, bar);
let ty = alias.ty(&model);
assert!(ty.is_class_literal());
assert!(matches!(ty, Type::Class(_)));
Ok(())
}

View File

@@ -37,13 +37,6 @@ fn core_module_symbol_ty<'db>(
) -> Type<'db> {
resolve_module(db, &core_module.name())
.map(|module| global_symbol_ty(db, module.file(), symbol))
.map(|ty| {
if ty.is_unbound() {
ty
} else {
ty.replace_unbound_with(db, Type::Never)
}
})
.unwrap_or(Type::Unbound)
}

View File

@@ -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, HasTy, Module, SemanticModel};
use crate::{Db, FxOrderSet, Module};
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
@@ -60,7 +60,7 @@ fn symbol_ty_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymb
use_def.public_bindings(symbol),
use_def
.public_may_be_unbound(symbol)
.then_some(Type::Unbound),
.then_some(Type::Unknown),
))
} else {
None
@@ -79,7 +79,7 @@ fn symbol_ty_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymb
}
}
/// Shorthand for `symbol_ty_by_id` that takes a symbol name instead of an ID.
/// Shorthand for `symbol_ty` that takes a symbol name instead of an ID.
fn symbol_ty<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Type<'db> {
let table = symbol_table(db, scope);
table
@@ -148,18 +148,18 @@ fn bindings_ty<'db>(
binding,
constraints,
}| {
let mut constraint_tys = constraints
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
.peekable();
let mut constraint_tys =
constraints.filter_map(|constraint| narrowing_constraint(db, constraint, binding));
let binding_ty = binding_ty(db, binding);
if constraint_tys.peek().is_some() {
constraint_tys
.fold(
IntersectionBuilder::new(db).add_positive(binding_ty),
IntersectionBuilder::add_positive,
)
.build()
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()
} else {
binding_ty
}
@@ -211,13 +211,7 @@ 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) {
// 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() {
if !first.is_equivalent_to(db, other) {
conflicting.push(other);
}
builder = builder.add(other);
@@ -261,11 +255,11 @@ pub enum Type<'db> {
/// `Todo` would change the output type.
Todo,
/// A specific function object
FunctionLiteral(FunctionType<'db>),
Function(FunctionType<'db>),
/// A specific module object
ModuleLiteral(File),
Module(File),
/// A specific class object
ClassLiteral(ClassType<'db>),
Class(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
@@ -298,38 +292,28 @@ impl<'db> Type<'db> {
matches!(self, Type::Never)
}
pub const fn is_todo(&self) -> bool {
matches!(self, Type::Todo)
}
pub const fn into_class_literal_type(self) -> Option<ClassType<'db>> {
pub const fn into_class_type(self) -> Option<ClassType<'db>> {
match self {
Type::ClassLiteral(class_type) => Some(class_type),
Type::Class(class_type) => Some(class_type),
_ => None,
}
}
#[track_caller]
pub fn expect_class_literal(self) -> ClassType<'db> {
self.into_class_literal_type()
.expect("Expected a Type::ClassLiteral variant")
pub fn expect_class(self) -> ClassType<'db> {
self.into_class_type()
.expect("Expected a Type::Class variant")
}
pub const fn is_class_literal(&self) -> bool {
matches!(self, Type::ClassLiteral(..))
}
pub const fn into_module_literal_type(self) -> Option<File> {
pub const fn into_module_type(self) -> Option<File> {
match self {
Type::ModuleLiteral(file) => Some(file),
Type::Module(file) => Some(file),
_ => None,
}
}
#[track_caller]
pub fn expect_module_literal(self) -> File {
self.into_module_literal_type()
.expect("Expected a Type::ModuleLiteral variant")
pub fn expect_module(self) -> File {
self.into_module_type()
.expect("Expected a Type::Module variant")
}
pub const fn into_union_type(self) -> Option<UnionType<'db>> {
@@ -339,16 +323,11 @@ 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),
@@ -356,27 +335,21 @@ 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_literal_type(self) -> Option<FunctionType<'db>> {
pub const fn into_function_type(self) -> Option<FunctionType<'db>> {
match self {
Type::FunctionLiteral(function_type) => Some(function_type),
Type::Function(function_type) => Some(function_type),
_ => None,
}
}
#[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 fn expect_function(self) -> FunctionType<'db> {
self.into_function_type()
.expect("Expected a Type::Function variant")
}
pub const fn into_int_literal_type(self) -> Option<i64> {
@@ -386,20 +359,11 @@ 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,
@@ -417,14 +381,24 @@ impl<'db> Type<'db> {
Type::Union(union) => {
union.map(db, |element| element.replace_unbound_with(db, replacement))
}
_ => *self,
ty => *ty,
}
}
pub fn is_stdlib_symbol(&self, db: &'db dyn Db, module_name: &str, name: &str) -> bool {
match self {
Type::ClassLiteral(class) => class.is_stdlib_symbol(db, module_name, name),
Type::FunctionLiteral(function) => function.is_stdlib_symbol(db, module_name, name),
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
_ => false,
}
}
@@ -441,13 +415,6 @@ 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) =>
{
true
}
(Type::IntLiteral(_), Type::Instance(class)) if class.is_known(db, KnownClass::Int) => {
true
}
@@ -462,22 +429,12 @@ 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(self_class), Type::Instance(target_class)) => {
self_class.is_subclass_of(db, target_class)
}
(_, Type::Instance(class)) if class.is_known(db, KnownClass::Object) => true,
(Type::Instance(class), _) if class.is_known(db, KnownClass::Object) => false,
// TODO
_ => false,
}
@@ -487,9 +444,6 @@ impl<'db> Type<'db> {
///
/// [assignable to]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
if self.is_equivalent_to(db, target) {
return true;
}
match (self, target) {
(Type::Unknown | Type::Any | Type::Todo, _) => true,
(_, Type::Unknown | Type::Any | Type::Todo) => true,
@@ -509,155 +463,11 @@ 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`
/// for more complicated types that are actually singletons.
pub(crate) fn is_singleton(self) -> bool {
pub(crate) fn is_singleton(self, db: &'db dyn Db) -> bool {
match self {
Type::Any
| Type::Never
@@ -674,14 +484,15 @@ impl<'db> Type<'db> {
// are both of type Literal[345], for example.
false
}
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
// should not be relied on for type narrowing, so we do not treat it as one.
// See:
// https://docs.python.org/3/reference/expressions.html#parenthesized-forms
false
Type::None | Type::BooleanLiteral(_) | Type::Function(..) | Type::Class(..) | Type::Module(..) => true,
Type::Tuple(tuple) => {
// We deliberately deviate from the language specification [1] here and claim
// that the empty tuple type is a singleton type. The reasoning is that `()`
// is often used as a sentinel value in user code. Declaring the empty tuple to
// be of singleton type allows us to narrow types in `is not ()` conditionals.
//
// [1] https://docs.python.org/3/reference/expressions.html#parenthesized-forms
tuple.elements(db).is_empty()
}
Type::Union(..) => {
// A single-element union, where the sole element was a singleton, would itself
@@ -690,67 +501,19 @@ impl<'db> Type<'db> {
false
}
Type::Intersection(..) => {
// Here, we assume that all intersection types that are singletons would have
// been reduced to a different form via [`IntersectionBuilder::build`] by now.
// For example:
// Intersection types are hard to analyze. The following types are technically
// all singleton types, but it is not straightforward to compute this. Again,
// we simply return false.
//
// bool & ~Literal[False] = Literal[True]
// None & (None | int) = None | None & int = None
// bool & ~Literal[False]`
// None & (None | int)
// (A | B) & (B | C) with A, B, C disjunct and B a singleton
//
false
}
}
}
/// 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>)`,
@@ -778,12 +541,12 @@ impl<'db> Type<'db> {
// TODO: attribute lookup on None type
Type::Todo
}
Type::FunctionLiteral(_) => {
Type::Function(_) => {
// TODO: attribute lookup on function type
Type::Todo
}
Type::ModuleLiteral(file) => global_symbol_ty(db, *file, name),
Type::ClassLiteral(class) => class.class_member(db, name),
Type::Module(file) => global_symbol_ty(db, *file, name),
Type::Class(class) => class.class_member(db, name),
Type::Instance(_) => {
// TODO MRO? get_own_instance_member, get_instance_member
Type::Todo
@@ -831,9 +594,9 @@ impl<'db> Type<'db> {
Truthiness::Ambiguous
}
Type::None => Truthiness::AlwaysFalse,
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
Type::ClassLiteral(_) => {
Type::Function(_) => Truthiness::AlwaysTrue,
Type::Module(_) => Truthiness::AlwaysTrue,
Type::Class(_) => {
// 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
@@ -878,19 +641,16 @@ 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::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))
}
}
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),
),
},
// TODO annotated return type on `__new__` or metaclass `__call__`
Type::ClassLiteral(class) => {
Type::Class(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
@@ -950,7 +710,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)),
};
}
@@ -1010,7 +770,7 @@ impl<'db> Type<'db> {
Type::Unknown => Type::Unknown,
Type::Unbound => Type::Unknown,
Type::Never => Type::Never,
Type::ClassLiteral(class) => Type::Instance(*class),
Type::Class(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,
@@ -1018,9 +778,9 @@ impl<'db> Type<'db> {
// since they already indicate that the object is an instance of some kind:
Type::BooleanLiteral(_)
| Type::BytesLiteral(_)
| Type::FunctionLiteral(_)
| Type::Function(_)
| Type::Instance(_)
| Type::ModuleLiteral(_)
| Type::Module(_)
| Type::IntLiteral(_)
| Type::StringLiteral(_)
| Type::Tuple(_)
@@ -1036,17 +796,17 @@ impl<'db> Type<'db> {
match self {
Type::Unbound => Type::Unbound,
Type::Never => Type::Never,
Type::Instance(class) => Type::ClassLiteral(*class),
Type::Instance(class) => Type::Class(*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::FunctionLiteral(_) => KnownClass::FunctionType.to_class(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class(db),
Type::Function(_) => KnownClass::FunctionType.to_class(db),
Type::Module(_) => 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::ClassLiteral(_) => KnownClass::Type.to_class(db),
Type::Class(_) => 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]`?
@@ -1397,7 +1157,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);
@@ -1608,10 +1368,6 @@ 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
@@ -1620,8 +1376,6 @@ impl<'db> FunctionType<'db> {
pub enum KnownFunction {
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
RevealType,
/// `builtins.isinstance`
IsInstance,
}
#[salsa::interned]
@@ -1665,29 +1419,7 @@ impl<'db> ClassType<'db> {
class_stmt_node
.bases()
.iter()
.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,
})
.map(move |base_expr| definition_expression_ty(db, definition, base_expr))
}
/// Returns the class member of this class named `name`.
@@ -1696,8 +1428,7 @@ impl<'db> ClassType<'db> {
pub fn class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
let member = self.own_class_member(db, name);
if !member.is_unbound() {
// TODO diagnostic if maybe unbound?
return member.replace_unbound_with(db, Type::Never);
return member;
}
self.inherited_class_member(db, name)
@@ -1810,8 +1541,8 @@ impl<'db> TupleType<'db> {
#[cfg(test)]
mod tests {
use super::{
builtins_symbol_ty, BytesLiteralType, IntersectionBuilder, StringLiteralType, Truthiness,
TupleType, Type, UnionType,
builtins_symbol_ty, BytesLiteralType, StringLiteralType, Truthiness, TupleType, Type,
UnionType,
};
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
@@ -1849,13 +1580,12 @@ mod tests {
None,
Any,
IntLiteral(i64),
BooleanLiteral(bool),
BoolLiteral(bool),
StringLiteral(&'static str),
LiteralString,
BytesLiteral(&'static str),
BuiltinInstance(&'static str),
Union(Vec<Ty>),
Intersection { pos: Vec<Ty>, neg: Vec<Ty> },
Tuple(Vec<Ty>),
}
@@ -1868,23 +1598,13 @@ mod tests {
Ty::Any => Type::Any,
Ty::IntLiteral(n) => Type::IntLiteral(n),
Ty::StringLiteral(s) => Type::StringLiteral(StringLiteralType::new(db, s)),
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
Ty::BoolLiteral(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))
@@ -1907,7 +1627,6 @@ mod tests {
#[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("bytes"))]
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::Unknown, Ty::BuiltinInstance("str")]))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
fn is_assignable_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
@@ -1924,24 +1643,13 @@ 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)));
@@ -1954,8 +1662,6 @@ 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) {
@@ -1963,32 +1669,6 @@ 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)])
@@ -1999,138 +1679,26 @@ 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::BooleanLiteral(true))]
#[test_case(Ty::BooleanLiteral(false))]
#[test_case(Ty::BoolLiteral(true))]
#[test_case(Ty::BoolLiteral(false))]
#[test_case(Ty::Tuple(vec![]))]
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));
assert!(from.into_type(&db).is_singleton(&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::BooleanLiteral(true)]))]
#[test_case(Ty::Tuple(vec![Ty::None, Ty::BoolLiteral(true)]))]
fn is_not_singleton(from: Ty) {
let db = setup_db();
assert!(!from.into_type(&db).is_singleton());
assert!(!from.into_type(&db).is_singleton(&db));
}
#[test_case(Ty::IntLiteral(1); "is_int_literal_truthy")]
@@ -2162,8 +1730,8 @@ mod tests {
}
#[test_case(Ty::IntLiteral(1), Ty::StringLiteral("1"))]
#[test_case(Ty::BooleanLiteral(true), Ty::StringLiteral("True"))]
#[test_case(Ty::BooleanLiteral(false), Ty::StringLiteral("False"))]
#[test_case(Ty::BoolLiteral(true), Ty::StringLiteral("True"))]
#[test_case(Ty::BoolLiteral(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"))]
@@ -2174,8 +1742,8 @@ mod tests {
}
#[test_case(Ty::IntLiteral(1), Ty::StringLiteral("1"))]
#[test_case(Ty::BooleanLiteral(true), Ty::StringLiteral("True"))]
#[test_case(Ty::BooleanLiteral(false), Ty::StringLiteral("False"))]
#[test_case(Ty::BoolLiteral(true), Ty::StringLiteral("True"))]
#[test_case(Ty::BoolLiteral(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"))]

View File

@@ -216,145 +216,76 @@ impl<'db> InnerIntersectionBuilder<'db> {
}
/// Adds a positive type to this intersection.
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);
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));
}
for neg in other.negative(db) {
self.add_negative(db, *neg);
_ => {
if !self.negative.remove(&ty) {
self.positive.insert(ty);
};
}
} 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, 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);
}
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));
}
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));
}
_ => {
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);
if !self.positive.remove(&ty) {
self.negative.insert(ty);
};
}
}
}
fn simplify_unbound(&mut self) {
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();
}
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_unbound();
self.simplify();
match (self.positive.len(), self.negative.len()) {
(0, 0) => KnownClass::Object.to_instance(db),
(0, 0) => Type::Never,
(1, 0) => self.positive[0],
_ => {
self.positive.shrink_to_fit();
@@ -371,10 +302,9 @@ mod tests {
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::types::{KnownClass, StringLiteralType, UnionBuilder};
use crate::types::{KnownClass, UnionBuilder};
use crate::ProgramSettings;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use test_case::test_case;
fn setup_db() -> TestDb {
let db = TestDb::new();
@@ -444,12 +374,6 @@ mod tests {
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
assert_eq!(union.elements(&db), &[bool_instance_ty, t3]);
let result_ty = UnionType::from_elements(&db, [bool_instance_ty, t0]);
assert_eq!(result_ty, bool_instance_ty);
let result_ty = UnionType::from_elements(&db, [t0, bool_instance_ty]);
assert_eq!(result_ty, bool_instance_ty);
}
#[test]
@@ -526,15 +450,6 @@ 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();
@@ -552,7 +467,7 @@ mod tests {
.expect_intersection();
assert_eq!(intersection.pos_vec(&db), &[t2, ta]);
assert_eq!(intersection.neg_vec(&db), &[]);
assert_eq!(intersection.neg_vec(&db), &[t1]);
}
#[test]
@@ -560,7 +475,7 @@ mod tests {
let db = setup_db();
let ta = Type::Any;
let t1 = Type::IntLiteral(1);
let t2 = KnownClass::Int.to_instance(&db);
let t2 = Type::IntLiteral(2);
let i0 = IntersectionBuilder::new(&db)
.add_positive(ta)
.add_negative(t1)
@@ -571,8 +486,8 @@ mod tests {
.build()
.expect_intersection();
assert_eq!(intersection.pos_vec(&db), &[ta, t1]);
assert_eq!(intersection.neg_vec(&db), &[]);
assert_eq!(intersection.pos_vec(&db), &[t2, t1]);
assert_eq!(intersection.neg_vec(&db), &[ta]);
}
#[test]
@@ -653,269 +568,11 @@ 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);
}
}

View File

@@ -34,8 +34,8 @@ impl Display for DisplayType<'_> {
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::ClassLiteral(_)
| Type::FunctionLiteral(_)
| Type::Class(_)
| Type::Function(_)
) {
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::ModuleLiteral(file) => {
Type::Module(file) => {
write!(f, "<module '{:?}'>", file.path(self.db))
}
// TODO functions and classes should display using a fully qualified name
Type::ClassLiteral(class) => f.write_str(class.name(self.db)),
Type::Class(class) => f.write_str(class.name(self.db)),
Type::Instance(class) => f.write_str(class.name(self.db)),
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
Type::Function(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 condensed-display types by kind.
let mut grouped_condensed_kinds = FxHashMap::default();
// Group literal types by kind.
let mut grouped_literals = FxHashMap::default();
for element in elements {
if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) {
grouped_condensed_kinds
.entry(kind)
if let Ok(literal_kind) = LiteralTypeKind::try_from(*element) {
grouped_literals
.entry(literal_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(kind) = CondensedDisplayTypeKind::try_from(*element) {
let Some(mut condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
if let Ok(literal_kind) = LiteralTypeKind::try_from(*element) {
let Some(mut literals) = grouped_literals.remove(&literal_kind) else {
continue;
};
if kind == CondensedDisplayTypeKind::Int {
condensed_kind.sort_unstable_by_key(|ty| ty.expect_int_literal());
if literal_kind == LiteralTypeKind::IntLiteral {
literals.sort_unstable_by_key(|ty| ty.expect_int_literal());
}
join.entry(&DisplayLiteralGroup {
literals: condensed_kind,
literals,
db: self.db,
});
} else {
@@ -152,7 +152,7 @@ impl Display for DisplayUnionType<'_> {
join.finish()?;
debug_assert!(grouped_condensed_kinds.is_empty());
debug_assert!(grouped_literals.is_empty());
Ok(())
}
@@ -179,31 +179,25 @@ 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 CondensedDisplayTypeKind {
enum LiteralTypeKind {
Class,
Function,
Int,
String,
Bytes,
IntLiteral,
StringLiteral,
BytesLiteral,
}
impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
impl TryFrom<Type<'_>> for LiteralTypeKind {
type Error = ();
fn try_from(value: Type<'_>) -> Result<Self, Self::Error> {
match value {
Type::ClassLiteral(_) => Ok(Self::Class),
Type::FunctionLiteral(_) => Ok(Self::Function),
Type::IntLiteral(_) => Ok(Self::Int),
Type::StringLiteral(_) => Ok(Self::String),
Type::BytesLiteral(_) => Ok(Self::Bytes),
Type::Class(_) => Ok(Self::Class),
Type::Function(_) => Ok(Self::Function),
Type::IntLiteral(_) => Ok(Self::IntLiteral),
Type::StringLiteral(_) => Ok(Self::StringLiteral),
Type::BytesLiteral(_) => Ok(Self::BytesLiteral),
_ => Err(()),
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,8 @@ 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, KnownFunction, Type, UnionBuilder,
};
use crate::types::{infer_expression_types, IntersectionBuilder, Type};
use crate::Db;
use itertools::Itertools;
use ruff_python_ast as ast;
use rustc_hash::FxHashMap;
use std::sync::Arc;
@@ -62,28 +59,6 @@ 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> {
@@ -112,15 +87,10 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
}
fn evaluate_expression_constraint(&mut self, expression: Expression<'db>) {
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
if let ast::Expr::Compare(expr_compare) = expression.node_ref(self.db).node() {
self.add_expr_compare(expr_compare, expression);
}
// TODO other test expression kinds
}
fn evaluate_pattern_constraint(&mut self, pattern: PatternConstraint<'db>) {
@@ -172,30 +142,22 @@ 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);
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));
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));
match op {
ast::CmpOp::IsNot => {
if comp_ty.is_singleton() {
if comp_ty.is_singleton(self.db) {
let ty = IntersectionBuilder::new(self.db)
.add_negative(comp_ty)
.build();
@@ -207,14 +169,6 @@ 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
}
@@ -223,33 +177,6 @@ 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,

View File

@@ -1 +0,0 @@
pub(crate) mod subscript;

View File

@@ -1,83 +0,0 @@
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)
);
}
}

View File

@@ -1,24 +1,14 @@
use dir_test::{dir_test, Fixture};
use red_knot_test::run;
use std::path::Path;
use std::path::PathBuf;
/// See `crates/red_knot_test/README.md` for documentation on these tests.
#[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")
#[rstest::rstest]
fn mdtest(#[files("resources/mdtest/**/*.md")] path: PathBuf) {
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources")
.join("mdtest")
.canonicalize()
.unwrap();
let relative_path = path
.strip_prefix(crate_dir.to_str().unwrap())
.unwrap_or(path);
run(Path::new(path), relative_path);
let title = path.strip_prefix(crate_dir).unwrap();
run(&path, title.as_os_str().to_str().unwrap());
}

View File

@@ -1,8 +1,6 @@
//! 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;
@@ -121,8 +119,6 @@ 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>,

View File

@@ -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, notebook);
session.open_notebook_document(params.notebook_document.uri.clone(), notebook);
match path {
AnySystemPath::System(path) => {

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