Compare commits

...

32 Commits

Author SHA1 Message Date
David Peter
242d9b163d [red-knot] Eliminate None in equality-comparison narrowing 2025-04-22 21:47:27 +02:00
Carl Meyer
3872d57463 [red-knot] add regression test for fixed cycle panic (#17535)
Add a regression test for the cycle documented in
https://github.com/astral-sh/ruff/issues/14767, which no longer panics
(or even causes a cycle at all.)

Fixes https://github.com/astral-sh/ruff/issues/14767
2025-04-22 09:20:53 -07:00
Carl Meyer
27ada26ddb [red-knot] fix unions of literals, again (#17534)
## Summary

#17451 was incomplete. `AlwaysFalsy` and `AlwaysTruthy` are not the only
two types that are super-types of some literals (of a given kind) and
not others. That set also includes intersections containing
`AlwaysTruthy` or `AlwaysFalsy`, and intersections containing literal
types of the same kind. Cover these cases as well.

Fixes #17478.

## Test Plan

Added mdtests.

`QUICKCHECK_TESTS=1000000 cargo test -p red_knot_python_semantic --
--ignored types::property_tests::stable` failed on both
`all_fully_static_type_pairs_are_subtypes_of_their_union` and
`all_type_pairs_are_assignable_to_their_union` prior to this PR, passes
after it.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-22 16:12:52 +00:00
Andrew Gallant
810478f68b red_knot_python_semantic: remove last vestige of old diagnostics! 2025-04-22 12:08:03 -04:00
Andrew Gallant
17f799424a red_knot_python_semantic: migrate types to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
c12640fea8 red_knot_python_semantic: migrate types/diagnostic to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
3796b13ea2 red_knot_python_semantic: migrate types/call/bind to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
ad5a659f29 red_knot_python_semantic: migrate types/string_annotation to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
27a377f077 red_knot_python_semantic: migrate types/infer to new diagnostic model
I gave up trying to do this one lint at a time and just (mostly)
mechanically translated this entire file in one go.

Generally the messages stay the same (with most moving from an
annotation message to the diagnostic's main message). I added a couple
of `info` sub-diagnostics where it seemed to be the obvious intent.
2025-04-22 12:08:03 -04:00
Andrew Gallant
b8b624d890 red_knot_python_semantic: migrate INVALID_ASSIGNMENT for inference
This finishes the migration for the `INVALID_ASSIGNMENT` lint.

Notice how I'm steadily losing steam in terms of actually improving the
diagnostics. This change is more mechanical, because taking the time to
revamp every diagnostic is a ton of effort. Probably future migrations
will be similar unless there are easy pickings.
2025-04-22 12:08:03 -04:00
Andrew Gallant
6dc2d29966 red_knot_python_semantic: migrate INVALID_ASSIGNMENT for shadowing
We mostly keep things the same here, but the message has been moved from
the annotation to the diagnostic's top-line message. I think this is
perhaps a little worse, but some bigger improvements could be made here.
Indeed, we could perhaps even add a "fix" here.
2025-04-22 12:08:03 -04:00
Andrew Gallant
890ba725d9 red_knot_python_semantic: migrate INVALID_ASSIGNMENT for unpacking
This moves all INVALID_ASSIGNMENT lints related to unpacking over to the new
diagnostic model.

While we're here, we improve the diagnostic a bit by adding a secondary
annotation covering where the value is. We also split apart the original
singular message into one message for the diagnostic and the "expected
versus got" into annotation messages.
2025-04-22 12:08:03 -04:00
Andrew Gallant
298f43f34e red_knot_python_semantic: add invalid assignment diagnostic snapshot
This tests the diagnostic rendering of a case that wasn't previously
covered by snapshots: when unpacking fails because there are too few
values, but where the left hand side can tolerate "N or more." In the
code, this is a distinct diagnostic, so we capture it here.

(Sorry about the diff here, but it made sense to rename the other
sections and that changes the name of the snapshot file.)
2025-04-22 12:08:03 -04:00
Andrew Gallant
3b300559ab red_knot_python_semantic: remove #[must_use] on diagnostic guard constructor
I believe this was an artifact of an older iteration of the diagnostic
reporting API. But this is strictly not necessary now, and indeed, might
even be annoying. It is okay, but perhaps looks a little odd, to do
`builder.into_diagnostic("...")` if you don't want to add anything else
to the diagnostic.
2025-04-22 12:08:03 -04:00
Andrew Gallant
14f71ceb83 red_knot_python_semantic: add helper method for creating a secondary annotation
I suspect this will be used pretty frequently (I wanted it
immediately). And more practically, this avoids needing to
import `Annotation` to create it.
2025-04-22 12:08:03 -04:00
David Peter
4775719abf [red-knot] mypy_primer: larger depot runner (#17547)
## Summary

A switch from 16 to 32 cores reduces the `mypy_primer` CI time from
3.5-4 min to 2.5-3 min. There's also a 64-core runner, but the 4 min ->
3 min change when doubling the cores once does suggest that it doesn't
parallelize *this* well.
2025-04-22 17:36:13 +02:00
Alex Waygood
6bdffc3cbf [red-knot] Consider two instance types disjoint if the underlying classes have disjoint metaclasses (#17545) 2025-04-22 15:14:10 +01:00
Aria Desires
775815ef22 Update cargo-dist and apply config improvements (#17453) 2025-04-22 10:05:15 -04:00
Carl Meyer
0299a52fb1 [red-knot] Add list of failing/slow ecosystem projects (#17474)
## Summary

I ran red-knot on every project in mypy-primer. I moved every project
where red-knot ran to completion (fast enough, and mypy-primer could
handle its output) into `good.txt`, so it will run in our CI.

The remaining projects I left listed in `bad.txt`, with a comment
summarizing the failure mode (a few don't fail, they are just slow -- on
a debug build, at least -- or output too many diagnostics for
mypy-primer to handle.)

We will now run CI on 109 projects; 34 are left in `bad.txt`.

## Test Plan

CI on this PR!

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-04-22 14:15:36 +02:00
David Peter
83d5ad8983 [red-knot] mypy_primer: extend ecosystem checks (#17544)
## Summary

Takes the `good.txt` changes from #17474, and removes the following
projects:
- arrow (not part of mypy_primer upstream)
- freqtrade, hydpy, ibis, pandera, xarray (saw panics locally, all
related to try_metaclass cycles)

Increases the mypy_primer CI run time to ~4 min.

## Test Plan

Three successful CI runs.
2025-04-22 13:39:42 +02:00
Alex Waygood
ae6fde152c [red-knot] Move InstanceType to its own submodule (#17525) 2025-04-22 11:34:46 +00:00
David Peter
d2b20f7367 [red-knot] mypy_primer: capture backtraces (#17543)
## Summary

`mypy_primer` is not deterministic (we pin `mypy_primer` itself, but
projects change over time and we just pull in the latest version). We've
also seen occasional panics being caught in `mypy_primer` runs, so this
is trying to make these CI failures more helpful.
2025-04-22 12:05:57 +02:00
David Peter
38a3b056e3 [red-knot] mypy_primer: Use upstream repo (#17500)
## Summary

Switch to the official version of
[`mypy_primer`](https://github.com/hauntsaninja/mypy_primer), now that
Red Knot support has been upstreamed (see
https://github.com/hauntsaninja/mypy_primer/pull/138,
https://github.com/hauntsaninja/mypy_primer/pull/135,
https://github.com/hauntsaninja/mypy_primer/pull/151,
https://github.com/hauntsaninja/mypy_primer/pull/155).

## Test Plan

Locally and in CI
2025-04-22 11:55:16 +02:00
David Peter
37a0836bd2 [red-knot] typing.dataclass_transform (#17445)
## Summary

* Add initial support for `typing.dataclass_transform`
* Support decorating a function decorator with `@dataclass_transform(…)`
(used by `attrs`, `strawberry`)
* Support decorating a metaclass with `@dataclass_transform(…)` (used by
`pydantic`, but doesn't work yet, because we don't seem to model
`__new__` calls correctly?)
* *No* support yet for decorating base classes with
`@dataclass_transform(…)`. I haven't figured out how this even supposed
to work. And haven't seen it being used.
* Add `strawberry` as an ecosystem project, as it makes heavy use of
`@dataclass_transform`

## Test Plan

New Markdown tests
2025-04-22 10:33:02 +02:00
renovate[bot]
f83295fe51 Update dependency react-resizable-panels to v2.1.8 (#17513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:30:07 +02:00
renovate[bot]
c4581788b2 Update dependency smol-toml to v1.3.3 (#17505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:19:55 +02:00
renovate[bot]
2894aaa943 Update dependency uuid to v11.1.0 (#17517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:18:54 +02:00
renovate[bot]
ed4866a00b Update actions/setup-node action to v4.4.0 (#17514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:18:13 +02:00
Matthew Mckee
9b5fe51b32 [red-knot] Fix variable name (#17532) 2025-04-21 17:20:04 -07:00
Matthew Mckee
53ffe7143f [red-knot] Add basic subtyping between class literal and callable (#17469)
## Summary

This covers step 1 from
https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable

Part of #17343

## Test Plan

Update is_subtype_of.md and is_assignable_to.md

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-21 22:29:36 +00:00
Hans
21561000b1 [pyupgrade] Add fix safety section to docs (UP030) (#17443)
## Summary

add fix safety section to format_literals, for #15584
2025-04-21 14:14:58 -04:00
w0nder1ng
9c0772d8f0 [perflint] Allow list function calls to be replaced with a comprehension (PERF401) (#17519)
This is an implementation of the discussion from #16719. 

This change will allow list function calls to be replaced with
comprehensions:

```python
result = list()
for i in range(3):
    result.append(i + 1)
# becomes
result = [i + 1 for i in range(3)]
```

I added a new test to `PERF401.py` to verify that this fix will now work
for `list()`.
2025-04-21 13:29:24 -04:00
95 changed files with 2719 additions and 1668 deletions

View File

@@ -6,5 +6,6 @@ self-hosted-runner:
labels:
- depot-ubuntu-latest-8
- depot-ubuntu-22.04-16
- depot-ubuntu-22.04-32
- github-windows-2025-x86_64-8
- github-windows-2025-x86_64-16

View File

@@ -346,7 +346,7 @@ jobs:
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: "npm"
@@ -821,7 +821,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"

View File

@@ -21,11 +21,12 @@ env:
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
jobs:
mypy_primer:
name: Run mypy_primer
runs-on: depot-ubuntu-22.04-16
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -45,7 +46,7 @@ jobs:
- name: Install mypy_primer
run: |
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v5"
uv tool install "git+https://github.com/hauntsaninja/mypy_primer@ebaa9fd27b51a278873b63676fd25490cec6823b"
- name: Run mypy_primer
shell: bash

View File

@@ -35,7 +35,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"

View File

@@ -45,7 +45,7 @@ jobs:
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
mv /tmp/package.json crates/ruff_wasm/pkg
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -40,6 +40,7 @@ permissions:
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
workflow_dispatch:
inputs:
tag:
@@ -60,7 +61,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive
@@ -68,9 +69,9 @@ jobs:
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4-prerelease.1/cargo-dist-installer.sh | sh"
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
@@ -86,7 +87,7 @@ jobs:
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
@@ -123,7 +124,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive
@@ -153,7 +154,7 @@ jobs:
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: artifacts-build-global
path: |
@@ -174,7 +175,7 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive
@@ -200,7 +201,7 @@ jobs:
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
@@ -250,7 +251,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive

View File

@@ -231,6 +231,10 @@ unused_peekable = "warn"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"
# Salsa generates functions with parameters for each field of a `salsa::interned` struct.
# If we don't allow this, we get warnings for structs with too many fields.
too_many_arguments = "allow"
[profile.release]
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times
@@ -272,7 +276,9 @@ inherits = "release"
# Config for 'dist'
[workspace.metadata.dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.28.4-prerelease.1"
cargo-dist-version = "0.28.4"
# Make distability of apps opt-in instead of opt-out
dist = false
# CI backends to support
ci = "github"
# The installers to generate for each app
@@ -306,7 +312,7 @@ auto-includes = false
# Whether dist should create a Github Release or use an existing draft
create-release = true
# Which actions to run on pull requests
pr-run-mode = "skip"
pr-run-mode = "plan"
# Whether CI should trigger releases with dispatches instead of tag pushes
dispatch-releases = true
# Which phase dist should use to create the GitHub release
@@ -334,7 +340,7 @@ install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
global = "depot-ubuntu-latest-4"
[workspace.metadata.dist.github-action-commits]
"actions/checkout" = "11bd71901bbe5b1630ceea73d27597364c9af683" # v4
"actions/upload-artifact" = "ea165f8d65b6e75b540449e92b4886f43607fa02" # v4.6.2
"actions/checkout" = "85e6279cec87321a52edac9c87bce653a07cf6c2" # v4
"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2
"actions/download-artifact" = "95815c38cf2ff2164869cbab79da8d1f422bc89e" # v4.2.1
"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3

View File

@@ -2,16 +2,16 @@
## Basics
For now, we use our own [fork of mypy primer]. It can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run:
`mypy_primer` can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run:
```sh
uvx --from "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support" mypy_primer -h
uvx --from "git+https://github.com/hauntsaninja/mypy_primer" mypy_primer -h
```
Alternatively, you can install the forked version of `mypy_primer` using:
```sh
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
uv tool install "git+https://github.com/hauntsaninja/mypy_primer"
```
and then run it using `uvx mypy_primer` or just `mypy_primer`, if your `PATH` is set up accordingly (see: [Tool executables]).
@@ -56,6 +56,5 @@ mypy_primer --repo /path/to/ruff --old origin/main --new my/local-branch …
Note that you might need to clean up `/tmp/mypy_primer` in order for this to work correctly.
[fork of mypy primer]: https://github.com/astral-sh/mypy_primer/tree/add-red-knot-support
[full list of ecosystem projects]: https://github.com/astral-sh/mypy_primer/blob/add-red-knot-support/mypy_primer/projects.py
[full list of ecosystem projects]: https://github.com/hauntsaninja/mypy_primer/blob/master/mypy_primer/projects.py
[tool executables]: https://docs.astral.sh/uv/concepts/tools/#tool-executables

View File

@@ -32,12 +32,12 @@ fn config_override_python_version() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-attribute
error: lint:unresolved-attribute: Type `<module 'sys'>` has no attribute `last_exc`
--> <temp_dir>/test.py:5:7
|
4 | # Access `sys.last_exc` that was only added in Python 3.12
5 | print(sys.last_exc)
| ^^^^^^^^^^^^ Type `<module 'sys'>` has no attribute `last_exc`
| ^^^^^^^^^^^^
|
Found 1 diagnostic
@@ -165,11 +165,11 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `utils`
--> <temp_dir>/child/test.py:2:6
|
2 | from utils import add
| ^^^^^ Cannot resolve import `utils`
| ^^^^^
3 |
4 | stat = add(10, 15)
|
@@ -265,22 +265,22 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> <temp_dir>/test.py:7:7
|
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -301,11 +301,11 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
@@ -341,33 +341,33 @@ fn cli_rule_severity() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `does_not_exit`
--> <temp_dir>/test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
| ^^^^^^^^^^^^^
3 |
4 | y = 4 / 0
|
error: lint:division-by-zero
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
5 |
6 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> <temp_dir>/test.py:9:7
|
7 | x = a
8 |
9 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| ^
|
Found 3 diagnostics
@@ -388,22 +388,22 @@ fn cli_rule_severity() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-import
warning: lint:unresolved-import: Cannot resolve import `does_not_exit`
--> <temp_dir>/test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
| ^^^^^^^^^^^^^
3 |
4 | y = 4 / 0
|
warning: lint:division-by-zero
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
5 |
6 | for a in range(0, int(y)):
|
@@ -439,22 +439,22 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> <temp_dir>/test.py:7:7
|
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -476,11 +476,11 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
@@ -555,11 +555,11 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
|
Found 1 diagnostic
@@ -638,11 +638,11 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
|
Found 1 diagnostic
@@ -670,11 +670,11 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
|
Found 1 diagnostic
@@ -699,20 +699,20 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
| ^
|
Found 2 diagnostics
@@ -737,20 +737,20 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
| ^
|
Found 2 diagnostics
@@ -775,20 +775,20 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
| ^
|
Found 2 diagnostics
@@ -835,22 +835,22 @@ fn user_configuration() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -877,22 +877,22 @@ fn user_configuration() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:division-by-zero
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
error: lint:possibly-unresolved-reference
error: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -935,25 +935,25 @@ fn check_specific_paths() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
--> <temp_dir>/project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
| ^^^^^^^^^^^^^^
|
error: lint:division-by-zero
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0 # error: division-by-zero
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
|
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `main2`
--> <temp_dir>/project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^ Cannot resolve import `main2`
| ^^^^^
3 |
4 | print(z)
|
@@ -972,18 +972,18 @@ fn check_specific_paths() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
--> <temp_dir>/project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
| ^^^^^^^^^^^^^^
|
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `main2`
--> <temp_dir>/project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^ Cannot resolve import `main2`
| ^^^^^
3 |
4 | print(z)
|

View File

@@ -0,0 +1,4 @@
from __future__ import annotations
def foo(a: foo()):
pass

View File

@@ -162,6 +162,31 @@ def _(flag: bool):
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
```
## Unions with literals and negations
```py
from typing import Literal, Union
from knot_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to
static_assert(is_subtype_of(Literal["a", ""], Union[Literal["a", ""], Not[AlwaysFalsy]]))
static_assert(is_subtype_of(Not[AlwaysFalsy], Union[Literal["", "a"], Not[AlwaysFalsy]]))
static_assert(is_subtype_of(Literal["a", ""], Union[Not[AlwaysFalsy], Literal["a", ""]]))
static_assert(is_subtype_of(Not[AlwaysFalsy], Union[Not[AlwaysFalsy], Literal["a", ""]]))
static_assert(is_subtype_of(Literal["a", ""], Union[Literal["a", ""], Not[Literal[""]]]))
static_assert(is_subtype_of(Not[Literal[""]], Union[Literal["a", ""], Not[Literal[""]]]))
static_assert(is_subtype_of(Literal["a", ""], Union[Not[Literal[""]], Literal["a", ""]]))
static_assert(is_subtype_of(Not[Literal[""]], Union[Not[Literal[""]], Literal["a", ""]]))
def _(
x: Union[Literal["a", ""], Not[AlwaysFalsy]],
y: Union[Literal["a", ""], Not[Literal[""]]],
):
reveal_type(x) # revealed: Literal[""] | ~AlwaysFalsy
# TODO should be `object`
reveal_type(y) # revealed: Literal[""] | ~Literal[""]
```
## Cannot use an argument as both a value and a type form
```py

View File

@@ -0,0 +1,293 @@
# `typing.dataclass_transform`
```toml
[environment]
python-version = "3.12"
```
`dataclass_transform` is a decorator that can be used to let type checkers know that a function,
class, or metaclass is a `dataclass`-like construct.
## Basic example
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]:
# modify cls
return cls
@my_dataclass
class Person:
name: str
age: int | None = None
Person("Alice", 20)
Person("Bob", None)
Person("Bob")
# error: [missing-argument]
Person()
```
## Decorating decorators that take parameters themselves
If we want our `dataclass`-like decorator to also take parameters, that is also possible:
```py
from typing_extensions import dataclass_transform, Callable
@dataclass_transform()
def versioned_class[T](*, version: int = 1):
def decorator(cls):
# modify cls
return cls
return decorator
@versioned_class(version=2)
class Person:
name: str
age: int | None = None
Person("Alice", 20)
# error: [missing-argument]
Person()
```
We properly type-check the arguments to the decorator:
```py
from typing_extensions import dataclass_transform, Callable
# error: [invalid-argument-type]
@versioned_class(version="a string")
class C:
name: str
```
## Types of decorators
The examples from this section are straight from the Python documentation on
[`typing.dataclass_transform`].
### Decorating a decorator function
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def create_model[T](cls: type[T]) -> type[T]:
...
return cls
@create_model
class CustomerModel:
id: int
name: str
CustomerModel(id=1, name="Test")
```
### Decorating a metaclass
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
class ModelMeta(type): ...
class ModelBase(metaclass=ModelMeta): ...
class CustomerModel(ModelBase):
id: int
name: str
CustomerModel(id=1, name="Test")
# error: [missing-argument]
CustomerModel()
```
### Decorating a base class
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
class ModelBase: ...
class CustomerModel(ModelBase):
id: int
name: str
# TODO: this is not supported yet
# error: [unknown-argument]
# error: [unknown-argument]
CustomerModel(id=1, name="Test")
```
## Arguments to `dataclass_transform`
### `eq_default`
`eq=True/False` does not have a observable effect (apart from a minor change regarding whether
`other` is positional-only or not, which is not modelled at the moment).
### `order_default`
The `order_default` argument controls whether methods such as `__lt__` are generated by default.
This can be overwritten using the `order` argument to the custom decorator:
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def normal(*, order: bool = False):
raise NotImplementedError
@dataclass_transform(order_default=False)
def order_default_false(*, order: bool = False):
raise NotImplementedError
@dataclass_transform(order_default=True)
def order_default_true(*, order: bool = True):
raise NotImplementedError
@normal
class Normal:
inner: int
Normal(1) < Normal(2) # error: [unsupported-operator]
@normal(order=True)
class NormalOverwritten:
inner: int
NormalOverwritten(1) < NormalOverwritten(2)
@order_default_false
class OrderFalse:
inner: int
OrderFalse(1) < OrderFalse(2) # error: [unsupported-operator]
@order_default_false(order=True)
class OrderFalseOverwritten:
inner: int
OrderFalseOverwritten(1) < OrderFalseOverwritten(2)
@order_default_true
class OrderTrue:
inner: int
OrderTrue(1) < OrderTrue(2)
@order_default_true(order=False)
class OrderTrueOverwritten:
inner: int
# error: [unsupported-operator]
OrderTrueOverwritten(1) < OrderTrueOverwritten(2)
```
### `kw_only_default`
To do
### `field_specifiers`
To do
## Overloaded dataclass-like decorators
In the case of an overloaded decorator, the `dataclass_transform` decorator can be applied to the
implementation, or to *one* of the overloads.
### Applying `dataclass_transform` to the implementation
```py
from typing_extensions import dataclass_transform, TypeVar, Callable, overload
T = TypeVar("T", bound=type)
@overload
def versioned_class(
cls: T,
*,
version: int = 1,
) -> T: ...
@overload
def versioned_class(
*,
version: int = 1,
) -> Callable[[T], T]: ...
@dataclass_transform()
def versioned_class(
cls: T | None = None,
*,
version: int = 1,
) -> T | Callable[[T], T]:
raise NotImplementedError
@versioned_class
class D1:
x: str
@versioned_class(version=2)
class D2:
x: str
D1("a")
D2("a")
D1(1.2) # error: [invalid-argument-type]
D2(1.2) # error: [invalid-argument-type]
```
### Applying `dataclass_transform` to an overload
```py
from typing_extensions import dataclass_transform, TypeVar, Callable, overload
T = TypeVar("T", bound=type)
@overload
@dataclass_transform()
def versioned_class(
cls: T,
*,
version: int = 1,
) -> T: ...
@overload
def versioned_class(
*,
version: int = 1,
) -> Callable[[T], T]: ...
def versioned_class(
cls: T | None = None,
*,
version: int = 1,
) -> T | Callable[[T], T]:
raise NotImplementedError
@versioned_class
class D1:
x: str
@versioned_class(version=2)
class D2:
x: str
# TODO: these should not be errors
D1("a") # error: [too-many-positional-arguments]
D2("a") # error: [too-many-positional-arguments]
# TODO: these should be invalid-argument-type errors
D1(1.2) # error: [too-many-positional-arguments]
D2(1.2) # error: [too-many-positional-arguments]
```
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform

View File

@@ -689,7 +689,7 @@ from dataclasses import dataclass
dataclass_with_order = dataclass(order=True)
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclasses.dataclass>
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclass-like function>
@dataclass_with_order
class C:

View File

@@ -0,0 +1,19 @@
# Shadowing
<!-- snapshot-diagnostics -->
## Implicit class shadowing
```py
class C: ...
C = 1 # error: [invalid-assignment]
```
## Implicit function shadowing
```py
def f(): ...
f = 1 # error: [invalid-assignment]
```

View File

@@ -8,14 +8,20 @@
a, b = 1 # error: [not-iterable]
```
## Too many values to unpack
## Exactly too many values to unpack
```py
a, b = (1, 2, 3) # error: [invalid-assignment]
```
## Too few values to unpack
## Exactly too few values to unpack
```py
a, b = (1,) # error: [invalid-assignment]
```
## Too few values to unpack
```py
[a, *b, c, d] = (1, 2) # error: [invalid-assignment]
```

View File

@@ -5,7 +5,7 @@
```py
class C: ...
C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional"
C = 1 # error: "Implicit shadowing of class `C`"
```
## Explicit

View File

@@ -15,7 +15,7 @@ def f(x: str):
```py
def f(): ...
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
f = 1 # error: "Implicit shadowing of function `f`"
```
## Explicit shadowing

View File

@@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
--> /src/mdtest_snippet.py:11:1
|
10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
11 | instance.attr = 1 # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
| ^^^^^^^^^^^^^
|
```

View File

@@ -29,12 +29,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
--> /src/mdtest_snippet.py:12:1
|
11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
12 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
| ^^^^^^^^^^^^^
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> /src/mdtest_snippet.py:6:1
|
4 | instance = C()
5 | instance.attr = 1 # fine
6 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^^^^^^^^
7 |
8 | C.attr = 1 # fine
|
@@ -40,12 +40,12 @@ error: lint:invalid-assignment
```
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> /src/mdtest_snippet.py:9:1
|
8 | C.attr = 1 # fine
9 | C.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
warning: lint:possibly-unbound-attribute
warning: lint:possibly-unbound-attribute: Attribute `attr` on type `Literal[C]` is possibly unbound
--> /src/mdtest_snippet.py:6:5
|
4 | attr: int = 0
5 |
6 | C.attr = 1 # error: [possibly-unbound-attribute]
| ^^^^^^ Attribute `attr` on type `Literal[C]` is possibly unbound
| ^^^^^^
7 |
8 | instance = C()
|
@@ -40,12 +40,12 @@ warning: lint:possibly-unbound-attribute
```
```
warning: lint:possibly-unbound-attribute
warning: lint:possibly-unbound-attribute: Attribute `attr` on type `C` is possibly unbound
--> /src/mdtest_snippet.py:9:5
|
8 | instance = C()
9 | instance.attr = 1 # error: [possibly-unbound-attribute]
| ^^^^^^^^^^^^^ Attribute `attr` on type `C` is possibly unbound
| ^^^^^^^^^^^^^
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> /src/mdtest_snippet.py:7:1
|
5 | instance = C()
6 | instance.attr = 1 # fine
7 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^^^^^^^^
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
|
@@ -40,13 +40,13 @@ error: lint:invalid-assignment
```
```
error: lint:invalid-attribute-access
error: lint:invalid-attribute-access: Cannot assign to instance attribute `attr` from the class object `Literal[C]`
--> /src/mdtest_snippet.py:9:1
|
7 | instance.attr = "wrong" # error: [invalid-assignment]
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
| ^^^^^^ Cannot assign to instance attribute `attr` from the class object `Literal[C]`
| ^^^^^^
|
```

View File

@@ -37,12 +37,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
--> /src/mdtest_snippet.py:11:5
|
10 | # TODO: The error message here could be improved to explain why the assignment fails.
11 | C1.attr = 1 # error: [invalid-assignment]
| ^^^^^^^ Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
| ^^^^^^^
12 |
13 | class C2:
|

View File

@@ -23,13 +23,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:unresolved-attribute
error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `Literal[C]`.
--> /src/mdtest_snippet.py:3:1
|
1 | class C: ...
2 |
3 | C.non_existent = 1 # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `Literal[C]`.
| ^^^^^^^^^^^^^^
4 |
5 | instance = C()
|
@@ -37,12 +37,12 @@ error: lint:unresolved-attribute
```
```
error: lint:unresolved-attribute
error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `C`.
--> /src/mdtest_snippet.py:6:1
|
5 | instance = C()
6 | instance.non_existent = 1 # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `C`.
| ^^^^^^^^^^^^^^^^^^^^^
|
```

View File

@@ -27,12 +27,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> /src/mdtest_snippet.py:7:1
|
6 | C.attr = 1 # fine
7 | C.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^
8 |
9 | instance = C()
|
@@ -40,12 +40,12 @@ error: lint:invalid-assignment
```
```
error: lint:invalid-attribute-access
error: lint:invalid-attribute-access: Cannot assign to ClassVar `attr` from an instance of type `C`
--> /src/mdtest_snippet.py:10:1
|
9 | instance = C()
10 | instance.attr = 1 # error: [invalid-attribute-access]
| ^^^^^^^^^^^^^ Cannot assign to ClassVar `attr` from an instance of type `C`
| ^^^^^^^^^^^^^
|
```

View File

@@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Diagnostics
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `zqzqzqzqzqzqzq`
--> /src/mdtest_snippet.py:1:8
|
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
| ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq`
| ^^^^^^^^^^^^^^
|
```

View File

@@ -27,12 +27,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Diagnostics
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `a.foo`
--> /src/mdtest_snippet.py:2:8
|
1 | # Topmost component resolvable, submodule not resolvable:
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
| ^^^^^ Cannot resolve import `a.foo`
| ^^^^^
3 |
4 | # Topmost component unresolvable:
|
@@ -40,12 +40,12 @@ error: lint:unresolved-import
```
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `b.foo`
--> /src/mdtest_snippet.py:5:8
|
4 | # Topmost component unresolvable:
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
| ^^^^^ Cannot resolve import `b.foo`
| ^^^^^
|
```

View File

@@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
--> /src/mdtest_snippet.py:10:10
|
9 | # error: [not-iterable]
10 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^
11 | reveal_type(x) # revealed: int
|

View File

@@ -20,12 +20,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
--> /src/mdtest_snippet.py:2:10
|
1 | nonsense = 123
2 | for x in nonsense: # error: [not-iterable]
| ^^^^^^^^ Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
| ^^^^^^^^
3 | pass
|

View File

@@ -24,13 +24,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable
--> /src/mdtest_snippet.py:6:10
|
4 | __iter__: None = None
5 |
6 | for x in NotIterable(): # error: [not-iterable]
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable
| ^^^^^^^^^^^^^
7 | pass
|

View File

@@ -25,12 +25,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
--> /src/mdtest_snippet.py:7:10
|
6 | # error: [not-iterable]
7 | for x in Bad():
| ^^^^^ Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
| ^^^^^
8 | reveal_type(x) # revealed: Unknown
|

View File

@@ -46,12 +46,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable
--> /src/mdtest_snippet.py:22:14
|
21 | # error: [not-iterable]
22 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable
| ^^^^^^^^^^^
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
|
@@ -73,12 +73,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable
--> /src/mdtest_snippet.py:27:14
|
26 | # error: [not-iterable]
27 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable
| ^^^^^^^^^^^
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
|

View File

@@ -43,12 +43,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
--> /src/mdtest_snippet.py:20:14
|
19 | # error: [not-iterable]
20 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
| ^^^^^^^^^^^
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
|
@@ -70,12 +70,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
--> /src/mdtest_snippet.py:25:14
|
24 | # error: [not-iterable]
25 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^^
26 | reveal_type(y) # revealed: str | int
|

View File

@@ -47,12 +47,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`)
--> /src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`)
| ^^^^^^^^^^^
18 | reveal_type(x) # revealed: int
|
@@ -73,12 +73,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable
--> /src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable
| ^^^^^^^^^^^
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
|

View File

@@ -51,12 +51,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`)
--> /src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`)
| ^^^^^^^^^^^
29 | reveal_type(x) # revealed: int | str
|
@@ -77,12 +77,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
--> /src/mdtest_snippet.py:32:14
|
31 | # error: [not-iterable]
32 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
| ^^^^^^^^^^^
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
|

View File

@@ -36,12 +36,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
--> /src/mdtest_snippet.py:18:14
|
17 | # error: [not-iterable]
18 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^
19 | reveal_type(x) # revealed: int | bytes
|

View File

@@ -54,12 +54,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
--> /src/mdtest_snippet.py:31:14
|
30 | # error: [not-iterable]
31 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
| ^^^^^^^^^^^
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
@@ -81,12 +81,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
--> /src/mdtest_snippet.py:36:14
|
35 | # error: [not-iterable]
36 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^^
37 | reveal_type(y) # revealed: bytes | str | int
|

View File

@@ -35,12 +35,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method
--> /src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method
| ^^^^^^^^^^
18 | reveal_type(x) # revealed: int | bytes
|

View File

@@ -36,13 +36,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
--> /src/mdtest_snippet.py:18:14
|
16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
19 | reveal_type(x) # revealed: int
|

View File

@@ -31,13 +31,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method
--> /src/mdtest_snippet.py:13:14
|
11 | def _(flag: bool):
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
| ^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method
| ^^^^^^^^^^^^^^^^^^^^^^
14 | reveal_type(x) # revealed: int
|

View File

@@ -33,25 +33,25 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable
--> /src/mdtest_snippet.py:11:14
|
10 | # error: [not-iterable]
11 | for x in NotIterable():
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable
| ^^^^^^^^^^^^^
12 | pass
|
```
```
warning: lint:possibly-unresolved-reference
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> /src/mdtest_snippet.py:16:17
|
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]
16 | reveal_type(x)
| ^ Name `x` used when possibly not defined
| ^
|
```

View File

@@ -26,12 +26,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method
--> /src/mdtest_snippet.py:8:10
|
7 | # error: [not-iterable]
8 | for x in Bad():
| ^^^^^ Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method
| ^^^^^
9 | reveal_type(x) # revealed: Unknown
|

View File

@@ -30,12 +30,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`)
--> /src/mdtest_snippet.py:12:10
|
11 | # error: [not-iterable]
12 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`)
| ^^^^^^^^^^
13 | reveal_type(x) # revealed: int
|

View File

@@ -41,12 +41,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`)
--> /src/mdtest_snippet.py:19:10
|
18 | # error: [not-iterable]
19 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`)
| ^^^^^^^^^^^
20 | reveal_type(x) # revealed: int
|
@@ -67,12 +67,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable
--> /src/mdtest_snippet.py:23:10
|
22 | # error: [not-iterable]
23 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable
| ^^^^^^^^^^^
24 | reveal_type(y) # revealed: Unknown
|

View File

@@ -24,12 +24,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.m
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
--> /src/mdtest_snippet.py:7:8
|
6 | # error: [unsupported-bool-conversion]
7 | 10 and a and True
| ^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^
|
```

View File

@@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
--> /src/mdtest_snippet.py:9:1
|
8 | # error: [unsupported-bool-conversion]
9 | 10 in WithContains()
| ^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^^^^^
10 | # error: [unsupported-bool-conversion]
11 | 10 not in WithContains()
|
@@ -41,13 +41,13 @@ error: lint:unsupported-bool-conversion
```
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
--> /src/mdtest_snippet.py:11:1
|
9 | 10 in WithContains()
10 | # error: [unsupported-bool-conversion]
11 | 10 not in WithContains()
| ^^^^^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
```

View File

@@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_mat
# Diagnostics
```
error: lint:no-matching-overload
error: lint:no-matching-overload: No overload of class `type` matches arguments
--> /src/mdtest_snippet.py:1:1
|
1 | type("Foo", ()) # error: [no-matching-overload]
| ^^^^^^^^^^^^^^^ No overload of class `type` matches arguments
| ^^^^^^^^^^^^^^^
|
```

View File

@@ -22,12 +22,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
--> /src/mdtest_snippet.py:5:1
|
4 | # error: [unsupported-bool-conversion]
5 | not NotBoolable()
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^^
|
```

View File

@@ -56,12 +56,12 @@ error: lint:invalid-return-type: Return type does not match returned value
```
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.py:7:22
|
6 | # error: [invalid-return-type]
7 | def f(cond: bool) -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
8 | if cond:
9 | return 1
|
@@ -69,12 +69,12 @@ error: lint:invalid-return-type
```
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.py:12:22
|
11 | # error: [invalid-return-type]
12 | def f(cond: bool) -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
13 | if cond:
14 | raise ValueError()
|
@@ -82,12 +82,12 @@ error: lint:invalid-return-type
```
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.py:17:22
|
16 | # error: [invalid-return-type]
17 | def f(cond: bool) -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
18 | if cond:
19 | cond = False
|

View File

@@ -35,12 +35,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty
# Diagnostics
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.py:2:12
|
1 | # error: [invalid-return-type]
2 | def f() -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
3 | 1
|

View File

@@ -45,12 +45,12 @@ error: lint:invalid-return-type: Return type does not match returned value
```
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.pyi:6:14
|
5 | # error: [invalid-return-type]
6 | def foo() -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
7 | print("...")
8 | ...
|
@@ -58,12 +58,12 @@ error: lint:invalid-return-type
```
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.pyi:11:14
|
10 | # error: [invalid-return-type]
11 | def foo() -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
12 | f"""{foo} is a function that ..."""
13 | ...
|

View File

@@ -33,12 +33,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
--> /src/mdtest_snippet.py:12:1
|
11 | # error: [unsupported-bool-conversion]
12 | 10 < Comparable() < 20
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^^
13 | # error: [unsupported-bool-conversion]
14 | 10 < Comparable() < Comparable()
|
@@ -46,13 +46,13 @@ error: lint:unsupported-bool-conversion
```
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
--> /src/mdtest_snippet.py:14:1
|
12 | 10 < Comparable() < 20
13 | # error: [unsupported-bool-conversion]
14 | 10 < Comparable() < Comparable()
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^^
15 |
16 | Comparable() < Comparable() # fine
|

View File

@@ -0,0 +1,33 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: shadowing.md - Shadowing - Implicit class shadowing
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md
---
# Python source files
## mdtest_snippet.py
```
1 | class C: ...
2 |
3 | C = 1 # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment: Implicit shadowing of class `C`
--> /src/mdtest_snippet.py:3:1
|
1 | class C: ...
2 |
3 | C = 1 # error: [invalid-assignment]
| ^
|
info: Annotate to make it explicit if this is intentional
```

View File

@@ -0,0 +1,33 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: shadowing.md - Shadowing - Implicit function shadowing
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md
---
# Python source files
## mdtest_snippet.py
```
1 | def f(): ...
2 |
3 | f = 1 # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment: Implicit shadowing of function `f`
--> /src/mdtest_snippet.py:3:1
|
1 | def f(): ...
2 |
3 | f = 1 # error: [invalid-assignment]
| ^
|
info: Annotate to make it explicit if this is intentional
```

View File

@@ -34,12 +34,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable
--> /src/mdtest_snippet.py:15:1
|
14 | # error: [unsupported-bool-conversion]
15 | a < b < b
| ^^^^^ Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable
| ^^^^^
16 |
17 | a < b # fine
|

View File

@@ -26,12 +26,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
--> /src/mdtest_snippet.py:9:1
|
8 | # error: [unsupported-bool-conversion]
9 | (A(),) == (A(),)
| ^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^
|
```

View File

@@ -0,0 +1,30 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Exactly too few values to unpack
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
# Python source files
## mdtest_snippet.py
```
1 | a, b = (1,) # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment: Not enough values to unpack
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1,) # error: [invalid-assignment]
| ^^^^ ---- Got 1
| |
| Expected 2
|
```

View File

@@ -3,7 +3,7 @@ source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Too many values to unpack
mdtest name: unpacking.md - Unpacking - Exactly too many values to unpack
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
@@ -18,11 +18,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Too many values to unpack
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
| ^^^^ Too many values to unpack (expected 2, got 3)
| ^^^^ --------- Got 3
| |
| Expected 2
|
```

View File

@@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
--> /src/mdtest_snippet.py:1:8
|
1 | a, b = 1 # error: [not-iterable]
| ^ Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
| ^
|
```

View File

@@ -12,17 +12,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack
## mdtest_snippet.py
```
1 | a, b = (1,) # error: [invalid-assignment]
1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Not enough values to unpack
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1,) # error: [invalid-assignment]
| ^^^^ Not enough values to unpack (expected 2, got 1)
1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment]
| ^^^^^^^^^^^^^ ------ Got 2
| |
| Expected 3 or more
|
```

View File

@@ -20,11 +20,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Diagnostics
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
--> /src/mdtest_snippet.py:1:8
|
1 | import does_not_exist # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
| ^^^^^^^^^^^^^^
2 |
3 | x = does_not_exist.foo
|

View File

@@ -25,11 +25,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Diagnostics
```
error: lint:unresolved-import
error: lint:unresolved-import: Module `a` has no member `does_not_exist`
--> /src/mdtest_snippet.py:1:28
|
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Module `a` has no member `does_not_exist`
| ^^^^^^^^^^^^^^
|
```

View File

@@ -20,11 +20,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Diagnostics
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `.does_not_exist`
--> /src/mdtest_snippet.py:1:7
|
1 | from .does_not_exist import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist`
| ^^^^^^^^^^^^^^
2 |
3 | stat = add(10, 15)
|

View File

@@ -20,11 +20,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Diagnostics
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `.does_not_exist.foo.bar`
--> /src/mdtest_snippet.py:1:7
|
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist.foo.bar`
| ^^^^^^^^^^^^^^^^^^^^^^
2 |
3 | stat = add(10, 15)
|

View File

@@ -20,11 +20,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Diagnostics
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
--> /src/mdtest_snippet.py:1:6
|
1 | from does_not_exist import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
| ^^^^^^^^^^^^^^
2 |
3 | stat = add(10, 15)
|

View File

@@ -32,11 +32,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Diagnostics
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `....foo`
--> /src/package/subpackage/subsubpackage/__init__.py:1:10
|
1 | from ....foo import add # error: [unresolved-import]
| ^^^ Cannot resolve import `....foo`
| ^^^
2 |
3 | stat = add(10, 15)
|

View File

@@ -56,6 +56,19 @@ static_assert(not is_disjoint_from(FinalSubclass, A))
# ... which makes it disjoint from B1, B2:
static_assert(is_disjoint_from(B1, FinalSubclass))
static_assert(is_disjoint_from(B2, FinalSubclass))
# Instance types can also be disjoint if they have disjoint metaclasses.
# No possible subclass of `Meta1` and `Meta2` could exist, therefore
# no possible subclass of `UsesMeta1` and `UsesMeta2` can exist:
class Meta1(type): ...
class UsesMeta1(metaclass=Meta1): ...
@final
class Meta2(type): ...
class UsesMeta2(metaclass=Meta2): ...
static_assert(is_disjoint_from(UsesMeta1, UsesMeta2))
```
## Tuple types
@@ -342,8 +355,8 @@ static_assert(is_disjoint_from(Meta1, type[UsesMeta2]))
### `type[T]` versus `type[S]`
By the same token, `type[T]` is disjoint from `type[S]` if the metaclass of `T` is disjoint from the
metaclass of `S`.
By the same token, `type[T]` is disjoint from `type[S]` if `T` is `@final`, `S` is `@final`, or the
metaclass of `T` is disjoint from the metaclass of `S`.
```py
from typing import final
@@ -353,6 +366,9 @@ from knot_extensions import static_assert, is_disjoint_from
class Meta1(type): ...
class Meta2(type): ...
static_assert(is_disjoint_from(type[Meta1], type[Meta2]))
class UsesMeta1(metaclass=Meta1): ...
class UsesMeta2(metaclass=Meta2): ...

View File

@@ -1125,6 +1125,47 @@ def f(fn: Callable[[int], int]) -> None: ...
f(a)
```
### Class literals
#### Classes with metaclasses
```py
from typing import Callable, overload
from typing_extensions import Self
from knot_extensions import TypeOf, static_assert, is_subtype_of
class MetaWithReturn(type):
def __call__(cls) -> "A":
return super().__call__()
class A(metaclass=MetaWithReturn): ...
static_assert(is_subtype_of(TypeOf[A], Callable[[], A]))
static_assert(not is_subtype_of(TypeOf[A], Callable[[object], A]))
class MetaWithDifferentReturn(type):
def __call__(cls) -> int:
return super().__call__()
class B(metaclass=MetaWithDifferentReturn): ...
static_assert(is_subtype_of(TypeOf[B], Callable[[], int]))
static_assert(not is_subtype_of(TypeOf[B], Callable[[], B]))
class MetaWithOverloadReturn(type):
@overload
def __call__(cls, x: int) -> int: ...
@overload
def __call__(cls) -> str: ...
def __call__(cls, x: int | None = None) -> str | int:
return super().__call__()
class C(metaclass=MetaWithOverloadReturn): ...
static_assert(is_subtype_of(TypeOf[C], Callable[[int], int]))
static_assert(is_subtype_of(TypeOf[C], Callable[[], str]))
```
### Bound methods
```py

View File

@@ -65,7 +65,7 @@ reveal_type(c) # revealed: Literal[4]
### Uneven unpacking (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
(a, b, c) = (1, 2)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -75,7 +75,7 @@ reveal_type(c) # revealed: Unknown
### Uneven unpacking (2)
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
(a, b) = (1, 2, 3)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -84,7 +84,7 @@ reveal_type(b) # revealed: Unknown
### Nested uneven unpacking (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
(a, (b, c), d) = (1, (2,), 3)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Unknown
@@ -95,7 +95,7 @@ reveal_type(d) # revealed: Literal[3]
### Nested uneven unpacking (2)
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
(a, (b, c), d) = (1, (2, 3, 4), 5)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Unknown
@@ -106,7 +106,7 @@ reveal_type(d) # revealed: Literal[5]
### Starred expression (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
[a, *b, c, d] = (1, 2)
reveal_type(a) # revealed: Unknown
# TODO: Should be list[Any] once support for assigning to starred expression is added
@@ -159,7 +159,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
### Starred expression (6)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 5 or more, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 5 or more"
(a, b, c, *d, e, f) = (1,)
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -225,7 +225,7 @@ reveal_type(b) # revealed: LiteralString
### Uneven unpacking (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
a, b, c = "ab"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -235,7 +235,7 @@ reveal_type(c) # revealed: Unknown
### Uneven unpacking (2)
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
a, b = "abc"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -244,7 +244,7 @@ reveal_type(b) # revealed: Unknown
### Starred expression (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
(a, *b, c, d) = "ab"
reveal_type(a) # revealed: Unknown
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
@@ -254,7 +254,7 @@ reveal_type(d) # revealed: Unknown
```
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3 or more"
(a, b, *c, d) = "a"
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -306,7 +306,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
### Unicode
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
(a, b) = "é"
reveal_type(a) # revealed: Unknown
@@ -316,7 +316,7 @@ reveal_type(b) # revealed: Unknown
### Unicode escape (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
(a, b) = "\u9e6c"
reveal_type(a) # revealed: Unknown
@@ -326,7 +326,7 @@ reveal_type(b) # revealed: Unknown
### Unicode escape (2)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
(a, b) = "\U0010ffff"
reveal_type(a) # revealed: Unknown
@@ -420,8 +420,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
```py
def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 5)"
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
# error: [invalid-assignment] "Too many values to unpack: Expected 2"
a, b = arg
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -431,8 +431,8 @@ def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
```py
def _(arg: tuple[int, bytes] | tuple[int, str]):
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
a, b, c = arg
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -575,7 +575,7 @@ for a, b in ((1, 2), ("a", "b")):
# error: "Object of type `Literal[1]` is not iterable"
# error: "Object of type `Literal[2]` is not iterable"
# error: "Object of type `Literal[4]` is not iterable"
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"):
reveal_type(a) # revealed: Unknown | Literal[3, 5]
reveal_type(b) # revealed: Unknown | Literal["a", "b"]
@@ -702,7 +702,7 @@ class ContextManager:
def __exit__(self, *args) -> None:
pass
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
with ContextManager() as (a, b, c):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -765,7 +765,7 @@ def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, s
# error: "Object of type `Literal[1]` is not iterable"
# error: "Object of type `Literal[2]` is not iterable"
# error: "Object of type `Literal[4]` is not iterable"
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# error: [invalid-assignment] "Not enough values to unpack: Expected 2"
# revealed: tuple[Unknown | Literal[3, 5], Unknown | Literal["a", "b"]]
[reveal_type((a, b)) for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c")]
```

View File

@@ -0,0 +1,34 @@
Tanjun # cycle panic (signature_)
aiohttp # missing expression ID
alerta # missing expression ID
altair # cycle panics (try_metaclass_)
antidote # hangs / slow
artigraph # cycle panics (value_type_)
colour # cycle panics (try_metaclass_)
core # cycle panics (value_type_)
cpython # missing expression ID, access to field whilst being initialized, too many cycle iterations
discord.py # some kind of hang, only when multi-threaded?
freqtrade # cycle panics (try_metaclass_)
hydpy # cycle panics (try_metaclass_)
ibis # cycle panics (try_metaclass_)
manticore # stack overflow
materialize # stack overflow
meson # missing expression ID
mypy # cycle panic (signature_)
pandas # slow
pandas-stubs # cycle panics (try_metaclass_)
pandera # cycle panics (try_metaclass_)
prefect # slow
pytest # cycle panics (signature_), missing expression ID
pywin32 # bad use-def map (binding with definitely-visible unbound)
schemathesis # cycle panics (signature_)
scikit-learn # success, but mypy-primer hangs processing the output
scipy # missing expression ID
spack # success, but mypy-primer hangs processing the output
spark # cycle panics (try_metaclass_)
sphinx # missing expression ID
steam.py # missing expression ID
streamlit # cycle panic (signature_)
sympy # stack overflow
trio # missing expression ID
xarray # cycle panics (try_metaclass_)

View File

@@ -1,23 +1,109 @@
arrow
AutoSplit
Expression
PyGithub
PyWinCtl
SinbadCogs
aiohttp-devtools
aioredis
aiortc
alectryon
anyio
apprise
arviz
async-utils
asynq
attrs
bandersnatch
beartype
bidict
black
bokeh
boostedblob
check-jsonschema
cki-lib
cloud-init
com2ann
comtypes
cwltool
dacite
dd-trace-py
dedupe
django-stubs
downforeveryone
dragonchain
dulwich
flake8
flake8-pyi
git-revise
graphql-core
httpx-caching
hydra-zen
ignite
imagehash
isort
itsdangerous
janus
jax
jinja
koda-validate
kopf
kornia
mitmproxy
mkdocs
mkosi
mongo-python-driver
more-itertools
mypy-protobuf
mypy_primer
nionutils
nox
openlibrary
operator
optuna
paasta
packaging
paroxython
parso
pegen
pip
poetry
porcupine
ppb-vector
psycopg
pwndbg
pybind11
pycryptodome
pydantic
pyinstrument
pyjwt
pylint
pylox
pyodide
pyp
pyppeteer
pytest-robotframework
python-chess
python-htmlgen
python-sop
rclip
rich
rotki
schema_salad
scrapy
setuptools
sockeye
speedrun.com_global_scoreboard_webapp
starlette
static-frame
stone
tornado
twine
typeshed-stats
urllib3
vision
websockets
werkzeug
xarray-dataclasses
yarl
zipp
zulip

View File

@@ -47,9 +47,10 @@ pub(crate) use crate::types::narrow::infer_narrowing_constraint;
use crate::types::signatures::{Parameter, ParameterForm, Parameters};
use crate::{Db, FxOrderSet, Module, Program};
pub(crate) use class::{
Class, ClassLiteralType, ClassType, GenericAlias, GenericClass, InstanceType, KnownClass,
KnownInstanceType, NonGenericClass,
Class, ClassLiteralType, ClassType, GenericAlias, GenericClass, KnownClass, NonGenericClass,
};
pub(crate) use instance::InstanceType;
pub(crate) use known_instance::KnownInstanceType;
mod builder;
mod call;
@@ -60,6 +61,8 @@ mod diagnostic;
mod display;
mod generics;
mod infer;
mod instance;
mod known_instance;
mod mro;
mod narrow;
mod signatures;
@@ -339,12 +342,12 @@ impl<'db> PropertyInstanceType<'db> {
}
bitflags! {
/// Used as the return type of `dataclass(…)` calls. Keeps track of the arguments
/// Used for the return type of `dataclass(…)` calls. Keeps track of the arguments
/// that were passed in. For the precise meaning of the fields, see [1].
///
/// [1]: https://docs.python.org/3/library/dataclasses.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DataclassMetadata: u16 {
pub struct DataclassParams: u16 {
const INIT = 0b0000_0000_0001;
const REPR = 0b0000_0000_0010;
const EQ = 0b0000_0000_0100;
@@ -358,12 +361,57 @@ bitflags! {
}
}
impl Default for DataclassMetadata {
impl Default for DataclassParams {
fn default() -> Self {
Self::INIT | Self::REPR | Self::EQ | Self::MATCH_ARGS
}
}
impl From<DataclassTransformerParams> for DataclassParams {
fn from(params: DataclassTransformerParams) -> Self {
let mut result = Self::default();
result.set(
Self::EQ,
params.contains(DataclassTransformerParams::EQ_DEFAULT),
);
result.set(
Self::ORDER,
params.contains(DataclassTransformerParams::ORDER_DEFAULT),
);
result.set(
Self::KW_ONLY,
params.contains(DataclassTransformerParams::KW_ONLY_DEFAULT),
);
result.set(
Self::FROZEN,
params.contains(DataclassTransformerParams::FROZEN_DEFAULT),
);
result
}
}
bitflags! {
/// Used for the return type of `dataclass_transform(…)` calls. Keeps track of the
/// arguments that were passed in. For the precise meaning of the fields, see [1].
///
/// [1]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub struct DataclassTransformerParams: u8 {
const EQ_DEFAULT = 0b0000_0001;
const ORDER_DEFAULT = 0b0000_0010;
const KW_ONLY_DEFAULT = 0b0000_0100;
const FROZEN_DEFAULT = 0b0000_1000;
}
}
impl Default for DataclassTransformerParams {
fn default() -> Self {
Self::EQ_DEFAULT
}
}
/// Representation of a type: a set of possible values at runtime.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub enum Type<'db> {
@@ -404,7 +452,9 @@ pub enum Type<'db> {
/// A special callable that is returned by a `dataclass(…)` call. It is usually
/// used as a decorator. Note that this is only used as a return type for actual
/// `dataclass` calls, not for the argumentless `@dataclass` decorator.
DataclassDecorator(DataclassMetadata),
DataclassDecorator(DataclassParams),
/// A special callable that is returned by a `dataclass_transform(…)` call.
DataclassTransformer(DataclassTransformerParams),
/// The type of an arbitrary callable object with a certain specified signature.
Callable(CallableType<'db>),
/// A specific module object
@@ -415,7 +465,8 @@ pub enum Type<'db> {
GenericAlias(GenericAlias<'db>),
/// The set of all class objects that are subclasses of the given class (C), spelled `type[C]`.
SubclassOf(SubclassOfType<'db>),
/// The set of Python objects with the given class in their __class__'s method resolution order
/// The set of Python objects with the given class in their __class__'s method resolution order.
/// Construct this variant using the `Type::instance` constructor function.
Instance(InstanceType<'db>),
/// A single Python object that requires special treatment in the type system
KnownInstance(KnownInstanceType<'db>),
@@ -480,17 +531,20 @@ impl<'db> Type<'db> {
fn is_none(&self, db: &'db dyn Db) -> bool {
self.into_instance()
.is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType))
.is_some_and(|instance| instance.class().is_known(db, KnownClass::NoneType))
}
pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool {
self.into_instance()
.is_some_and(|instance| instance.class.is_known(db, KnownClass::NotImplementedType))
self.into_instance().is_some_and(|instance| {
instance
.class()
.is_known(db, KnownClass::NotImplementedType)
})
}
pub fn is_object(&self, db: &'db dyn Db) -> bool {
self.into_instance()
.is_some_and(|instance| instance.class.is_object(db))
.is_some_and(|instance| instance.class().is_object(db))
}
pub const fn is_todo(&self) -> bool {
@@ -524,7 +578,8 @@ impl<'db> Type<'db> {
| Self::BoundMethod(_)
| Self::WrapperDescriptor(_)
| Self::MethodWrapper(_)
| Self::DataclassDecorator(_) => false,
| Self::DataclassDecorator(_)
| Self::DataclassTransformer(_) => false,
Self::GenericAlias(generic) => generic
.specialization(db)
@@ -641,10 +696,6 @@ impl<'db> Type<'db> {
)
}
pub const fn is_instance(&self) -> bool {
matches!(self, Type::Instance(..))
}
pub const fn is_property_instance(&self) -> bool {
matches!(self, Type::PropertyInstance(..))
}
@@ -745,13 +796,6 @@ impl<'db> Type<'db> {
.expect("Expected a Type::IntLiteral variant")
}
pub const fn into_instance(self) -> Option<InstanceType<'db>> {
match self {
Type::Instance(instance_type) => Some(instance_type),
_ => None,
}
}
pub const fn into_known_instance(self) -> Option<KnownInstanceType<'db>> {
match self {
Type::KnownInstance(known_instance) => Some(known_instance),
@@ -780,10 +824,6 @@ impl<'db> Type<'db> {
matches!(self, Type::LiteralString)
}
pub const fn instance(class: ClassType<'db>) -> Self {
Self::Instance(InstanceType { class })
}
pub fn string_literal(db: &'db dyn Db, string: &str) -> Self {
Self::StringLiteral(StringLiteralType::new(db, string))
}
@@ -837,7 +877,8 @@ impl<'db> Type<'db> {
| Type::MethodWrapper(_)
| Type::BoundMethod(_)
| Type::WrapperDescriptor(_)
| Self::DataclassDecorator(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
| Type::KnownInstance(_)
@@ -914,7 +955,7 @@ impl<'db> Type<'db> {
(_, Type::Never) => false,
// Everything is a subtype of `object`.
(_, Type::Instance(InstanceType { class })) if class.is_object(db) => true,
(_, Type::Instance(instance)) if instance.class().is_object(db) => true,
// A fully static typevar is always a subtype of itself, and is never a subtype of any
// other typevar, since there is no guarantee that they will be specialized to the same
@@ -1073,7 +1114,7 @@ impl<'db> Type<'db> {
self_callable.is_subtype_of(db, other_callable)
}
(Type::DataclassDecorator(_), _) => {
(Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => {
// TODO: Implement subtyping using an equivalent `Callable` type.
false
}
@@ -1134,6 +1175,29 @@ impl<'db> Type<'db> {
self_subclass_ty.is_subtype_of(db, target_subclass_ty)
}
(Type::ClassLiteral(_), Type::Callable(_)) => {
let metaclass_call_function_symbol = self
.member_lookup_with_policy(
db,
"__call__".into(),
MemberLookupPolicy::NO_INSTANCE_FALLBACK
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
)
.symbol;
if let Symbol::Type(Type::BoundMethod(metaclass_call_function), _) =
metaclass_call_function_symbol
{
// TODO: this intentionally diverges from step 1 in
// https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable
// by always respecting the signature of the metaclass `__call__`, rather than
// using a heuristic which makes unwarranted assumptions to sometimes ignore it.
let metaclass_call_function = metaclass_call_function.into_callable_type(db);
return metaclass_call_function.is_subtype_of(db, target);
}
false
}
// `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`.
// `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object
// is an instance of its metaclass `abc.ABCMeta`.
@@ -1213,7 +1277,7 @@ impl<'db> Type<'db> {
// All types are assignable to `object`.
// TODO this special case might be removable once the below cases are comprehensive
(_, Type::Instance(InstanceType { class })) if class.is_object(db) => true,
(_, Type::Instance(instance)) if instance.class().is_object(db) => true,
// A typevar is always assignable to itself, and is never assignable to any other
// typevar, since there is no guarantee that they will be specialized to the same
@@ -1368,13 +1432,13 @@ impl<'db> Type<'db> {
// TODO: This is a workaround to avoid false positives (e.g. when checking function calls
// with `SupportsIndex` parameters), which should be removed when we understand protocols.
(lhs, Type::Instance(InstanceType { class }))
if class.is_known(db, KnownClass::SupportsIndex) =>
(lhs, Type::Instance(instance))
if instance.class().is_known(db, KnownClass::SupportsIndex) =>
{
match lhs {
Type::Instance(InstanceType { class })
Type::Instance(instance)
if matches!(
class.known(db),
instance.class().known(db),
Some(KnownClass::Int | KnownClass::SupportsIndex)
) =>
{
@@ -1386,9 +1450,7 @@ impl<'db> Type<'db> {
}
// TODO: ditto for avoiding false positives when checking function calls with `Sized` parameters.
(lhs, Type::Instance(InstanceType { class }))
if class.is_known(db, KnownClass::Sized) =>
{
(lhs, Type::Instance(instance)) if instance.class().is_known(db, KnownClass::Sized) => {
matches!(
lhs.to_meta_type(db).member(db, "__len__"),
SymbolAndQualifiers {
@@ -1605,6 +1667,7 @@ impl<'db> Type<'db> {
| Type::MethodWrapper(..)
| Type::WrapperDescriptor(..)
| Type::DataclassDecorator(..)
| Type::DataclassTransformer(..)
| Type::IntLiteral(..)
| Type::SliceLiteral(..)
| Type::StringLiteral(..)
@@ -1621,6 +1684,7 @@ impl<'db> Type<'db> {
| Type::MethodWrapper(..)
| Type::WrapperDescriptor(..)
| Type::DataclassDecorator(..)
| Type::DataclassTransformer(..)
| Type::IntLiteral(..)
| Type::SliceLiteral(..)
| Type::StringLiteral(..)
@@ -1697,9 +1761,9 @@ impl<'db> Type<'db> {
.is_disjoint_from(db, other),
},
(Type::KnownInstance(known_instance), Type::Instance(InstanceType { class }))
| (Type::Instance(InstanceType { class }), Type::KnownInstance(known_instance)) => {
!known_instance.is_instance_of(db, class)
(Type::KnownInstance(known_instance), Type::Instance(instance))
| (Type::Instance(instance), Type::KnownInstance(known_instance)) => {
!known_instance.is_instance_of(db, instance.class())
}
(known_instance_ty @ Type::KnownInstance(_), Type::Tuple(_))
@@ -1707,20 +1771,20 @@ impl<'db> Type<'db> {
known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db))
}
(Type::BooleanLiteral(..), Type::Instance(InstanceType { class }))
| (Type::Instance(InstanceType { class }), Type::BooleanLiteral(..)) => {
(Type::BooleanLiteral(..), Type::Instance(instance))
| (Type::Instance(instance), Type::BooleanLiteral(..)) => {
// A `Type::BooleanLiteral()` must be an instance of exactly `bool`
// (it cannot be an instance of a `bool` subclass)
!KnownClass::Bool.is_subclass_of(db, class)
!KnownClass::Bool.is_subclass_of(db, instance.class())
}
(Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true,
(Type::IntLiteral(..), Type::Instance(InstanceType { class }))
| (Type::Instance(InstanceType { class }), Type::IntLiteral(..)) => {
(Type::IntLiteral(..), Type::Instance(instance))
| (Type::Instance(instance), Type::IntLiteral(..)) => {
// A `Type::IntLiteral()` must be an instance of exactly `int`
// (it cannot be an instance of an `int` subclass)
!KnownClass::Int.is_subclass_of(db, class)
!KnownClass::Int.is_subclass_of(db, instance.class())
}
(Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true,
@@ -1728,34 +1792,28 @@ impl<'db> Type<'db> {
(Type::StringLiteral(..), Type::LiteralString)
| (Type::LiteralString, Type::StringLiteral(..)) => false,
(
Type::StringLiteral(..) | Type::LiteralString,
Type::Instance(InstanceType { class }),
)
| (
Type::Instance(InstanceType { class }),
Type::StringLiteral(..) | Type::LiteralString,
) => {
(Type::StringLiteral(..) | Type::LiteralString, Type::Instance(instance))
| (Type::Instance(instance), Type::StringLiteral(..) | Type::LiteralString) => {
// A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str`
// (it cannot be an instance of a `str` subclass)
!KnownClass::Str.is_subclass_of(db, class)
!KnownClass::Str.is_subclass_of(db, instance.class())
}
(Type::LiteralString, Type::LiteralString) => false,
(Type::LiteralString, _) | (_, Type::LiteralString) => true,
(Type::BytesLiteral(..), Type::Instance(InstanceType { class }))
| (Type::Instance(InstanceType { class }), Type::BytesLiteral(..)) => {
(Type::BytesLiteral(..), Type::Instance(instance))
| (Type::Instance(instance), Type::BytesLiteral(..)) => {
// A `Type::BytesLiteral()` must be an instance of exactly `bytes`
// (it cannot be an instance of a `bytes` subclass)
!KnownClass::Bytes.is_subclass_of(db, class)
!KnownClass::Bytes.is_subclass_of(db, instance.class())
}
(Type::SliceLiteral(..), Type::Instance(InstanceType { class }))
| (Type::Instance(InstanceType { class }), Type::SliceLiteral(..)) => {
(Type::SliceLiteral(..), Type::Instance(instance))
| (Type::Instance(instance), Type::SliceLiteral(..)) => {
// A `Type::SliceLiteral` must be an instance of exactly `slice`
// (it cannot be an instance of a `slice` subclass)
!KnownClass::Slice.is_subclass_of(db, class)
!KnownClass::Slice.is_subclass_of(db, instance.class())
}
// A class-literal type `X` is always disjoint from an instance type `Y`,
@@ -1770,11 +1828,11 @@ impl<'db> Type<'db> {
.metaclass_instance_type(db)
.is_subtype_of(db, instance),
(Type::FunctionLiteral(..), Type::Instance(InstanceType { class }))
| (Type::Instance(InstanceType { class }), Type::FunctionLiteral(..)) => {
(Type::FunctionLiteral(..), Type::Instance(instance))
| (Type::Instance(instance), Type::FunctionLiteral(..)) => {
// A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType`
// (it cannot be an instance of a `types.FunctionType` subclass)
!KnownClass::FunctionType.is_subclass_of(db, class)
!KnownClass::FunctionType.is_subclass_of(db, instance.class())
}
(Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType
@@ -1815,8 +1873,14 @@ impl<'db> Type<'db> {
true
}
(Type::Callable(_) | Type::DataclassDecorator(_), _)
| (_, Type::Callable(_) | Type::DataclassDecorator(_)) => {
(
Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_),
_,
)
| (
_,
Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_),
) => {
// TODO: Implement disjointness for general callable type with other types
false
}
@@ -1827,13 +1891,7 @@ impl<'db> Type<'db> {
other.is_disjoint_from(db, KnownClass::ModuleType.to_instance(db))
}
(
Type::Instance(InstanceType { class: left_class }),
Type::Instance(InstanceType { class: right_class }),
) => {
(left_class.is_final(db) && !left_class.is_subclass_of(db, right_class))
|| (right_class.is_final(db) && !right_class.is_subclass_of(db, left_class))
}
(Type::Instance(left), Type::Instance(right)) => left.is_disjoint_from(db, right),
(Type::Tuple(tuple), Type::Tuple(other_tuple)) => {
let self_elements = tuple.elements(db);
@@ -1879,6 +1937,7 @@ impl<'db> Type<'db> {
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::ModuleLiteral(..)
| Type::IntLiteral(_)
| Type::BooleanLiteral(_)
@@ -2010,10 +2069,8 @@ impl<'db> Type<'db> {
// (this variant represents `f.__get__`, where `f` is any function)
false
}
Type::DataclassDecorator(_) => false,
Type::Instance(InstanceType { class }) => {
class.known(db).is_some_and(KnownClass::is_singleton)
}
Type::DataclassDecorator(_) | Type::DataclassTransformer(_) => false,
Type::Instance(instance) => instance.is_singleton(db),
Type::PropertyInstance(_) => false,
Type::Tuple(..) => {
// The empty tuple is a singleton on CPython and PyPy, but not on other Python
@@ -2085,9 +2142,7 @@ impl<'db> Type<'db> {
.iter()
.all(|elem| elem.is_single_valued(db)),
Type::Instance(InstanceType { class }) => {
class.known(db).is_some_and(KnownClass::is_single_valued)
}
Type::Instance(instance) => instance.is_single_valued(db),
Type::BoundSuper(_) => {
// At runtime two super instances never compare equal, even if their arguments are identical.
@@ -2103,7 +2158,8 @@ impl<'db> Type<'db> {
| Type::AlwaysFalsy
| Type::Callable(_)
| Type::PropertyInstance(_)
| Type::DataclassDecorator(_) => false,
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_) => false,
}
}
@@ -2227,7 +2283,7 @@ impl<'db> Type<'db> {
// We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type).
// So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the
// MRO of the class `object`.
Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Type) => {
Type::Instance(instance) if instance.class().is_known(db, KnownClass::Type) => {
KnownClass::Object
.to_class_literal(db)
.find_name_in_mro_with_policy(db, name, policy)
@@ -2239,6 +2295,7 @@ impl<'db> Type<'db> {
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::ModuleLiteral(_)
| Type::KnownInstance(_)
| Type::AlwaysTruthy
@@ -2316,7 +2373,7 @@ impl<'db> Type<'db> {
Type::Dynamic(_) | Type::Never => Symbol::bound(self).into(),
Type::Instance(InstanceType { class }) => class.instance_member(db, name),
Type::Instance(instance) => instance.class().instance_member(db, name),
Type::FunctionLiteral(_) => KnownClass::FunctionType
.to_instance(db)
@@ -2334,7 +2391,9 @@ impl<'db> Type<'db> {
Type::DataclassDecorator(_) => KnownClass::FunctionType
.to_instance(db)
.instance_member(db, name),
Type::Callable(_) => KnownClass::Object.to_instance(db).instance_member(db, name),
Type::Callable(_) | Type::DataclassTransformer(_) => {
KnownClass::Object.to_instance(db).instance_member(db, name)
}
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => KnownClass::Object.to_instance(db).instance_member(db, name),
@@ -2751,13 +2810,13 @@ impl<'db> Type<'db> {
Type::DataclassDecorator(_) => KnownClass::FunctionType
.to_instance(db)
.member_lookup_with_policy(db, name, policy),
Type::Callable(_) => KnownClass::Object
Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Object
.to_instance(db)
.member_lookup_with_policy(db, name, policy),
Type::Instance(InstanceType { class })
Type::Instance(instance)
if matches!(name.as_str(), "major" | "minor")
&& class.is_known(db, KnownClass::VersionInfo) =>
&& instance.class().is_known(db, KnownClass::VersionInfo) =>
{
let python_version = Program::get(db).python_version(db);
let segment = if name == "major" {
@@ -2827,10 +2886,11 @@ impl<'db> Type<'db> {
// attributes on the original type. But in typeshed its return type is `Any`.
// It will need a special handling, so it remember the origin type to properly
// resolve the attribute.
if self.into_instance().is_some_and(|instance| {
instance.class.is_known(db, KnownClass::ModuleType)
|| instance.class.is_known(db, KnownClass::GenericAlias)
}) {
if matches!(
self.into_instance()
.and_then(|instance| instance.class().known(db)),
Some(KnownClass::ModuleType | KnownClass::GenericAlias)
) {
return Symbol::Unbound.into();
}
@@ -3057,6 +3117,7 @@ impl<'db> Type<'db> {
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::ModuleLiteral(_)
| Type::SliceLiteral(_)
| Type::AlwaysTruthy => Truthiness::AlwaysTrue,
@@ -3087,7 +3148,7 @@ impl<'db> Type<'db> {
}
},
Type::Instance(InstanceType { class }) => match class.known(db) {
Type::Instance(instance) => match instance.class().known(db) {
Some(known_class) => known_class.bool(),
None => try_dunder_bool()?,
},
@@ -3364,6 +3425,18 @@ impl<'db> Type<'db> {
))
}
// TODO: We should probably also check the original return type of the function
// that was decorated with `@dataclass_transform`, to see if it is consistent with
// with what we configure here.
Type::DataclassTransformer(_) => Signatures::single(CallableSignature::single(
self,
Signature::new(
Parameters::new([Parameter::positional_only(Some(Name::new_static("func")))
.with_annotated_type(Type::object(db))]),
None,
),
)),
Type::FunctionLiteral(function_type) => match function_type.known(db) {
Some(
KnownFunction::IsEquivalentTo
@@ -3477,8 +3550,7 @@ impl<'db> Type<'db> {
Parameters::new([Parameter::positional_only(Some(
Name::new_static("cls"),
))
// TODO: type[_T]
.with_annotated_type(Type::any())]),
.with_annotated_type(KnownClass::Type.to_instance(db))]),
None,
),
// TODO: make this overload Python-version-dependent
@@ -4266,6 +4338,7 @@ impl<'db> Type<'db> {
| Type::BoundMethod(_)
| Type::WrapperDescriptor(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::Instance(_)
| Type::KnownInstance(_)
| Type::PropertyInstance(_)
@@ -4336,6 +4409,7 @@ impl<'db> Type<'db> {
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::Never
| Type::FunctionLiteral(_)
| Type::BoundSuper(_)
@@ -4462,7 +4536,7 @@ impl<'db> Type<'db> {
Type::Dynamic(_) => Ok(*self),
Type::Instance(InstanceType { class }) => match class.known(db) {
Type::Instance(instance) => match instance.class().known(db) {
Some(KnownClass::TypeVar) => Ok(todo_type!(
"Support for `typing.TypeVar` instances in type expressions"
)),
@@ -4538,8 +4612,8 @@ impl<'db> Type<'db> {
pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::Never => Type::Never,
Type::Instance(InstanceType { class }) => SubclassOfType::from(db, *class),
Type::KnownInstance(known_instance) => known_instance.class().to_class_literal(db),
Type::Instance(instance) => instance.to_meta_type(db),
Type::KnownInstance(known_instance) => known_instance.to_meta_type(db),
Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db),
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
Type::BooleanLiteral(_) => KnownClass::Bool.to_class_literal(db),
@@ -4551,7 +4625,7 @@ impl<'db> Type<'db> {
Type::MethodWrapper(_) => KnownClass::MethodWrapperType.to_class_literal(db),
Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType.to_class_literal(db),
Type::DataclassDecorator(_) => KnownClass::FunctionType.to_class_literal(db),
Type::Callable(_) => KnownClass::Type.to_instance(db),
Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db),
@@ -4691,6 +4765,7 @@ impl<'db> Type<'db> {
| Type::WrapperDescriptor(_)
| Type::MethodWrapper(MethodWrapperKind::StrStartswith(_))
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::ModuleLiteral(_)
// A non-generic class never needs to be specialized. A generic class is specialized
// explicitly (via a subscript expression) or implicitly (via a call), and not because
@@ -4772,7 +4847,9 @@ impl<'db> Type<'db> {
Some(TypeDefinition::Class(class_literal.definition(db)))
}
Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))),
Self::Instance(instance) => Some(TypeDefinition::Class(instance.class.definition(db))),
Self::Instance(instance) => {
Some(TypeDefinition::Class(instance.class().definition(db)))
}
Self::KnownInstance(instance) => match instance {
KnownInstanceType::TypeVar(var) => {
Some(TypeDefinition::TypeVar(var.definition(db)))
@@ -4797,6 +4874,7 @@ impl<'db> Type<'db> {
| Self::MethodWrapper(_)
| Self::WrapperDescriptor(_)
| Self::DataclassDecorator(_)
| Self::DataclassTransformer(_)
| Self::PropertyInstance(_)
| Self::BoundSuper(_)
| Self::Tuple(_) => self.to_meta_type(db).definition(db),
@@ -4951,11 +5029,10 @@ impl<'db> InvalidTypeExpressionError<'db> {
} = self;
if is_reachable {
for error in invalid_expressions {
context.report_lint_old(
&INVALID_TYPE_FORM,
node,
format_args!("{}", error.reason(context.db())),
);
let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, node) else {
continue;
};
builder.into_diagnostic(error.reason(context.db()));
}
}
fallback_type
@@ -5140,6 +5217,11 @@ impl<'db> ContextManagerError<'db> {
context_expression_type: Type<'db>,
context_expression_node: ast::AnyNodeRef,
) {
let Some(builder) = context.report_lint(&INVALID_CONTEXT_MANAGER, context_expression_node)
else {
return;
};
let format_call_dunder_error = |call_dunder_error: &CallDunderError<'db>, name: &str| {
match call_dunder_error {
CallDunderError::MethodNotAvailable => format!("it does not implement `{name}`"),
@@ -5191,9 +5273,7 @@ impl<'db> ContextManagerError<'db> {
} => format_call_dunder_errors(enter_error, "__enter__", exit_error, "__exit__"),
};
context.report_lint_old(
&INVALID_CONTEXT_MANAGER,
context_expression_node,
builder.into_diagnostic(
format_args!(
"Object of type `{context_expression}` cannot be used with `with` because {formatted_errors}",
context_expression = context_expression_type.display(db)
@@ -5291,10 +5371,13 @@ impl<'db> IterationError<'db> {
iterable_type: Type<'db>,
iterable_node: ast::AnyNodeRef,
) {
let Some(builder) = context.report_lint(&NOT_ITERABLE, iterable_node) else {
return;
};
let db = context.db();
let report_not_iterable = |arguments: std::fmt::Arguments| {
context.report_lint_old(&NOT_ITERABLE, iterable_node, arguments);
builder.into_diagnostic(arguments);
};
// TODO: for all of these error variants, the "explanation" for the diagnostic
@@ -5568,13 +5651,14 @@ impl<'db> BoolError<'db> {
}
fn report_diagnostic_impl(&self, context: &InferContext, condition: TextRange) {
let Some(builder) = context.report_lint(&UNSUPPORTED_BOOL_CONVERSION, condition) else {
return;
};
match self {
Self::IncorrectArguments {
not_boolable_type, ..
} => {
context.report_lint_old(
&UNSUPPORTED_BOOL_CONVERSION,
condition,
builder.into_diagnostic(
format_args!(
"Boolean conversion is unsupported for type `{}`; it incorrectly implements `__bool__`",
not_boolable_type.display(context.db())
@@ -5585,25 +5669,20 @@ impl<'db> BoolError<'db> {
not_boolable_type,
return_type,
} => {
context.report_lint_old(
&UNSUPPORTED_BOOL_CONVERSION,
condition,
format_args!(
"Boolean conversion is unsupported for type `{not_boolable}`; the return type of its bool method (`{return_type}`) isn't assignable to `bool",
not_boolable = not_boolable_type.display(context.db()),
return_type = return_type.display(context.db())
),
);
builder.into_diagnostic(format_args!(
"Boolean conversion is unsupported for type `{not_boolable}`; \
the return type of its bool method (`{return_type}`) \
isn't assignable to `bool",
not_boolable = not_boolable_type.display(context.db()),
return_type = return_type.display(context.db())
));
}
Self::NotCallable { not_boolable_type } => {
context.report_lint_old(
&UNSUPPORTED_BOOL_CONVERSION,
condition,
format_args!(
"Boolean conversion is unsupported for type `{}`; its `__bool__` method isn't callable",
not_boolable_type.display(context.db())
),
);
builder.into_diagnostic(format_args!(
"Boolean conversion is unsupported for type `{}`; \
its `__bool__` method isn't callable",
not_boolable_type.display(context.db())
));
}
Self::Union { union, .. } => {
let first_error = union
@@ -5612,26 +5691,20 @@ impl<'db> BoolError<'db> {
.find_map(|element| element.try_bool(context.db()).err())
.unwrap();
context.report_lint_old(
&UNSUPPORTED_BOOL_CONVERSION,
condition,
format_args!(
"Boolean conversion is unsupported for union `{}` because `{}` doesn't implement `__bool__` correctly",
Type::Union(*union).display(context.db()),
first_error.not_boolable_type().display(context.db()),
),
);
builder.into_diagnostic(format_args!(
"Boolean conversion is unsupported for union `{}` \
because `{}` doesn't implement `__bool__` correctly",
Type::Union(*union).display(context.db()),
first_error.not_boolable_type().display(context.db()),
));
}
Self::Other { not_boolable_type } => {
context.report_lint_old(
&UNSUPPORTED_BOOL_CONVERSION,
condition,
format_args!(
"Boolean conversion is unsupported for type `{}`; it incorrectly implements `__bool__`",
not_boolable_type.display(context.db())
),
);
builder.into_diagnostic(format_args!(
"Boolean conversion is unsupported for type `{}`; \
it incorrectly implements `__bool__`",
not_boolable_type.display(context.db())
));
}
}
}
@@ -5662,27 +5735,28 @@ impl<'db> ConstructorCallError<'db> {
) {
let report_init_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error {
CallDunderError::MethodNotAvailable => {
// If we are using vendored typeshed, it should be impossible to have missing
// or unbound `__init__` method on a class, as all classes have `object` in MRO.
// Thus the following may only trigger if a custom typeshed is used.
context.report_lint_old(
&CALL_POSSIBLY_UNBOUND_METHOD,
context_expression_node,
format_args!(
"`__init__` method is missing on type `{}`. Make sure your `object` in typeshed has its definition.",
if let Some(builder) =
context.report_lint(&CALL_POSSIBLY_UNBOUND_METHOD, context_expression_node)
{
// If we are using vendored typeshed, it should be impossible to have missing
// or unbound `__init__` method on a class, as all classes have `object` in MRO.
// Thus the following may only trigger if a custom typeshed is used.
builder.into_diagnostic(format_args!(
"`__init__` method is missing on type `{}`. \
Make sure your `object` in typeshed has its definition.",
context_expression_type.display(context.db()),
),
);
));
}
}
CallDunderError::PossiblyUnbound(bindings) => {
context.report_lint_old(
&CALL_POSSIBLY_UNBOUND_METHOD,
context_expression_node,
format_args!(
if let Some(builder) =
context.report_lint(&CALL_POSSIBLY_UNBOUND_METHOD, context_expression_node)
{
builder.into_diagnostic(format_args!(
"Method `__init__` on type `{}` is possibly unbound.",
context_expression_type.display(context.db()),
),
);
));
}
bindings.report_diagnostics(context, context_expression_node);
}
@@ -5698,14 +5772,14 @@ impl<'db> ConstructorCallError<'db> {
unreachable!("`__new__` method may not be called if missing");
}
CallDunderError::PossiblyUnbound(bindings) => {
context.report_lint_old(
&CALL_POSSIBLY_UNBOUND_METHOD,
context_expression_node,
format_args!(
if let Some(builder) =
context.report_lint(&CALL_POSSIBLY_UNBOUND_METHOD, context_expression_node)
{
builder.into_diagnostic(format_args!(
"Method `__new__` on type `{}` is possibly unbound.",
context_expression_type.display(context.db()),
),
);
));
}
bindings.report_diagnostics(context, context_expression_node);
}
@@ -5860,6 +5934,10 @@ pub struct FunctionType<'db> {
/// A set of special decorators that were applied to this function
decorators: FunctionDecorators,
/// The arguments to `dataclass_transformer`, if this function was annotated
/// with `@dataclass_transformer(...)`.
dataclass_transformer_params: Option<DataclassTransformerParams>,
/// The generic context of a generic function.
generic_context: Option<GenericContext<'db>>,
@@ -5996,6 +6074,7 @@ impl<'db> FunctionType<'db> {
self.known(db),
self.body_scope(db),
self.decorators(db),
self.dataclass_transformer_params(db),
Some(generic_context),
self.specialization(db),
)
@@ -6012,6 +6091,7 @@ impl<'db> FunctionType<'db> {
self.known(db),
self.body_scope(db),
self.decorators(db),
self.dataclass_transformer_params(db),
self.generic_context(db),
Some(specialization),
)
@@ -6056,6 +6136,8 @@ pub enum KnownFunction {
GetProtocolMembers,
/// `typing(_extensions).runtime_checkable`
RuntimeCheckable,
/// `typing(_extensions).dataclass_transform`
DataclassTransform,
/// `abc.abstractmethod`
#[strum(serialize = "abstractmethod")]
@@ -6120,6 +6202,7 @@ impl KnownFunction {
| Self::IsProtocol
| Self::GetProtocolMembers
| Self::RuntimeCheckable
| Self::DataclassTransform
| Self::NoTypeCheck => {
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
}
@@ -7043,34 +7126,31 @@ impl BoundSuperError<'_> {
pub(super) fn report_diagnostic(&self, context: &InferContext, node: AnyNodeRef) {
match self {
BoundSuperError::InvalidPivotClassType { pivot_class } => {
context.report_lint_old(
&INVALID_SUPER_ARGUMENT,
node,
format_args!(
if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) {
builder.into_diagnostic(format_args!(
"`{pivot_class}` is not a valid class",
pivot_class = pivot_class.display(context.db()),
),
);
));
}
}
BoundSuperError::FailingConditionCheck { pivot_class, owner } => {
context.report_lint_old(
&INVALID_SUPER_ARGUMENT,
node,
format_args!(
"`{owner}` is not an instance or subclass of `{pivot_class}` in `super({pivot_class}, {owner})` call",
if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) {
builder.into_diagnostic(format_args!(
"`{owner}` is not an instance or subclass of \
`{pivot_class}` in `super({pivot_class}, {owner})` call",
pivot_class = pivot_class.display(context.db()),
owner = owner.display(context.db()),
),
);
));
}
}
BoundSuperError::UnavailableImplicitArguments => {
context.report_lint_old(
&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS,
node,
format_args!(
if let Some(builder) =
context.report_lint(&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, node)
{
builder.into_diagnostic(format_args!(
"Cannot determine implicit arguments for 'super()' in this context",
),
);
));
}
}
}
}
@@ -7088,7 +7168,7 @@ impl<'db> SuperOwnerKind<'db> {
match self {
SuperOwnerKind::Dynamic(dynamic) => Either::Left(ClassBase::Dynamic(dynamic).mro(db)),
SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)),
SuperOwnerKind::Instance(instance) => Either::Right(instance.class.iter_mro(db)),
SuperOwnerKind::Instance(instance) => Either::Right(instance.class().iter_mro(db)),
}
}
@@ -7104,7 +7184,7 @@ impl<'db> SuperOwnerKind<'db> {
match self {
SuperOwnerKind::Dynamic(_) => None,
SuperOwnerKind::Class(class) => Some(class),
SuperOwnerKind::Instance(instance) => Some(instance.class),
SuperOwnerKind::Instance(instance) => Some(instance.class()),
}
}
@@ -7275,35 +7355,38 @@ impl<'db> BoundSuperType<'db> {
policy: MemberLookupPolicy,
) -> SymbolAndQualifiers<'db> {
let owner = self.owner(db);
match owner {
SuperOwnerKind::Dynamic(_) => owner
.into_type()
.find_name_in_mro_with_policy(db, name, policy)
.expect("Calling `find_name_in_mro` on dynamic type should return `Some`"),
SuperOwnerKind::Class(class) | SuperOwnerKind::Instance(InstanceType { class }) => {
let (class_literal, _) = class.class_literal(db);
// TODO properly support super() with generic types
// * requires a fix for https://github.com/astral-sh/ruff/issues/17432
// * also requires understanding how we should handle cases like this:
// ```python
// b_int: B[int]
// b_unknown: B
//
// super(B, b_int)
// super(B[int], b_unknown)
// ```
match class_literal {
ClassLiteralType::Generic(_) => {
Symbol::bound(todo_type!("super in generic class")).into()
}
ClassLiteralType::NonGeneric(_) => class_literal.class_member_from_mro(
db,
name,
policy,
self.skip_until_after_pivot(db, owner.iter_mro(db)),
),
}
let class = match owner {
SuperOwnerKind::Dynamic(_) => {
return owner
.into_type()
.find_name_in_mro_with_policy(db, name, policy)
.expect("Calling `find_name_in_mro` on dynamic type should return `Some`")
}
SuperOwnerKind::Class(class) => *class,
SuperOwnerKind::Instance(instance) => instance.class(),
};
let (class_literal, _) = class.class_literal(db);
// TODO properly support super() with generic types
// * requires a fix for https://github.com/astral-sh/ruff/issues/17432
// * also requires understanding how we should handle cases like this:
// ```python
// b_int: B[int]
// b_unknown: B
//
// super(B, b_int)
// super(B[int], b_unknown)
// ```
match class_literal {
ClassLiteralType::Generic(_) => {
Symbol::bound(todo_type!("super in generic class")).into()
}
ClassLiteralType::NonGeneric(_) => class_literal.class_member_from_mro(
db,
name,
policy,
self.skip_until_after_pivot(db, owner.iter_mro(db)),
),
}
}
}
@@ -7493,6 +7576,7 @@ pub(crate) mod tests {
| KnownFunction::IsProtocol
| KnownFunction::GetProtocolMembers
| KnownFunction::RuntimeCheckable
| KnownFunction::DataclassTransform
| KnownFunction::NoTypeCheck => KnownModule::TypingExtensions,
KnownFunction::IsSingleton

View File

@@ -44,6 +44,40 @@ use crate::types::{
use crate::{Db, FxOrderSet};
use smallvec::SmallVec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LiteralKind {
Int,
String,
Bytes,
}
impl<'db> Type<'db> {
/// Return `true` if this type can be a supertype of some literals of `kind` and not others.
fn splits_literals(self, db: &'db dyn Db, kind: LiteralKind) -> bool {
match (self, kind) {
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => true,
(Type::StringLiteral(_), LiteralKind::String) => true,
(Type::BytesLiteral(_), LiteralKind::Bytes) => true,
(Type::IntLiteral(_), LiteralKind::Int) => true,
(Type::Intersection(intersection), _) => {
intersection
.positive(db)
.iter()
.any(|ty| ty.splits_literals(db, kind))
|| intersection
.negative(db)
.iter()
.any(|ty| ty.splits_literals(db, kind))
}
(Type::Union(union), _) => union
.elements(db)
.iter()
.any(|ty| ty.splits_literals(db, kind)),
_ => false,
}
}
}
enum UnionElement<'db> {
IntLiterals(FxOrderSet<i64>),
StringLiterals(FxOrderSet<StringLiteralType<'db>>),
@@ -61,12 +95,9 @@ impl<'db> UnionElement<'db> {
/// If this `UnionElement` is some other type, return `ReduceResult::Type` so `UnionBuilder`
/// can perform more complex checks on it.
fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> {
// `AlwaysTruthy` and `AlwaysFalsy` are the only types which can be a supertype of only
// _some_ literals of the same kind, so we need to walk the full set in this case.
let needs_filter = matches!(other_type, Type::AlwaysTruthy | Type::AlwaysFalsy);
match self {
UnionElement::IntLiterals(literals) => {
ReduceResult::KeepIf(if needs_filter {
ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::Int) {
literals.retain(|literal| {
!Type::IntLiteral(*literal).is_subtype_of(db, other_type)
});
@@ -77,7 +108,7 @@ impl<'db> UnionElement<'db> {
})
}
UnionElement::StringLiterals(literals) => {
ReduceResult::KeepIf(if needs_filter {
ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::String) {
literals.retain(|literal| {
!Type::StringLiteral(*literal).is_subtype_of(db, other_type)
});
@@ -88,7 +119,7 @@ impl<'db> UnionElement<'db> {
})
}
UnionElement::BytesLiterals(literals) => {
ReduceResult::KeepIf(if needs_filter {
ReduceResult::KeepIf(if other_type.splits_literals(db, LiteralKind::Bytes) {
literals.retain(|literal| {
!Type::BytesLiteral(*literal).is_subtype_of(db, other_type)
});
@@ -493,7 +524,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
_ => {
let known_instance = new_positive
.into_instance()
.and_then(|instance| instance.class.known(db));
.and_then(|instance| instance.class().known(db));
if known_instance == Some(KnownClass::Object) {
// `object & T` -> `T`; it is always redundant to add `object` to an intersection
@@ -513,7 +544,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
new_positive = Type::BooleanLiteral(false);
}
Type::Instance(instance)
if instance.class.is_known(db, KnownClass::Bool) =>
if instance.class().is_known(db, KnownClass::Bool) =>
{
match new_positive {
// `bool & AlwaysTruthy` -> `Literal[True]`
@@ -607,7 +638,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
self.positive
.iter()
.filter_map(|ty| ty.into_instance())
.filter_map(|instance| instance.class.known(db))
.filter_map(|instance| instance.class().known(db))
.any(KnownClass::is_bool)
};
@@ -623,7 +654,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
Type::Never => {
// Adding ~Never to an intersection is a no-op.
}
Type::Instance(instance) if instance.class.is_object(db) => {
Type::Instance(instance) if instance.class().is_object(db) => {
// Adding ~object to an intersection results in Never.
*self = Self::default();
self.positive.insert(Type::Never);

View File

@@ -19,8 +19,9 @@ use crate::types::diagnostic::{
use crate::types::generics::{Specialization, SpecializationBuilder};
use crate::types::signatures::{Parameter, ParameterForm};
use crate::types::{
BoundMethodType, DataclassMetadata, FunctionDecorators, KnownClass, KnownFunction,
KnownInstanceType, MethodWrapperKind, PropertyInstanceType, UnionType, WrapperDescriptorKind,
BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, FunctionType,
KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType,
UnionType, WrapperDescriptorKind,
};
use ruff_db::diagnostic::{Annotation, Severity, Span, SubDiagnostic};
use ruff_python_ast as ast;
@@ -178,24 +179,23 @@ impl<'db> Bindings<'db> {
// If all union elements are not callable, report that the union as a whole is not
// callable.
if self.into_iter().all(|b| !b.is_callable()) {
context.report_lint_old(
&CALL_NON_CALLABLE,
node,
format_args!(
if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) {
builder.into_diagnostic(format_args!(
"Object of type `{}` is not callable",
self.callable_type().display(context.db())
),
);
));
}
return;
}
for (index, conflicting_form) in self.conflicting_forms.iter().enumerate() {
if *conflicting_form {
context.report_lint_old(
&CONFLICTING_ARGUMENT_FORMS,
BindingError::get_node(node, Some(index)),
format_args!("Argument is used as both a value and a type form in call"),
);
let node = BindingError::get_node(node, Some(index));
if let Some(builder) = context.report_lint(&CONFLICTING_ARGUMENT_FORMS, node) {
builder.into_diagnostic(
"Argument is used as both a value and a type form in call",
);
}
}
}
@@ -210,8 +210,17 @@ impl<'db> Bindings<'db> {
/// Evaluates the return type of certain known callables, where we have special-case logic to
/// determine the return type in a way that isn't directly expressible in the type system.
fn evaluate_known_cases(&mut self, db: &'db dyn Db) {
let to_bool = |ty: &Option<Type<'_>>, default: bool| -> bool {
if let Some(Type::BooleanLiteral(value)) = ty {
*value
} else {
// TODO: emit a diagnostic if we receive `bool`
default
}
};
// Each special case listed here should have a corresponding clause in `Type::signatures`.
for binding in &mut self.elements {
for (binding, callable_signature) in self.elements.iter_mut().zip(self.signatures.iter()) {
let binding_type = binding.callable_type;
let Some((overload_index, overload)) = binding.matching_overload_mut() else {
continue;
@@ -413,6 +422,21 @@ impl<'db> Bindings<'db> {
}
}
Type::DataclassTransformer(params) => {
if let [Some(Type::FunctionLiteral(function))] = overload.parameter_types() {
overload.set_return_type(Type::FunctionLiteral(FunctionType::new(
db,
function.name(db),
function.known(db),
function.body_scope(db),
function.decorators(db),
Some(params),
function.generic_context(db),
function.specialization(db),
)));
}
}
Type::BoundMethod(bound_method)
if bound_method.self_instance(db).is_property_instance() =>
{
@@ -598,53 +622,90 @@ impl<'db> Bindings<'db> {
if let [init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot] =
overload.parameter_types()
{
let to_bool = |ty: &Option<Type<'_>>, default: bool| -> bool {
if let Some(Type::BooleanLiteral(value)) = ty {
*value
} else {
// TODO: emit a diagnostic if we receive `bool`
default
}
};
let mut metadata = DataclassMetadata::empty();
let mut params = DataclassParams::empty();
if to_bool(init, true) {
metadata |= DataclassMetadata::INIT;
params |= DataclassParams::INIT;
}
if to_bool(repr, true) {
metadata |= DataclassMetadata::REPR;
params |= DataclassParams::REPR;
}
if to_bool(eq, true) {
metadata |= DataclassMetadata::EQ;
params |= DataclassParams::EQ;
}
if to_bool(order, false) {
metadata |= DataclassMetadata::ORDER;
params |= DataclassParams::ORDER;
}
if to_bool(unsafe_hash, false) {
metadata |= DataclassMetadata::UNSAFE_HASH;
params |= DataclassParams::UNSAFE_HASH;
}
if to_bool(frozen, false) {
metadata |= DataclassMetadata::FROZEN;
params |= DataclassParams::FROZEN;
}
if to_bool(match_args, true) {
metadata |= DataclassMetadata::MATCH_ARGS;
params |= DataclassParams::MATCH_ARGS;
}
if to_bool(kw_only, false) {
metadata |= DataclassMetadata::KW_ONLY;
params |= DataclassParams::KW_ONLY;
}
if to_bool(slots, false) {
metadata |= DataclassMetadata::SLOTS;
params |= DataclassParams::SLOTS;
}
if to_bool(weakref_slot, false) {
metadata |= DataclassMetadata::WEAKREF_SLOT;
params |= DataclassParams::WEAKREF_SLOT;
}
overload.set_return_type(Type::DataclassDecorator(metadata));
overload.set_return_type(Type::DataclassDecorator(params));
}
}
_ => {}
Some(KnownFunction::DataclassTransform) => {
if let [eq_default, order_default, kw_only_default, frozen_default, _field_specifiers, _kwargs] =
overload.parameter_types()
{
let mut params = DataclassTransformerParams::empty();
if to_bool(eq_default, true) {
params |= DataclassTransformerParams::EQ_DEFAULT;
}
if to_bool(order_default, false) {
params |= DataclassTransformerParams::ORDER_DEFAULT;
}
if to_bool(kw_only_default, false) {
params |= DataclassTransformerParams::KW_ONLY_DEFAULT;
}
if to_bool(frozen_default, false) {
params |= DataclassTransformerParams::FROZEN_DEFAULT;
}
overload.set_return_type(Type::DataclassTransformer(params));
}
}
_ => {
if let Some(params) = function_type.dataclass_transformer_params(db) {
// This is a call to a custom function that was decorated with `@dataclass_transformer`.
// If this function was called with a keyword argument like `order=False`, we extract
// the argument type and overwrite the corresponding flag in `dataclass_params` after
// constructing them from the `dataclass_transformer`-parameter defaults.
let mut dataclass_params = DataclassParams::from(params);
if let Some(Some(Type::BooleanLiteral(order))) = callable_signature
.iter()
.nth(overload_index)
.and_then(|signature| {
let (idx, _) =
signature.parameters().keyword_by_name("order")?;
overload.parameter_types().get(idx)
})
{
dataclass_params.set(DataclassParams::ORDER, *order);
}
overload.set_return_type(Type::DataclassDecorator(dataclass_params));
}
}
},
Type::ClassLiteral(class) => match class.known(db) {
@@ -845,43 +906,37 @@ impl<'db> CallableBinding<'db> {
fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
if !self.is_callable() {
context.report_lint_old(
&CALL_NON_CALLABLE,
node,
format_args!(
if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) {
builder.into_diagnostic(format_args!(
"Object of type `{}` is not callable",
self.callable_type.display(context.db()),
),
);
));
}
return;
}
if self.dunder_call_is_possibly_unbound {
context.report_lint_old(
&CALL_NON_CALLABLE,
node,
format_args!(
if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) {
builder.into_diagnostic(format_args!(
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
self.callable_type.display(context.db()),
),
);
));
}
return;
}
let callable_description = CallableDescription::new(context.db(), self.callable_type);
if self.overloads.len() > 1 {
context.report_lint_old(
&NO_MATCHING_OVERLOAD,
node,
format_args!(
if let Some(builder) = context.report_lint(&NO_MATCHING_OVERLOAD, node) {
builder.into_diagnostic(format_args!(
"No overload{} matches arguments",
if let Some(CallableDescription { kind, name }) = callable_description {
format!(" of {kind} `{name}`")
} else {
String::new()
}
),
);
));
}
return;
}
@@ -1382,10 +1437,9 @@ impl<'db> BindingError<'db> {
expected_positional_count,
provided_positional_count,
} => {
context.report_lint_old(
&TOO_MANY_POSITIONAL_ARGUMENTS,
Self::get_node(node, *first_excess_argument_index),
format_args!(
let node = Self::get_node(node, *first_excess_argument_index);
if let Some(builder) = context.report_lint(&TOO_MANY_POSITIONAL_ARGUMENTS, node) {
builder.into_diagnostic(format_args!(
"Too many positional arguments{}: expected \
{expected_positional_count}, got {provided_positional_count}",
if let Some(CallableDescription { kind, name }) = callable_description {
@@ -1393,75 +1447,70 @@ impl<'db> BindingError<'db> {
} else {
String::new()
}
),
);
));
}
}
Self::MissingArguments { parameters } => {
let s = if parameters.0.len() == 1 { "" } else { "s" };
context.report_lint_old(
&MISSING_ARGUMENT,
node,
format_args!(
if let Some(builder) = context.report_lint(&MISSING_ARGUMENT, node) {
let s = if parameters.0.len() == 1 { "" } else { "s" };
builder.into_diagnostic(format_args!(
"No argument{s} provided for required parameter{s} {parameters}{}",
if let Some(CallableDescription { kind, name }) = callable_description {
format!(" of {kind} `{name}`")
} else {
String::new()
}
),
);
));
}
}
Self::UnknownArgument {
argument_name,
argument_index,
} => {
context.report_lint_old(
&UNKNOWN_ARGUMENT,
Self::get_node(node, *argument_index),
format_args!(
let node = Self::get_node(node, *argument_index);
if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, node) {
builder.into_diagnostic(format_args!(
"Argument `{argument_name}` does not match any known parameter{}",
if let Some(CallableDescription { kind, name }) = callable_description {
format!(" of {kind} `{name}`")
} else {
String::new()
}
),
);
));
}
}
Self::ParameterAlreadyAssigned {
argument_index,
parameter,
} => {
context.report_lint_old(
&PARAMETER_ALREADY_ASSIGNED,
Self::get_node(node, *argument_index),
format_args!(
let node = Self::get_node(node, *argument_index);
if let Some(builder) = context.report_lint(&PARAMETER_ALREADY_ASSIGNED, node) {
builder.into_diagnostic(format_args!(
"Multiple values provided for parameter {parameter}{}",
if let Some(CallableDescription { kind, name }) = callable_description {
format!(" of {kind} `{name}`")
} else {
String::new()
}
),
);
));
}
}
Self::InternalCallError(reason) => {
context.report_lint_old(
&CALL_NON_CALLABLE,
Self::get_node(node, None),
format_args!(
let node = Self::get_node(node, None);
if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) {
builder.into_diagnostic(format_args!(
"Call{} failed: {reason}",
if let Some(CallableDescription { kind, name }) = callable_description {
format!(" of {kind} `{name}`")
} else {
String::new()
}
),
);
));
}
}
}
}

View File

@@ -4,13 +4,15 @@ use std::sync::{LazyLock, Mutex};
use super::{
class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder,
KnownFunction, MemberLookupPolicy, Mro, MroError, MroIterator, SubclassOfType, Truthiness,
Type, TypeAliasType, TypeQualifiers, TypeVarInstance,
Type, TypeQualifiers,
};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::DeclarationWithConstraint;
use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{Parameter, Parameters};
use crate::types::{CallableType, DataclassMetadata, Signature};
use crate::types::{
CallableType, DataclassParams, DataclassTransformerParams, KnownInstanceType, Signature,
};
use crate::{
module_resolver::file_to_module,
semantic_index::{
@@ -106,7 +108,8 @@ pub struct Class<'db> {
pub(crate) known: Option<KnownClass>,
pub(crate) dataclass_metadata: Option<DataclassMetadata>,
pub(crate) dataclass_params: Option<DataclassParams>,
pub(crate) dataclass_transformer_params: Option<DataclassTransformerParams>,
}
impl<'db> Class<'db> {
@@ -469,8 +472,8 @@ impl<'db> ClassLiteralType<'db> {
self.class(db).known
}
pub(crate) fn dataclass_metadata(self, db: &'db dyn Db) -> Option<DataclassMetadata> {
self.class(db).dataclass_metadata
pub(crate) fn dataclass_params(self, db: &'db dyn Db) -> Option<DataclassParams> {
self.class(db).dataclass_params
}
/// Return `true` if this class represents `known_class`
@@ -699,6 +702,7 @@ impl<'db> ClassLiteralType<'db> {
/// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred.
pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
self.try_metaclass(db)
.map(|(ty, _)| ty)
.unwrap_or_else(|_| SubclassOfType::subclass_of_unknown())
}
@@ -712,7 +716,10 @@ impl<'db> ClassLiteralType<'db> {
/// Return the metaclass of this class, or an error if the metaclass cannot be inferred.
#[salsa::tracked]
pub(super) fn try_metaclass(self, db: &'db dyn Db) -> Result<Type<'db>, MetaclassError<'db>> {
pub(super) fn try_metaclass(
self,
db: &'db dyn Db,
) -> Result<(Type<'db>, Option<DataclassTransformerParams>), MetaclassError<'db>> {
let class = self.class(db);
tracing::trace!("ClassLiteralType::try_metaclass: {}", class.name);
@@ -723,7 +730,7 @@ impl<'db> ClassLiteralType<'db> {
// We emit diagnostics for cyclic class definitions elsewhere.
// Avoid attempting to infer the metaclass if the class is cyclically defined:
// it would be easy to enter an infinite loop.
return Ok(SubclassOfType::subclass_of_unknown());
return Ok((SubclassOfType::subclass_of_unknown(), None));
}
let explicit_metaclass = self.explicit_metaclass(db);
@@ -768,7 +775,7 @@ impl<'db> ClassLiteralType<'db> {
}),
};
return return_ty_result.map(|ty| ty.to_meta_type(db));
return return_ty_result.map(|ty| (ty.to_meta_type(db), None));
};
// Reconcile all base classes' metaclasses with the candidate metaclass.
@@ -805,7 +812,10 @@ impl<'db> ClassLiteralType<'db> {
});
}
Ok(candidate.metaclass.into())
Ok((
candidate.metaclass.into(),
candidate.metaclass.class(db).dataclass_transformer_params,
))
}
/// Returns the class member of this class named `name`.
@@ -969,12 +979,8 @@ impl<'db> ClassLiteralType<'db> {
});
if symbol.symbol.is_unbound() {
if let Some(metadata) = self.dataclass_metadata(db) {
if let Some(dataclass_member) =
self.own_dataclass_member(db, specialization, metadata, name)
{
return Symbol::bound(dataclass_member).into();
}
if let Some(dataclass_member) = self.own_dataclass_member(db, specialization, name) {
return Symbol::bound(dataclass_member).into();
}
}
@@ -986,70 +992,97 @@ impl<'db> ClassLiteralType<'db> {
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
metadata: DataclassMetadata,
name: &str,
) -> Option<Type<'db>> {
if name == "__init__" && metadata.contains(DataclassMetadata::INIT) {
let mut parameters = vec![];
let params = self.dataclass_params(db);
let has_dataclass_param = |param| params.is_some_and(|params| params.contains(param));
for (name, (mut attr_ty, mut default_ty)) in self.dataclass_fields(db, specialization) {
// The descriptor handling below is guarded by this fully-static check, because dynamic
// types like `Any` are valid (data) descriptors: since they have all possible attributes,
// they also have a (callable) `__set__` method. The problem is that we can't determine
// the type of the value parameter this way. Instead, we want to use the dynamic type
// itself in this case, so we skip the special descriptor handling.
if attr_ty.is_fully_static(db) {
let dunder_set = attr_ty.class_member(db, "__set__".into());
if let Some(dunder_set) = dunder_set.symbol.ignore_possibly_unbound() {
// This type of this attribute is a data descriptor. Instead of overwriting the
// descriptor attribute, data-classes will (implicitly) call the `__set__` method
// of the descriptor. This means that the synthesized `__init__` parameter for
// this attribute is determined by possible `value` parameter types with which
// the `__set__` method can be called. We build a union of all possible options
// to account for possible overloads.
let mut value_types = UnionBuilder::new(db);
for signature in &dunder_set.signatures(db) {
for overload in signature {
if let Some(value_param) = overload.parameters().get_positional(2) {
value_types = value_types.add(
value_param.annotated_type().unwrap_or_else(Type::unknown),
);
} else if overload.parameters().is_gradual() {
value_types = value_types.add(Type::unknown());
match name {
"__init__" => {
let has_synthesized_dunder_init = has_dataclass_param(DataclassParams::INIT)
|| self
.try_metaclass(db)
.is_ok_and(|(_, transformer_params)| transformer_params.is_some());
if !has_synthesized_dunder_init {
return None;
}
let mut parameters = vec![];
for (name, (mut attr_ty, mut default_ty)) in
self.dataclass_fields(db, specialization)
{
// The descriptor handling below is guarded by this fully-static check, because dynamic
// types like `Any` are valid (data) descriptors: since they have all possible attributes,
// they also have a (callable) `__set__` method. The problem is that we can't determine
// the type of the value parameter this way. Instead, we want to use the dynamic type
// itself in this case, so we skip the special descriptor handling.
if attr_ty.is_fully_static(db) {
let dunder_set = attr_ty.class_member(db, "__set__".into());
if let Some(dunder_set) = dunder_set.symbol.ignore_possibly_unbound() {
// This type of this attribute is a data descriptor. Instead of overwriting the
// descriptor attribute, data-classes will (implicitly) call the `__set__` method
// of the descriptor. This means that the synthesized `__init__` parameter for
// this attribute is determined by possible `value` parameter types with which
// the `__set__` method can be called. We build a union of all possible options
// to account for possible overloads.
let mut value_types = UnionBuilder::new(db);
for signature in &dunder_set.signatures(db) {
for overload in signature {
if let Some(value_param) =
overload.parameters().get_positional(2)
{
value_types = value_types.add(
value_param
.annotated_type()
.unwrap_or_else(Type::unknown),
);
} else if overload.parameters().is_gradual() {
value_types = value_types.add(Type::unknown());
}
}
}
}
attr_ty = value_types.build();
attr_ty = value_types.build();
// The default value of the attribute is *not* determined by the right hand side
// of the class-body assignment. Instead, the runtime invokes `__get__` on the
// descriptor, as if it had been called on the class itself, i.e. it passes `None`
// for the `instance` argument.
// The default value of the attribute is *not* determined by the right hand side
// of the class-body assignment. Instead, the runtime invokes `__get__` on the
// descriptor, as if it had been called on the class itself, i.e. it passes `None`
// for the `instance` argument.
if let Some(ref mut default_ty) = default_ty {
*default_ty = default_ty
.try_call_dunder_get(db, Type::none(db), Type::ClassLiteral(self))
.map(|(return_ty, _)| return_ty)
.unwrap_or_else(Type::unknown);
if let Some(ref mut default_ty) = default_ty {
*default_ty = default_ty
.try_call_dunder_get(
db,
Type::none(db),
Type::ClassLiteral(self),
)
.map(|(return_ty, _)| return_ty)
.unwrap_or_else(Type::unknown);
}
}
}
let mut parameter =
Parameter::positional_or_keyword(name).with_annotated_type(attr_ty);
if let Some(default_ty) = default_ty {
parameter = parameter.with_default_type(default_ty);
}
parameters.push(parameter);
}
let mut parameter =
Parameter::positional_or_keyword(name).with_annotated_type(attr_ty);
let init_signature =
Signature::new(Parameters::new(parameters), Some(Type::none(db)));
if let Some(default_ty) = default_ty {
parameter = parameter.with_default_type(default_ty);
}
parameters.push(parameter);
Some(Type::Callable(CallableType::single(db, init_signature)))
}
"__lt__" | "__le__" | "__gt__" | "__ge__" => {
if !has_dataclass_param(DataclassParams::ORDER) {
return None;
}
let init_signature = Signature::new(Parameters::new(parameters), Some(Type::none(db)));
return Some(Type::Callable(CallableType::single(db, init_signature)));
} else if matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") {
if metadata.contains(DataclassMetadata::ORDER) {
let signature = Signature::new(
Parameters::new([Parameter::positional_or_keyword(Name::new_static("other"))
// TODO: could be `Self`.
@@ -1059,11 +1092,17 @@ impl<'db> ClassLiteralType<'db> {
Some(KnownClass::Bool.to_instance(db)),
);
return Some(Type::Callable(CallableType::single(db, signature)));
Some(Type::Callable(CallableType::single(db, signature)))
}
_ => None,
}
}
None
fn is_dataclass(self, db: &'db dyn Db) -> bool {
self.dataclass_params(db).is_some()
|| self
.try_metaclass(db)
.is_ok_and(|(_, transformer_params)| transformer_params.is_some())
}
/// Returns a list of all annotated attributes defined in this class, or any of its superclasses.
@@ -1079,7 +1118,7 @@ impl<'db> ClassLiteralType<'db> {
.filter_map(|superclass| {
if let Some(class) = superclass.into_class() {
let class_literal = class.class_literal(db).0;
if class_literal.dataclass_metadata(db).is_some() {
if class_literal.is_dataclass(db) {
Some(class_literal)
} else {
None
@@ -1653,40 +1692,6 @@ impl InheritanceCycle {
}
}
/// A type representing the set of runtime objects which are instances of a certain class.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)]
pub struct InstanceType<'db> {
pub class: ClassType<'db>,
}
impl<'db> InstanceType<'db> {
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool {
// N.B. The subclass relation is fully static
self.class.is_subclass_of(db, other.class)
}
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool {
self.class.is_equivalent_to(db, other.class)
}
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: InstanceType<'db>) -> bool {
self.class.is_assignable_to(db, other.class)
}
pub(super) fn is_gradual_equivalent_to(
self,
db: &'db dyn Db,
other: InstanceType<'db>,
) -> bool {
self.class.is_gradual_equivalent_to(db, other.class)
}
}
impl<'db> From<InstanceType<'db>> for Type<'db> {
fn from(value: InstanceType<'db>) -> Self {
Self::Instance(value)
}
}
/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow
/// for easier syntax when interacting with very common classes.
///
@@ -2410,357 +2415,6 @@ impl<'db> KnownClassLookupError<'db> {
}
}
/// Enumeration of specific runtime that are special enough to be considered their own type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub enum KnownInstanceType<'db> {
/// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`)
Annotated,
/// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`)
Literal,
/// The symbol `typing.LiteralString` (which can also be found as `typing_extensions.LiteralString`)
LiteralString,
/// The symbol `typing.Optional` (which can also be found as `typing_extensions.Optional`)
Optional,
/// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`)
Union,
/// The symbol `typing.NoReturn` (which can also be found as `typing_extensions.NoReturn`)
NoReturn,
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
Never,
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
/// This is not used since typeshed switched to representing `Any` as a class; now we use
/// `KnownClass::Any` instead. But we still support the old `Any = object()` representation, at
/// least for now. TODO maybe remove?
Any,
/// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`)
Tuple,
/// The symbol `typing.List` (which can also be found as `typing_extensions.List`)
List,
/// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`)
Dict,
/// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`)
Set,
/// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`)
FrozenSet,
/// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`)
ChainMap,
/// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`)
Counter,
/// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`)
DefaultDict,
/// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`)
Deque,
/// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`)
OrderedDict,
/// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`)
Protocol,
/// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`)
Generic,
/// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`)
Type,
/// A single instance of `typing.TypeVar`
TypeVar(TypeVarInstance<'db>),
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
TypeAliasType(TypeAliasType<'db>),
/// The symbol `knot_extensions.Unknown`
Unknown,
/// The symbol `knot_extensions.AlwaysTruthy`
AlwaysTruthy,
/// The symbol `knot_extensions.AlwaysFalsy`
AlwaysFalsy,
/// The symbol `knot_extensions.Not`
Not,
/// The symbol `knot_extensions.Intersection`
Intersection,
/// The symbol `knot_extensions.TypeOf`
TypeOf,
/// The symbol `knot_extensions.CallableTypeOf`
CallableTypeOf,
// Various special forms, special aliases and type qualifiers that we don't yet understand
// (all currently inferred as TODO in most contexts):
TypingSelf,
Final,
ClassVar,
Callable,
Concatenate,
Unpack,
Required,
NotRequired,
TypeAlias,
TypeGuard,
TypeIs,
ReadOnly,
// TODO: fill this enum out with more special forms, etc.
}
impl<'db> KnownInstanceType<'db> {
/// Evaluate the known instance in boolean context
pub(crate) const fn bool(self) -> Truthiness {
match self {
Self::Annotated
| Self::Literal
| Self::LiteralString
| Self::Optional
| Self::TypeVar(_)
| Self::Union
| Self::NoReturn
| Self::Never
| Self::Any
| Self::Tuple
| Self::Type
| Self::TypingSelf
| Self::Final
| Self::ClassVar
| Self::Callable
| Self::Concatenate
| Self::Unpack
| Self::Required
| Self::NotRequired
| Self::TypeAlias
| Self::TypeGuard
| Self::TypeIs
| Self::List
| Self::Dict
| Self::DefaultDict
| Self::Set
| Self::FrozenSet
| Self::Counter
| Self::Deque
| Self::ChainMap
| Self::OrderedDict
| Self::Protocol
| Self::Generic
| Self::ReadOnly
| Self::TypeAliasType(_)
| Self::Unknown
| Self::AlwaysTruthy
| Self::AlwaysFalsy
| Self::Not
| Self::Intersection
| Self::TypeOf
| Self::CallableTypeOf => Truthiness::AlwaysTrue,
}
}
/// Return the repr of the symbol at runtime
pub(crate) fn repr(self, db: &'db dyn Db) -> &'db str {
match self {
Self::Annotated => "typing.Annotated",
Self::Literal => "typing.Literal",
Self::LiteralString => "typing.LiteralString",
Self::Optional => "typing.Optional",
Self::Union => "typing.Union",
Self::NoReturn => "typing.NoReturn",
Self::Never => "typing.Never",
Self::Any => "typing.Any",
Self::Tuple => "typing.Tuple",
Self::Type => "typing.Type",
Self::TypingSelf => "typing.Self",
Self::Final => "typing.Final",
Self::ClassVar => "typing.ClassVar",
Self::Callable => "typing.Callable",
Self::Concatenate => "typing.Concatenate",
Self::Unpack => "typing.Unpack",
Self::Required => "typing.Required",
Self::NotRequired => "typing.NotRequired",
Self::TypeAlias => "typing.TypeAlias",
Self::TypeGuard => "typing.TypeGuard",
Self::TypeIs => "typing.TypeIs",
Self::List => "typing.List",
Self::Dict => "typing.Dict",
Self::DefaultDict => "typing.DefaultDict",
Self::Set => "typing.Set",
Self::FrozenSet => "typing.FrozenSet",
Self::Counter => "typing.Counter",
Self::Deque => "typing.Deque",
Self::ChainMap => "typing.ChainMap",
Self::OrderedDict => "typing.OrderedDict",
Self::Protocol => "typing.Protocol",
Self::Generic => "typing.Generic",
Self::ReadOnly => "typing.ReadOnly",
Self::TypeVar(typevar) => typevar.name(db),
Self::TypeAliasType(_) => "typing.TypeAliasType",
Self::Unknown => "knot_extensions.Unknown",
Self::AlwaysTruthy => "knot_extensions.AlwaysTruthy",
Self::AlwaysFalsy => "knot_extensions.AlwaysFalsy",
Self::Not => "knot_extensions.Not",
Self::Intersection => "knot_extensions.Intersection",
Self::TypeOf => "knot_extensions.TypeOf",
Self::CallableTypeOf => "knot_extensions.CallableTypeOf",
}
}
/// Return the [`KnownClass`] which this symbol is an instance of
pub(crate) const fn class(self) -> KnownClass {
match self {
Self::Annotated => KnownClass::SpecialForm,
Self::Literal => KnownClass::SpecialForm,
Self::LiteralString => KnownClass::SpecialForm,
Self::Optional => KnownClass::SpecialForm,
Self::Union => KnownClass::SpecialForm,
Self::NoReturn => KnownClass::SpecialForm,
Self::Never => KnownClass::SpecialForm,
Self::Any => KnownClass::Object,
Self::Tuple => KnownClass::SpecialForm,
Self::Type => KnownClass::SpecialForm,
Self::TypingSelf => KnownClass::SpecialForm,
Self::Final => KnownClass::SpecialForm,
Self::ClassVar => KnownClass::SpecialForm,
Self::Callable => KnownClass::SpecialForm,
Self::Concatenate => KnownClass::SpecialForm,
Self::Unpack => KnownClass::SpecialForm,
Self::Required => KnownClass::SpecialForm,
Self::NotRequired => KnownClass::SpecialForm,
Self::TypeAlias => KnownClass::SpecialForm,
Self::TypeGuard => KnownClass::SpecialForm,
Self::TypeIs => KnownClass::SpecialForm,
Self::ReadOnly => KnownClass::SpecialForm,
Self::List => KnownClass::StdlibAlias,
Self::Dict => KnownClass::StdlibAlias,
Self::DefaultDict => KnownClass::StdlibAlias,
Self::Set => KnownClass::StdlibAlias,
Self::FrozenSet => KnownClass::StdlibAlias,
Self::Counter => KnownClass::StdlibAlias,
Self::Deque => KnownClass::StdlibAlias,
Self::ChainMap => KnownClass::StdlibAlias,
Self::OrderedDict => KnownClass::StdlibAlias,
Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says
Self::Generic => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
Self::TypeOf => KnownClass::SpecialForm,
Self::Not => KnownClass::SpecialForm,
Self::Intersection => KnownClass::SpecialForm,
Self::CallableTypeOf => KnownClass::SpecialForm,
Self::Unknown => KnownClass::Object,
Self::AlwaysTruthy => KnownClass::Object,
Self::AlwaysFalsy => KnownClass::Object,
}
}
/// Return the instance type which this type is a subtype of.
///
/// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`,
/// so `KnownInstanceType::Literal.instance_fallback(db)`
/// returns `Type::Instance(InstanceType { class: <typing._SpecialForm> })`.
pub(super) fn instance_fallback(self, db: &dyn Db) -> Type {
self.class().to_instance(db)
}
/// Return `true` if this symbol is an instance of `class`.
pub(super) fn is_instance_of(self, db: &'db dyn Db, class: ClassType<'db>) -> bool {
self.class().is_subclass_of(db, class)
}
pub(super) fn try_from_file_and_name(
db: &'db dyn Db,
file: File,
symbol_name: &str,
) -> Option<Self> {
let candidate = match symbol_name {
"Any" => Self::Any,
"ClassVar" => Self::ClassVar,
"Deque" => Self::Deque,
"List" => Self::List,
"Dict" => Self::Dict,
"DefaultDict" => Self::DefaultDict,
"Set" => Self::Set,
"FrozenSet" => Self::FrozenSet,
"Counter" => Self::Counter,
"ChainMap" => Self::ChainMap,
"OrderedDict" => Self::OrderedDict,
"Generic" => Self::Generic,
"Protocol" => Self::Protocol,
"Optional" => Self::Optional,
"Union" => Self::Union,
"NoReturn" => Self::NoReturn,
"Tuple" => Self::Tuple,
"Type" => Self::Type,
"Callable" => Self::Callable,
"Annotated" => Self::Annotated,
"Literal" => Self::Literal,
"Never" => Self::Never,
"Self" => Self::TypingSelf,
"Final" => Self::Final,
"Unpack" => Self::Unpack,
"Required" => Self::Required,
"TypeAlias" => Self::TypeAlias,
"TypeGuard" => Self::TypeGuard,
"TypeIs" => Self::TypeIs,
"ReadOnly" => Self::ReadOnly,
"Concatenate" => Self::Concatenate,
"NotRequired" => Self::NotRequired,
"LiteralString" => Self::LiteralString,
"Unknown" => Self::Unknown,
"AlwaysTruthy" => Self::AlwaysTruthy,
"AlwaysFalsy" => Self::AlwaysFalsy,
"Not" => Self::Not,
"Intersection" => Self::Intersection,
"TypeOf" => Self::TypeOf,
"CallableTypeOf" => Self::CallableTypeOf,
_ => return None,
};
candidate
.check_module(file_to_module(db, file)?.known()?)
.then_some(candidate)
}
/// Return `true` if `module` is a module from which this `KnownInstance` variant can validly originate.
///
/// Most variants can only exist in one module, which is the same as `self.class().canonical_module()`.
/// Some variants could validly be defined in either `typing` or `typing_extensions`, however.
pub(super) fn check_module(self, module: KnownModule) -> bool {
match self {
Self::Any
| Self::ClassVar
| Self::Deque
| Self::List
| Self::Dict
| Self::DefaultDict
| Self::Set
| Self::FrozenSet
| Self::Counter
| Self::ChainMap
| Self::OrderedDict
| Self::Optional
| Self::Union
| Self::NoReturn
| Self::Tuple
| Self::Type
| Self::Generic
| Self::Callable => module.is_typing(),
Self::Annotated
| Self::Protocol
| Self::Literal
| Self::LiteralString
| Self::Never
| Self::TypingSelf
| Self::Final
| Self::Concatenate
| Self::Unpack
| Self::Required
| Self::NotRequired
| Self::TypeAlias
| Self::TypeGuard
| Self::TypeIs
| Self::ReadOnly
| Self::TypeAliasType(_)
| Self::TypeVar(_) => {
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
}
Self::Unknown
| Self::AlwaysTruthy
| Self::AlwaysFalsy
| Self::Not
| Self::Intersection
| Self::TypeOf
| Self::CallableTypeOf => module.is_knot_extensions(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
pub(super) struct MetaclassError<'db> {
kind: MetaclassErrorKind<'db>,

View File

@@ -90,6 +90,7 @@ impl<'db> ClassBase<'db> {
| Type::MethodWrapper(_)
| Type::WrapperDescriptor(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::BytesLiteral(_)
| Type::IntLiteral(_)
| Type::StringLiteral(_)

View File

@@ -65,6 +65,14 @@ impl<'db> InferContext<'db> {
Span::from(self.file()).with_range(ranged.range())
}
/// Create a secondary annotation attached to the range of the given value in
/// the file currently being type checked.
///
/// The annotation returned has no message attached to it.
pub(crate) fn secondary<T: Ranged>(&self, ranged: T) -> Annotation {
Annotation::secondary(self.span(ranged))
}
pub(crate) fn db(&self) -> &'db dyn Db {
self.db
}
@@ -73,22 +81,6 @@ impl<'db> InferContext<'db> {
self.diagnostics.get_mut().extend(other);
}
/// Reports a lint located at `ranged`.
pub(super) fn report_lint_old<T>(
&self,
lint: &'static LintMetadata,
ranged: T,
message: fmt::Arguments,
) where
T: Ranged,
{
let Some(builder) = self.report_lint(lint, ranged) else {
return;
};
let mut diag = builder.into_diagnostic("");
diag.set_primary_message(message);
}
/// Optionally return a builder for a lint diagnostic guard.
///
/// If the current context believes a diagnostic should be reported for
@@ -396,7 +388,6 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
///
/// The diagnostic can be further mutated on the guard via its `DerefMut`
/// impl to `Diagnostic`.
#[must_use]
pub(super) fn into_diagnostic(
self,
message: impl std::fmt::Display,
@@ -533,7 +524,6 @@ impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> {
///
/// The diagnostic can be further mutated on the guard via its `DerefMut`
/// impl to `Diagnostic`.
#[must_use]
pub(super) fn into_diagnostic(
self,
message: impl std::fmt::Display,

View File

@@ -1075,14 +1075,13 @@ pub(super) fn report_index_out_of_bounds(
length: usize,
index: i64,
) {
context.report_lint_old(
&INDEX_OUT_OF_BOUNDS,
node,
format_args!(
"Index {index} is out of bounds for {kind} `{}` with length {length}",
tuple_ty.display(context.db())
),
);
let Some(builder) = context.report_lint(&INDEX_OUT_OF_BOUNDS, node) else {
return;
};
builder.into_diagnostic(format_args!(
"Index {index} is out of bounds for {kind} `{}` with length {length}",
tuple_ty.display(context.db())
));
}
/// Emit a diagnostic declaring that a type does not support subscripting.
@@ -1092,22 +1091,20 @@ pub(super) fn report_non_subscriptable(
non_subscriptable_ty: Type,
method: &str,
) {
context.report_lint_old(
&NON_SUBSCRIPTABLE,
node,
format_args!(
"Cannot subscript object of type `{}` with no `{method}` method",
non_subscriptable_ty.display(context.db())
),
);
let Some(builder) = context.report_lint(&NON_SUBSCRIPTABLE, node) else {
return;
};
builder.into_diagnostic(format_args!(
"Cannot subscript object of type `{}` with no `{method}` method",
non_subscriptable_ty.display(context.db())
));
}
pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeRef) {
context.report_lint_old(
&ZERO_STEPSIZE_IN_SLICE,
node,
format_args!("Slice step size can not be zero"),
);
let Some(builder) = context.report_lint(&ZERO_STEPSIZE_IN_SLICE, node) else {
return;
};
builder.into_diagnostic("Slice step size can not be zero");
}
fn report_invalid_assignment_with_message(
@@ -1116,19 +1113,26 @@ fn report_invalid_assignment_with_message(
target_ty: Type,
message: std::fmt::Arguments,
) {
let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, node) else {
return;
};
match target_ty {
Type::ClassLiteral(class) => {
context.report_lint_old(&INVALID_ASSIGNMENT, node, format_args!(
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
class.name(context.db())));
let mut diag = builder.into_diagnostic(format_args!(
"Implicit shadowing of class `{}`",
class.name(context.db()),
));
diag.info("Annotate to make it explicit if this is intentional");
}
Type::FunctionLiteral(function) => {
context.report_lint_old(&INVALID_ASSIGNMENT, node, format_args!(
"Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional",
function.name(context.db())));
let mut diag = builder.into_diagnostic(format_args!(
"Implicit shadowing of function `{}`",
function.name(context.db()),
));
diag.info("Annotate to make it explicit if this is intentional");
}
_ => {
context.report_lint_old(&INVALID_ASSIGNMENT, node, message);
builder.into_diagnostic(message);
}
}
}
@@ -1202,21 +1206,21 @@ pub(super) fn report_implicit_return_type(
range: impl Ranged,
expected_ty: Type,
) {
context.report_lint_old(
&INVALID_RETURN_TYPE,
range,
format_args!(
"Function can implicitly return `None`, which is not assignable to return type `{}`",
expected_ty.display(context.db())
),
);
let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, range) else {
return;
};
builder.into_diagnostic(format_args!(
"Function can implicitly return `None`, which is not assignable to return type `{}`",
expected_ty.display(context.db())
));
}
pub(super) fn report_invalid_type_checking_constant(context: &InferContext, node: AnyNodeRef) {
context.report_lint_old(
&INVALID_TYPE_CHECKING_CONSTANT,
node,
format_args!("The name TYPE_CHECKING is reserved for use as a flag; only False can be assigned to it.",),
let Some(builder) = context.report_lint(&INVALID_TYPE_CHECKING_CONSTANT, node) else {
return;
};
builder.into_diagnostic(
"The name TYPE_CHECKING is reserved for use as a flag; only False can be assigned to it",
);
}
@@ -1224,13 +1228,12 @@ pub(super) fn report_possibly_unresolved_reference(
context: &InferContext,
expr_name_node: &ast::ExprName,
) {
let ast::ExprName { id, .. } = expr_name_node;
let Some(builder) = context.report_lint(&POSSIBLY_UNRESOLVED_REFERENCE, expr_name_node) else {
return;
};
context.report_lint_old(
&POSSIBLY_UNRESOLVED_REFERENCE,
expr_name_node,
format_args!("Name `{id}` used when possibly not defined"),
);
let ast::ExprName { id, .. } = expr_name_node;
builder.into_diagnostic(format_args!("Name `{id}` used when possibly not defined"));
}
pub(super) fn report_possibly_unbound_attribute(
@@ -1239,93 +1242,86 @@ pub(super) fn report_possibly_unbound_attribute(
attribute: &str,
object_ty: Type,
) {
context.report_lint_old(
&POSSIBLY_UNBOUND_ATTRIBUTE,
target,
format_args!(
"Attribute `{attribute}` on type `{}` is possibly unbound",
object_ty.display(context.db()),
),
);
let Some(builder) = context.report_lint(&POSSIBLY_UNBOUND_ATTRIBUTE, target) else {
return;
};
builder.into_diagnostic(format_args!(
"Attribute `{attribute}` on type `{}` is possibly unbound",
object_ty.display(context.db()),
));
}
pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node: &ast::ExprName) {
let ast::ExprName { id, .. } = expr_name_node;
let Some(builder) = context.report_lint(&UNRESOLVED_REFERENCE, expr_name_node) else {
return;
};
context.report_lint_old(
&UNRESOLVED_REFERENCE,
expr_name_node,
format_args!("Name `{id}` used when not defined"),
);
let ast::ExprName { id, .. } = expr_name_node;
builder.into_diagnostic(format_args!("Name `{id}` used when not defined"));
}
pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) {
context.report_lint_old(
&INVALID_EXCEPTION_CAUGHT,
node,
format_args!(
"Cannot catch object of type `{}` in an exception handler \
let Some(builder) = context.report_lint(&INVALID_EXCEPTION_CAUGHT, node) else {
return;
};
builder.into_diagnostic(format_args!(
"Cannot catch object of type `{}` in an exception handler \
(must be a `BaseException` subclass or a tuple of `BaseException` subclasses)",
ty.display(context.db())
),
);
ty.display(context.db())
));
}
pub(crate) fn report_invalid_exception_raised(context: &InferContext, node: &ast::Expr, ty: Type) {
context.report_lint_old(
&INVALID_RAISE,
node,
format_args!(
"Cannot raise object of type `{}` (must be a `BaseException` subclass or instance)",
ty.display(context.db())
),
);
let Some(builder) = context.report_lint(&INVALID_RAISE, node) else {
return;
};
builder.into_diagnostic(format_args!(
"Cannot raise object of type `{}` (must be a `BaseException` subclass or instance)",
ty.display(context.db())
));
}
pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) {
context.report_lint_old(
&INVALID_RAISE,
node,
format_args!(
"Cannot use object of type `{}` as exception cause \
(must be a `BaseException` subclass or instance or `None`)",
ty.display(context.db())
),
);
let Some(builder) = context.report_lint(&INVALID_RAISE, node) else {
return;
};
builder.into_diagnostic(format_args!(
"Cannot use object of type `{}` as exception cause \
(must be a `BaseException` subclass or instance or `None`)",
ty.display(context.db())
));
}
pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node: &ast::Expr) {
context.report_lint_old(
&INCOMPATIBLE_SLOTS,
node,
format_args!("Class base has incompatible `__slots__`"),
);
let Some(builder) = context.report_lint(&INCOMPATIBLE_SLOTS, node) else {
return;
};
builder.into_diagnostic("Class base has incompatible `__slots__`");
}
pub(crate) fn report_invalid_arguments_to_annotated(
context: &InferContext,
subscript: &ast::ExprSubscript,
) {
context.report_lint_old(
&INVALID_TYPE_FORM,
subscript,
format_args!(
"Special form `{}` expected at least 2 arguments (one type and at least one metadata element)",
KnownInstanceType::Annotated.repr(context.db())
),
);
let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, subscript) else {
return;
};
builder.into_diagnostic(format_args!(
"Special form `{}` expected at least 2 arguments \
(one type and at least one metadata element)",
KnownInstanceType::Annotated.repr(context.db())
));
}
pub(crate) fn report_invalid_arguments_to_callable(
context: &InferContext,
subscript: &ast::ExprSubscript,
) {
context.report_lint_old(
&INVALID_TYPE_FORM,
subscript,
format_args!(
"Special form `{}` expected exactly two arguments (parameter types and return type)",
KnownInstanceType::Callable.repr(context.db())
),
);
let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, subscript) else {
return;
};
builder.into_diagnostic(format_args!(
"Special form `{}` expected exactly two arguments (parameter types and return type)",
KnownInstanceType::Callable.repr(context.db())
));
}

View File

@@ -10,9 +10,9 @@ use crate::types::class::{ClassType, GenericAlias, GenericClass};
use crate::types::generics::{GenericContext, Specialization};
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::{
FunctionSignature, InstanceType, IntersectionType, KnownClass, MethodWrapperKind,
StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance,
UnionType, WrapperDescriptorKind,
FunctionSignature, IntersectionType, KnownClass, MethodWrapperKind, StringLiteralType,
SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType,
WrapperDescriptorKind,
};
use crate::Db;
use rustc_hash::FxHashMap;
@@ -73,7 +73,7 @@ impl Display for DisplayRepresentation<'_> {
match self.ty {
Type::Dynamic(dynamic) => dynamic.fmt(f),
Type::Never => f.write_str("Never"),
Type::Instance(InstanceType { class }) => match (class, class.known(self.db)) {
Type::Instance(instance) => match (instance.class(), instance.class().known(self.db)) {
(_, Some(KnownClass::NoneType)) => f.write_str("None"),
(_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"),
(ClassType::NonGeneric(class), _) => f.write_str(&class.class(self.db).name),
@@ -195,7 +195,10 @@ impl Display for DisplayRepresentation<'_> {
write!(f, "<wrapper-descriptor `{method}` of `{object}` objects>")
}
Type::DataclassDecorator(_) => {
f.write_str("<decorator produced by dataclasses.dataclass>")
f.write_str("<decorator produced by dataclass-like function>")
}
Type::DataclassTransformer(_) => {
f.write_str("<decorator produced by typing.dataclass_transform>")
}
Type::Union(union) => union.display(self.db).fmt(f),
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
//! Instance types: both nominal and structural.
use super::{ClassType, KnownClass, SubclassOfType, Type};
use crate::Db;
impl<'db> Type<'db> {
pub(crate) const fn instance(class: ClassType<'db>) -> Self {
Self::Instance(InstanceType { class })
}
pub(crate) const fn into_instance(self) -> Option<InstanceType<'db>> {
match self {
Type::Instance(instance_type) => Some(instance_type),
_ => None,
}
}
}
/// A type representing the set of runtime objects which are instances of a certain nominal class.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update)]
pub struct InstanceType<'db> {
// Keep this field private, so that the only way of constructing `InstanceType` instances
// is through the `Type::instance` constructor function.
class: ClassType<'db>,
}
impl<'db> InstanceType<'db> {
pub(super) fn class(self) -> ClassType<'db> {
self.class
}
pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
// N.B. The subclass relation is fully static
self.class.is_subclass_of(db, other.class)
}
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.class.is_equivalent_to(db, other.class)
}
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
self.class.is_assignable_to(db, other.class)
}
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
if self.class.is_final(db) && !self.class.is_subclass_of(db, other.class) {
return true;
}
if other.class.is_final(db) && !other.class.is_subclass_of(db, self.class) {
return true;
}
// Check to see whether the metaclasses of `self` and `other` are disjoint.
// Avoid this check if the metaclass of either `self` or `other` is `type`,
// however, since we end up with infinite recursion in that case due to the fact
// that `type` is its own metaclass (and we know that `type` cannot be disjoint
// from any metaclass, anyway).
let type_type = KnownClass::Type.to_instance(db);
let self_metaclass = self.class.metaclass_instance_type(db);
if self_metaclass == type_type {
return false;
}
let other_metaclass = other.class.metaclass_instance_type(db);
if other_metaclass == type_type {
return false;
}
self_metaclass.is_disjoint_from(db, other_metaclass)
}
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.class.is_gradual_equivalent_to(db, other.class)
}
pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool {
self.class.known(db).is_some_and(KnownClass::is_singleton)
}
pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool {
self.class
.known(db)
.is_some_and(KnownClass::is_single_valued)
}
pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> {
SubclassOfType::from(db, self.class)
}
}
impl<'db> From<InstanceType<'db>> for Type<'db> {
fn from(value: InstanceType<'db>) -> Self {
Self::Instance(value)
}
}

View File

@@ -0,0 +1,372 @@
//! The `KnownInstance` type.
//!
//! Despite its name, this is quite a different type from [`super::InstanceType`].
//! For the vast majority of instance-types in Python, we cannot say how many possible
//! inhabitants there are or could be of that type at runtime. Each variant of the
//! [`KnownInstanceType`] enum, however, represents a specific runtime symbol
//! that requires heavy special-casing in the type system. Thus any one `KnownInstance`
//! variant can only be inhabited by one or two specific objects at runtime with
//! locations that are known in advance.
use super::{class::KnownClass, ClassType, Truthiness, Type, TypeAliasType, TypeVarInstance};
use crate::db::Db;
use crate::module_resolver::{file_to_module, KnownModule};
use ruff_db::files::File;
/// Enumeration of specific runtime symbols that are special enough
/// that they can each be considered to inhabit a unique type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub enum KnownInstanceType<'db> {
/// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`)
Annotated,
/// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`)
Literal,
/// The symbol `typing.LiteralString` (which can also be found as `typing_extensions.LiteralString`)
LiteralString,
/// The symbol `typing.Optional` (which can also be found as `typing_extensions.Optional`)
Optional,
/// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`)
Union,
/// The symbol `typing.NoReturn` (which can also be found as `typing_extensions.NoReturn`)
NoReturn,
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
Never,
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
/// This is not used since typeshed switched to representing `Any` as a class; now we use
/// `KnownClass::Any` instead. But we still support the old `Any = object()` representation, at
/// least for now. TODO maybe remove?
Any,
/// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`)
Tuple,
/// The symbol `typing.List` (which can also be found as `typing_extensions.List`)
List,
/// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`)
Dict,
/// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`)
Set,
/// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`)
FrozenSet,
/// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`)
ChainMap,
/// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`)
Counter,
/// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`)
DefaultDict,
/// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`)
Deque,
/// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`)
OrderedDict,
/// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`)
Protocol,
/// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`)
Generic,
/// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`)
Type,
/// A single instance of `typing.TypeVar`
TypeVar(TypeVarInstance<'db>),
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
TypeAliasType(TypeAliasType<'db>),
/// The symbol `knot_extensions.Unknown`
Unknown,
/// The symbol `knot_extensions.AlwaysTruthy`
AlwaysTruthy,
/// The symbol `knot_extensions.AlwaysFalsy`
AlwaysFalsy,
/// The symbol `knot_extensions.Not`
Not,
/// The symbol `knot_extensions.Intersection`
Intersection,
/// The symbol `knot_extensions.TypeOf`
TypeOf,
/// The symbol `knot_extensions.CallableTypeOf`
CallableTypeOf,
/// The symbol `typing.Callable`
/// (which can also be found as `typing_extensions.Callable` or as `collections.abc.Callable`)
Callable,
// Various special forms, special aliases and type qualifiers that we don't yet understand
// (all currently inferred as TODO in most contexts):
TypingSelf,
Final,
ClassVar,
Concatenate,
Unpack,
Required,
NotRequired,
TypeAlias,
TypeGuard,
TypeIs,
ReadOnly,
// TODO: fill this enum out with more special forms, etc.
}
impl<'db> KnownInstanceType<'db> {
/// Evaluate the known instance in boolean context
pub(crate) const fn bool(self) -> Truthiness {
match self {
Self::Annotated
| Self::Literal
| Self::LiteralString
| Self::Optional
| Self::TypeVar(_)
| Self::Union
| Self::NoReturn
| Self::Never
| Self::Any
| Self::Tuple
| Self::Type
| Self::TypingSelf
| Self::Final
| Self::ClassVar
| Self::Callable
| Self::Concatenate
| Self::Unpack
| Self::Required
| Self::NotRequired
| Self::TypeAlias
| Self::TypeGuard
| Self::TypeIs
| Self::List
| Self::Dict
| Self::DefaultDict
| Self::Set
| Self::FrozenSet
| Self::Counter
| Self::Deque
| Self::ChainMap
| Self::OrderedDict
| Self::Protocol
| Self::Generic
| Self::ReadOnly
| Self::TypeAliasType(_)
| Self::Unknown
| Self::AlwaysTruthy
| Self::AlwaysFalsy
| Self::Not
| Self::Intersection
| Self::TypeOf
| Self::CallableTypeOf => Truthiness::AlwaysTrue,
}
}
/// Return the repr of the symbol at runtime
pub(crate) fn repr(self, db: &'db dyn Db) -> &'db str {
match self {
Self::Annotated => "typing.Annotated",
Self::Literal => "typing.Literal",
Self::LiteralString => "typing.LiteralString",
Self::Optional => "typing.Optional",
Self::Union => "typing.Union",
Self::NoReturn => "typing.NoReturn",
Self::Never => "typing.Never",
Self::Any => "typing.Any",
Self::Tuple => "typing.Tuple",
Self::Type => "typing.Type",
Self::TypingSelf => "typing.Self",
Self::Final => "typing.Final",
Self::ClassVar => "typing.ClassVar",
Self::Callable => "typing.Callable",
Self::Concatenate => "typing.Concatenate",
Self::Unpack => "typing.Unpack",
Self::Required => "typing.Required",
Self::NotRequired => "typing.NotRequired",
Self::TypeAlias => "typing.TypeAlias",
Self::TypeGuard => "typing.TypeGuard",
Self::TypeIs => "typing.TypeIs",
Self::List => "typing.List",
Self::Dict => "typing.Dict",
Self::DefaultDict => "typing.DefaultDict",
Self::Set => "typing.Set",
Self::FrozenSet => "typing.FrozenSet",
Self::Counter => "typing.Counter",
Self::Deque => "typing.Deque",
Self::ChainMap => "typing.ChainMap",
Self::OrderedDict => "typing.OrderedDict",
Self::Protocol => "typing.Protocol",
Self::Generic => "typing.Generic",
Self::ReadOnly => "typing.ReadOnly",
Self::TypeVar(typevar) => typevar.name(db),
Self::TypeAliasType(_) => "typing.TypeAliasType",
Self::Unknown => "knot_extensions.Unknown",
Self::AlwaysTruthy => "knot_extensions.AlwaysTruthy",
Self::AlwaysFalsy => "knot_extensions.AlwaysFalsy",
Self::Not => "knot_extensions.Not",
Self::Intersection => "knot_extensions.Intersection",
Self::TypeOf => "knot_extensions.TypeOf",
Self::CallableTypeOf => "knot_extensions.CallableTypeOf",
}
}
/// Return the [`KnownClass`] which this symbol is an instance of
pub(crate) const fn class(self) -> KnownClass {
match self {
Self::Annotated => KnownClass::SpecialForm,
Self::Literal => KnownClass::SpecialForm,
Self::LiteralString => KnownClass::SpecialForm,
Self::Optional => KnownClass::SpecialForm,
Self::Union => KnownClass::SpecialForm,
Self::NoReturn => KnownClass::SpecialForm,
Self::Never => KnownClass::SpecialForm,
Self::Any => KnownClass::Object,
Self::Tuple => KnownClass::SpecialForm,
Self::Type => KnownClass::SpecialForm,
Self::TypingSelf => KnownClass::SpecialForm,
Self::Final => KnownClass::SpecialForm,
Self::ClassVar => KnownClass::SpecialForm,
Self::Callable => KnownClass::SpecialForm,
Self::Concatenate => KnownClass::SpecialForm,
Self::Unpack => KnownClass::SpecialForm,
Self::Required => KnownClass::SpecialForm,
Self::NotRequired => KnownClass::SpecialForm,
Self::TypeAlias => KnownClass::SpecialForm,
Self::TypeGuard => KnownClass::SpecialForm,
Self::TypeIs => KnownClass::SpecialForm,
Self::ReadOnly => KnownClass::SpecialForm,
Self::List => KnownClass::StdlibAlias,
Self::Dict => KnownClass::StdlibAlias,
Self::DefaultDict => KnownClass::StdlibAlias,
Self::Set => KnownClass::StdlibAlias,
Self::FrozenSet => KnownClass::StdlibAlias,
Self::Counter => KnownClass::StdlibAlias,
Self::Deque => KnownClass::StdlibAlias,
Self::ChainMap => KnownClass::StdlibAlias,
Self::OrderedDict => KnownClass::StdlibAlias,
Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says
Self::Generic => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
Self::TypeOf => KnownClass::SpecialForm,
Self::Not => KnownClass::SpecialForm,
Self::Intersection => KnownClass::SpecialForm,
Self::CallableTypeOf => KnownClass::SpecialForm,
Self::Unknown => KnownClass::Object,
Self::AlwaysTruthy => KnownClass::Object,
Self::AlwaysFalsy => KnownClass::Object,
}
}
/// Return the instance type which this type is a subtype of.
///
/// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`,
/// so `KnownInstanceType::Literal.instance_fallback(db)`
/// returns `Type::Instance(InstanceType { class: <typing._SpecialForm> })`.
pub(super) fn instance_fallback(self, db: &dyn Db) -> Type {
self.class().to_instance(db)
}
/// Return `true` if this symbol is an instance of `class`.
pub(super) fn is_instance_of(self, db: &'db dyn Db, class: ClassType<'db>) -> bool {
self.class().is_subclass_of(db, class)
}
pub(super) fn try_from_file_and_name(
db: &'db dyn Db,
file: File,
symbol_name: &str,
) -> Option<Self> {
let candidate = match symbol_name {
"Any" => Self::Any,
"ClassVar" => Self::ClassVar,
"Deque" => Self::Deque,
"List" => Self::List,
"Dict" => Self::Dict,
"DefaultDict" => Self::DefaultDict,
"Set" => Self::Set,
"FrozenSet" => Self::FrozenSet,
"Counter" => Self::Counter,
"ChainMap" => Self::ChainMap,
"OrderedDict" => Self::OrderedDict,
"Generic" => Self::Generic,
"Protocol" => Self::Protocol,
"Optional" => Self::Optional,
"Union" => Self::Union,
"NoReturn" => Self::NoReturn,
"Tuple" => Self::Tuple,
"Type" => Self::Type,
"Callable" => Self::Callable,
"Annotated" => Self::Annotated,
"Literal" => Self::Literal,
"Never" => Self::Never,
"Self" => Self::TypingSelf,
"Final" => Self::Final,
"Unpack" => Self::Unpack,
"Required" => Self::Required,
"TypeAlias" => Self::TypeAlias,
"TypeGuard" => Self::TypeGuard,
"TypeIs" => Self::TypeIs,
"ReadOnly" => Self::ReadOnly,
"Concatenate" => Self::Concatenate,
"NotRequired" => Self::NotRequired,
"LiteralString" => Self::LiteralString,
"Unknown" => Self::Unknown,
"AlwaysTruthy" => Self::AlwaysTruthy,
"AlwaysFalsy" => Self::AlwaysFalsy,
"Not" => Self::Not,
"Intersection" => Self::Intersection,
"TypeOf" => Self::TypeOf,
"CallableTypeOf" => Self::CallableTypeOf,
_ => return None,
};
candidate
.check_module(file_to_module(db, file)?.known()?)
.then_some(candidate)
}
/// Return `true` if `module` is a module from which this `KnownInstance` variant can validly originate.
///
/// Most variants can only exist in one module, which is the same as `self.class().canonical_module()`.
/// Some variants could validly be defined in either `typing` or `typing_extensions`, however.
pub(super) fn check_module(self, module: KnownModule) -> bool {
match self {
Self::Any
| Self::ClassVar
| Self::Deque
| Self::List
| Self::Dict
| Self::DefaultDict
| Self::Set
| Self::FrozenSet
| Self::Counter
| Self::ChainMap
| Self::OrderedDict
| Self::Optional
| Self::Union
| Self::NoReturn
| Self::Tuple
| Self::Type
| Self::Generic
| Self::Callable => module.is_typing(),
Self::Annotated
| Self::Protocol
| Self::Literal
| Self::LiteralString
| Self::Never
| Self::TypingSelf
| Self::Final
| Self::Concatenate
| Self::Unpack
| Self::Required
| Self::NotRequired
| Self::TypeAlias
| Self::TypeGuard
| Self::TypeIs
| Self::ReadOnly
| Self::TypeAliasType(_)
| Self::TypeVar(_) => {
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
}
Self::Unknown
| Self::AlwaysTruthy
| Self::AlwaysFalsy
| Self::Not
| Self::Intersection
| Self::TypeOf
| Self::CallableTypeOf => module.is_knot_extensions(),
}
}
pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> {
self.class().to_class_literal(db)
}
}

View File

@@ -446,6 +446,11 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
}
}
ast::CmpOp::Eq if lhs_ty.is_literal_string() => Some(rhs_ty),
ast::CmpOp::Eq if !rhs_ty.is_none(self.db) => Some(
IntersectionBuilder::new(self.db)
.add_negative(Type::none(self.db))
.build(),
),
ast::CmpOp::In => self.evaluate_expr_in(lhs_ty, rhs_ty),
ast::CmpOp::NotIn => self
.evaluate_expr_in(lhs_ty, rhs_ty)

View File

@@ -142,38 +142,38 @@ pub(crate) fn parse_string_annotation(
if let Some(string_literal) = string_expr.as_single_part_string() {
let prefix = string_literal.flags.prefix();
if prefix.is_raw() {
context.report_lint_old(
&RAW_STRING_TYPE_ANNOTATION,
string_literal,
format_args!("Type expressions cannot use raw string literal"),
);
if let Some(builder) = context.report_lint(&RAW_STRING_TYPE_ANNOTATION, string_literal)
{
builder.into_diagnostic("Type expressions cannot use raw string literal");
}
// Compare the raw contents (without quotes) of the expression with the parsed contents
// contained in the string literal.
} else if &source[string_literal.content_range()] == string_literal.as_str() {
match ruff_python_parser::parse_string_annotation(source.as_str(), string_literal) {
Ok(parsed) => return Some(parsed),
Err(parse_error) => context.report_lint_old(
&INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
string_literal,
format_args!("Syntax error in forward annotation: {}", parse_error.error),
),
Err(parse_error) => {
if let Some(builder) =
context.report_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION, string_literal)
{
builder.into_diagnostic(format_args!(
"Syntax error in forward annotation: {}",
parse_error.error
));
}
}
}
} else {
} else if let Some(builder) =
context.report_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, string_expr)
{
// The raw contents of the string doesn't match the parsed content. This could be the
// case for annotations that contain escape sequences.
context.report_lint_old(
&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION,
string_expr,
format_args!("Type expressions cannot contain escape characters"),
);
builder.into_diagnostic("Type expressions cannot contain escape characters");
}
} else {
} else if let Some(builder) =
context.report_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, string_expr)
{
// String is implicitly concatenated.
context.report_lint_old(
&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION,
string_expr,
format_args!("Type expressions cannot span multiple string literals"),
);
builder.into_diagnostic("Type expressions cannot span multiple string literals");
}
None

View File

@@ -3,8 +3,8 @@ use std::cmp::Ordering;
use crate::db::Db;
use super::{
class_base::ClassBase, subclass_of::SubclassOfInner, DynamicType, InstanceType,
KnownInstanceType, SuperOwnerKind, TodoType, Type,
class_base::ClassBase, subclass_of::SubclassOfInner, DynamicType, KnownInstanceType,
SuperOwnerKind, TodoType, Type,
};
/// Return an [`Ordering`] that describes the canonical order in which two types should appear
@@ -79,6 +79,12 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::DataclassDecorator(_), _) => Ordering::Less,
(_, Type::DataclassDecorator(_)) => Ordering::Greater,
(Type::DataclassTransformer(left), Type::DataclassTransformer(right)) => {
left.bits().cmp(&right.bits())
}
(Type::DataclassTransformer(_), _) => Ordering::Less,
(_, Type::DataclassTransformer(_)) => Ordering::Greater,
(Type::Callable(left), Type::Callable(right)) => {
debug_assert_eq!(*left, left.normalized(db));
debug_assert_eq!(*right, right.normalized(db));
@@ -120,10 +126,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::SubclassOf(_), _) => Ordering::Less,
(_, Type::SubclassOf(_)) => Ordering::Greater,
(
Type::Instance(InstanceType { class: left }),
Type::Instance(InstanceType { class: right }),
) => left.cmp(right),
(Type::Instance(left), Type::Instance(right)) => left.class().cmp(&right.class()),
(Type::Instance(_), _) => Ordering::Less,
(_, Type::Instance(_)) => Ordering::Greater,
@@ -155,10 +158,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => left.cmp(right),
(SuperOwnerKind::Class(_), _) => Ordering::Less,
(_, SuperOwnerKind::Class(_)) => Ordering::Greater,
(
SuperOwnerKind::Instance(InstanceType { class: left }),
SuperOwnerKind::Instance(InstanceType { class: right }),
) => left.cmp(right),
(SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => {
left.class().cmp(&right.class())
}
(SuperOwnerKind::Instance(_), _) => Ordering::Less,
(_, SuperOwnerKind::Instance(_)) => Ordering::Greater,
(SuperOwnerKind::Dynamic(left), SuperOwnerKind::Dynamic(right)) => {

View File

@@ -134,35 +134,45 @@ impl<'db> Unpacker<'db> {
};
if let Some(tuple_ty) = ty.into_tuple() {
let tuple_ty_elements = self.tuple_ty_elements(target, elts, tuple_ty);
let tuple_ty_elements =
self.tuple_ty_elements(target, elts, tuple_ty, value_expr);
let length_mismatch = match elts.len().cmp(&tuple_ty_elements.len()) {
Ordering::Less => {
self.context.report_lint_old(
&INVALID_ASSIGNMENT,
target,
format_args!(
"Too many values to unpack (expected {}, got {})",
elts.len(),
tuple_ty_elements.len()
),
);
true
}
Ordering::Greater => {
self.context.report_lint_old(
&INVALID_ASSIGNMENT,
target,
format_args!(
"Not enough values to unpack (expected {}, got {})",
elts.len(),
tuple_ty_elements.len()
),
);
true
}
Ordering::Equal => false,
};
let length_mismatch =
match elts.len().cmp(&tuple_ty_elements.len()) {
Ordering::Less => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Too many values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(self.context.secondary(value_expr).message(
format_args!("Got {}", tuple_ty_elements.len()),
));
}
true
}
Ordering::Greater => {
if let Some(builder) =
self.context.report_lint(&INVALID_ASSIGNMENT, target)
{
let mut diag =
builder.into_diagnostic("Not enough values to unpack");
diag.set_primary_message(format_args!(
"Expected {}",
elts.len(),
));
diag.annotate(self.context.secondary(value_expr).message(
format_args!("Got {}", tuple_ty_elements.len()),
));
}
true
}
Ordering::Equal => false,
};
for (index, ty) in tuple_ty_elements.iter().enumerate() {
if let Some(element_types) = target_types.get_mut(index) {
@@ -203,11 +213,15 @@ impl<'db> Unpacker<'db> {
/// Returns the [`Type`] elements inside the given [`TupleType`] taking into account that there
/// can be a starred expression in the `elements`.
///
/// `value_expr` is an AST reference to the value being unpacked. It is
/// only used for diagnostics.
fn tuple_ty_elements(
&self,
expr: &ast::Expr,
targets: &[ast::Expr],
tuple_ty: TupleType<'db>,
value_expr: AnyNodeRef<'_>,
) -> Cow<'_, [Type<'db>]> {
// If there is a starred expression, it will consume all of the types at that location.
let Some(starred_index) = targets.iter().position(ast::Expr::is_starred_expr) else {
@@ -254,15 +268,15 @@ impl<'db> Unpacker<'db> {
Cow::Owned(element_types)
} else {
self.context.report_lint_old(
&INVALID_ASSIGNMENT,
expr,
format_args!(
"Not enough values to unpack (expected {} or more, got {})",
targets.len() - 1,
tuple_ty.len(self.db())
),
);
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, expr) {
let mut diag = builder.into_diagnostic("Not enough values to unpack");
diag.set_primary_message(format_args!("Expected {} or more", targets.len() - 1));
diag.annotate(
self.context
.secondary(value_expr)
.message(format_args!("Got {}", tuple_ty.len(self.db()))),
);
}
Cow::Owned(vec![Type::unknown(); targets.len()])
}

View File

@@ -77,6 +77,9 @@ test-case = { workspace = true }
# Used via macro expansion.
ignored = ["jiff"]
[package.metadata.dist]
dist = true
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true }

View File

@@ -260,3 +260,9 @@ def f():
for i in range(5):
if j := i:
items.append(j)
def f():
values = [1, 2, 3]
result = list() # this should be replaced with a comprehension
for i in values:
result.append(i + 1) # PERF401

View File

@@ -270,6 +270,15 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF
list_binding_value.is_some_and(|binding_value| match binding_value {
// `value = []`
Expr::List(list_expr) => list_expr.is_empty(),
// `value = list()`
// This might be linted against, but turning it into a list comprehension will also remove it
Expr::Call(call) => {
checker
.semantic()
.resolve_builtin_symbol(&call.func)
.is_some_and(|name| name == "list")
&& call.arguments.is_empty()
}
_ => false,
});

View File

@@ -208,5 +208,16 @@ PERF401.py:262:13: PERF401 Use a list comprehension to create a transformed list
261 | if j := i:
262 | items.append(j)
| ^^^^^^^^^^^^^^^ PERF401
263 |
264 | def f():
|
= help: Replace for loop with list comprehension
PERF401.py:268:9: PERF401 Use a list comprehension to create a transformed list
|
266 | result = list() # this should be replaced with a comprehension
267 | for i in values:
268 | result.append(i + 1) # PERF401
| ^^^^^^^^^^^^^^^^^^^^ PERF401
|
= help: Replace for loop with list comprehension

View File

@@ -492,6 +492,8 @@ PERF401.py:262:13: PERF401 [*] Use a list comprehension to create a transformed
261 | if j := i:
262 | items.append(j)
| ^^^^^^^^^^^^^^^ PERF401
263 |
264 | def f():
|
= help: Replace for loop with list comprehension
@@ -505,3 +507,25 @@ PERF401.py:262:13: PERF401 [*] Use a list comprehension to create a transformed
261 |- if j := i:
262 |- items.append(j)
259 |+ items = [j for i in range(5) if (j := i)]
263 260 |
264 261 | def f():
265 262 | values = [1, 2, 3]
PERF401.py:268:9: PERF401 [*] Use a list comprehension to create a transformed list
|
266 | result = list() # this should be replaced with a comprehension
267 | for i in values:
268 | result.append(i + 1) # PERF401
| ^^^^^^^^^^^^^^^^^^^^ PERF401
|
= help: Replace for loop with list comprehension
Unsafe fix
263 263 |
264 264 | def f():
265 265 | values = [1, 2, 3]
266 |- result = list() # this should be replaced with a comprehension
267 |- for i in values:
268 |- result.append(i + 1) # PERF401
266 |+ # this should be replaced with a comprehension
267 |+ result = [i + 1 for i in values] # PERF401

View File

@@ -39,6 +39,10 @@ use crate::Locator;
/// "{}, {}".format("Hello", "World") # "Hello, World"
/// ```
///
/// This fix is marked as unsafe because:
/// - Comments attached to arguments are not moved, which can cause comments to mismatch the actual arguments.
/// - If arguments have side effects (e.g., print), reordering may change program behavior.
///
/// ## References
/// - [Python documentation: Format String Syntax](https://docs.python.org/3/library/string.html#format-string-syntax)
/// - [Python documentation: `str.format`](https://docs.python.org/3/library/stdtypes.html#str.format)

View File

@@ -1708,9 +1708,9 @@
}
},
"node_modules/uuid": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"

View File

@@ -41,10 +41,10 @@
"pyodide": "^0.27.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-resizable-panels": "^2.1.7",
"react-resizable-panels": "^2.1.8",
"red_knot_wasm": "file:red_knot_wasm",
"shared": "0.0.0",
"smol-toml": "^1.3.1"
"smol-toml": "^1.3.3"
},
"devDependencies": {
"vite-plugin-static-copy": "^2.3.0"
@@ -4969,9 +4969,9 @@
"license": "MIT"
},
"node_modules/react-resizable-panels": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz",
"integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==",
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.8.tgz",
"integrity": "sha512-oDvD0sw34Ecx00cQFLiRJpAE2fCgNLBr8DMrBzkrsaUiLpAycIQoY3eAWfMblDql3pTIMZ60wJ/P89RO1htM2w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
@@ -5393,9 +5393,9 @@
}
},
"node_modules/smol-toml": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.1.tgz",
"integrity": "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.3.tgz",
"integrity": "sha512-KMVLNWu490KlNfD0lbfDBUktJIEaZRBj1eeK0SMfdpO/rfyARIzlnPVI1Ge4l0vtSJmQUAiGKxMyLGrCT38iyA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">= 18"
@@ -6087,10 +6087,10 @@
"monaco-editor": "^0.52.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-resizable-panels": "^2.0.0",
"react-resizable-panels": "^2.1.8",
"ruff_wasm": "file:ruff_wasm",
"shared": "0.0.0",
"smol-toml": "^1.3.0"
"smol-toml": "^1.3.3"
}
},
"ruff/ruff_wasm": {
@@ -6103,7 +6103,7 @@
"@monaco-editor/react": "^4.7.0",
"classnames": "^2.3.2",
"react": "^19.0.0",
"react-resizable-panels": "^2.1.7"
"react-resizable-panels": "^2.1.8"
}
}
}