Compare commits

..

71 Commits

Author SHA1 Message Date
Micha Reiser
ca4c006f7d Experiment with Located trait 2024-10-26 13:10:14 +02:00
Micha Reiser
6aaf1d9446 [red-knot] Remove lint-phase (#13922)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-10-25 18:40:52 +00:00
Alex Waygood
5eb87aa56e [red-knot] Infer Todo, not Unknown, for PEP-604 unions in annotations (#13908) 2024-10-25 18:21:31 +00:00
David Peter
085a43a262 [red-knot] knot benchmark: fix --knot-path arg (#13923)
## Summary

Previously, this would fail with

```
AttributeError: 'str' object has no attribute 'is_file'
```

if I tried to use the `--knot-path` option. I wish we had a type checker
for Python*.

## Test Plan

```sh
uv run benchmark --knot-path ~/.cargo-target/release/red_knot
```

\* to be fair, this would probably require special handling for
`argparse` in the typechecker.
2024-10-25 11:43:39 +02:00
Micha Reiser
32b57b2ee4 Enable nursery rules: 'redundant_clone', 'debug_assert_with_mut_call', and 'unused_peekable' (#13920) 2024-10-25 09:46:30 +02:00
Dhruv Manilawala
337af836d3 Bump version to 0.7.1 (#13913) 2024-10-24 20:57:07 +05:30
Micha Reiser
113ce840a6 Fix normalize arguments when fstring_formatting is disabled (#13910) 2024-10-24 13:07:18 +00:00
Micha Reiser
7272f83868 Fix preview style name in can_omit_parentheses to is_f_string_formatting_enabled (#13907) 2024-10-24 11:32:28 +00:00
Alex Waygood
3eb454699a [red-knot] Format mdtest Python snippets more concisely (#13905) 2024-10-24 11:09:31 +00:00
David Peter
77ae0ccf0f [red-knot] Infer subscript expression types for bytes literals (#13901)
## Summary

Infer subscript expression types for bytes literals:
```py
b = b"\x00abc\xff"

reveal_type(b[0])  # revealed: Literal[b"\x00"]
reveal_type(b[1])  # revealed: Literal[b"a"]
reveal_type(b[-1])  # revealed: Literal[b"\xff"]
reveal_type(b[-2])  # revealed: Literal[b"c"]

reveal_type(b[False])  # revealed: Literal[b"\x00"]
reveal_type(b[True])  # revealed: Literal[b"a"]
```


part of #13689
(https://github.com/astral-sh/ruff/issues/13689#issuecomment-2404285064)

## Test Plan

- New Markdown-based tests (see `mdtest/subscript/bytes.md`)
- Added missing test for `string_literal[bool_literal]`
2024-10-24 12:07:41 +02:00
Micha Reiser
73ee72b665 Join implicit concatenated strings when they fit on a line (#13663) 2024-10-24 11:52:22 +02:00
Micha Reiser
e402e27a09 Use referencial equality in traversal helper methods (#13895) 2024-10-24 11:30:22 +02:00
Mihai Capotă
de4181d7dd Remove "default" remark from ruff check (#13900)
## Summary

`ruff check` has not been the default in a long time. However, the help
message and code comment still designate it as the default. The remark
should have been removed in the deprecation PR #10169.

## Test Plan

Not tested.
2024-10-23 21:17:21 -04:00
David Peter
2c57c2dc8a [red-knot] Type narrowing for isinstance checks (#13894)
## Summary

Add type narrowing for `isinstance(object, classinfo)` [1] checks:
```py
x = 1 if flag else "a"

if isinstance(x, int):
    reveal_type(x)  # revealed: Literal[1]
```

closes #13893

[1] https://docs.python.org/3/library/functions.html#isinstance

## Test Plan

New Markdown-based tests in `narrow/isinstance.md`.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-10-23 20:51:33 +02:00
Micha Reiser
72c18c8225 Fix E221 and E222 to flag missing or extra whitespace around == operator (#13890) 2024-10-23 15:02:29 +02:00
Micha Reiser
00b078268b Fix stale syntax errors in playground (#13888) 2024-10-23 12:30:10 +00:00
Shaygan Hooshyari
4d109514d6 [flake8-type-checking] Support auto-quoting when annotations contain quotes (#11811)
## Summary

This PR updates the fix generation logic for auto-quoting an annotation
to generate an edit even when there's a quote character present.

The logic uses the visitor pattern, maintaining it's state on where it
is and generating the string value one node at a time. This can be
considered as a specialized form of `Generator`. The state required to
maintain is whether we're currently inside a `typing.Literal` or
`typing.Annotated` because the string value in those types should not be
un-quoted i.e., `Generic[Literal["int"]]` should become
`"Generic[Literal['int']]`, the quotes inside the `Literal` should be
preserved.

Fixes: https://github.com/astral-sh/ruff/issues/9137

## Test Plan

Add various test cases to validate this change, validate the snapshots.
There are no ecosystem changes to go through.

---------

Signed-off-by: Shaygan <hey@glyphack.com>
Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-10-23 16:34:03 +05:30
David Peter
387076d212 [red-knot] Use track_caller for expect_ methods (#13884)
## Summary

A minor quality-of-life improvement: add
[`#[track_caller]`](https://doc.rust-lang.org/reference/attributes/codegen.html#the-track_caller-attribute)
attribute to `Type::expect_xyz()` methods and some `TypeInference` methods such that the panic-location
is reported one level higher up in the stack trace.

before: reports location inside the `Type::expect_class_literal()`
method. Not very useful.
```
thread 'types::infer::tests::deferred_annotation_builtin' panicked at crates/red_knot_python_semantic/src/types.rs:304:14:
Expected a Type::ClassLiteral variant
```

after: reports location at the `Type::expect_class_literal()` call site,
where the error was made.
```
thread 'types::infer::tests::deferred_annotation_builtin' panicked at crates/red_knot_python_semantic/src/types/infer.rs:4302:14:
Expected a Type::ClassLiteral variant
```

## Test Plan

Called `expect_class_literal()` on something that's not a
`Type::ClassLiteral` and saw that the error was reported at the call
site.
2024-10-23 12:48:19 +02:00
Micha Reiser
2f88f84972 Alternate quotes for strings inside f-strings in preview (#13860) 2024-10-23 07:57:53 +02:00
David Peter
f335fe4d4a [red-knot] rename {Class,Module,Function} => {Class,Module,Function}Literal (#13873)
## Summary

* Rename `Type::Class` => `Type::ClassLiteral`
* Rename `Type::Function` => `Type::FunctionLiteral`
* Do not rename `Type::Module`
* Remove `*Literal` suffixes in `display::LiteralTypeKind` variants, as
per clippy suggestion
* Get rid of `Type::is_class()` in favor of `is_subtype_of(…, 'type')`;
modifiy `is_subtype_of` to support this.
* Add new `Type::is_xyz()` methods and use them instead of matching on
`Type` variants.

closes #13863 

## Test Plan

New `is_subtype_of_class_literals` unit test.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-10-22 22:10:53 +02:00
David Peter
c6ce52c29e [red-knot] Treat empty intersection as 'object', fix intersection simplification (#13880)
## Summary

- Properly treat the empty intersection as being of type `object`.
- Consequently, change the simplification method to explicitly add
`Never` to the positive side of the intersection when collapsing a type
such as `int & str` to `Never`, as opposed to just clearing both the
positive and the negative side.
- Minor code improvement in `bindings_ty`: use `peekable()` to check
whether the iterator over constraints is empty, instead of handling
first and subsequent elements separately.

fixes #13870

## Test Plan

- New unit tests for `IntersectionBuilder` to make sure the empty
intersection represents `object`.
- Markdown-based regression test for the original issue in #13870
2024-10-22 21:02:46 +02:00
Micha Reiser
5d4edd61bf Fix D204's documentation to correctly mention the conventions when it is enabled (#13867) 2024-10-22 16:51:57 +02:00
samypr100
7dbd8f0f8e ci(docker): incorporate docker release enhancements from uv (#13274)
## Summary

This PR updates `ruff` to match `uv` updated [docker releases
approach](https://github.com/astral-sh/uv/blob/main/.github/workflows/build-docker.yml).
It's a combined PR with changes from these PR's
* https://github.com/astral-sh/uv/pull/6053
* https://github.com/astral-sh/uv/pull/6556
* https://github.com/astral-sh/uv/pull/6734
* https://github.com/astral-sh/uv/pull/7568

Summary of changes / features

1. This change would publish an additional tags that includes only
`major.minor`.

    For a release with `x.y.z`, this would publish the tags:

    * ghcr.io/astral-sh/ruff:latest
    * ghcr.io/astral-sh/ruff:x.y.z
    * ghcr.io/astral-sh/ruff:x.y

2. Parallelizes multi-platform builds using multiple workers (hence the
new docker-build / docker-publish jobs), which cuts docker releases time
in half.

3. This PR introduces additional images with the ruff binaries from
scratch for both amd64/arm64 and makes the mapping easy to configure by
generating the Dockerfile on the fly. This approach focuses on
minimizing CI time by taking advantage of dedicating a worker per
mapping (20-30s~ per job). For example, on release `x.y.z`, this will
publish the following image tags with format
`ghcr.io/astral-sh/ruff:{tag}` with manifests for both amd64/arm64. This
also include `x.y` tags for each respective additional tag. Note, this
version does not include the python based images, unlike `uv`.

* From **scratch**: `latest`, `x.y.z`, `x.y` (currently being published)
* From **alpine:3.20**: `alpine`, `alpine3.20`, `x.y.z-alpine`,
`x.y.z-alpine3.20`
* From **debian:bookworm-slim**: `debian-slim`, `bookworm-slim`,
`x.y.z-debian-slim`, `x.y.z-bookworm-slim`
* From **buildpack-deps:bookworm**: `debian`, `bookworm`,
`x.y.z-debian`, `x.y.z-bookworm`

4. This PR also fixes `org.opencontainers.image.version` for all tags
(including the one from `scratch`) to contain the right release version
instead of branch name `main` (current behavior).

    ```
> docker inspect ghcr.io/astral-sh/ruff:0.6.4 | jq -r
'.[0].Config.Labels'
    {
      ...
      "org.opencontainers.image.version": "main"
    }
    ```

Closes https://github.com/astral-sh/ruff/issues/13481

## Test Plan

Approach mimics `uv` with almost no changes so risk is low but I still
tested the full workflow.

* I have a working CI release pipeline on my fork run
https://github.com/samypr100/ruff/actions/runs/10966657733
* The resulting images were published to
https://github.com/samypr100/ruff/pkgs/container/ruff
2024-10-22 07:06:49 -05:00
David Peter
46c0961b0b [red-knot] is_subtype_of: treat literals as subtype of 'object' (#13876)
Add the following subtype relations:
- `BooleanLiteral <: object`
- `IntLiteral <: object`
- `StringLiteral <: object`
- `LiteralString <: object`
- `BytesLiteral <: object`

Added a test case for `bool <: int`.

## Test Plan

New unit tests.
2024-10-22 13:32:51 +02:00
aditya pillai
cd6c937194 [red-knot] Report line numbers in mdtest relative to the markdown file, not the test snippet (#13804)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Carl Meyer <carl@oddbird.net>
2024-10-22 07:42:40 +00:00
Alex
9d102799f9 [red-knot] Support for not-equal narrowing (#13749)
Add type narrowing for `!=` expression as stated in
#13694.

###  Test Plan

Add tests in new md format.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2024-10-21 23:08:33 +02:00
renovate[bot]
e39110e18b Update cloudflare/wrangler-action action to v3.9.0 (#13846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 20:53:32 +01:00
Micha Reiser
155be88373 Speedup mdtest parser (#13835) 2024-10-21 19:49:20 +00:00
TomerBin
a77512df68 [red-knot] Improve chained comparisons handling (#13825)
## Summary

A small fix for comparisons of multiple comparators.
Instead of comparing each comparator to the leftmost item, we should
compare it to the closest item on the left.

While implementing this, I noticed that we don’t yet narrow Yoda
comparisons (e.g., `True is x`), so I didn’t change that behavior in
this PR.

## Test Plan

Added some mdtests 🎉
2024-10-21 12:38:08 -07:00
Micha Reiser
e9dd92107c formatter: Introduce QuoteMetadata (#13858) 2024-10-21 20:23:46 +01:00
Micha Reiser
9e3cf14dde Speed up mdtests (#13832)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-10-21 20:06:41 +01:00
David Peter
fa7626160b [red-knot] handle unions on the LHS of is_subtype_of (#13857)
## Summary

Just a drive-by change that occurred to me while I was looking at
`Type::is_subtype_of`: the existing pattern for unions on the *right
hand side*:
```rs
            (ty, Type::Union(union)) => union
                .elements(db)
                .iter()
                .any(|&elem_ty| ty.is_subtype_of(db, elem_ty)),
```
is not (generally) correct if the *left hand side* is a union.

## Test Plan

Added new test cases for `is_subtype_of` and `!is_subtype_of`
2024-10-21 20:12:03 +02:00
David Peter
d9ef83bfef [red-knot] Consistently rename BoolLiteral => BooleanLiteral (#13856)
## Summary

- Consistent naming: `BoolLiteral` => `BooleanLiteral` (it's mainly the
`Ty::BoolLiteral` variant that was renamed)

  I tripped over this a few times now, so I thought I'll smooth it out.
- Add a new test case for `Literal[True] <: bool`, as suggested here:
https://github.com/astral-sh/ruff/pull/13781#discussion_r1804922827
2024-10-21 13:55:50 +02:00
Steve C
f3612c2717 [pylint] - restrict iteration-over-set to only work on sets of literals (PLC0208) (#13731) 2024-10-21 12:14:02 +01:00
renovate[bot]
c2dc502f3b Update NPM Development dependencies (#13851)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@cloudflare/workers-types](https://redirect.github.com/cloudflare/workerd)
| [`4.20241004.0` ->
`4.20241018.0`](https://renovatebot.com/diffs/npm/@cloudflare%2fworkers-types/4.20241004.0/4.20241018.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@cloudflare%2fworkers-types/4.20241018.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@cloudflare%2fworkers-types/4.20241018.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@cloudflare%2fworkers-types/4.20241004.0/4.20241018.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@cloudflare%2fworkers-types/4.20241004.0/4.20241018.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@types/react-dom](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react-dom)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom))
| [`18.3.0` ->
`18.3.1`](https://renovatebot.com/diffs/npm/@types%2freact-dom/18.3.0/18.3.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2freact-dom/18.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@types%2freact-dom/18.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@types%2freact-dom/18.3.0/18.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2freact-dom/18.3.0/18.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@typescript-eslint/eslint-plugin](https://typescript-eslint.io/packages/eslint-plugin)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin))
| [`8.8.0` ->
`8.10.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/8.8.0/8.10.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2feslint-plugin/8.10.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@typescript-eslint%2feslint-plugin/8.10.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@typescript-eslint%2feslint-plugin/8.8.0/8.10.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2feslint-plugin/8.8.0/8.10.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@typescript-eslint/parser](https://typescript-eslint.io/packages/parser)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser))
| [`8.8.0` ->
`8.10.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2fparser/8.8.0/8.10.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2fparser/8.10.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@typescript-eslint%2fparser/8.10.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@typescript-eslint%2fparser/8.8.0/8.10.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2fparser/8.8.0/8.10.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [eslint-plugin-react-hooks](https://react.dev/)
([source](https://redirect.github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks))
| [`^4.6.0` ->
`^5.0.0`](https://renovatebot.com/diffs/npm/eslint-plugin-react-hooks/4.6.2/5.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-react-hooks/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/eslint-plugin-react-hooks/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/eslint-plugin-react-hooks/4.6.2/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-react-hooks/4.6.2/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[miniflare](https://redirect.github.com/cloudflare/workers-sdk/tree/main/packages/miniflare#readme)
([source](https://redirect.github.com/cloudflare/workers-sdk/tree/HEAD/packages/miniflare))
| [`3.20240925.0` ->
`3.20241011.0`](https://renovatebot.com/diffs/npm/miniflare/3.20240925.0/3.20241011.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/miniflare/3.20241011.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/miniflare/3.20241011.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/miniflare/3.20240925.0/3.20241011.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/miniflare/3.20240925.0/3.20241011.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [tailwindcss](https://tailwindcss.com)
([source](https://redirect.github.com/tailwindlabs/tailwindcss)) |
[`3.4.13` ->
`3.4.14`](https://renovatebot.com/diffs/npm/tailwindcss/3.4.13/3.4.14) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/tailwindcss/3.4.14?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/tailwindcss/3.4.14?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/tailwindcss/3.4.13/3.4.14?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/tailwindcss/3.4.13/3.4.14?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [typescript](https://www.typescriptlang.org/)
([source](https://redirect.github.com/microsoft/TypeScript)) | [`5.6.2`
-> `5.6.3`](https://renovatebot.com/diffs/npm/typescript/5.6.2/5.6.3) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/typescript/5.6.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/typescript/5.6.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/typescript/5.6.2/5.6.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/typescript/5.6.2/5.6.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [vite](https://vite.dev)
([source](https://redirect.github.com/vitejs/vite/tree/HEAD/packages/vite))
| [`5.4.8` ->
`5.4.9`](https://renovatebot.com/diffs/npm/vite/5.4.8/5.4.9) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vite/5.4.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vite/5.4.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vite/5.4.8/5.4.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/5.4.8/5.4.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [wrangler](https://redirect.github.com/cloudflare/workers-sdk)
([source](https://redirect.github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler))
| [`3.80.0` ->
`3.81.0`](https://renovatebot.com/diffs/npm/wrangler/3.80.0/3.81.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/wrangler/3.81.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/wrangler/3.81.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/wrangler/3.80.0/3.81.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/wrangler/3.80.0/3.81.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>cloudflare/workerd (@&#8203;cloudflare/workers-types)</summary>

###
[`v4.20241018.0`](caeb4e0d9e...fa7168988f)

[Compare
Source](caeb4e0d9e...fa7168988f)

###
[`v4.20241011.0`](7619848185...caeb4e0d9e)

[Compare
Source](7619848185...caeb4e0d9e)

</details>

<details>
<summary>typescript-eslint/typescript-eslint
(@&#8203;typescript-eslint/eslint-plugin)</summary>

###
[`v8.10.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8100-2024-10-17)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.9.0...v8.10.0)

##### 🚀 Features

- support TypeScript 5.6
([#&#8203;9972](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/9972))

##### ❤️  Thank You

-   Josh Goldberg 

You can read about our [versioning
strategy](https://main--typescript-eslint.netlify.app/users/versioning)
and
[releases](https://main--typescript-eslint.netlify.app/users/releases)
on our website.

###
[`v8.9.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#890-2024-10-14)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.8.1...v8.9.0)

##### 🩹 Fixes

- **eslint-plugin:** \[no-unnecessary-type-parameters] cannot assume
variables are either type or value

- **scope-manager:** \[no-use-before-define] do not treat nested
namespace aliases as variable references

- **eslint-plugin:** \[return-await] sync the behavior with
await-thenable

- **eslint-plugin:** \[prefer-literal-enum-member] report a different
error message when `allowBitwiseExpressions` is enabled

-   **eslint-plugin:** \[no-loop-func] sync from upstream base rule

- **eslint-plugin:** \[no-unused-vars] never report the naming of an
enum member

-   **eslint-plugin:** correct use-at-your-own-risk type definitions

-   **eslint-plugin:** handle unions in await...for

##### ❤️  Thank You

-   Abraham Guo
-   Anna Bocharova
-   Arya Emami
-   auvred
-   Joshua Chen
-   Kirk Waiblinger
-   Lotfi Meklati
-   mdm317
-   Ronen Amiel
-   Sukka
-   YeonJuan

You can read about our [versioning
strategy](https://main--typescript-eslint.netlify.app/users/versioning)
and
[releases](https://main--typescript-eslint.netlify.app/users/releases)
on our website.

###
[`v8.8.1`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#881-2024-10-07)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.8.0...v8.8.1)

##### 🩹 Fixes

- **eslint-plugin:** stop warning on
[@&#8203;ts-nocheck](https://redirect.github.com/ts-nocheck) comments
which aren't at the beginning of the file

##### ❤️  Thank You

-   Brad Zacher
-   Ronen Amiel
-   WhitePiano

You can read about our [versioning
strategy](https://main--typescript-eslint.netlify.app/users/versioning)
and
[releases](https://main--typescript-eslint.netlify.app/users/releases)
on our website.

</details>

<details>
<summary>typescript-eslint/typescript-eslint
(@&#8203;typescript-eslint/parser)</summary>

###
[`v8.10.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/parser/CHANGELOG.md#8100-2024-10-17)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.9.0...v8.10.0)

##### 🚀 Features

- support TypeScript 5.6
([#&#8203;9972](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/9972))

##### ❤️  Thank You

-   Josh Goldberg 

You can read about our [versioning
strategy](https://main--typescript-eslint.netlify.app/users/versioning)
and
[releases](https://main--typescript-eslint.netlify.app/users/releases)
on our website.

###
[`v8.9.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/parser/CHANGELOG.md#890-2024-10-14)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.8.1...v8.9.0)

This was a version bump only for parser to align it with other projects,
there were no code changes.

You can read about our [versioning
strategy](https://main--typescript-eslint.netlify.app/users/versioning)
and
[releases](https://main--typescript-eslint.netlify.app/users/releases)
on our website.

###
[`v8.8.1`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/parser/CHANGELOG.md#881-2024-10-07)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.8.0...v8.8.1)

This was a version bump only for parser to align it with other projects,
there were no code changes.

You can read about our [versioning
strategy](https://main--typescript-eslint.netlify.app/users/versioning)
and
[releases](https://main--typescript-eslint.netlify.app/users/releases)
on our website.

</details>

<details>
<summary>facebook/react (eslint-plugin-react-hooks)</summary>

###
[`v5.0.0`](https://redirect.github.com/facebook/react/blob/HEAD/packages/eslint-plugin-react-hooks/CHANGELOG.md#500)

[Compare
Source](a87edf62d7...eslint-plugin-react-hooks@5.0.0)

- **New Violations:** Component names now need to start with an
uppercase letter instead of a non-lowercase letter. This means `_Button`
or `_component` are no longer valid.
([@&#8203;kassens](https://redirect.github.com/kassens)) in
[#&#8203;25162](https://redirect.github.com/facebook/react/pull/25162)

<!---->

- Consider dispatch from `useActionState` stable.
([@&#8203;eps1lon](https://redirect.github.com/eps1lon) in
[#&#8203;29665](https://redirect.github.com/facebook/react/pull/29665))
- Add support for ESLint v9.
([@&#8203;eps1lon](https://redirect.github.com/eps1lon) in
[#&#8203;28773](https://redirect.github.com/facebook/react/pull/28773))
- Accept `as` expression in callback.
([@&#8203;StyleShit](https://redirect.github.com/StyleShit) in
[#&#8203;28202](https://redirect.github.com/facebook/react/pull/28202))
- Accept `as` expressions in deps array.
([@&#8203;StyleShit](https://redirect.github.com/StyleShit) in
[#&#8203;28189](https://redirect.github.com/facebook/react/pull/28189))
- Treat `React.use()` the same as `use()`.
([@&#8203;kassens](https://redirect.github.com/kassens) in
[#&#8203;27769](https://redirect.github.com/facebook/react/pull/27769))
- Move `use()` lint to non-experimental.
([@&#8203;kassens](https://redirect.github.com/kassens) in
[#&#8203;27768](https://redirect.github.com/facebook/react/pull/27768))
- Support Flow `as` expressions.
([@&#8203;cpojer](https://redirect.github.com/cpojer) in
[#&#8203;27590](https://redirect.github.com/facebook/react/pull/27590))
- Allow `useEffect(fn, undefined)`.
([@&#8203;kassens](https://redirect.github.com/kassens) in
[#&#8203;27525](https://redirect.github.com/facebook/react/pull/27525))
- Disallow hooks in async functions.
([@&#8203;acdlite](https://redirect.github.com/acdlite) in
[#&#8203;27045](https://redirect.github.com/facebook/react/pull/27045))
- Rename experimental `useEvent` to `useEffectEvent`.
([@&#8203;sebmarkbage](https://redirect.github.com/sebmarkbage) in
[#&#8203;25881](https://redirect.github.com/facebook/react/pull/25881))
- Lint for presence of `useEvent` functions in dependency lists.
([@&#8203;poteto](https://redirect.github.com/poteto) in
[#&#8203;25512](https://redirect.github.com/facebook/react/pull/25512))
- Check `useEvent` references instead.
([@&#8203;poteto](https://redirect.github.com/poteto) in
[#&#8203;25319](https://redirect.github.com/facebook/react/pull/25319))
- Update `RulesOfHooks` with `useEvent` rules.
([@&#8203;poteto](https://redirect.github.com/poteto) in
[#&#8203;25285](https://redirect.github.com/facebook/react/pull/25285))

</details>

<details>
<summary>cloudflare/workers-sdk (miniflare)</summary>

###
[`v3.20241011.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/miniflare/CHANGELOG.md#3202410110)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/miniflare@3.20241004.0...miniflare@3.20241011.0)

##### Patch Changes

-
[#&#8203;6961](https://redirect.github.com/cloudflare/workers-sdk/pull/6961)
[`5761020`](5761020cb4)
Thanks
[@&#8203;dependabot](https://redirect.github.com/apps/dependabot)! -
chore: update dependencies of "miniflare" package

    The following dependency versions have been updated:

    | Dependency                | From          | To            |
    | ------------------------- | ------------- | ------------- |
    | workerd                   | 1.20241004.0  | 1.20241011.1  |
|
[@&#8203;cloudflare/workers-types](https://redirect.github.com/cloudflare/workers-types)
| ^4.20241004.0 | ^4.20241011.0 |

-
[#&#8203;6943](https://redirect.github.com/cloudflare/workers-sdk/pull/6943)
[`7859a04`](7859a04bcd)
Thanks [@&#8203;sdnts](https://redirect.github.com/sdnts)! - fix: local
queues now respect consumer max delays and retry delays properly

###
[`v3.20241004.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/miniflare/CHANGELOG.md#3202410040)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/miniflare@3.20240925.1...miniflare@3.20241004.0)

##### Patch Changes

-
[#&#8203;6949](https://redirect.github.com/cloudflare/workers-sdk/pull/6949)
[`c863183`](c86318354f)
Thanks
[@&#8203;dependabot](https://redirect.github.com/apps/dependabot)! -
chore: update dependencies of "miniflare" package

    The following dependency versions have been updated:

    | Dependency                | From          | To            |
    | ------------------------- | ------------- | ------------- |
    | workerd                   | 1.20240925.0  | 1.20241004.0  |
|
[@&#8203;cloudflare/workers-types](https://redirect.github.com/cloudflare/workers-types)
| ^4.20240925.0 | ^4.20241004.0 |

###
[`v3.20240925.1`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/miniflare/CHANGELOG.md#3202409251)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/miniflare@3.20240925.0...miniflare@3.20240925.1)

##### Patch Changes

-
[#&#8203;6835](https://redirect.github.com/cloudflare/workers-sdk/pull/6835)
[`5c50949`](5c50949480)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: rename asset plugin options slightly to match wrangler.toml better

    Renamed `path` -> `directory`, `bindingName` -> `binding`.

</details>

<details>
<summary>tailwindlabs/tailwindcss (tailwindcss)</summary>

###
[`v3.4.14`](https://redirect.github.com/tailwindlabs/tailwindcss/releases/tag/v3.4.14)

[Compare
Source](https://redirect.github.com/tailwindlabs/tailwindcss/compare/v3.4.13...v3.4.14)

##### Fixed

- Don't set `display: none` on elements that use `hidden="until-found"`
([#&#8203;14625](https://redirect.github.com/tailwindlabs/tailwindcss/pull/14625))

</details>

<details>
<summary>microsoft/TypeScript (typescript)</summary>

###
[`v5.6.3`](https://redirect.github.com/microsoft/TypeScript/compare/v5.6.2...d48a5cf89a62a62d6c6ed53ffa18f070d9458b85)

[Compare
Source](https://redirect.github.com/microsoft/TypeScript/compare/v5.6.2...v5.6.3)

</details>

<details>
<summary>vitejs/vite (vite)</summary>

###
[`v5.4.9`](https://redirect.github.com/vitejs/vite/releases/tag/v5.4.9)

[Compare
Source](https://redirect.github.com/vitejs/vite/compare/v5.4.8...v5.4.9)

Please refer to
[CHANGELOG.md](https://redirect.github.com/vitejs/vite/blob/v5.4.9/packages/vite/CHANGELOG.md)
for details.

</details>

<details>
<summary>cloudflare/workers-sdk (wrangler)</summary>

###
[`v3.81.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#3810)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@3.80.5...wrangler@3.81.0)

##### Minor Changes

-
[#&#8203;6990](https://redirect.github.com/cloudflare/workers-sdk/pull/6990)
[`586c253`](586c253f7d)
Thanks
[@&#8203;courtney-sims](https://redirect.github.com/courtney-sims)! -
feat: Adds new detailed pages deployment output type

##### Patch Changes

-
[#&#8203;6963](https://redirect.github.com/cloudflare/workers-sdk/pull/6963)
[`a5ac45d`](a5ac45d7d5)
Thanks [@&#8203;RamIdeas](https://redirect.github.com/RamIdeas)! - fix:
make `wrangler dev --remote` respect wrangler.toml's `account_id`
property.

This was a regression in the `--x-dev-env` flow recently turned on by
default.

-
[#&#8203;6996](https://redirect.github.com/cloudflare/workers-sdk/pull/6996)
[`b8ab809`](b8ab8093b9)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: improve error messaging when accidentally using Workers commands in
Pages project

If we detect a Workers command used with a Pages project (i.e.
wrangler.toml contains `pages_output_build_dir`), error with Pages
version of command rather than "missing entry-point" etc.

###
[`v3.80.5`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#3805)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@3.80.4...wrangler@3.80.5)

##### Patch Changes

- Updated dependencies
\[[`5761020`](5761020cb4),
[`7859a04`](7859a04bcd)]:
    -   miniflare@3.20241011.0

###
[`v3.80.4`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#3804)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@3.80.3...wrangler@3.80.4)

##### Patch Changes

-
[#&#8203;6937](https://redirect.github.com/cloudflare/workers-sdk/pull/6937)
[`51aedd4`](51aedd4333)
Thanks [@&#8203;lrapoport-cf](https://redirect.github.com/lrapoport-cf)!
- fix: show help when kv commands are run without parameters

- Updated dependencies
\[[`c863183`](c86318354f),
[`fd43068`](fd430687ec)]:
    -   miniflare@3.20241004.0
-
[@&#8203;cloudflare/workers-shared](https://redirect.github.com/cloudflare/workers-shared)[@&#8203;0](https://redirect.github.com/0).6.0

###
[`v3.80.3`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#3803)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@3.80.2...wrangler@3.80.3)

##### Patch Changes

-
[#&#8203;6927](https://redirect.github.com/cloudflare/workers-sdk/pull/6927)
[`2af75ed`](2af75edb3c)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: respect `CLOUDFLARE_ACCOUNT_ID` with `wrangler pages project`
commands

Fixes
[#&#8203;4947](https://redirect.github.com/cloudflare/workers-sdk/issues/4947)

-
[#&#8203;6894](https://redirect.github.com/cloudflare/workers-sdk/pull/6894)
[`eaf71b8`](eaf71b86cc)
Thanks
[@&#8203;petebacondarwin](https://redirect.github.com/petebacondarwin)!
- fix: improve the rendering of build errors when bundling

-
[#&#8203;6920](https://redirect.github.com/cloudflare/workers-sdk/pull/6920)
[`2e64968`](2e649686c2)
Thanks [@&#8203;vicb](https://redirect.github.com/vicb)! - chore: update
unenv dependency version

Pulls in [feat(node/net): implement Server
mock](https://redirect.github.com/unjs/unenv/pull/316).

-
[#&#8203;6932](https://redirect.github.com/cloudflare/workers-sdk/pull/6932)
[`4c6aad0`](4c6aad05b9)
Thanks [@&#8203;vicb](https://redirect.github.com/vicb)! - fix: allow
`require`ing unenv aliased packages

    Before this PR `require`ing packages aliased in unenv would fail.
    That's because `require` would load the mjs file.

This PR adds wraps the mjs file in a virtual ES module to allow
`require`ing it.

###
[`v3.80.2`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#3802)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@3.80.1...wrangler@3.80.2)

##### Patch Changes

-
[#&#8203;6923](https://redirect.github.com/cloudflare/workers-sdk/pull/6923)
[`1320f20`](1320f20b38)
Thanks [@&#8203;andyjessop](https://redirect.github.com/andyjessop)! -
chore: adds eslint-disable for ESLint error on empty typescript
interface in workers-configuration.d.ts

###
[`v3.80.1`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#3801)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@3.80.0...wrangler@3.80.1)

##### Patch Changes

-
[#&#8203;6908](https://redirect.github.com/cloudflare/workers-sdk/pull/6908)
[`d696850`](d6968507b7)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - fix:
debounce restarting worker on assets dir file changes when `--x-dev-env`
is enabled.

-
[#&#8203;6902](https://redirect.github.com/cloudflare/workers-sdk/pull/6902)
[`dc92af2`](dc92af28c5)
Thanks
[@&#8203;threepointone](https://redirect.github.com/threepointone)! -
fix: enable esbuild's keepNames: true to set .name on functions/classes

-
[#&#8203;6909](https://redirect.github.com/cloudflare/workers-sdk/pull/6909)
[`82180a7`](82180a7a76)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - fix:
Various fixes for logging in `--x-dev-env`, primarily to ensure the
hotkeys don't wipe useful output and are cleaned up correctly

-
[#&#8203;6903](https://redirect.github.com/cloudflare/workers-sdk/pull/6903)
[`54924a4`](54924a4303)
Thanks
[@&#8203;petebacondarwin](https://redirect.github.com/petebacondarwin)!
- fix: ensure that `alias` config gets passed through to the bundler
when using new `--x-dev-env`

Fixes
[#&#8203;6898](https://redirect.github.com/cloudflare/workers-sdk/issues/6898)

-
[#&#8203;6911](https://redirect.github.com/cloudflare/workers-sdk/pull/6911)
[`30b7328`](30b7328073)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: infer experimentalJsonConfig from file extension

Fixes
[#&#8203;5768](https://redirect.github.com/cloudflare/workers-sdk/issues/5768)
- issue with vitest and Pages projects with wrangler.toml

- Updated dependencies
\[[`5c50949`](5c50949480)]:
    -   miniflare@3.20240925.1

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMjAuMSIsInVwZGF0ZWRJblZlciI6IjM4LjEyMC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 09:18:08 +05:30
renovate[bot]
a3a83635f7 Update pre-commit dependencies (#13850)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[abravalheri/validate-pyproject](https://redirect.github.com/abravalheri/validate-pyproject)
| repository | minor | `v0.20.2` -> `v0.21` |
|
[astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit)
| repository | minor | `v0.6.9` -> `v0.7.0` |
| [crate-ci/typos](https://redirect.github.com/crate-ci/typos) |
repository | minor | `v1.25.0` -> `v1.26.0` |
|
[executablebooks/mdformat](https://redirect.github.com/executablebooks/mdformat)
| repository | patch | `0.7.17` -> `0.7.18` |

Note: The `pre-commit` manager in Renovate is not supported by the
`pre-commit` maintainers or community. Please do not report any problems
there, instead [create a Discussion in the Renovate
repository](https://redirect.github.com/renovatebot/renovate/discussions/new)
if you have any questions.

---

### Release Notes

<details>
<summary>abravalheri/validate-pyproject
(abravalheri/validate-pyproject)</summary>

###
[`v0.21`](https://redirect.github.com/abravalheri/validate-pyproject/releases/tag/v0.21)

[Compare
Source](https://redirect.github.com/abravalheri/validate-pyproject/compare/v0.20.2...v0.21)

#### What's Changed

- Added support PEP 735 by
[@&#8203;henryiii](https://redirect.github.com/henryiii) in
[https://github.com/abravalheri/validate-pyproject/pull/208](https://redirect.github.com/abravalheri/validate-pyproject/pull/208)
- Added support PEP 639 by
[@&#8203;henryiii](https://redirect.github.com/henryiii) in
[https://github.com/abravalheri/validate-pyproject/pull/210](https://redirect.github.com/abravalheri/validate-pyproject/pull/210)
- Renamed testing extra to test by
[@&#8203;henryiii](https://redirect.github.com/henryiii) in
[https://github.com/abravalheri/validate-pyproject/pull/212](https://redirect.github.com/abravalheri/validate-pyproject/pull/212)
-   General updates in CI setup

**Full Changelog**:
https://github.com/abravalheri/validate-pyproject/compare/v0.20.2...v0.21

</details>

<details>
<summary>astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit)</summary>

###
[`v0.7.0`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.7.0)

[Compare
Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.6.9...v0.7.0)

See: https://github.com/astral-sh/ruff/releases/tag/0.7.0

</details>

<details>
<summary>crate-ci/typos (crate-ci/typos)</summary>

###
[`v1.26.0`](https://redirect.github.com/crate-ci/typos/releases/tag/v1.26.0)

[Compare
Source](https://redirect.github.com/crate-ci/typos/compare/v1.25.0...v1.26.0)

#### \[1.26.0] - 2024-10-07

##### Compatibility

-   *(pre-commit)* Requires 3.2+

##### Fixes

- *(pre-commit)* Resolve deprecations in 4.0 about deprecated stage
names

</details>

<details>
<summary>executablebooks/mdformat (executablebooks/mdformat)</summary>

###
[`v0.7.18`](https://redirect.github.com/executablebooks/mdformat/compare/0.7.17...0.7.18)

[Compare
Source](https://redirect.github.com/executablebooks/mdformat/compare/0.7.17...0.7.18)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMjAuMSIsInVwZGF0ZWRJblZlciI6IjM4LjEyMC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 09:14:43 +05:30
renovate[bot]
28e995023d Update dependency tomli_w to v1.1.0 (#13849)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [tomli_w](https://redirect.github.com/hukkin/tomli-w)
([changelog](https://redirect.github.com/hukkin/tomli-w/blob/master/CHANGELOG.md))
| `==1.0.0` -> `==1.1.0` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/tomli_w/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/tomli_w/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/tomli_w/1.0.0/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/tomli_w/1.0.0/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>hukkin/tomli-w (tomli_w)</summary>

###
[`v1.1.0`](https://redirect.github.com/hukkin/tomli-w/blob/HEAD/CHANGELOG.md#110)

[Compare
Source](https://redirect.github.com/hukkin/tomli-w/compare/1.0.0...1.1.0)

-   Removed
    -   Support for Python 3.7 and 3.8
-   Added
- Accept generic `collections.abc.Mapping`, not just `dict`, as input.
Thank you [Watal M. Iwasaki](https://redirect.github.com/heavywatal) for
the
        [PR](https://redirect.github.com/hukkin/tomli-w/pull/46).
- `indent` keyword argument for customizing indent width of arrays.
Thank you [Wim Jeantine-Glenn](https://redirect.github.com/wimglenn) for
the
        [PR](https://redirect.github.com/hukkin/tomli-w/pull/49).
-   Type annotations
- Type annotate `dump` function's output stream object as
`typing.IO[bytes]` (previously `typing.BinaryIO`)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMjAuMSIsInVwZGF0ZWRJblZlciI6IjM4LjEyMC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 09:05:30 +05:30
renovate[bot]
a80d515be3 Update Rust crate uuid to v1.11.0 (#13845) 2024-10-20 22:18:49 -05:00
renovate[bot]
9477058790 Update Rust crate serde_json to v1.0.132 (#13848) 2024-10-20 22:18:16 -05:00
renovate[bot]
19a2fd1c82 Update Rust crate fern to 0.7.0 (#13844) 2024-10-21 01:51:49 +00:00
renovate[bot]
cf26676c60 Update Rust crate syn to v2.0.82 (#13842) 2024-10-21 01:50:19 +00:00
renovate[bot]
7060bf87c0 Update Rust crate proc-macro2 to v1.0.88 (#13841) 2024-10-21 01:49:56 +00:00
renovate[bot]
ca8f10862e Update Rust crate anyhow to v1.0.90 (#13839) 2024-10-21 01:49:36 +00:00
renovate[bot]
c9b74eda4a Update Rust crate libc to v0.2.161 (#13840) 2024-10-21 01:49:11 +00:00
renovate[bot]
a5d52b00ca Update dependency ruff to v0.7.0 (#13847) 2024-10-21 01:48:00 +00:00
renovate[bot]
efa798b4c4 Update dependency mdformat to v0.7.18 (#13843) 2024-10-21 01:46:21 +00:00
Alex Waygood
02bca9a8d0 Modernize build scripts (#13837)
Use the modern `cargo::KEY=VALUE` syntax that was stabilised in MSRV 1.77, rather than the deprecated `cargo:KEY=VALUE` syntax.
2024-10-20 22:35:35 +01:00
Alex Waygood
72adb09bf3 Simplify iteration idioms (#13834)
Remove unnecessary uses of `.as_ref()`, `.iter()`, `&**` and similar, mostly in situations when iterating over variables. Many of these changes are only possible following #13826, when we bumped our MSRV to 1.80: several useful implementations on `&Box<[T]>` were only stabilised in Rust 1.80. Some of these changes we could have done earlier, however.
2024-10-20 22:25:27 +01:00
Aditya Pratap Singh
7fd8e30eed [red-knot] Cleanup generated names of mdtest tests (#13831)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-10-20 15:11:53 +00:00
renovate[bot]
7ca3571194 Update Rust crate pep440_rs to 0.7.1 (#13654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-10-20 11:52:55 +02:00
Micha Reiser
27c50bebec Bump MSRV to Rust 1.80 (#13826) 2024-10-20 10:55:36 +02:00
Alex Waygood
075e378b0f Update BREAKING_CHANGES.md for Ruff 0.7 (#13828) 2024-10-20 10:32:58 +02:00
Shaygan Hooshyari
0f0fff4d5a [red-knot] Implement more types in binary and unary expressions (#13803)
Implemented some points from
https://github.com/astral-sh/ruff/issues/12701

- Handle Unknown and Any in Unary operation
- Handle Boolean in binary operations
- Handle instances in unary operation
- Consider division by False to be division by zero

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-10-20 01:57:21 +00:00
Neil Mitchell
2d2baeca23 [python_ast] Make the iter_mut functions public (#13542) 2024-10-19 20:04:00 +01:00
cake-monotone
fb66f715f3 [red-knot] Enhancing Diagnostics for Compare Expression Inference (#13819)
## Summary

- Refactored comparison type inference functions in `infer.rs`: Changed
the return type from `Option` to `Result` to lay the groundwork for
providing more detailed diagnostics.
- Updated diagnostic messages.

This is a small step toward improving diagnostics in the future.

Please refer to #13787

## Test Plan

mdtest included!

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-10-19 18:17:01 +00:00
Alex Waygood
55bccf6680 [red-knot] Fix edge case for binary-expression inference where the lhs and rhs are the exact same type (#13823)
## Summary

This fixes an edge case that @carljm and I missed when implementing
https://github.com/astral-sh/ruff/pull/13800. Namely, if the left-hand
operand is the _exact same type_ as the right-hand operand, the
reflected dunder on the right-hand operand is never tried:

```pycon
>>> class Foo:
...     def __radd__(self, other):
...         return 42
...         
>>> Foo() + Foo()
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    Foo() + Foo()
    ~~~~~~^~~~~~~
TypeError: unsupported operand type(s) for +: 'Foo' and 'Foo'
```

This edge case _is_ covered in Brett's blog at
https://snarky.ca/unravelling-binary-arithmetic-operations-in-python/,
but I missed it amongst all the other subtleties of this algorithm. The
motivations and history behind it were discussed in
https://mail.python.org/archives/list/python-dev@python.org/thread/7NZUCODEAPQFMRFXYRMGJXDSIS3WJYIV/

## Test Plan

I added an mdtest for this cornercase.
2024-10-19 11:09:54 -07:00
Carl Meyer
f4b5e70fae [red-knot] binary arithmetic on instances (#13800)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-10-19 15:22:54 +00:00
Alex Waygood
36cb1199cc [red-knot] Autoformat mdtest Python snippets using blacken-docs (#13809) 2024-10-19 15:57:06 +01:00
Micha Reiser
2ff36530c3 Upgrade to Rust 1.82 (#13816) 2024-10-19 16:05:50 +02:00
Micha Reiser
bd33b4972d Short circuit lex_identifier if the name is longer or shorter than any known keyword (#13815) 2024-10-19 11:07:15 +00:00
David Peter
6964eef369 [red knot] add Type::is_disjoint_from and intersection simplifications (#13775)
## Summary

- Add `Type::is_disjoint_from` as a way to test whether two types
overlap
- Add a first set of simplification rules for intersection types
  - `S & T = S` for `S <: T`
  - `S & ~T = Never` for `S <: T`
  - `~S & ~T = ~T` for `S <: T`
  - `A & ~B = A` for `A` disjoint from `B`
  - `A & B = Never` for `A` disjoint from `B`
  - `bool & ~Literal[bool] = Literal[!bool]`

resolves one item in #12694

## Open questions:

- Can we somehow leverage the (anti) symmetry between `positive` and
`negative` contributions? I could imagine that there would be a way if
we had `Type::Not(type)`/`Type::Negative(type)`, but with the
`positive`/`negative` architecture, I'm not sure. Note that there is a
certain duplication in the `add_positive`/`add_negative` functions (e.g.
`S & ~T = Never` is implemented twice), but other rules are actually not
perfectly symmetric: `S & T = S` vs `~S & ~T = ~T`.
- I'm not particularly proud of the way `add_positive`/`add_negative`
turned out. They are long imperative-style functions with some
mutability mixed in (`to_remove`). I'm happy to look into ways to
improve this code *if we decide to go with this approach* of
implementing a set of ad-hoc rules for simplification.
- ~~Is it useful to perform simplifications eagerly in
`add_positive`/`add_negative`? (@carljm)~~ This is what I did for now.

## Test Plan

- Unit tests for `Type::is_disjoint_from`
- Observe changes in Markdown-based tests
- Unit tests for `IntersectionBuilder::build()`

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-10-18 21:34:43 +00:00
Alex Waygood
c93a7c7878 Set fail_fast: false in .pre-commit-config.yaml (#13811) 2024-10-18 16:03:59 +01:00
Micha Reiser
6d7da7bdbe Revert "Upgrade to Rust 1.82 toolchain" (#13810) 2024-10-18 12:18:26 +00:00
Micha Reiser
ff72055558 Upgrade to Rust 1.82 toolchain (#13808) 2024-10-18 12:08:15 +00:00
Steve C
4ecfe95295 Update to macOS14 runner image (#13728)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-10-18 11:43:30 +02:00
David Peter
c2f7c39987 [red-knot] mdtest suite: formatting and cleanup (#13806)
Minor cleanup and consistent formatting of the Markdown-based tests.

- Removed lots of unnecessary `a`, `b`, `c`, … variables.
- Moved test assertions (`# revealed:` comments) closer to the tested
object.
- Always separate `# revealed` and `# error` comments from the code by
two spaces, according to the discussion
[here](https://github.com/astral-sh/ruff/pull/13746/files#r1799385758).
This trades readability for consistency in some cases.
- Fixed some headings
2024-10-18 11:07:53 +02:00
Matthew Spero
f80528fbf2 Make ARG002 compatible with EM101 when raising NotImplementedError (#13714)
## Summary

This pull request resolves some rule thrashing identified in #12427 by
allowing for unused arguments when using `NotImplementedError` with a
variable per [this
comment](https://github.com/astral-sh/ruff/issues/12427#issuecomment-2384727468).

**Note**

This feels a little heavy-handed / edge-case-prone. So, to be clear, I'm
happy to scrap this code and just update the docs to communicate that
`abstractmethod` and friends should be used in this scenario (or
similar). Just let me know what you'd like done!

fixes: #12427 

## Test Plan

I added a test-case to the existing `ARG.py` file and ran...

```sh
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py --no-cache --preview --select ARG002
```

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-10-18 06:44:22 +00:00
Dhruv Manilawala
040a591cad Avoid indexing the workspace for single-file mode (#13770)
## Summary

This PR updates the language server to avoid indexing the workspace for
single-file mode.

**What's a single-file mode?**

When a user opens the file directly in an editor, and not the folder
that represents the workspace, the editor usually can't determine the
workspace root. This means that during initializing the server, the
`workspaceFolders` field will be empty / nil.

Now, in this case, the server defaults to using the current working
directory which is a reasonable default assuming that the directory
would point to the one where this open file is present. This would allow
the server to index the directory itself for any config file, if
present.

It turns out that in VS Code the current working directory in the above
scenario is the system root directory `/` and so the server will try to
index the entire root directory which would take a lot of time. This is
the issue as described in
https://github.com/astral-sh/ruff-vscode/issues/627. To reproduce, refer
https://github.com/astral-sh/ruff-vscode/issues/627#issuecomment-2401440767.

This PR updates the indexer to avoid traversing the workspace to read
any config file that might be present. The first commit
(8dd2a31eef)
refactors the initialization and introduces two structs `Workspaces` and
`Workspace`. The latter struct includes a field to determine whether
it's the default workspace. The second commit
(61fc39bdb6)
utilizes this field to avoid traversing.

Closes: #11366

## Editor behavior

This is to document the behavior as seen in different editors. The test
scenario used has the following directory tree structure:
```
.
├── nested
│   ├── nested.py
│   └── pyproject.toml
└── test.py
```

where, the contents of the files are:

**test.py**
```py
import os
```

**nested/nested.py**
```py
import os
import math
```

**nested/pyproject.toml**
```toml
[tool.ruff.lint]
select = ["I"]
```

Steps:
1. Open `test.py` directly in the editor
2. Validate that it raises the `F401` violation
3. Open `nested/nested.py` in the same editor instance
4. This file would raise only `I001` if the `nested/pyproject.toml` was
indexed

### VS Code

When (1) is done from above, the current working directory is `/` which
means the server will try to index the entire system to build up the
settings index. This will include the `nested/pyproject.toml` file as
well. This leads to bad user experience because the user would need to
wait for minutes for the server to finish indexing.

This PR avoids that by not traversing the workspace directory in
single-file mode. But, in VS Code, this means that per (4), the file
wouldn't raise `I001` but only raise two `F401` violations because the
`nested/pyproject.toml` was never resolved.

One solution here would be to fix this in the extension itself where we
would detect this scenario and pass in the workspace directory that is
the one containing this open file in (1) above.

### Neovim

**tl;dr** it works as expected because the client considers the presence
of certain files (depending on the server) as the root of the workspace.
For Ruff, they are `pyproject.toml`, `ruff.toml`, and `.ruff.toml`. This
means that the client notifies us as the user moves between single-file
mode and workspace mode.

https://github.com/astral-sh/ruff/pull/13770#issuecomment-2416608055

### Helix

Same as Neovim, additional context in
https://github.com/astral-sh/ruff/pull/13770#issuecomment-2417362097

### Sublime Text

**tl;dr** It works similar to VS Code except that the current working
directory of the current process is different and thus the config file
is never read. So, the behavior remains unchanged with this PR.

https://github.com/astral-sh/ruff/pull/13770#issuecomment-2417362097

### Zed

Zed seems to be starting a separate language server instance for each
file when the editor is running in a single-file mode even though all
files have been opened in a single editor instance.

(Separated the logs into sections separated by a single blank line
indicating 3 different server instances that the editor started for 3
files.)

```
   0.000053375s  INFO main ruff_server::server: No workspace settings found for file:///Users/dhruv/projects/ruff-temp, using default settings
   0.009448792s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/projects/ruff-temp
   0.009906334s DEBUG ruff:main ruff_server::resolve: Included path via `include`: /Users/dhruv/projects/ruff-temp/test.py
   0.011775917s  INFO ruff:main ruff_server::server: Configuration file watcher successfully registered

   0.000060583s  INFO main ruff_server::server: No workspace settings found for file:///Users/dhruv/projects/ruff-temp/nested, using default settings
   0.010387125s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/projects/ruff-temp/nested
   0.011061875s DEBUG ruff:main ruff_server::resolve: Included path via `include`: /Users/dhruv/projects/ruff-temp/nested/nested.py
   0.011545208s  INFO ruff:main ruff_server::server: Configuration file watcher successfully registered

   0.000059125s  INFO main ruff_server::server: No workspace settings found for file:///Users/dhruv/projects/ruff-temp/nested, using default settings
   0.010857583s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/projects/ruff-temp/nested
   0.011428958s DEBUG ruff:main ruff_server::resolve: Included path via `include`: /Users/dhruv/projects/ruff-temp/nested/other.py
   0.011893792s  INFO ruff:main ruff_server::server: Configuration file watcher successfully registered
```

## Test Plan

When using the `ruff` server from this PR, we see that the server starts
quickly as seen in the logs. Next, when I switch to the release binary,
it starts indexing the root directory.

For more details, refer to the "Editor Behavior" section above.
2024-10-18 10:51:43 +05:30
Raphael Gaschignard
3d0bdb426a [red-knot] Use the right scope when considering class bases (#13766)
Summary
---------

PEP 695 Generics introduce a scope inside a class statement's arguments
and keywords.

```
class C[T](A[T]):  # the T in A[T] is not from the global scope but from a type-param-specfic scope
   ...
```

When doing inference on the class bases, we currently have been doing
base class expression lookups in the global scope. Not an issue without
generics (since a scope is only created when generics are present).

This change instead makes sure to stop the global scope inference from
going into expressions within this sub-scope. Since there is a separate
scope, `check_file` and friends will trigger inference on these
expressions still.

Another change as a part of this is making sure that `ClassType` looks
up its bases in the right scope.

Test Plan
----------
`cargo test --package red_knot_python_semantic generics` will run the
markdown test that previously would panic due to scope lookup issues

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Carl Meyer <carl@astral.sh>
2024-10-17 22:29:46 +00:00
Carl Meyer
e2a30b71f4 [red-knot] revert change to emit fewer division by zero errors (#13801)
This reverts https://github.com/astral-sh/ruff/pull/13799, and restores
the previous behavior, which I think was the most pragmatic and useful
version of the divide-by-zero error, if we will emit it at all.

In general, a type checker _does_ emit diagnostics when it can detect
something that will definitely be a problem for some inhabitants of a
type, but not others. For example, `x.foo` if `x` is typed as `object`
is a type error, even though some inhabitants of the type `object` will
have a `foo` attribute! The correct fix is to make your type annotations
more precise, so that `x` is assigned a type which definitely has the
`foo` attribute.

If we will emit it divide-by-zero errors, it should follow the same
logic. Dividing an inhabitant of the type `int` by zero may not emit an
error, if the inhabitant is an instance of a subclass of `builtins.int`
that overrides division. But it may emit an error (more likely it will).
If you don't want the diagnostic, you can clarify your type annotations
to require an instance of your safe subclass.

Because the Python type system doesn't have the ability to explicitly
reflect the fact that divide-by-zero is an error in type annotations
(e.g. for `int.__truediv__`), or conversely to declare a type as safe
from divide-by-zero, or include a "nonzero integer" type which it is
always safe to divide by, the analogy doesn't fully apply. You can't
explicitly mark your subclass of `int` as safe from divide-by-zero, we
just semi-arbitrarily choose to silence the diagnostic for subclasses,
to avoid false positives.

Also, if we fully followed the above logic, we'd have to error on every
`int / int` because the RHS `int` might be zero! But this would likely
cause too many false positives, because of the lack of a "nonzero
integer" type.

So this is just a pragmatic choice to emit the diagnostic when it is
very likely to be an error. It's unclear how useful this diagnostic is
in practice, but this version of it is at least very unlikely to cause
harm.
2024-10-17 20:17:22 +00:00
Carl Meyer
5c537b6dbb [red-knot] don't emit divide-by-zero error if we can't be sure (#13799)
If the LHS is just `int` or `float` type, that type includes custom
subclasses which can arbitrarily override division behavior, so we
shouldn't emit a divide-by-zero error in those cases.

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-10-17 17:11:07 +00:00
363 changed files with 11817 additions and 3581 deletions

View File

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

View File

@@ -17,12 +17,21 @@ on:
paths:
- .github/workflows/build-docker.yml
env:
RUFF_BASE_IMG: ghcr.io/${{ github.repository_owner }}/ruff
jobs:
docker-publish:
name: Build Docker image (ghcr.io/astral-sh/ruff)
docker-build:
name: Build Docker image (ghcr.io/astral-sh/ruff) for ${{ matrix.platform }}
runs-on: ubuntu-latest
environment:
name: release
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- uses: actions/checkout@v4
with:
@@ -36,12 +45,6 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/astral-sh/ruff
- name: Check tag consistency
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
run: |
@@ -55,14 +58,233 @@ jobs:
echo "Releasing ${version}"
fi
- name: "Build and push Docker image"
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.RUFF_BASE_IMG }}
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
tags: |
type=raw,value=dry-run,enable=${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}
type=pep440,pattern={{ version }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
- name: Normalize Platform Pair (replace / with -)
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_TUPLE=${platform//\//-}" >> $GITHUB_ENV
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
cache-from: type=gha,scope=ruff-${{ env.PLATFORM_TUPLE }}
cache-to: type=gha,mode=min,scope=ruff-${{ env.PLATFORM_TUPLE }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.RUFF_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
- name: Export digests
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digests
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_TUPLE }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
docker-publish:
name: Publish Docker image (ghcr.io/astral-sh/ruff)
runs-on: ubuntu-latest
environment:
name: release
needs:
- docker-build
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.RUFF_BASE_IMG }}
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
tags: |
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Create manifest list and push
working-directory: /tmp/digests
# The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array
# The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
docker-publish-extra:
name: Publish additional Docker image based on ${{ matrix.image-mapping }}
runs-on: ubuntu-latest
environment:
name: release
needs:
- docker-publish
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
strategy:
fail-fast: false
matrix:
# Mapping of base image followed by a comma followed by one or more base tags (comma separated)
# Note, org.opencontainers.image.version label will use the first base tag (use the most specific tag first)
image-mapping:
- alpine:3.20,alpine3.20,alpine
- debian:bookworm-slim,bookworm-slim,debian-slim
- buildpack-deps:bookworm,bookworm,debian
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Dynamic Dockerfile Tags
shell: bash
run: |
set -euo pipefail
# Extract the image and tags from the matrix variable
IFS=',' read -r BASE_IMAGE BASE_TAGS <<< "${{ matrix.image-mapping }}"
# Generate Dockerfile content
cat <<EOF > Dockerfile
FROM ${BASE_IMAGE}
COPY --from=${{ env.RUFF_BASE_IMG }}:latest /ruff /usr/local/bin/ruff
ENTRYPOINT []
CMD ["/usr/local/bin/ruff"]
EOF
# Initialize a variable to store all tag docker metadata patterns
TAG_PATTERNS=""
# Loop through all base tags and append its docker metadata pattern to the list
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
IFS=','; for TAG in ${BASE_TAGS}; do
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n"
done
# Remove the trailing newline from the pattern list
TAG_PATTERNS="${TAG_PATTERNS%\\n}"
# Export image cache name
echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> $GITHUB_ENV
# Export tag patterns using the multiline env var syntax
{
echo "TAG_PATTERNS<<EOF"
echo -e "${TAG_PATTERNS}"
echo EOF
} >> $GITHUB_ENV
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
# ghcr.io prefers index level annotations
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
with:
images: ${{ env.RUFF_BASE_IMG }}
flavor: |
latest=false
tags: |
${{ env.TAG_PATTERNS }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
# Reuse the builder
cache-from: type=gha
cache-to: type=gha,mode=max
push: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
tags: ghcr.io/astral-sh/ruff:latest,ghcr.io/astral-sh/ruff:${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || 'dry-run' }}
# We do not really need to cache here as the Dockerfile is tiny
#cache-from: type=gha,scope=ruff-${{ env.IMAGE_REF }}
#cache-to: type=gha,mode=min,scope=ruff-${{ env.IMAGE_REF }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
# This is effectively a duplicate of `docker-publish` to make https://github.com/astral-sh/ruff/pkgs/container/ruff
# show the ruff base image first since GitHub always shows the last updated image digests
# This works by annotating the original digests (previously non-annotated) which triggers an update to ghcr.io
docker-republish:
name: Annotate Docker image (ghcr.io/astral-sh/ruff)
runs-on: ubuntu-latest
environment:
name: release
needs:
- docker-publish-extra
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
with:
images: ${{ env.RUFF_BASE_IMG }}
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
tags: |
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Create manifest list and push
working-directory: /tmp/digests
# The readarray part is used to make sure the quoting and special characters are preserved on expansion (e.g. spaces)
# The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array
# The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
run: |
readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done
docker buildx imagetools create \
"${annotations[@]}" \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)

View File

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

View File

@@ -29,7 +29,7 @@ jobs:
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-pack-action@v0.4.0
@@ -47,7 +47,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.7.0
uses: cloudflare/wrangler-action@v3.9.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -43,7 +43,7 @@ jobs:
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: "Publish (dry-run)"
if: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}

View File

@@ -1,4 +1,4 @@
fail_fast: true
fail_fast: false
exclude: |
(?x)^(
@@ -17,12 +17,12 @@ exclude: |
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.20.2
rev: v0.21
hooks:
- id: validate-pyproject
- repo: https://github.com/executablebooks/mdformat
rev: 0.7.17
rev: 0.7.18
hooks:
- id: mdformat
additional_dependencies:
@@ -45,8 +45,17 @@ repos:
| docs/\w+\.md
)$
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.19.1
hooks:
- id: blacken-docs
args: ["--pyi", "--line-length", "130"]
files: '^crates/.*/resources/mdtest/.*\.md'
additional_dependencies:
- black==24.10.0
- repo: https://github.com/crate-ci/typos
rev: v1.25.0
rev: v1.26.0
hooks:
- id: typos
@@ -60,7 +69,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
rev: v0.7.0
hooks:
- id: ruff-format
- id: ruff

View File

@@ -1,5 +1,18 @@
# Breaking Changes
## 0.7.0
- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments
([#12838](https://github.com/astral-sh/ruff/pull/12838), [#13292](https://github.com/astral-sh/ruff/pull/13292)).
This was a change that we attempted to make in Ruff v0.6.0, but only partially made due to an error on our part.
See the [blog post](https://astral.sh/blog/ruff-v0.7.0) for more details.
- The `useless-try-except` rule (in our `tryceratops` category) has been recoded from `TRY302` to
`TRY203` ([#13502](https://github.com/astral-sh/ruff/pull/13502)). This ensures Ruff's code is consistent with
the same rule in the [`tryceratops`](https://github.com/guilatrova/tryceratops) linter.
- The `lint.allow-unused-imports` setting has been removed ([#13677](https://github.com/astral-sh/ruff/pull/13677)). Use
[`lint.pyflakes.allow-unused-imports`](https://docs.astral.sh/ruff/settings/#lint_pyflakes_allowed-unused-imports)
instead.
## 0.6.0
- Detect imports in `src` layouts by default for `isort` rules ([#12848](https://github.com/astral-sh/ruff/pull/12848))

View File

@@ -1,5 +1,30 @@
# Changelog
## 0.7.1
### Preview features
- Fix `E221` and `E222` to flag missing or extra whitespace around `==` operator ([#13890](https://github.com/astral-sh/ruff/pull/13890))
- Formatter: Alternate quotes for strings inside f-strings in preview ([#13860](https://github.com/astral-sh/ruff/pull/13860))
- Formatter: Join implicit concatenated strings when they fit on a line ([#13663](https://github.com/astral-sh/ruff/pull/13663))
- \[`pylint`\] Restrict `iteration-over-set` to only work on sets of literals (`PLC0208`) ([#13731](https://github.com/astral-sh/ruff/pull/13731))
### Rule changes
- \[`flake8-type-checking`\] Support auto-quoting when annotations contain quotes ([#11811](https://github.com/astral-sh/ruff/pull/11811))
### Server
- Avoid indexing the workspace for single-file mode ([#13770](https://github.com/astral-sh/ruff/pull/13770))
### Bug fixes
- Make `ARG002` compatible with `EM101` when raising `NotImplementedError` ([#13714](https://github.com/astral-sh/ruff/pull/13714))
### Other changes
- Introduce more Docker tags for Ruff (similar to uv) ([#13274](https://github.com/astral-sh/ruff/pull/13274))
## 0.7.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.7.0) for a migration guide and overview of the changes!

221
Cargo.lock generated
View File

@@ -69,7 +69,7 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e"
dependencies = [
"unicode-width",
"unicode-width 0.1.13",
"yansi-term",
]
@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.89"
version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95"
[[package]]
name = "append-only-vec"
@@ -407,7 +407,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -491,7 +491,7 @@ dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"unicode-width",
"unicode-width 0.1.13",
"windows-sys 0.52.0",
]
@@ -687,7 +687,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -698,7 +698,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -750,6 +750,27 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dir-test"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c44bdf9319ad5223afb7eb15a7110452b0adf0373ea6756561b2c708eef0dd1"
dependencies = [
"dir-test-macros",
]
[[package]]
name = "dir-test-macros"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "644f96047137dfaa7a09e34d4623f9e52a1926ecc25ba32ad2ba3fc422536b25"
dependencies = [
"glob",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "dirs"
version = "4.0.0"
@@ -879,9 +900,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "fern"
version = "0.6.2"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee"
checksum = "69ff9c9d5fb3e6da8ac2f77ab76fe7e8087d512ce095200f8f29ac5b656cf6dc"
dependencies = [
"log",
]
@@ -957,7 +978,7 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
"unicode-width 0.1.13",
]
[[package]]
@@ -1160,7 +1181,7 @@ dependencies = [
"instant",
"number_prefix",
"portable-atomic",
"unicode-width",
"unicode-width 0.1.13",
"vt100",
]
@@ -1246,7 +1267,7 @@ dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -1346,9 +1367,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.159"
version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "libcst"
@@ -1372,7 +1393,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
dependencies = [
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -1760,18 +1781,17 @@ dependencies = [
"once_cell",
"regex",
"serde",
"unicode-width",
"unicode-width 0.1.13",
]
[[package]]
name = "pep440_rs"
version = "0.6.6"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "466eada3179c2e069ca897b99006cbb33f816290eaeec62464eea907e22ae385"
checksum = "7c8ee724d21f351f9d47276614ac9710975db827ba9fe2ca5a517ba648193307"
dependencies = [
"once_cell",
"serde",
"unicode-width",
"unicode-width 0.2.0",
"unscanny",
]
@@ -1787,7 +1807,7 @@ dependencies = [
"serde",
"thiserror",
"tracing",
"unicode-width",
"unicode-width 0.1.13",
"url",
]
@@ -1828,7 +1848,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -1943,9 +1963,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.87"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
@@ -2080,6 +2100,7 @@ dependencies = [
"camino",
"compact_str",
"countme",
"dir-test",
"hashbrown 0.15.0",
"insta",
"itertools 0.13.0",
@@ -2087,7 +2108,6 @@ dependencies = [
"ordermap",
"red_knot_test",
"red_knot_vendored",
"rstest",
"ruff_db",
"ruff_index",
"ruff_python_ast",
@@ -2136,7 +2156,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"colored",
"once_cell",
"memchr",
"red_knot_python_semantic",
"red_knot_vendored",
"regex",
@@ -2154,7 +2174,6 @@ dependencies = [
name = "red_knot_vendored"
version = "0.0.0"
dependencies = [
"once_cell",
"path-slash",
"ruff_db",
"walkdir",
@@ -2270,12 +2289,6 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "ring"
version = "0.17.8"
@@ -2291,36 +2304,9 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rstest"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936"
dependencies = [
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42"
dependencies = [
"cfg-if",
"glob",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]]
name = "ruff"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"anyhow",
"argfile",
@@ -2382,7 +2368,6 @@ dependencies = [
"codspeed-criterion-compat",
"criterion",
"mimalloc",
"once_cell",
"rayon",
"red_knot_python_semantic",
"red_knot_workspace",
@@ -2506,7 +2491,7 @@ dependencies = [
"serde",
"static_assertions",
"tracing",
"unicode-width",
"unicode-width 0.1.13",
]
[[package]]
@@ -2515,7 +2500,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"once_cell",
"red_knot_python_semantic",
"ruff_cache",
"ruff_db",
@@ -2539,7 +2523,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2560,10 +2544,9 @@ dependencies = [
"log",
"memchr",
"natord",
"once_cell",
"path-absolutize",
"pathdiff",
"pep440_rs 0.6.6",
"pep440_rs 0.7.1",
"pyproject-toml",
"quick-junit",
"regex",
@@ -2594,7 +2577,7 @@ dependencies = [
"toml",
"typed-arena",
"unicode-normalization",
"unicode-width",
"unicode-width 0.1.13",
"unicode_names2",
"url",
]
@@ -2607,7 +2590,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -2616,7 +2599,6 @@ version = "0.0.0"
dependencies = [
"anyhow",
"itertools 0.13.0",
"once_cell",
"rand",
"ruff_diagnostics",
"ruff_source_file",
@@ -2638,7 +2620,6 @@ dependencies = [
"compact_str",
"is-macro",
"itertools 0.13.0",
"once_cell",
"ruff_cache",
"ruff_macros",
"ruff_python_trivia",
@@ -2664,7 +2645,6 @@ dependencies = [
name = "ruff_python_codegen"
version = "0.0.0"
dependencies = [
"once_cell",
"ruff_python_ast",
"ruff_python_literal",
"ruff_python_parser",
@@ -2682,7 +2662,6 @@ dependencies = [
"insta",
"itertools 0.13.0",
"memchr",
"once_cell",
"regex",
"ruff_cache",
"ruff_formatter",
@@ -2833,6 +2812,7 @@ dependencies = [
"serde",
"serde_json",
"shellexpand",
"thiserror",
"tracing",
"tracing-subscriber",
]
@@ -2842,7 +2822,6 @@ name = "ruff_source_file"
version = "0.0.0"
dependencies = [
"memchr",
"once_cell",
"ruff_text_size",
"serde",
]
@@ -2859,7 +2838,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -2898,7 +2877,7 @@ dependencies = [
"matchit",
"path-absolutize",
"path-slash",
"pep440_rs 0.6.6",
"pep440_rs 0.7.1",
"regex",
"ruff_cache",
"ruff_formatter",
@@ -2940,15 +2919,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.37"
@@ -3009,7 +2979,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "salsa"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
dependencies = [
"append-only-vec",
"arc-swap",
@@ -3029,17 +2999,17 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
[[package]]
name = "salsa-macros"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
"synstructure",
]
@@ -3073,7 +3043,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -3094,12 +3064,6 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.210"
@@ -3128,7 +3092,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -3139,14 +3103,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
@@ -3162,7 +3126,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -3203,7 +3167,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -3305,7 +3269,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -3316,9 +3280,20 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "2.0.79"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
dependencies = [
"proc-macro2",
"quote",
@@ -3333,7 +3308,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -3396,7 +3371,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -3407,7 +3382,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
"test-case-core",
]
@@ -3428,7 +3403,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -3540,7 +3515,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -3704,6 +3679,12 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "unicode_names2"
version = "1.3.0"
@@ -3774,9 +3755,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
"rand",
@@ -3786,13 +3767,13 @@ dependencies = [
[[package]]
name = "uuid-macro-internal"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee1cd046f83ea2c4e920d6ee9f7c3537ef928d75dce5d84a87c2c5d6b3999a3a"
checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -3815,7 +3796,7 @@ checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de"
dependencies = [
"itoa",
"log",
"unicode-width",
"unicode-width 0.1.13",
"vte",
]
@@ -3878,7 +3859,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
"wasm-bindgen-shared",
]
@@ -3912,7 +3893,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3946,7 +3927,7 @@ checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]
@@ -4234,7 +4215,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.82",
]
[[package]]

View File

@@ -4,7 +4,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
rust-version = "1.76"
rust-version = "1.80"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
@@ -65,10 +65,11 @@ compact_str = "0.8.0"
criterion = { version = "0.5.1", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
dir-test = { version = "0.3.0" }
drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.11.0" }
etcetera = { version = "0.8.0" }
fern = { version = "0.6.1" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
@@ -101,12 +102,11 @@ memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "6.1.1" }
once_cell = { version = "1.19.0" }
ordermap = { version = "0.5.0" }
path-absolutize = { version = "3.1.1" }
path-slash = { version = "0.2.1" }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.6.0", features = ["serde"] }
pep440_rs = { version = "0.7.1" }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.79" }
pyproject-toml = { version = "0.9.0" }
@@ -115,9 +115,8 @@ quote = { version = "1.0.23" }
rand = { version = "0.8.5" }
rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rstest = { version = "0.22.0", default-features = false }
rustc-hash = { version = "2.0.0" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "b14be5c0392f4c55eca60b92e457a35549372382" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "254c749b02cde2fd29852a7463a33e800b771758" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -203,6 +202,10 @@ get_unwrap = "warn"
rc_buffer = "warn"
rc_mutex = "warn"
rest_pat_in_fully_bound_structs = "warn"
# nursery rules
redundant_clone = "warn"
debug_assert_with_mut_call = "warn"
unused_peekable = "warn"
[profile.release]
# Note that we set these explicitly, and these values

View File

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

View File

@@ -136,8 +136,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.7.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.7.0/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.7.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.7.1/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -170,7 +170,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.7.0
rev: v0.7.1
hooks:
# Run the linter.
- id: ruff

View File

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

View File

@@ -43,8 +43,8 @@ red_knot_test = { workspace = true }
red_knot_vendored = { workspace = true }
anyhow = { workspace = true }
dir-test = {workspace = true}
insta = { workspace = true }
rstest = { workspace = true }
tempfile = { workspace = true }
[lints]

View File

@@ -1,4 +1,4 @@
/// Rebuild the crate if a test file is added or removed from
pub fn main() {
println!("cargo:rerun-if-changed=resources/mdtest");
println!("cargo::rerun-if-changed=resources/mdtest");
}

View File

@@ -13,13 +13,22 @@ reveal_type(y) # revealed: Literal[1]
## Violates own annotation
```py
x: int = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
x: int = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
```
## Violates previous annotation
```py
x: int
x = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
```
## PEP-604 annotations not yet supported
```py
def f() -> str | None:
return None
# TODO: should be `str | None` (but Todo is better than `Unknown`)
reveal_type(f()) # revealed: @Todo
```

View File

@@ -3,9 +3,12 @@
## Unbound
```py
x = foo
x = foo # error: [unresolved-reference] "Name `foo` used when not defined"
foo = 1
reveal_type(x) # revealed: Unbound
# error: [unresolved-reference]
# revealed: Unbound
reveal_type(x)
```
## Unbound class variable
@@ -13,12 +16,17 @@ reveal_type(x) # revealed: Unbound
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1
class C:
y = x
if flag:
x = 2
reveal_type(C.x) # revealed: Literal[2]
reveal_type(C.y) # revealed: Literal[1]
reveal_type(C.x) # revealed: Literal[2]
reveal_type(C.y) # revealed: Literal[1]
```

View File

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

View File

@@ -0,0 +1,48 @@
## Binary operations on booleans
## Basic Arithmetic
We try to be precise and all operations except for division will result in Literal type.
```py
a = True
b = False
reveal_type(a + a) # revealed: Literal[2]
reveal_type(a + b) # revealed: Literal[1]
reveal_type(b + a) # revealed: Literal[1]
reveal_type(b + b) # revealed: Literal[0]
reveal_type(a - a) # revealed: Literal[0]
reveal_type(a - b) # revealed: Literal[1]
reveal_type(b - a) # revealed: Literal[-1]
reveal_type(b - b) # revealed: Literal[0]
reveal_type(a * a) # revealed: Literal[1]
reveal_type(a * b) # revealed: Literal[0]
reveal_type(b * a) # revealed: Literal[0]
reveal_type(b * b) # revealed: Literal[0]
reveal_type(a % a) # revealed: Literal[0]
reveal_type(b % a) # revealed: Literal[0]
reveal_type(a // a) # revealed: Literal[1]
reveal_type(b // a) # revealed: Literal[0]
reveal_type(a**a) # revealed: Literal[1]
reveal_type(a**b) # revealed: Literal[1]
reveal_type(b**a) # revealed: Literal[0]
reveal_type(b**b) # revealed: Literal[1]
# Division
reveal_type(a / a) # revealed: float
reveal_type(b / a) # revealed: float
b / b # error: [division-by-zero] "Cannot divide object of type `Literal[False]` by zero"
a / b # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
# bitwise OR
reveal_type(a | a) # revealed: Literal[True]
reveal_type(a | b) # revealed: Literal[True]
reveal_type(b | a) # revealed: Literal[True]
reveal_type(b | b) # revealed: Literal[False]
```

View File

@@ -0,0 +1,447 @@
# Binary operations on instances
Binary operations in Python are implemented by means of magic double-underscore methods.
For references, see:
- <https://snarky.ca/unravelling-binary-arithmetic-operations-in-python/>
- <https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types>
## Operations
We support inference for all Python's binary operators:
`+`, `-`, `*`, `@`, `/`, `//`, `%`, `**`, `<<`, `>>`, `&`, `^`, and `|`.
```py
class A:
def __add__(self, other) -> A:
return self
def __sub__(self, other) -> A:
return self
def __mul__(self, other) -> A:
return self
def __matmul__(self, other) -> A:
return self
def __truediv__(self, other) -> A:
return self
def __floordiv__(self, other) -> A:
return self
def __mod__(self, other) -> A:
return self
def __pow__(self, other) -> A:
return self
def __lshift__(self, other) -> A:
return self
def __rshift__(self, other) -> A:
return self
def __and__(self, other) -> A:
return self
def __xor__(self, other) -> A:
return self
def __or__(self, other) -> A:
return self
class B: ...
reveal_type(A() + B()) # revealed: A
reveal_type(A() - B()) # revealed: A
reveal_type(A() * B()) # revealed: A
reveal_type(A() @ B()) # revealed: A
reveal_type(A() / B()) # revealed: A
reveal_type(A() // B()) # revealed: A
reveal_type(A() % B()) # revealed: A
reveal_type(A() ** B()) # revealed: A
reveal_type(A() << B()) # revealed: A
reveal_type(A() >> B()) # revealed: A
reveal_type(A() & B()) # revealed: A
reveal_type(A() ^ B()) # revealed: A
reveal_type(A() | B()) # revealed: A
```
## Reflected
We also support inference for reflected operations:
```py
class A:
def __radd__(self, other) -> A:
return self
def __rsub__(self, other) -> A:
return self
def __rmul__(self, other) -> A:
return self
def __rmatmul__(self, other) -> A:
return self
def __rtruediv__(self, other) -> A:
return self
def __rfloordiv__(self, other) -> A:
return self
def __rmod__(self, other) -> A:
return self
def __rpow__(self, other) -> A:
return self
def __rlshift__(self, other) -> A:
return self
def __rrshift__(self, other) -> A:
return self
def __rand__(self, other) -> A:
return self
def __rxor__(self, other) -> A:
return self
def __ror__(self, other) -> A:
return self
class B: ...
reveal_type(B() + A()) # revealed: A
reveal_type(B() - A()) # revealed: A
reveal_type(B() * A()) # revealed: A
reveal_type(B() @ A()) # revealed: A
reveal_type(B() / A()) # revealed: A
reveal_type(B() // A()) # revealed: A
reveal_type(B() % A()) # revealed: A
reveal_type(B() ** A()) # revealed: A
reveal_type(B() << A()) # revealed: A
reveal_type(B() >> A()) # revealed: A
reveal_type(B() & A()) # revealed: A
reveal_type(B() ^ A()) # revealed: A
reveal_type(B() | A()) # revealed: A
```
## Returning a different type
The magic methods aren't required to return the type of `self`:
```py
class A:
def __add__(self, other) -> int:
return 1
def __rsub__(self, other) -> int:
return 1
class B: ...
reveal_type(A() + B()) # revealed: int
reveal_type(B() - A()) # revealed: int
```
## Non-reflected precedence in general
In general, if the left-hand side defines `__add__` and the right-hand side
defines `__radd__` and the right-hand side is not a subtype of the left-hand
side, `lhs.__add__` will take precedence:
```py
class A:
def __add__(self, other: B) -> int:
return 42
class B:
def __radd__(self, other: A) -> str:
return "foo"
reveal_type(A() + B()) # revealed: int
# Edge case: C is a subtype of C, *but* if the two sides are of *equal* types,
# the lhs *still* takes precedence
class C:
def __add__(self, other: C) -> int:
return 42
def __radd__(self, other: C) -> str:
return "foo"
reveal_type(C() + C()) # revealed: int
```
## Reflected precedence for subtypes (in some cases)
If the right-hand operand is a subtype of the left-hand operand and has a
different implementation of the reflected method, the reflected method on the
right-hand operand takes precedence.
```py
class A:
def __add__(self, other) -> str:
return "foo"
def __radd__(self, other) -> str:
return "foo"
class MyString(str): ...
class B(A):
def __radd__(self, other) -> MyString:
return MyString()
reveal_type(A() + B()) # revealed: MyString
# N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__`
class C(B): ...
# TODO: we currently only understand direct subclasses as subtypes of the superclass.
# We need to iterate through the full MRO rather than just the class's bases;
# if we do, we'll understand `C` as a subtype of `A`, and correctly understand this as being
# `MyString` rather than `str`
reveal_type(A() + C()) # revealed: str
```
## Reflected precedence 2
If the right-hand operand is a subtype of the left-hand operand, but does not
override the reflected method, the left-hand operand's non-reflected method
still takes precedence:
```py
class A:
def __add__(self, other) -> str:
return "foo"
def __radd__(self, other) -> int:
return 42
class B(A): ...
reveal_type(A() + B()) # revealed: str
```
## Only reflected supported
For example, at runtime, `(1).__add__(1.2)` is `NotImplemented`, but
`(1.2).__radd__(1) == 2.2`, meaning that `1 + 1.2` succeeds at runtime
(producing `2.2`). The runtime tries the second one only if the first one
returns `NotImplemented` to signal failure.
Typeshed and other stubs annotate dunder-method calls that would return
`NotImplemented` as being "illegal" calls. `int.__add__` is annotated as only
"accepting" `int`s, even though it strictly-speaking "accepts" any other object
without raising an exception -- it will simply return `NotImplemented`,
allowing the runtime to try the `__radd__` method of the right-hand operand
as well.
```py
class A:
def __sub__(self, other: A) -> A:
return A()
class B:
def __rsub__(self, other: A) -> B:
return B()
# TODO: this should be `B` (the return annotation of `B.__rsub__`),
# because `A.__sub__` is annotated as only accepting `A`,
# but `B.__rsub__` will accept `A`.
reveal_type(A() - B()) # revealed: A
```
## Callable instances as dunders
Believe it or not, this is supported at runtime:
```py
class A:
def __call__(self, other) -> int:
return 42
class B:
__add__ = A()
reveal_type(B() + B()) # revealed: int
```
## Integration test: numbers from typeshed
```py
reveal_type(3j + 3.14) # revealed: complex
reveal_type(4.2 + 42) # revealed: float
reveal_type(3j + 3) # revealed: complex
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3.14 + 3j) # revealed: float
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(42 + 4.2) # revealed: int
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3 + 3j) # revealed: int
def returns_int() -> int:
return 42
def returns_bool() -> bool:
return True
x = returns_bool()
y = returns_int()
reveal_type(x + y) # revealed: int
reveal_type(4.2 + x) # revealed: float
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(y + 4.12) # revealed: int
```
## With literal types
When we have a literal type for one operand, we're able to fall back to the
instance handling for its instance super-type.
```py
class A:
def __add__(self, other) -> A:
return self
def __radd__(self, other) -> A:
return self
reveal_type(A() + 1) # revealed: A
# TODO should be `A` since `int.__add__` doesn't support `A` instances
reveal_type(1 + A()) # revealed: int
reveal_type(A() + "foo") # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances
# TODO overloads
reveal_type("foo" + A()) # revealed: @Todo
reveal_type(A() + b"foo") # revealed: A
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
reveal_type(b"foo" + A()) # revealed: bytes
reveal_type(A() + ()) # revealed: A
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
reveal_type(() + A()) # revealed: @Todo
literal_string_instance = "foo" * 1_000_000_000
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
reveal_type(literal_string_instance) # revealed: LiteralString
reveal_type(A() + literal_string_instance) # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances
# TODO overloads
reveal_type(literal_string_instance + A()) # revealed: @Todo
```
## Operations involving instances of classes inheriting from `Any`
`Any` and `Unknown` represent a set of possible runtime objects, wherein the
bounds of the set are unknown. Whether the left-hand operand's dunder or the
right-hand operand's reflected dunder depends on whether the right-hand operand
is an instance of a class that is a subclass of the left-hand operand's class
and overrides the reflected dunder. In the following example, because of the
unknowable nature of `Any`/`Unknown`, we must consider both possibilities:
`Any`/`Unknown` might resolve to an unknown third class that inherits from `X`
and overrides `__radd__`; but it also might not. Thus, the correct answer here
for the `reveal_type` is `int | Unknown`.
```py
from does_not_exist import Foo # error: [unresolved-import]
reveal_type(Foo) # revealed: Unknown
class X:
def __add__(self, other: object) -> int:
return 42
class Y(Foo): ...
# TODO: Should be `int | Unknown`; see above discussion.
reveal_type(X() + Y()) # revealed: int
```
## Unsupported
### Dunder as instance attribute
The magic method must exist on the class, not just on the instance:
```py
def add_impl(self, other) -> int:
return 1
class A:
def __init__(self):
self.__add__ = add_impl
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `A` and `A`"
# revealed: Unknown
reveal_type(A() + A())
```
### Missing dunder
```py
class A: ...
# error: [unsupported-operator]
# revealed: Unknown
reveal_type(A() + A())
```
### Wrong position
A left-hand dunder method doesn't apply for the right-hand operand, or vice versa:
```py
class A:
def __add__(self, other) -> int: ...
class B:
def __radd__(self, other) -> int: ...
class C: ...
# error: [unsupported-operator]
# revealed: Unknown
reveal_type(C() + A())
# error: [unsupported-operator]
# revealed: Unknown
reveal_type(B() + C())
```
### Reflected dunder is not tried between two objects of the same type
For the specific case where the left-hand operand is the exact same type as the
right-hand operand, the reflected dunder of the right-hand operand is not
tried; the runtime short-circuits after trying the unreflected dunder of the
left-hand operand. For context, see
[this mailing list discussion](https://mail.python.org/archives/list/python-dev@python.org/thread/7NZUCODEAPQFMRFXYRMGJXDSIS3WJYIV/).
```py
class Foo:
def __radd__(self, other: Foo) -> Foo:
return self
# error: [unsupported-operator]
# revealed: Unknown
reveal_type(Foo() + Foo())
```
### Wrong type
TODO: check signature and error if `other` is the wrong type

View File

@@ -1,36 +1,70 @@
## Binary operations on integers
# Binary operations on integers
## Basic Arithmetic
```py
a = 2 + 1
b = a - 4
c = a * b
d = c // 3
e = c / 3
f = 5 % 3
reveal_type(2 + 1) # revealed: Literal[3]
reveal_type(3 - 4) # revealed: Literal[-1]
reveal_type(3 * -1) # revealed: Literal[-3]
reveal_type(-3 // 3) # revealed: Literal[-1]
reveal_type(-3 / 3) # revealed: float
reveal_type(5 % 3) # revealed: Literal[2]
```
reveal_type(a) # revealed: Literal[3]
reveal_type(b) # revealed: Literal[-1]
reveal_type(c) # revealed: Literal[-3]
reveal_type(d) # revealed: Literal[-1]
reveal_type(e) # revealed: float
reveal_type(f) # revealed: Literal[2]
## Power
For power if the result fits in the int literal type it will be a Literal type. Otherwise the
outcome is int.
```py
largest_u32 = 4_294_967_295
reveal_type(2**2) # revealed: Literal[4]
reveal_type(1 ** (largest_u32 + 1)) # revealed: int
reveal_type(2**largest_u32) # revealed: int
```
## Division by Zero
```py
# TODO: `a` should be `int` and `e` should be `float` once we support inference.
a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero"
b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero"
c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
d = int() / 0 # error: "Cannot divide object of type `int` by zero"
e = 1.0 / 0 # error: "Cannot divide object of type `float` by zero"
This error is really outside the current Python type system, because e.g. `int.__truediv__` and
friends are not annotated to indicate that it's an error, and we don't even have a facility to
permit such an annotation. So arguably divide-by-zero should be a lint error rather than a type
checker error. But we choose to go ahead and error in the cases that are very likely to be an error:
dividing something typed as `int` or `float` by something known to be `Literal[0]`.
This isn't _definitely_ an error, because the object typed as `int` or `float` could be an instance
of a custom subclass which overrides division behavior to handle zero without error. But if this
unusual case occurs, the error can be avoided by explicitly typing the dividend as that safe custom
subclass; we only emit the error if the LHS type is exactly `int` or `float`, not if its a subclass.
```py
a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero"
reveal_type(a) # revealed: float
b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero"
reveal_type(b) # revealed: int
c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
reveal_type(c) # revealed: int
reveal_type(d) # revealed: @Todo
reveal_type(e) # revealed: @Todo
# error: "Cannot divide object of type `int` by zero"
# revealed: float
reveal_type(int() / 0)
# error: "Cannot divide object of type `Literal[1]` by zero"
# revealed: float
reveal_type(1 / False)
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
True / False
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
bool(1) / False
# error: "Cannot divide object of type `float` by zero"
# revealed: float
reveal_type(1.0 / 0)
class MyInt(int): ...
# No error for a subclass of int
# revealed: float
reveal_type(MyInt(3) / 0)
```

View File

@@ -11,11 +11,10 @@ class Multiplier:
return number * self.factor
a = Multiplier(2.0)(3.0)
reveal_type(a) # revealed: float
class Unit: ...
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
reveal_type(a) # revealed: float
reveal_type(b) # revealed: Unknown
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
reveal_type(b) # revealed: Unknown
```

View File

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

View File

@@ -6,8 +6,7 @@
def get_int() -> int:
return 42
x = get_int()
reveal_type(x) # revealed: int
reveal_type(get_int()) # revealed: int
```
## Async
@@ -16,10 +15,8 @@ reveal_type(x) # revealed: int
async def get_int_async() -> int:
return 42
x = get_int_async()
# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]`
reveal_type(x) # revealed: @Todo
reveal_type(get_int_async()) # revealed: @Todo
```
## Decorated
@@ -35,12 +32,10 @@ def decorator(func) -> Callable[[], int]:
@decorator
def bar() -> str:
return 'bar'
x = bar()
return "bar"
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
reveal_type(x) # revealed: @Todo
reveal_type(bar()) # revealed: @Todo
```
## Invalid callable

View File

@@ -3,28 +3,40 @@
## Union of return types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
def f() -> int:
return 1
else:
def f() -> str:
return 'foo'
x = f()
reveal_type(x) # revealed: int | str
else:
def f() -> str:
return "foo"
reveal_type(f()) # revealed: int | str
```
## Calling with an unknown union
```py
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
def f() -> int:
return 1
x = f()
reveal_type(x) # revealed: Unknown | int
reveal_type(f()) # revealed: Unknown | int
```
## Non-callable elements in a union
@@ -32,9 +44,15 @@ reveal_type(x) # revealed: Unknown | int
Calling a union with a non-callable element should emit a diagnostic.
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
f = 1
else:
def f() -> int:
return 1
@@ -47,16 +65,23 @@ reveal_type(x) # revealed: Unknown | int
Calling a union with multiple non-callable elements should mention all of them in the diagnostic.
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
if flag:
f = 1
elif flag2:
f = 'foo'
f = "foo"
else:
def f() -> int:
return 1
x = f() # error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
reveal_type(x) # revealed: Unknown | int
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
reveal_type(f())
```
## All non-callable union elements
@@ -64,10 +89,15 @@ reveal_type(x) # revealed: Unknown | int
Calling a union with no callable elements can emit a simpler diagnostic.
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
f = 1
else:
f = 'foo'
f = "foo"
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
reveal_type(x) # revealed: Unknown

View File

@@ -1,43 +1,43 @@
### Comparison: Byte literals
# Comparison: Byte literals
These tests assert that we infer precise `Literal` types for comparisons between objects
inferred as having `Literal` bytes types:
```py
reveal_type(b"abc" == b"abc") # revealed: Literal[True]
reveal_type(b"abc" == b"ab") # revealed: Literal[False]
reveal_type(b"abc" == b"ab") # revealed: Literal[False]
reveal_type(b"abc" != b"abc") # revealed: Literal[False]
reveal_type(b"abc" != b"ab") # revealed: Literal[True]
reveal_type(b"abc" != b"ab") # revealed: Literal[True]
reveal_type(b"abc" < b"abd") # revealed: Literal[True]
reveal_type(b"abc" < b"abb") # revealed: Literal[False]
reveal_type(b"abc" < b"abd") # revealed: Literal[True]
reveal_type(b"abc" < b"abb") # revealed: Literal[False]
reveal_type(b"abc" <= b"abc") # revealed: Literal[True]
reveal_type(b"abc" <= b"abb") # revealed: Literal[False]
reveal_type(b"abc" > b"abd") # revealed: Literal[False]
reveal_type(b"abc" > b"abb") # revealed: Literal[True]
reveal_type(b"abc" > b"abd") # revealed: Literal[False]
reveal_type(b"abc" > b"abb") # revealed: Literal[True]
reveal_type(b"abc" >= b"abc") # revealed: Literal[True]
reveal_type(b"abc" >= b"abd") # revealed: Literal[False]
reveal_type(b"" in b"") # revealed: Literal[True]
reveal_type(b"" in b"abc") # revealed: Literal[True]
reveal_type(b"abc" in b"") # revealed: Literal[False]
reveal_type(b"ab" in b"abc") # revealed: Literal[True]
reveal_type(b"abc" in b"abc") # revealed: Literal[True]
reveal_type(b"d" in b"abc") # revealed: Literal[False]
reveal_type(b"ac" in b"abc") # revealed: Literal[False]
reveal_type(b"" in b"") # revealed: Literal[True]
reveal_type(b"" in b"abc") # revealed: Literal[True]
reveal_type(b"abc" in b"") # revealed: Literal[False]
reveal_type(b"ab" in b"abc") # revealed: Literal[True]
reveal_type(b"abc" in b"abc") # revealed: Literal[True]
reveal_type(b"d" in b"abc") # revealed: Literal[False]
reveal_type(b"ac" in b"abc") # revealed: Literal[False]
reveal_type(b"\x81\x82" in b"\x80\x81\x82") # revealed: Literal[True]
reveal_type(b"\x82\x83" in b"\x80\x81\x82") # revealed: Literal[False]
reveal_type(b"ab" not in b"abc") # revealed: Literal[False]
reveal_type(b"ac" not in b"abc") # revealed: Literal[True]
reveal_type(b"ab" not in b"abc") # revealed: Literal[False]
reveal_type(b"ac" not in b"abc") # revealed: Literal[True]
reveal_type(b"abc" is b"abc") # revealed: bool
reveal_type(b"abc" is b"ab") # revealed: Literal[False]
reveal_type(b"abc" is b"abc") # revealed: bool
reveal_type(b"abc" is b"ab") # revealed: Literal[False]
reveal_type(b"abc" is not b"abc") # revealed: bool
reveal_type(b"abc" is not b"ab") # revealed: Literal[True]
reveal_type(b"abc" is not b"ab") # revealed: Literal[True]
```

View File

@@ -1,29 +1,18 @@
# Comparing integers
# Comparison: Integers
## Integer literals
```py
a = 1 == 1 == True
b = 1 == 1 == 2 == 4
c = False < True <= 2 < 3 != 6
d = 1 < 1
e = 1 > 1
f = 1 is 1
g = 1 is not 1
h = 1 is 2
i = 1 is not 7
j = 1 <= "" and 0 < 1
reveal_type(a) # revealed: Literal[True]
reveal_type(b) # revealed: Literal[False]
reveal_type(c) # revealed: Literal[True]
reveal_type(d) # revealed: Literal[False]
reveal_type(e) # revealed: Literal[False]
reveal_type(f) # revealed: bool
reveal_type(g) # revealed: bool
reveal_type(h) # revealed: Literal[False]
reveal_type(i) # revealed: Literal[True]
reveal_type(j) # revealed: @Todo | Literal[True]
reveal_type(1 == 1 == True) # revealed: Literal[True]
reveal_type(1 == 1 == 2 == 4) # revealed: Literal[False]
reveal_type(False < True <= 2 < 3 != 6) # revealed: Literal[True]
reveal_type(1 < 1) # revealed: Literal[False]
reveal_type(1 > 1) # revealed: Literal[False]
reveal_type(1 is 1) # revealed: bool
reveal_type(1 is not 1) # revealed: bool
reveal_type(1 is 2) # revealed: Literal[False]
reveal_type(1 is not 7) # revealed: Literal[True]
reveal_type(1 <= "" and 0 < 1) # revealed: @Todo | Literal[True]
```
## Integer instance
@@ -31,11 +20,8 @@ reveal_type(j) # revealed: @Todo | Literal[True]
```py
# TODO: implement lookup of `__eq__` on typeshed `int` stub.
def int_instance() -> int: ...
a = 1 == int_instance()
b = 9 < int_instance()
c = int_instance() < int_instance()
reveal_type(a) # revealed: @Todo
reveal_type(b) # revealed: bool
reveal_type(c) # revealed: bool
reveal_type(1 == int_instance()) # revealed: @Todo
reveal_type(9 < int_instance()) # revealed: bool
reveal_type(int_instance() < int_instance()) # revealed: bool
```

View File

@@ -1,4 +1,4 @@
# Non boolean returns
# Comparison: Non boolean returns
Walking through examples:
@@ -20,18 +20,22 @@ Walking through examples:
```py
from __future__ import annotations
class A:
def __lt__(self, other) -> A: ...
class B:
def __lt__(self, other) -> B: ...
class C:
def __lt__(self, other) -> C: ...
a = A() < B() < C()
b = 0 < 1 < A() < 3
c = 10 < 0 < A() < B() < C()
x = A() < B() < C()
reveal_type(x) # revealed: A | B
reveal_type(a) # revealed: A | B
reveal_type(b) # revealed: bool | A
reveal_type(c) # revealed: Literal[False]
y = 0 < 1 < A() < 3
reveal_type(y) # revealed: bool | A
z = 10 < 0 < A() < B() < C()
reveal_type(z) # revealed: Literal[False]
```

View File

@@ -1,29 +1,20 @@
# Comparing strings
# Comparison: Strings
## String literals
```py
def str_instance() -> str: ...
a = "abc" == "abc"
b = "ab_cd" <= "ab_ce"
c = "abc" in "ab cd"
d = "" not in "hello"
e = "--" is "--"
f = "A" is "B"
g = "--" is not "--"
h = "A" is not "B"
i = str_instance() < "..."
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
j = "ab" < "ab_cd"
reveal_type(a) # revealed: Literal[True]
reveal_type(b) # revealed: Literal[True]
reveal_type(c) # revealed: Literal[False]
reveal_type(d) # revealed: Literal[False]
reveal_type(e) # revealed: bool
reveal_type(f) # revealed: Literal[False]
reveal_type(g) # revealed: bool
reveal_type(h) # revealed: Literal[True]
reveal_type(i) # revealed: bool
reveal_type(j) # revealed: Literal[True]
reveal_type("abc" == "abc") # revealed: Literal[True]
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
reveal_type("abc" in "ab cd") # revealed: Literal[False]
reveal_type("" not in "hello") # revealed: Literal[False]
reveal_type("--" is "--") # revealed: bool
reveal_type("A" is "B") # revealed: Literal[False]
reveal_type("--" is not "--") # revealed: bool
reveal_type("A" is not "B") # revealed: Literal[True]
reveal_type(str_instance() < "...") # revealed: bool
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
```

View File

@@ -1,4 +1,4 @@
# Comparison - Tuples
# Comparison: Tuples
## Heterogeneous
@@ -18,16 +18,16 @@ b = (1, "test", (3, 14), False)
reveal_type(a == a) # revealed: Literal[True]
reveal_type(a != a) # revealed: Literal[False]
reveal_type(a < a) # revealed: Literal[False]
reveal_type(a < a) # revealed: Literal[False]
reveal_type(a <= a) # revealed: Literal[True]
reveal_type(a > a) # revealed: Literal[False]
reveal_type(a > a) # revealed: Literal[False]
reveal_type(a >= a) # revealed: Literal[True]
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
```
@@ -39,9 +39,9 @@ b = (1, 2, 3, 4)
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
c = ("a", "b", "c", "d")
@@ -49,9 +49,9 @@ d = ("a", "b", "c")
reveal_type(c == d) # revealed: Literal[False]
reveal_type(c != d) # revealed: Literal[True]
reveal_type(c < d) # revealed: Literal[False]
reveal_type(c < d) # revealed: Literal[False]
reveal_type(c <= d) # revealed: Literal[False]
reveal_type(c > d) # revealed: Literal[True]
reveal_type(c > d) # revealed: Literal[True]
reveal_type(c >= d) # revealed: Literal[True]
```
@@ -67,16 +67,16 @@ b = (int_instance(),)
# TODO: All @Todo should be `bool`
reveal_type(a == a) # revealed: @Todo
reveal_type(a != a) # revealed: @Todo
reveal_type(a < a) # revealed: @Todo
reveal_type(a < a) # revealed: @Todo
reveal_type(a <= a) # revealed: @Todo
reveal_type(a > a) # revealed: @Todo
reveal_type(a > a) # revealed: @Todo
reveal_type(a >= a) # revealed: @Todo
reveal_type(a == b) # revealed: @Todo
reveal_type(a != b) # revealed: @Todo
reveal_type(a < b) # revealed: @Todo
reveal_type(a < b) # revealed: @Todo
reveal_type(a <= b) # revealed: @Todo
reveal_type(a > b) # revealed: @Todo
reveal_type(a > b) # revealed: @Todo
reveal_type(a >= b) # revealed: @Todo
```
@@ -96,9 +96,9 @@ reveal_type(a == b) # revealed: @Todo
reveal_type(a != b) # revealed: @Todo
# TODO: should be Unknown and add more informative diagnostics
reveal_type(a < b) # revealed: @Todo
reveal_type(a < b) # revealed: @Todo
reveal_type(a <= b) # revealed: @Todo
reveal_type(a > b) # revealed: @Todo
reveal_type(a > b) # revealed: @Todo
reveal_type(a >= b) # revealed: @Todo
```
@@ -111,9 +111,9 @@ b = (999999, "hello")
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
```
@@ -126,16 +126,16 @@ c = (b, b, b)
reveal_type(c == c) # revealed: Literal[True]
reveal_type(c != c) # revealed: Literal[False]
reveal_type(c < c) # revealed: Literal[False]
reveal_type(c < c) # revealed: Literal[False]
reveal_type(c <= c) # revealed: Literal[True]
reveal_type(c > c) # revealed: Literal[False]
reveal_type(c > c) # revealed: Literal[False]
reveal_type(c >= c) # revealed: Literal[True]
```
#### Non Boolean Rich Comparisons
```py
class A():
class A:
def __eq__(self, o) -> str: ...
def __ne__(self, o) -> int: ...
def __lt__(self, o) -> float: ...
@@ -148,9 +148,9 @@ a = (A(), A())
# TODO: All @Todo should be bool
reveal_type(a == a) # revealed: @Todo
reveal_type(a != a) # revealed: @Todo
reveal_type(a < a) # revealed: @Todo
reveal_type(a < a) # revealed: @Todo
reveal_type(a <= a) # revealed: @Todo
reveal_type(a > a) # revealed: @Todo
reveal_type(a > a) # revealed: @Todo
reveal_type(a >= a) # revealed: @Todo
```

View File

@@ -5,6 +5,10 @@
Comparisons on union types need to consider all possible cases:
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
one_or_two = 1 if flag else 2
reveal_type(one_or_two <= 2) # revealed: Literal[True]
@@ -52,14 +56,18 @@ With unions on both sides, we need to consider the full cross product of
options when building the resulting (union) type:
```py
def bool_instance() -> bool:
return True
flag_s, flag_l = bool_instance(), bool_instance()
small = 1 if flag_s else 2
large = 2 if flag_l else 3
reveal_type(small <= large) # revealed: Literal[True]
reveal_type(small >= large) # revealed: bool
reveal_type(small < large) # revealed: bool
reveal_type(small > large) # revealed: Literal[False]
reveal_type(small < large) # revealed: bool
reveal_type(small > large) # revealed: Literal[False]
```
## Unsupported operations
@@ -69,6 +77,10 @@ unsupported. For now, we fall back to `bool` for the result type instead of
trying to infer something more precise from the other (supported) variants:
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = [1, 2] if flag else 1
result = 1 in x # error: "Operator `in` is not supported"

View File

@@ -1,15 +1,31 @@
# Unsupported operators
# Comparison: Unsupported operators
```py
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
c = object() < 5 # error: "Operator `<` is not supported for types `object` and `Literal[5]`"
def bool_instance() -> bool:
return True
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
reveal_type(a) # revealed: bool
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
reveal_type(b) # revealed: bool
c = object() < 5 # error: "Operator `<` is not supported for types `object` and `int`"
reveal_type(c) # revealed: Unknown
# TODO should error, need to check if __lt__ signature is valid for right operand
d = 5 < object()
reveal_type(a) # revealed: bool
reveal_type(b) # revealed: bool
reveal_type(c) # revealed: Unknown
# TODO: should be `Unknown`
reveal_type(d) # revealed: bool
flag = bool_instance()
int_literal_or_str_literal = 1 if flag else "foo"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
e = 42 in int_literal_or_str_literal
reveal_type(e) # revealed: bool
# TODO: should error, need to check if __lt__ signature is valid for right operand
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
f = (1, 2) < (1, "hello")
reveal_type(f) # revealed: @Todo
```

View File

@@ -3,6 +3,10 @@
## Simple if-expression
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else 2
reveal_type(x) # revealed: Literal[1, 2]
```
@@ -10,19 +14,25 @@ reveal_type(x) # revealed: Literal[1, 2]
## If-expression with walrus operator
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
y = 0
z = 0
x = (y := 1) if flag else (z := 2)
a = y
b = z
reveal_type(x) # revealed: Literal[1, 2]
reveal_type(a) # revealed: Literal[0, 1]
reveal_type(b) # revealed: Literal[0, 2]
reveal_type(y) # revealed: Literal[0, 1]
reveal_type(z) # revealed: Literal[0, 2]
```
## Nested if-expression
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
x = 1 if flag else 2 if flag2 else 3
reveal_type(x) # revealed: Literal[1, 2, 3]
```
@@ -30,6 +40,10 @@ reveal_type(x) # revealed: Literal[1, 2, 3]
## None
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else None
reveal_type(x) # revealed: Literal[1] | None
```

View File

@@ -3,20 +3,26 @@
## Simple if
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
y = 1
y = 2
if flag:
y = 3
x = y
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[2, 3]
```
## Simple if-elif-else
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
y = 1
y = 2
if flag:
@@ -30,13 +36,24 @@ else:
x = y
reveal_type(x) # revealed: Literal[3, 4, 5]
reveal_type(r) # revealed: Unbound | Literal[2]
reveal_type(s) # revealed: Unbound | Literal[5]
# revealed: Unbound | Literal[2]
# error: [possibly-unresolved-reference]
reveal_type(r)
# revealed: Unbound | Literal[5]
# error: [possibly-unresolved-reference]
reveal_type(s)
```
## Single symbol across if-elif-else
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
if flag:
y = 1
elif flag2:
@@ -49,6 +66,10 @@ reveal_type(y) # revealed: Literal[1, 2, 3]
## if-elif-else without else assignment
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
y = 1
@@ -62,6 +83,10 @@ reveal_type(y) # revealed: Literal[0, 1, 2]
## if-elif-else with intervening assignment
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
y = 1
@@ -76,6 +101,10 @@ reveal_type(y) # revealed: Literal[0, 1, 2]
## Nested if statement
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
if flag2:
@@ -86,13 +115,16 @@ reveal_type(y) # revealed: Literal[0, 1]
## if-elif without else
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
y = 1
y = 2
if flag:
y = 3
elif flag2:
y = 4
x = y
reveal_type(x) # revealed: Literal[2, 3, 4]
reveal_type(y) # revealed: Literal[2, 3, 4]
```

View File

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

View File

@@ -10,6 +10,10 @@ x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred t
## Incompatible declarations
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
x: str
else:
@@ -20,6 +24,10 @@ x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`:
## Partial declarations
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
x: int
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
@@ -28,6 +36,10 @@ x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`:
## Incompatible declarations with bad assignment
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
x: str
else:
@@ -35,5 +47,5 @@ else:
# error: [conflicting-declarations]
# error: [invalid-assignment]
x = b'foo'
x = b"foo"
```

View File

@@ -4,8 +4,9 @@
```py
import re
try:
x
help()
except NameError as e:
reveal_type(e) # revealed: NameError
except re.error as f:
@@ -15,10 +16,10 @@ except re.error as f:
## Unknown type in except handler does not cause spurious diagnostic
```py
from nonexistent_module import foo # error: [unresolved-import]
from nonexistent_module import foo # error: [unresolved-import]
try:
x
help()
except foo as e:
reveal_type(foo) # revealed: Unknown
reveal_type(e) # revealed: Unknown
@@ -30,7 +31,7 @@ except foo as e:
EXCEPTIONS = (AttributeError, TypeError)
try:
x
help()
except (RuntimeError, OSError) as e:
reveal_type(e) # revealed: RuntimeError | OSError
except EXCEPTIONS as f:
@@ -42,7 +43,7 @@ except EXCEPTIONS as f:
```py
def foo(x: type[AttributeError], y: tuple[type[OSError], type[RuntimeError]], z: tuple[type[BaseException], ...]):
try:
w
help()
except x as e:
# TODO: should be `AttributeError`
reveal_type(e) # revealed: @Todo

View File

@@ -38,7 +38,7 @@ to the `except` suite *after* that redefinition.
```py path=union_type_inferred.py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
x = 1
@@ -60,7 +60,7 @@ unify and `x` is not inferred as having a union type following the
```py path=branches_unify_to_non_union_type.py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
x = 1
@@ -84,7 +84,7 @@ possibility when it comes to control-flow analysis.
```py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
x = 1
@@ -114,7 +114,7 @@ the three suites:
```py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
x = 1
@@ -147,7 +147,7 @@ the end of the `except` suite:
```py path=single_except.py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
x = 1
@@ -175,7 +175,7 @@ in their entireties:
```py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
x = 1
@@ -208,7 +208,7 @@ therefore `Literal[2]`:
```py path=redef_in_finally.py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
x = 1
@@ -235,7 +235,7 @@ suites, however; this is still a TODO item for us.)
```py path=no_redef_in_finally.py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
x = 1
@@ -267,10 +267,10 @@ following possibilities inside `finally` suites:
```py path=redef_in_finally.py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
def could_raise_returns_bytes() -> bytes:
return b'foo'
return b"foo"
def could_raise_returns_bool() -> bool:
return True
@@ -305,10 +305,10 @@ conclusion of the `finally` suite.)
```py path=no_redef_in_finally.py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
def could_raise_returns_bytes() -> bytes:
return b'foo'
return b"foo"
def could_raise_returns_bool() -> bool:
return True
@@ -336,10 +336,10 @@ An example with multiple `except` branches and a `finally` branch:
```py path=multiple_except_branches.py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
def could_raise_returns_bytes() -> bytes:
return b'foo'
return b"foo"
def could_raise_returns_bool() -> bool:
return True
@@ -383,10 +383,10 @@ partway through the `else` suite due to an exception raised *there*.
```py path=single_except_branch.py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
def could_raise_returns_bytes() -> bytes:
return b'foo'
return b"foo"
def could_raise_returns_bool() -> bool:
return True
@@ -426,10 +426,10 @@ The same again, this time with multiple `except` branches:
```py path=multiple_except_branches.py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
def could_raise_returns_bytes() -> bytes:
return b'foo'
return b"foo"
def could_raise_returns_bool() -> bool:
return True
@@ -491,10 +491,10 @@ prior to the suite running to completion.
```py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
def could_raise_returns_bytes() -> bytes:
return b'foo'
return b"foo"
def could_raise_returns_bool() -> bool:
return True
@@ -585,10 +585,10 @@ variable by that name in the outer scope:
```py
def could_raise_returns_str() -> str:
return 'foo'
return "foo"
def could_raise_returns_bytes() -> bytes:
return b'foo'
return b"foo"
def could_raise_returns_range() -> range:
return range(42)
@@ -602,6 +602,7 @@ def could_raise_returns_float() -> float:
x = 1
try:
def foo(param=could_raise_returns_str()):
x = could_raise_returns_str()
@@ -619,7 +620,6 @@ try:
# TODO: should be `str | bytes | bytearray | float`
reveal_type(x) # revealed: bytes | float
reveal_type(x) # revealed: bytes | float
x = foo
reveal_type(x) # revealed: Literal[foo]
except:

View File

@@ -4,7 +4,7 @@
```py
try:
x
help()
except* BaseException as e:
reveal_type(e) # revealed: BaseExceptionGroup
```
@@ -13,7 +13,7 @@ except* BaseException as e:
```py
try:
x
help()
except* OSError as e:
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
reveal_type(e) # revealed: BaseExceptionGroup
@@ -23,8 +23,8 @@ except* OSError as e:
```py
try:
x
help()
except* (TypeError, AttributeError) as e:
#TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
# TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
reveal_type(e) # revealed: BaseExceptionGroup
```

View File

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

View File

@@ -0,0 +1,66 @@
# PEP 695 Generics
## Class Declarations
Basic PEP 695 generics
```py
class MyBox[T]:
# TODO: `T` is defined here
# error: [unresolved-reference] "Name `T` used when not defined"
data: T
box_model_number = 695
# TODO: `T` is defined here
# error: [unresolved-reference] "Name `T` used when not defined"
def __init__(self, data: T):
self.data = data
# TODO not error (should be subscriptable)
box: MyBox[int] = MyBox(5) # error: [non-subscriptable]
# TODO error differently (str and int don't unify)
wrong_innards: MyBox[int] = MyBox("five") # error: [non-subscriptable]
# TODO reveal int
reveal_type(box.data) # revealed: @Todo
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
```
## Subclassing
```py
class MyBox[T]:
# TODO: `T` is defined here
# error: [unresolved-reference] "Name `T` used when not defined"
data: T
# TODO: `T` is defined here
# error: [unresolved-reference] "Name `T` used when not defined"
def __init__(self, data: T):
self.data = data
# TODO not error on the subscripting or the use of type param
# error: [unresolved-reference] "Name `T` used when not defined"
# error: [non-subscriptable]
class MySecureBox[T](MyBox[T]): ...
secure_box: MySecureBox[int] = MySecureBox(5)
reveal_type(secure_box) # revealed: MySecureBox
# TODO reveal int
reveal_type(secure_box.data) # revealed: @Todo
```
## Cyclical class definition
In type stubs, classes can reference themselves in their base class definitions. For example, in `typeshed`, we have `class str(Sequence[str]): ...`.
This should hold true even with generics at play.
```py path=a.pyi
class Seq[T]: ...
# TODO not error on the subscripting
class S[T](Seq[S]): ... # error: [non-subscriptable]
reveal_type(S) # revealed: Literal[S]
```

View File

@@ -3,21 +3,25 @@
## Class import following
```py
from b import C as D; E = D
reveal_type(E) # revealed: Literal[C]
from b import C as D
E = D
reveal_type(E) # revealed: Literal[C]
```
```py path=b.py
class C: pass
class C: ...
```
## Module member resolution
```py
import b; D = b.C
reveal_type(D) # revealed: Literal[C]
import b
D = b.C
reveal_type(D) # revealed: Literal[C]
```
```py path=b.py
class C: pass
class C: ...
```

View File

@@ -1,6 +1,8 @@
# Importing builtin module
```py
import builtins; x = builtins.copyright
reveal_type(x) # revealed: Literal[copyright]
import builtins
x = builtins.copyright
reveal_type(x) # revealed: Literal[copyright]
```

View File

@@ -3,15 +3,27 @@
## Maybe unbound
```py path=maybe_unbound.py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
y = 3
x = y
reveal_type(x) # revealed: Unbound | Literal[3]
reveal_type(y) # revealed: Unbound | Literal[3]
x = y # error: [possibly-unresolved-reference]
# revealed: Unbound | Literal[3]
# error: [possibly-unresolved-reference]
reveal_type(x)
# revealed: Unbound | Literal[3]
# error: [possibly-unresolved-reference]
reveal_type(y)
```
```py
from maybe_unbound import x, y
reveal_type(x) # revealed: Literal[3]
reveal_type(y) # revealed: Literal[3]
```
@@ -19,17 +31,29 @@ reveal_type(y) # revealed: Literal[3]
## Maybe unbound annotated
```py path=maybe_unbound_annotated.py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
y: int = 3
x = y
reveal_type(x) # revealed: Unbound | Literal[3]
reveal_type(y) # revealed: Unbound | Literal[3]
x = y # error: [possibly-unresolved-reference]
# revealed: Unbound | Literal[3]
# error: [possibly-unresolved-reference]
reveal_type(x)
# revealed: Unbound | Literal[3]
# error: [possibly-unresolved-reference]
reveal_type(y)
```
Importing an annotated name prefers the declared type over the inferred type:
```py
from maybe_unbound_annotated import x, y
reveal_type(x) # revealed: Literal[3]
reveal_type(y) # revealed: int
```
@@ -41,14 +65,20 @@ def f(): ...
```
```py path=b.py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
from c import f
else:
def f(): ...
```
```py
from b import f
# TODO: We should disambiguate in such cases, showing `Literal[b.f, c.f]`.
reveal_type(f) # revealed: Literal[f, f]
```
@@ -63,6 +93,10 @@ x: int
```
```py path=b.py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
from c import x
else:
@@ -71,5 +105,6 @@ else:
```py
from b import x
reveal_type(x) # revealed: int
```

View File

@@ -3,14 +3,16 @@
## Unresolved import statement
```py
import bar # error: "Cannot resolve import `bar`"
import bar # error: "Cannot resolve import `bar`"
reveal_type(bar) # revealed: Unknown
```
## Unresolved import from statement
```py
from bar import baz # error: "Cannot resolve import `bar`"
from bar import baz # error: "Cannot resolve import `bar`"
reveal_type(baz) # revealed: Unknown
```
@@ -20,14 +22,16 @@ reveal_type(baz) # revealed: Unknown
```
```py
from a import thing # error: "Module `a` has no member `thing`"
from a import thing # error: "Module `a` has no member `thing`"
reveal_type(thing) # revealed: Unknown
```
## Resolved import of symbol from unresolved import
```py path=a.py
import foo as foo # error: "Cannot resolve import `foo`"
import foo as foo # error: "Cannot resolve import `foo`"
reveal_type(foo) # revealed: Unknown
```
@@ -36,6 +40,7 @@ import" violation:
```py
from a import foo
reveal_type(foo) # revealed: Unknown
```
@@ -48,5 +53,5 @@ x: int
```py
from b import x
x = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]"
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
```

View File

@@ -6,7 +6,8 @@
```
```py path=package/bar.py
from .foo import X # error: [unresolved-import]
from .foo import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
```
@@ -21,6 +22,7 @@ X = 42
```py path=package/bar.py
from .foo import X
reveal_type(X) # revealed: Literal[42]
```
@@ -35,6 +37,7 @@ X = 42
```py path=package/bar.py
from .foo.bar.baz import X
reveal_type(X) # revealed: Literal[42]
```
@@ -46,13 +49,15 @@ X = 42
```py path=package/bar.py
from . import X
reveal_type(X) # revealed: Literal[42]
```
## Non-existent + bare to package
```py path=package/bar.py
from . import X # error: [unresolved-import]
from . import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
```
@@ -60,6 +65,7 @@ reveal_type(X) # revealed: Unknown
```py path=package/__init__.py
from .foo import X
reveal_type(X) # revealed: Literal[42]
```
@@ -70,8 +76,9 @@ X = 42
## Non-existent + dunder init
```py path=package/__init__.py
from .foo import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
from .foo import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown
```
## Long relative import
@@ -85,6 +92,7 @@ X = 42
```py path=package/subpackage/subsubpackage/bar.py
from ...foo import X
reveal_type(X) # revealed: Literal[42]
```
@@ -94,12 +102,13 @@ reveal_type(X) # revealed: Literal[42]
```
```py path=package/foo.py
x
x # error: [unresolved-reference]
```
```py path=package/bar.py
from .foo import x # error: [unresolved-import]
reveal_type(x) # revealed: Unknown
from .foo import x # error: [unresolved-import]
reveal_type(x) # revealed: Unknown
```
## Bare to module
@@ -114,10 +123,11 @@ X = 42
```py path=package/bar.py
# TODO: support submodule imports
from . import foo # error: [unresolved-import]
y = foo.X
# TODO: should be `Literal[42]`
reveal_type(y) # revealed: Unknown
reveal_type(y) # revealed: Unknown
```
## Non-existent + bare to module
@@ -129,5 +139,5 @@ reveal_type(y) # revealed: Unknown
# TODO: support submodule imports
from . import foo # error: [unresolved-import]
reveal_type(foo) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
```

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
# Bytes literals
## Simple
```py
reveal_type(b"red" b"knot") # revealed: Literal[b"redknot"]
reveal_type(b"hello") # revealed: Literal[b"hello"]
reveal_type(b"world" + b"!") # revealed: Literal[b"world!"]
reveal_type(b"\xff\x00") # revealed: Literal[b"\xff\x00"]
```

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,38 +7,27 @@ x = 0
y = str()
z = False
a = f'hello'
b = f'h {x}'
c = 'one ' f'single ' f'literal'
d = 'first ' f'second({b})' f' third'
e = f'-{y}-'
f = f'-{y}-' f'--' '--'
g = f'{z} == {False} is {True}'
reveal_type(a) # revealed: Literal["hello"]
reveal_type(b) # revealed: Literal["h 0"]
reveal_type(c) # revealed: Literal["one single literal"]
reveal_type(d) # revealed: Literal["first second(h 0) third"]
reveal_type(e) # revealed: str
reveal_type(f) # revealed: str
reveal_type(g) # revealed: Literal["False == False is True"]
reveal_type(f"hello") # revealed: Literal["hello"]
reveal_type(f"h {x}") # revealed: Literal["h 0"]
reveal_type("one " f"single " f"literal") # revealed: Literal["one single literal"]
reveal_type("first " f"second({x})" f" third") # revealed: Literal["first second(0) third"]
reveal_type(f"-{y}-") # revealed: str
reveal_type(f"-{y}-" f"--" "--") # revealed: str
reveal_type(f"{z} == {False} is {True}") # revealed: Literal["False == False is True"]
```
## Conversion Flags
```py
string = 'hello'
a = f'{string!r}'
string = "hello"
# TODO: should be `Literal["'hello'"]`
reveal_type(a) # revealed: str
reveal_type(f"{string!r}") # revealed: str
```
## Format Specifiers
```py
a = f'{1:02}'
# TODO: should be `Literal["01"]`
reveal_type(a) # revealed: str
reveal_type(f"{1:02}") # revealed: str
```

View File

@@ -3,24 +3,19 @@
## Simple
```py
w = "Hello"
x = 'world'
y = "Guten " + 'tag'
z = 'bon ' + "jour"
reveal_type(w) # revealed: Literal["Hello"]
reveal_type(x) # revealed: Literal["world"]
reveal_type(y) # revealed: Literal["Guten tag"]
reveal_type(z) # revealed: Literal["bon jour"]
reveal_type("Hello") # revealed: Literal["Hello"]
reveal_type("world") # revealed: Literal["world"]
reveal_type("Guten " + "Tag") # revealed: Literal["Guten Tag"]
reveal_type("bon " + "jour") # revealed: Literal["bon jour"]
```
## Nested Quotes
```py
x = 'I say "hello" to you'
y = "You say \"hey\" back"
z = 'No "closure here'
reveal_type(x) # revealed: Literal["I say \"hello\" to you"]
reveal_type(y) # revealed: Literal["You say \"hey\" back"]
reveal_type(z) # revealed: Literal["No \"closure here"]
reveal_type('I say "hello" to you') # revealed: Literal["I say \"hello\" to you"]
# revealed: Literal["You say \"hey\" back"]
reveal_type("You say \"hey\" back") # fmt: skip
reveal_type('No "closure here') # revealed: Literal["No \"closure here"]
```

View File

@@ -17,8 +17,10 @@ async def foo():
async for x in Iterator():
pass
# TODO
reveal_type(x) # revealed: Unbound | @Todo
# TODO: should reveal `Unbound | Unknown` because `__aiter__` is not defined
# revealed: Unbound | @Todo
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## Basic async for loop
@@ -33,9 +35,11 @@ async def foo():
def __aiter__(self) -> IntAsyncIterator:
return IntAsyncIterator()
#TODO(Alex): async iterables/iterators!
# TODO(Alex): async iterables/iterators!
async for x in IntAsyncIterable():
pass
reveal_type(x) # revealed: Unbound | @Todo
# error: [possibly-unresolved-reference]
# revealed: Unbound | @Todo
reveal_type(x)
```

View File

@@ -14,7 +14,9 @@ class IntIterable:
for x in IntIterable():
pass
reveal_type(x) # revealed: Unbound | int
# revealed: Unbound | int
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## With previous definition
@@ -28,7 +30,7 @@ class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
x = 'foo'
x = "foo"
for x in IntIterable():
pass
@@ -50,7 +52,7 @@ class IntIterable:
for x in IntIterable():
pass
else:
x = 'foo'
x = "foo"
reveal_type(x) # revealed: Literal["foo"]
```
@@ -70,7 +72,7 @@ for x in IntIterable():
if x > 5:
break
else:
x = 'foo'
x = "foo"
reveal_type(x) # revealed: int | Literal["foo"]
```
@@ -85,38 +87,49 @@ class OldStyleIterable:
for x in OldStyleIterable():
pass
reveal_type(x) # revealed: Unbound | int
# revealed: Unbound | int
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## With heterogeneous tuple
```py
for x in (1, 'a', b'foo'):
for x in (1, "a", b"foo"):
pass
reveal_type(x) # revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
# revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## With non-callable iterator
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
class NotIterable:
if flag:
__iter__ = 1
else:
__iter__ = None
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
pass
reveal_type(x) # revealed: Unbound | Unknown
# revealed: Unbound | Unknown
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## Invalid iterable
```py
nonsense = 123
for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
pass
```
@@ -126,9 +139,8 @@ for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
class NotIterable:
def __getitem__(self, key: int) -> int:
return 42
__iter__ = None
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
pass
```

View File

@@ -3,7 +3,7 @@
## Yield must be iterable
```py
class NotIterable: pass
class NotIterable: ...
class Iterator:
def __next__(self) -> int:
@@ -14,5 +14,5 @@ class Iterable:
def generator_function():
yield from Iterable()
yield from NotIterable() # error: "Object of type `NotIterable` is not iterable"
yield from NotIterable() # error: "Object of type `NotIterable` is not iterable"
```

View File

@@ -3,6 +3,10 @@
## Basic While Loop
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1
while flag:
x = 2
@@ -13,20 +17,27 @@ reveal_type(x) # revealed: Literal[1, 2]
## While with else (no break)
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1
while flag:
x = 2
else:
y = x
reveal_type(x) # revealed: Literal[1, 2]
x = 3
reveal_type(x) # revealed: Literal[3]
reveal_type(y) # revealed: Literal[1, 2]
```
## While with Else (may break)
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
x = 1
y = 0
while flag:

View File

@@ -3,11 +3,14 @@
## `is None`
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = None if flag else 1
if x is None:
# TODO the following should be simplified to 'None'
reveal_type(x) # revealed: None | Literal[1] & None
reveal_type(x) # revealed: None
reveal_type(x) # revealed: None | Literal[1]
```
@@ -15,15 +18,36 @@ reveal_type(x) # revealed: None | Literal[1]
## `is` for other types
```py
class A:
...
def bool_instance() -> bool:
return True
flag = bool_instance()
class A: ...
x = A()
y = x if flag else None
if y is x:
# TODO the following should be simplified to 'A'
reveal_type(y) # revealed: A | None & A
reveal_type(y) # revealed: A
reveal_type(y) # revealed: A | None
```
## `is` in chained comparisons
```py
def bool_instance() -> bool:
return True
x_flag, y_flag = bool_instance(), bool_instance()
x = True if x_flag else False
y = True if y_flag else False
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is x is False: # Interpreted as `(y is x) and (x is False)`
reveal_type(x) # revealed: Literal[False]
reveal_type(y) # revealed: bool
```

View File

@@ -5,6 +5,10 @@
The type guard removes `None` from the union type:
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = None if flag else 1
if x is not None:
@@ -16,12 +20,15 @@ reveal_type(x) # revealed: None | Literal[1]
## `is not` for other singleton types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = True if flag else False
reveal_type(x) # revealed: bool
if x is not False:
# TODO the following should be `Literal[True]`
reveal_type(x) # revealed: bool & ~Literal[False]
reveal_type(x) # revealed: Literal[True]
```
## `is not` for non-singleton types
@@ -37,3 +44,23 @@ y = 345
if x is not y:
reveal_type(x) # revealed: Literal[345]
```
## `is not` in chained comparisons
The type guard removes `False` from the union type of the tested value only.
```py
def bool_instance() -> bool:
return True
x_flag, y_flag = bool_instance(), bool_instance()
x = True if x_flag else False
y = True if y_flag else False
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
reveal_type(x) # revealed: Literal[True]
reveal_type(y) # revealed: bool
```

View File

@@ -0,0 +1,29 @@
# Narrowing for nested conditionals
## Multiple negative contributions
```py
def int_instance() -> int: ...
x = int_instance()
if x != 1:
if x != 2:
if x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
```
## Multiple negative contributions with simplification
```py
def bool_instance() -> bool:
return True
flag1, flag2 = bool_instance(), bool_instance()
x = 1 if flag1 else 2 if flag2 else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x != 2:
reveal_type(x) # revealed: Literal[3]
```

View File

@@ -0,0 +1,76 @@
# Narrowing for `!=` conditionals
## `x != None`
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = None if flag else 1
if x != None:
reveal_type(x) # revealed: Literal[1]
```
## `!=` for other singleton types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = True if flag else False
if x != False:
reveal_type(x) # revealed: Literal[True]
```
## `x != y` where `y` is of literal type
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else 2
if x != 1:
reveal_type(x) # revealed: Literal[2]
```
## `x != y` where `y` is a single-valued type
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
class A: ...
class B: ...
C = A if flag else B
if C != A:
reveal_type(C) # revealed: Literal[B]
```
## `!=` for non-single-valued types
Only single-valued types should narrow the type:
```py
def bool_instance() -> bool:
return True
def int_instance() -> int:
return 42
flag = bool_instance()
x = int_instance() if flag else None
y = int_instance()
if x != y:
reveal_type(x) # revealed: int | None
```

View File

@@ -0,0 +1,192 @@
# Narrowing for `isinstance` checks
Narrowing for `isinstance(object, classinfo)` expressions.
## `classinfo` is a single type
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else "a"
if isinstance(x, int):
reveal_type(x) # revealed: Literal[1]
if isinstance(x, str):
reveal_type(x) # revealed: Literal["a"]
if isinstance(x, int):
reveal_type(x) # revealed: Never
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```
## `classinfo` is a tuple of types
Note: `isinstance(x, (int, str))` should not be confused with
`isinstance(x, tuple[(int, str)])`. The former is equivalent to
`isinstance(x, int | str)`:
```py
def bool_instance() -> bool:
return True
flag, flag1, flag2 = bool_instance(), bool_instance(), bool_instance()
x = 1 if flag else "a"
if isinstance(x, (int, str)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
if isinstance(x, (int, bytes)):
reveal_type(x) # revealed: Literal[1]
if isinstance(x, (bytes, str)):
reveal_type(x) # revealed: Literal["a"]
# No narrowing should occur if a larger type is also
# one of the possibilities:
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
y = 1 if flag1 else "a" if flag2 else b"b"
if isinstance(y, (int, str)):
reveal_type(y) # revealed: Literal[1] | Literal["a"]
if isinstance(y, (int, bytes)):
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
if isinstance(y, (str, bytes)):
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
```
## `classinfo` is a nested tuple of types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else "a"
if isinstance(x, (bool, (bytes, int))):
reveal_type(x) # revealed: Literal[1]
```
## Class types
```py
class A: ...
class B: ...
def get_object() -> object: ...
x = get_object()
if isinstance(x, A):
reveal_type(x) # revealed: A
if isinstance(x, B):
reveal_type(x) # revealed: A & B
```
## No narrowing for instances of `builtins.type`
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
t = type("t", (), {})
# This isn't testing what we want it to test if we infer anything more precise here:
reveal_type(t) # revealed: type
x = 1 if flag else "foo"
if isinstance(x, t):
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
```
## Do not use custom `isinstance` for narrowing
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
def isinstance(x, t):
return True
x = 1 if flag else "a"
if isinstance(x, int):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```
## Do support narrowing if `isinstance` is aliased
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
isinstance_alias = isinstance
x = 1 if flag else "a"
if isinstance_alias(x, int):
reveal_type(x) # revealed: Literal[1]
```
## Do support narrowing if `isinstance` is imported
```py
from builtins import isinstance as imported_isinstance
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else "a"
if imported_isinstance(x, int):
reveal_type(x) # revealed: Literal[1]
```
## Do not narrow if second argument is not a type
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else "a"
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "int"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```
## Do not narrow if there are keyword arguments
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else "a"
# TODO: this should cause us to emit a diagnostic
# (`isinstance` has no `foo` parameter)
if isinstance(x, int, foo="bar"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```

View File

@@ -3,6 +3,11 @@
## Single `match` pattern
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = None if flag else 1
reveal_type(x) # revealed: None | Literal[1]
@@ -12,6 +17,5 @@ match x:
case None:
y = x
# TODO intersection simplification: should be just Literal[0] | None
reveal_type(y) # revealed: Literal[0] | None | Literal[1] & None
reveal_type(y) # revealed: Literal[0] | None
```

View File

@@ -1,9 +0,0 @@
# `is not None` narrowing
```py
x = None if flag else 1
if x is not None:
reveal_type(x) # revealed: Literal[1]
reveal_type(x) # revealed: None | Literal[1]
```

View File

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

View File

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

View File

@@ -3,9 +3,14 @@
## Shadow after incompatible declarations is OK
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
x: str
else:
x: int
x: bytes = b'foo'
x: bytes = b"foo"
```

View File

@@ -6,5 +6,6 @@ In type stubs, classes can reference themselves in their base class definitions.
```py path=a.pyi
class C(C): ...
reveal_type(C) # revealed: Literal[C]
```

View File

@@ -3,13 +3,32 @@
## Simple
```py
w = b'red' b'knot'
x = b'hello'
y = b'world' + b'!'
z = b'\xff\x00'
b = b"\x00abc\xff"
reveal_type(w) # revealed: Literal[b"redknot"]
reveal_type(x) # revealed: Literal[b"hello"]
reveal_type(y) # revealed: Literal[b"world!"]
reveal_type(z) # revealed: Literal[b"\xff\x00"]
reveal_type(b[0]) # revealed: Literal[b"\x00"]
reveal_type(b[1]) # revealed: Literal[b"a"]
reveal_type(b[4]) # revealed: Literal[b"\xff"]
reveal_type(b[-1]) # revealed: Literal[b"\xff"]
reveal_type(b[-2]) # revealed: Literal[b"c"]
reveal_type(b[-5]) # revealed: Literal[b"\x00"]
reveal_type(b[False]) # revealed: Literal[b"\x00"]
reveal_type(b[True]) # revealed: Literal[b"a"]
x = b[5] # error: [index-out-of-bounds] "Index 5 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5"
reveal_type(x) # revealed: Unknown
y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5"
reveal_type(y) # revealed: Unknown
```
## Function return
```py
def int_instance() -> int: ...
a = b"abcde"[int_instance()]
# TODO: Support overloads... Should be `bytes`
reveal_type(a) # revealed: @Todo
```

View File

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

View File

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

View File

@@ -10,10 +10,13 @@ A list can be indexed into with:
```py
x = [1, 2, 3]
reveal_type(x) # revealed: list
# TODO reveal int
reveal_type(x[0]) # revealed: @Todo
# TODO reveal list
reveal_type(x[0:1]) # revealed: @Todo
# TODO error
reveal_type(x["a"]) # revealed: @Todo
```
@@ -26,8 +29,10 @@ This should also get type checked.
```py
x = [1, 2, 3]
x[0 if (y := 2) else 1] = 5
# TODO error? (indeterminite index type)
x["a" if (y := 2) else 1] = 6
# TODO error (can't index via string)
x["a" if (y := 2) else "b"] = 6
```

View File

@@ -3,30 +3,29 @@
## Simple
```py
s = 'abcde'
s = "abcde"
a = s[0]
b = s[1]
c = s[-1]
d = s[-2]
e = s[8] # error: [index-out-of-bounds] "Index 8 is out of bounds for string `Literal["abcde"]` with length 5"
f = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5"
reveal_type(s[0]) # revealed: Literal["a"]
reveal_type(s[1]) # revealed: Literal["b"]
reveal_type(s[-1]) # revealed: Literal["e"]
reveal_type(s[-2]) # revealed: Literal["d"]
reveal_type(a) # revealed: Literal["a"]
reveal_type(b) # revealed: Literal["b"]
reveal_type(c) # revealed: Literal["e"]
reveal_type(d) # revealed: Literal["d"]
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown
reveal_type(s[False]) # revealed: Literal["a"]
reveal_type(s[True]) # revealed: Literal["b"]
a = s[8] # error: [index-out-of-bounds] "Index 8 is out of bounds for string `Literal["abcde"]` with length 5"
reveal_type(a) # revealed: Unknown
b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5"
reveal_type(b) # revealed: Unknown
```
## Function return
```py
def add(x: int, y: int) -> int:
return x + y
def int_instance() -> int: ...
a = 'abcde'[add(0, 1)]
a = "abcde"[int_instance()]
# TODO: Support overloads... Should be `str`
reveal_type(a) # revealed: @Todo
```

View File

@@ -3,19 +3,16 @@
## Basic
```py
t = (1, 'a', 'b')
t = (1, "a", "b")
a = t[0]
b = t[1]
c = t[-1]
d = t[-2]
e = t[4] # error: [index-out-of-bounds]
f = t[-4] # error: [index-out-of-bounds]
reveal_type(t[0]) # revealed: Literal[1]
reveal_type(t[1]) # revealed: Literal["a"]
reveal_type(t[-1]) # revealed: Literal["b"]
reveal_type(t[-2]) # revealed: Literal["a"]
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal["a"]
reveal_type(c) # revealed: Literal["b"]
reveal_type(d) # revealed: Literal["a"]
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown
a = t[4] # error: [index-out-of-bounds]
reveal_type(a) # revealed: Unknown
b = t[-4] # error: [index-out-of-bounds]
reveal_type(b) # revealed: Unknown
```

View File

@@ -0,0 +1,29 @@
# Unary Operations
```py
class Number:
def __init__(self, value: int):
self.value = 1
def __pos__(self) -> int:
return +self.value
def __neg__(self) -> int:
return -self.value
def __invert__(self) -> Literal[True]:
return True
a = Number()
reveal_type(+a) # revealed: int
reveal_type(-a) # revealed: int
reveal_type(~a) # revealed: @Todo
class NoDunder: ...
b = NoDunder()
+b # error: [unsupported-operator] "Unary operator `+` is unsupported for type `NoDunder`"
-b # error: [unsupported-operator] "Unary operator `-` is unsupported for type `NoDunder`"
~b # error: [unsupported-operator] "Unary operator `~` is unsupported for type `NoDunder`"
```

View File

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

View File

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

View File

@@ -155,12 +155,10 @@ class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
(a, b) = Iterable()
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
@@ -173,12 +171,10 @@ class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
(a, (b, c), d) = (1, Iterable(), 2)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: int
@@ -191,7 +187,7 @@ reveal_type(d) # revealed: Literal[2]
### Simple unpacking
```py
a, b = 'ab'
a, b = "ab"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
```
@@ -200,7 +196,7 @@ reveal_type(b) # revealed: LiteralString
```py
# TODO: Add diagnostic (there aren't enough values to unpack)
a, b, c = 'ab'
a, b, c = "ab"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
reveal_type(c) # revealed: Unknown
@@ -210,7 +206,7 @@ reveal_type(c) # revealed: Unknown
```py
# TODO: Add diagnostic (too many values to unpack)
a, b = 'abc'
a, b = "abc"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
```

View File

@@ -132,7 +132,7 @@ mod tests {
#[test]
fn inequality() {
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
let parsed = ParsedModule::new(parsed_raw.clone());
let parsed = ParsedModule::new(parsed_raw);
let stmt = &parsed.syntax().body[0];
let node = unsafe { AstNodeRef::new(parsed.clone(), stmt) };
@@ -150,7 +150,7 @@ mod tests {
#[allow(unsafe_code)]
fn debug() {
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
let parsed = ParsedModule::new(parsed_raw.clone());
let parsed = ParsedModule::new(parsed_raw);
let stmt = &parsed.syntax().body[0];

View File

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

View File

@@ -1294,7 +1294,7 @@ mod tests {
search_paths: SearchPathSettings {
extra_paths: vec![],
src_root: src.clone(),
custom_typeshed: Some(custom_typeshed.clone()),
custom_typeshed: Some(custom_typeshed),
site_packages: SitePackages::Known(vec![site_packages]),
},
},
@@ -1445,7 +1445,7 @@ mod tests {
assert_function_query_was_not_run(
&db,
resolve_module_query,
ModuleNameIngredient::new(&db, functools_module_name.clone()),
ModuleNameIngredient::new(&db, functools_module_name),
&events,
);
assert_eq!(functools_module.search_path(), &stdlib);

View File

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

View File

@@ -1012,7 +1012,7 @@ where
// Add symbols and definitions for the parameters to the lambda scope.
if let Some(parameters) = &lambda.parameters {
for parameter in &**parameters {
for parameter in parameters {
self.declare_parameter(parameter);
}
}

View File

@@ -47,7 +47,13 @@ impl<'db> Definition<'db> {
self.kind(db).category().is_binding()
}
/// Return true if this is a symbol was defined in the `typing` or `typing_extensions` modules
pub(crate) fn is_builtin_definition(self, db: &'db dyn Db) -> bool {
file_to_module(db, self.file(db)).is_some_and(|module| {
module.search_path().is_standard_library() && matches!(&**module.name(), "builtins")
})
}
/// Return true if this symbol was defined in the `typing` or `typing_extensions` modules
pub(crate) fn is_typing_definition(self, db: &'db dyn Db) -> bool {
file_to_module(db, self.file(db)).is_some_and(|module| {
module.search_path().is_standard_library()
@@ -290,7 +296,7 @@ impl DefinitionNodeRef<'_> {
handler,
is_star,
}) => DefinitionKind::ExceptHandler(ExceptHandlerDefinitionKind {
handler: AstNodeRef::new(parsed.clone(), handler),
handler: AstNodeRef::new(parsed, handler),
is_star,
}),
}

View File

@@ -175,7 +175,6 @@ mod tests {
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::types::Type;
use crate::{HasTy, ProgramSettings, SemanticModel};
fn setup_db<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<TestDb> {
@@ -205,7 +204,7 @@ mod tests {
let model = SemanticModel::new(&db, foo);
let ty = function.ty(&model);
assert!(matches!(ty, Type::Function(_)));
assert!(ty.is_function_literal());
Ok(())
}
@@ -222,7 +221,7 @@ mod tests {
let model = SemanticModel::new(&db, foo);
let ty = class.ty(&model);
assert!(matches!(ty, Type::Class(_)));
assert!(ty.is_class_literal());
Ok(())
}
@@ -243,7 +242,7 @@ mod tests {
let model = SemanticModel::new(&db, bar);
let ty = alias.ty(&model);
assert!(matches!(ty, Type::Class(_)));
assert!(ty.is_class_literal());
Ok(())
}

View File

@@ -14,7 +14,7 @@ use crate::stdlib::{
builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty, typing_extensions_symbol_ty,
};
use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet, Module};
use crate::{Db, FxOrderSet, HasTy, Module, SemanticModel};
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
@@ -148,18 +148,18 @@ fn bindings_ty<'db>(
binding,
constraints,
}| {
let mut constraint_tys =
constraints.filter_map(|constraint| narrowing_constraint(db, constraint, binding));
let mut constraint_tys = constraints
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
.peekable();
let binding_ty = binding_ty(db, binding);
if let Some(first_constraint_ty) = constraint_tys.next() {
let mut builder = IntersectionBuilder::new(db);
builder = builder
.add_positive(binding_ty)
.add_positive(first_constraint_ty);
for constraint_ty in constraint_tys {
builder = builder.add_positive(constraint_ty);
}
builder.build()
if constraint_tys.peek().is_some() {
constraint_tys
.fold(
IntersectionBuilder::new(db).add_positive(binding_ty),
IntersectionBuilder::add_positive,
)
.build()
} else {
binding_ty
}
@@ -211,7 +211,13 @@ fn declarations_ty<'db>(
let declared_ty = if let Some(second) = all_types.next() {
let mut builder = UnionBuilder::new(db).add(first);
for other in [second].into_iter().chain(all_types) {
if !first.is_equivalent_to(db, other) {
// Make sure not to emit spurious errors relating to `Type::Todo`,
// since we only infer this type due to a limitation in our current model.
//
// `Unknown` is different here, since we might infer `Unknown`
// for one of these due to a variable being defined in one possible
// control-flow branch but not another one.
if !first.is_equivalent_to(db, other) && !first.is_todo() && !other.is_todo() {
conflicting.push(other);
}
builder = builder.add(other);
@@ -255,11 +261,11 @@ pub enum Type<'db> {
/// `Todo` would change the output type.
Todo,
/// A specific function object
Function(FunctionType<'db>),
FunctionLiteral(FunctionType<'db>),
/// A specific module object
Module(File),
ModuleLiteral(File),
/// A specific class object
Class(ClassType<'db>),
ClassLiteral(ClassType<'db>),
/// The set of Python objects with the given class in their __class__'s method resolution order
Instance(ClassType<'db>),
/// The set of objects in any of the types in the union
@@ -292,28 +298,38 @@ impl<'db> Type<'db> {
matches!(self, Type::Never)
}
pub const fn into_class_type(self) -> Option<ClassType<'db>> {
pub const fn is_todo(&self) -> bool {
matches!(self, Type::Todo)
}
pub const fn into_class_literal_type(self) -> Option<ClassType<'db>> {
match self {
Type::Class(class_type) => Some(class_type),
Type::ClassLiteral(class_type) => Some(class_type),
_ => None,
}
}
pub fn expect_class(self) -> ClassType<'db> {
self.into_class_type()
.expect("Expected a Type::Class variant")
#[track_caller]
pub fn expect_class_literal(self) -> ClassType<'db> {
self.into_class_literal_type()
.expect("Expected a Type::ClassLiteral variant")
}
pub const fn into_module_type(self) -> Option<File> {
pub const fn is_class_literal(&self) -> bool {
matches!(self, Type::ClassLiteral(..))
}
pub const fn into_module_literal_type(self) -> Option<File> {
match self {
Type::Module(file) => Some(file),
Type::ModuleLiteral(file) => Some(file),
_ => None,
}
}
pub fn expect_module(self) -> File {
self.into_module_type()
.expect("Expected a Type::Module variant")
#[track_caller]
pub fn expect_module_literal(self) -> File {
self.into_module_literal_type()
.expect("Expected a Type::ModuleLiteral variant")
}
pub const fn into_union_type(self) -> Option<UnionType<'db>> {
@@ -323,11 +339,16 @@ impl<'db> Type<'db> {
}
}
#[track_caller]
pub fn expect_union(self) -> UnionType<'db> {
self.into_union_type()
.expect("Expected a Type::Union variant")
}
pub const fn is_union(&self) -> bool {
matches!(self, Type::Union(..))
}
pub const fn into_intersection_type(self) -> Option<IntersectionType<'db>> {
match self {
Type::Intersection(intersection_type) => Some(intersection_type),
@@ -335,21 +356,27 @@ impl<'db> Type<'db> {
}
}
#[track_caller]
pub fn expect_intersection(self) -> IntersectionType<'db> {
self.into_intersection_type()
.expect("Expected a Type::Intersection variant")
}
pub const fn into_function_type(self) -> Option<FunctionType<'db>> {
pub const fn into_function_literal_type(self) -> Option<FunctionType<'db>> {
match self {
Type::Function(function_type) => Some(function_type),
Type::FunctionLiteral(function_type) => Some(function_type),
_ => None,
}
}
pub fn expect_function(self) -> FunctionType<'db> {
self.into_function_type()
.expect("Expected a Type::Function variant")
#[track_caller]
pub fn expect_function_literal(self) -> FunctionType<'db> {
self.into_function_literal_type()
.expect("Expected a Type::FunctionLiteral variant")
}
pub const fn is_function_literal(&self) -> bool {
matches!(self, Type::FunctionLiteral(..))
}
pub const fn into_int_literal_type(self) -> Option<i64> {
@@ -359,11 +386,20 @@ impl<'db> Type<'db> {
}
}
#[track_caller]
pub fn expect_int_literal(self) -> i64 {
self.into_int_literal_type()
.expect("Expected a Type::IntLiteral variant")
}
pub const fn is_boolean_literal(&self) -> bool {
matches!(self, Type::BooleanLiteral(..))
}
pub const fn is_literal_string(&self) -> bool {
matches!(self, Type::LiteralString)
}
pub fn may_be_unbound(&self, db: &'db dyn Db) -> bool {
match self {
Type::Unbound => true,
@@ -387,18 +423,8 @@ impl<'db> Type<'db> {
pub fn is_stdlib_symbol(&self, db: &'db dyn Db, module_name: &str, name: &str) -> bool {
match self {
Type::Class(class) => class.is_stdlib_symbol(db, module_name, name),
Type::Function(function) => function.is_stdlib_symbol(db, module_name, name),
_ => false,
}
}
/// Return true if the type is a class or a union of classes.
pub fn is_class(&self, db: &'db dyn Db) -> bool {
match self {
Type::Union(union) => union.elements(db).iter().all(|ty| ty.is_class(db)),
Type::Class(_) => true,
// / TODO include type[X], once we add that type
Type::ClassLiteral(class) => class.is_stdlib_symbol(db, module_name, name),
Type::FunctionLiteral(function) => function.is_stdlib_symbol(db, module_name, name),
_ => false,
}
}
@@ -415,6 +441,8 @@ impl<'db> Type<'db> {
(_, Type::Unknown | Type::Any | Type::Todo) => false,
(Type::Never, _) => true,
(_, Type::Never) => false,
(_, Type::Instance(class)) if class.is_known(db, KnownClass::Object) => true,
(Type::Instance(class), _) if class.is_known(db, KnownClass::Object) => false,
(Type::BooleanLiteral(_), Type::Instance(class))
if class.is_known(db, KnownClass::Bool) =>
{
@@ -434,12 +462,22 @@ impl<'db> Type<'db> {
{
true
}
(Type::ClassLiteral(..), Type::Instance(class))
if class.is_known(db, KnownClass::Type) =>
{
true
}
(Type::Union(union), ty) => union
.elements(db)
.iter()
.all(|&elem_ty| elem_ty.is_subtype_of(db, ty)),
(ty, Type::Union(union)) => union
.elements(db)
.iter()
.any(|&elem_ty| ty.is_subtype_of(db, elem_ty)),
(_, Type::Instance(class)) if class.is_known(db, KnownClass::Object) => true,
(Type::Instance(class), _) if class.is_known(db, KnownClass::Object) => false,
(Type::Instance(self_class), Type::Instance(target_class)) => {
self_class.is_subclass_of(db, target_class)
}
// TODO
_ => false,
}
@@ -471,6 +509,150 @@ impl<'db> Type<'db> {
self == other
}
/// Return true if this type and `other` have no common elements.
///
/// Note: This function aims to have no false positives, but might return
/// wrong `false` answers in some cases.
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
match (self, other) {
(Type::Never, _) | (_, Type::Never) => true,
(Type::Any, _) | (_, Type::Any) => false,
(Type::Unknown, _) | (_, Type::Unknown) => false,
(Type::Unbound, _) | (_, Type::Unbound) => false,
(Type::Todo, _) | (_, Type::Todo) => false,
(Type::Union(union), other) | (other, Type::Union(union)) => union
.elements(db)
.iter()
.all(|e| e.is_disjoint_from(db, other)),
(Type::Intersection(intersection), other)
| (other, Type::Intersection(intersection)) => {
if intersection
.positive(db)
.iter()
.any(|p| p.is_disjoint_from(db, other))
{
true
} else {
// TODO we can do better here. For example:
// X & ~Literal[1] is disjoint from Literal[1]
false
}
}
(
left @ (Type::None
| Type::BooleanLiteral(..)
| Type::IntLiteral(..)
| Type::StringLiteral(..)
| Type::BytesLiteral(..)
| Type::FunctionLiteral(..)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)),
right @ (Type::None
| Type::BooleanLiteral(..)
| Type::IntLiteral(..)
| Type::StringLiteral(..)
| Type::BytesLiteral(..)
| Type::FunctionLiteral(..)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)),
) => left != right,
(Type::None, Type::Instance(class_type)) | (Type::Instance(class_type), Type::None) => {
!matches!(
class_type.known(db),
Some(KnownClass::NoneType | KnownClass::Object)
)
}
(Type::None, _) | (_, Type::None) => true,
(Type::BooleanLiteral(..), Type::Instance(class_type))
| (Type::Instance(class_type), Type::BooleanLiteral(..)) => !matches!(
class_type.known(db),
Some(KnownClass::Bool | KnownClass::Int | KnownClass::Object)
),
(Type::BooleanLiteral(..), _) | (_, Type::BooleanLiteral(..)) => true,
(Type::IntLiteral(..), Type::Instance(class_type))
| (Type::Instance(class_type), Type::IntLiteral(..)) => !matches!(
class_type.known(db),
Some(KnownClass::Int | KnownClass::Object)
),
(Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true,
(Type::StringLiteral(..), Type::LiteralString)
| (Type::LiteralString, Type::StringLiteral(..)) => false,
(Type::StringLiteral(..), Type::Instance(class_type))
| (Type::Instance(class_type), Type::StringLiteral(..)) => !matches!(
class_type.known(db),
Some(KnownClass::Str | KnownClass::Object)
),
(Type::StringLiteral(..), _) | (_, Type::StringLiteral(..)) => true,
(Type::LiteralString, Type::LiteralString) => false,
(Type::LiteralString, Type::Instance(class_type))
| (Type::Instance(class_type), Type::LiteralString) => !matches!(
class_type.known(db),
Some(KnownClass::Str | KnownClass::Object)
),
(Type::LiteralString, _) | (_, Type::LiteralString) => true,
(Type::BytesLiteral(..), Type::Instance(class_type))
| (Type::Instance(class_type), Type::BytesLiteral(..)) => !matches!(
class_type.known(db),
Some(KnownClass::Bytes | KnownClass::Object)
),
(Type::BytesLiteral(..), _) | (_, Type::BytesLiteral(..)) => true,
(
Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..),
Type::Instance(class_type),
)
| (
Type::Instance(class_type),
Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..),
) => !class_type.is_known(db, KnownClass::Object),
(Type::Instance(..), Type::Instance(..)) => {
// TODO: once we have support for `final`, there might be some cases where
// we can determine that two types are disjoint. For non-final classes, we
// return false (multiple inheritance).
// TODO: is there anything specific to do for instances of KnownClass::Type?
false
}
(Type::Tuple(tuple), other) | (other, Type::Tuple(tuple)) => {
if let Type::Tuple(other_tuple) = other {
if tuple.len(db) == other_tuple.len(db) {
tuple
.elements(db)
.iter()
.zip(other_tuple.elements(db))
.any(|(e1, e2)| e1.is_disjoint_from(db, *e2))
} else {
true
}
} else {
// We can not be sure if the tuple is disjoint from 'other' because:
// - 'other' might be the homogeneous arbitrary-length tuple type
// tuple[T, ...] (which we don't have support for yet); if all of
// our element types are not disjoint with T, this is not disjoint
// - 'other' might be a user subtype of tuple, which, if generic
// over the same or compatible *Ts, would overlap with tuple.
//
// TODO: add checks for the above cases once we support them
false
}
}
}
}
/// Return true if there is just a single inhabitant for this type.
///
/// Note: This function aims to have no false positives, but might return `false`
@@ -492,7 +674,7 @@ impl<'db> Type<'db> {
// are both of type Literal[345], for example.
false
}
Type::None | Type::BooleanLiteral(_) | Type::Function(..) | Type::Class(..) | Type::Module(..) => true,
Type::None | Type::BooleanLiteral(_) | Type::FunctionLiteral(..) | Type::ClassLiteral(..) | Type::ModuleLiteral(..) => true,
Type::Tuple(..) => {
// The empty tuple is a singleton on CPython and PyPy, but not on other Python
// implementations such as GraalPy. Its *use* as a singleton is discouraged and
@@ -520,6 +702,55 @@ impl<'db> Type<'db> {
}
}
/// Return true if this type is non-empty and all inhabitants of this type compare equal.
pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool {
match self {
Type::None
| Type::FunctionLiteral(..)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)
| Type::IntLiteral(..)
| Type::BooleanLiteral(..)
| Type::StringLiteral(..)
| Type::BytesLiteral(..) => true,
Type::Tuple(tuple) => tuple
.elements(db)
.iter()
.all(|elem| elem.is_single_valued(db)),
Type::Instance(class_type) => match class_type.known(db) {
Some(KnownClass::NoneType) => true,
Some(
KnownClass::Bool
| KnownClass::Object
| KnownClass::Bytes
| KnownClass::Type
| KnownClass::Int
| KnownClass::Float
| KnownClass::Str
| KnownClass::List
| KnownClass::Tuple
| KnownClass::Set
| KnownClass::Dict
| KnownClass::GenericAlias
| KnownClass::ModuleType
| KnownClass::FunctionType,
) => false,
None => false,
},
Type::Any
| Type::Never
| Type::Unknown
| Type::Unbound
| Type::Todo
| Type::Union(..)
| Type::Intersection(..)
| Type::LiteralString => false,
}
}
/// Resolve a member access of a type.
///
/// For example, if `foo` is `Type::Instance(<Bar>)`,
@@ -547,12 +778,12 @@ impl<'db> Type<'db> {
// TODO: attribute lookup on None type
Type::Todo
}
Type::Function(_) => {
Type::FunctionLiteral(_) => {
// TODO: attribute lookup on function type
Type::Todo
}
Type::Module(file) => global_symbol_ty(db, *file, name),
Type::Class(class) => class.class_member(db, name),
Type::ModuleLiteral(file) => global_symbol_ty(db, *file, name),
Type::ClassLiteral(class) => class.class_member(db, name),
Type::Instance(_) => {
// TODO MRO? get_own_instance_member, get_instance_member
Type::Todo
@@ -600,9 +831,9 @@ impl<'db> Type<'db> {
Truthiness::Ambiguous
}
Type::None => Truthiness::AlwaysFalse,
Type::Function(_) => Truthiness::AlwaysTrue,
Type::Module(_) => Truthiness::AlwaysTrue,
Type::Class(_) => {
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
Type::ClassLiteral(_) => {
// TODO: lookup `__bool__` and `__len__` methods on the class's metaclass
// More info in https://docs.python.org/3/library/stdtypes.html#truth-value-testing
Truthiness::Ambiguous
@@ -647,16 +878,19 @@ impl<'db> Type<'db> {
fn call(self, db: &'db dyn Db, arg_types: &[Type<'db>]) -> CallOutcome<'db> {
match self {
// TODO validate typed call arguments vs callable signature
Type::Function(function_type) => match function_type.known(db) {
None => CallOutcome::callable(function_type.return_type(db)),
Some(KnownFunction::RevealType) => CallOutcome::revealed(
function_type.return_type(db),
*arg_types.first().unwrap_or(&Type::Unknown),
),
},
Type::FunctionLiteral(function_type) => {
if function_type.is_known(db, KnownFunction::RevealType) {
CallOutcome::revealed(
function_type.return_type(db),
*arg_types.first().unwrap_or(&Type::Unknown),
)
} else {
CallOutcome::callable(function_type.return_type(db))
}
}
// TODO annotated return type on `__new__` or metaclass `__call__`
Type::Class(class) => {
Type::ClassLiteral(class) => {
CallOutcome::callable(match class.known(db) {
// If the class is the builtin-bool class (for example `bool(1)`), we try to
// return the specific truthiness value of the input arg, `Literal[True]` for
@@ -716,7 +950,7 @@ impl<'db> Type<'db> {
fn iterate(self, db: &'db dyn Db) -> IterationOutcome<'db> {
if let Type::Tuple(tuple_type) = self {
return IterationOutcome::Iterable {
element_ty: UnionType::from_elements(db, &**tuple_type.elements(db)),
element_ty: UnionType::from_elements(db, tuple_type.elements(db)),
};
}
@@ -776,7 +1010,7 @@ impl<'db> Type<'db> {
Type::Unknown => Type::Unknown,
Type::Unbound => Type::Unknown,
Type::Never => Type::Never,
Type::Class(class) => Type::Instance(*class),
Type::ClassLiteral(class) => Type::Instance(*class),
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
// TODO: we can probably do better here: --Alex
Type::Intersection(_) => Type::Todo,
@@ -784,9 +1018,9 @@ impl<'db> Type<'db> {
// since they already indicate that the object is an instance of some kind:
Type::BooleanLiteral(_)
| Type::BytesLiteral(_)
| Type::Function(_)
| Type::FunctionLiteral(_)
| Type::Instance(_)
| Type::Module(_)
| Type::ModuleLiteral(_)
| Type::IntLiteral(_)
| Type::StringLiteral(_)
| Type::Tuple(_)
@@ -802,17 +1036,17 @@ impl<'db> Type<'db> {
match self {
Type::Unbound => Type::Unbound,
Type::Never => Type::Never,
Type::Instance(class) => Type::Class(*class),
Type::Instance(class) => Type::ClassLiteral(*class),
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
Type::BooleanLiteral(_) => KnownClass::Bool.to_class(db),
Type::BytesLiteral(_) => KnownClass::Bytes.to_class(db),
Type::IntLiteral(_) => KnownClass::Int.to_class(db),
Type::Function(_) => KnownClass::FunctionType.to_class(db),
Type::Module(_) => KnownClass::ModuleType.to_class(db),
Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class(db),
Type::Tuple(_) => KnownClass::Tuple.to_class(db),
Type::None => KnownClass::NoneType.to_class(db),
// TODO not accurate if there's a custom metaclass...
Type::Class(_) => KnownClass::Type.to_class(db),
Type::ClassLiteral(_) => KnownClass::Type.to_class(db),
// TODO can we do better here? `type[LiteralString]`?
Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class(db),
// TODO: `type[Any]`?
@@ -1163,7 +1397,7 @@ impl<'db> CallOutcome<'db> {
let mut not_callable = vec![];
let mut union_builder = UnionBuilder::new(db);
let mut revealed = false;
for outcome in &**outcomes {
for outcome in outcomes {
let return_ty = match outcome {
Self::NotCallable { not_callable_ty } => {
not_callable.push(*not_callable_ty);
@@ -1374,6 +1608,10 @@ impl<'db> FunctionType<'db> {
})
.unwrap_or(Type::Unknown)
}
pub fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool {
self.known(db) == Some(known_function)
}
}
/// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might
@@ -1382,6 +1620,8 @@ impl<'db> FunctionType<'db> {
pub enum KnownFunction {
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
RevealType,
/// `builtins.isinstance`
IsInstance,
}
#[salsa::interned]
@@ -1425,7 +1665,29 @@ impl<'db> ClassType<'db> {
class_stmt_node
.bases()
.iter()
.map(move |base_expr| definition_expression_ty(db, definition, base_expr))
.map(move |base_expr: &ast::Expr| {
if class_stmt_node.type_params.is_some() {
// when we have a specialized scope, we'll look up the inference
// within that scope
let model: SemanticModel<'db> = SemanticModel::new(db, definition.file(db));
base_expr.ty(&model)
} else {
// Otherwise, we can do the lookup based on the definition scope
definition_expression_ty(db, definition, base_expr)
}
})
}
pub fn is_subclass_of(self, db: &'db dyn Db, other: ClassType) -> bool {
// TODO: we need to iterate over the *MRO* here, not the bases
(other == self)
|| self.bases(db).any(|base| match base {
Type::ClassLiteral(base_class) => base_class == other,
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
// participate, so we should not return `True` if we find `Any/Unknown` in the
// bases.
_ => false,
})
}
/// Returns the class member of this class named `name`.
@@ -1548,8 +1810,8 @@ impl<'db> TupleType<'db> {
#[cfg(test)]
mod tests {
use super::{
builtins_symbol_ty, BytesLiteralType, StringLiteralType, Truthiness, TupleType, Type,
UnionType,
builtins_symbol_ty, BytesLiteralType, IntersectionBuilder, StringLiteralType, Truthiness,
TupleType, Type, UnionType,
};
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
@@ -1587,12 +1849,13 @@ mod tests {
None,
Any,
IntLiteral(i64),
BoolLiteral(bool),
BooleanLiteral(bool),
StringLiteral(&'static str),
LiteralString,
BytesLiteral(&'static str),
BuiltinInstance(&'static str),
Union(Vec<Ty>),
Intersection { pos: Vec<Ty>, neg: Vec<Ty> },
Tuple(Vec<Ty>),
}
@@ -1605,13 +1868,23 @@ mod tests {
Ty::Any => Type::Any,
Ty::IntLiteral(n) => Type::IntLiteral(n),
Ty::StringLiteral(s) => Type::StringLiteral(StringLiteralType::new(db, s)),
Ty::BoolLiteral(b) => Type::BooleanLiteral(b),
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
Ty::LiteralString => Type::LiteralString,
Ty::BytesLiteral(s) => Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes())),
Ty::BuiltinInstance(s) => builtins_symbol_ty(db, s).to_instance(db),
Ty::Union(tys) => {
UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db)))
}
Ty::Intersection { pos, neg } => {
let mut builder = IntersectionBuilder::new(db);
for p in pos {
builder = builder.add_positive(p.into_type(db));
}
for n in neg {
builder = builder.add_negative(n.into_type(db));
}
builder.build()
}
Ty::Tuple(tys) => {
let elements: Box<_> = tys.into_iter().map(|ty| ty.into_type(db)).collect();
Type::Tuple(TupleType::new(db, elements))
@@ -1651,13 +1924,24 @@ mod tests {
#[test_case(Ty::BuiltinInstance("str"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::BuiltinInstance("bool"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::BuiltinInstance("bool"), Ty::BuiltinInstance("int"))]
#[test_case(Ty::Never, Ty::IntLiteral(1))]
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("int"))]
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("object"))]
#[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("bool"))]
#[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("object"))]
#[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("str"))]
#[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::StringLiteral("foo"), Ty::LiteralString)]
#[test_case(Ty::LiteralString, Ty::BuiltinInstance("str"))]
#[test_case(Ty::LiteralString, Ty::BuiltinInstance("object"))]
#[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("bytes"))]
#[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
#[test_case(Ty::Union(vec![Ty::BuiltinInstance("str"), Ty::BuiltinInstance("int")]), Ty::BuiltinInstance("object"))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2), Ty::IntLiteral(3)]))]
#[test_case(Ty::BuiltinInstance("TypeError"), Ty::BuiltinInstance("Exception"))]
fn is_subtype_of(from: Ty, to: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
@@ -1670,6 +1954,8 @@ mod tests {
#[test_case(Ty::IntLiteral(1), Ty::Any)]
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::Unknown, Ty::BuiltinInstance("str")]))]
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("str"))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(1))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(3)]))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::IntLiteral(1))]
fn is_not_subtype_of(from: Ty, to: Ty) {
@@ -1677,6 +1963,32 @@ mod tests {
assert!(!from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
}
#[test]
fn is_subtype_of_class_literals() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
"
class A: ...
class B: ...
U = A if flag else B
",
)
.unwrap();
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
let type_a = super::global_symbol_ty(&db, module, "A");
let type_u = super::global_symbol_ty(&db, module, "U");
assert!(type_a.is_class_literal());
assert!(type_a.is_subtype_of(&db, Ty::BuiltinInstance("type").into_type(&db)));
assert!(type_a.is_subtype_of(&db, Ty::BuiltinInstance("object").into_type(&db)));
assert!(type_u.is_union());
assert!(type_u.is_subtype_of(&db, Ty::BuiltinInstance("type").into_type(&db)));
assert!(type_u.is_subtype_of(&db, Ty::BuiltinInstance("object").into_type(&db)));
}
#[test_case(
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]),
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)])
@@ -1687,22 +1999,134 @@ mod tests {
assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
}
#[test_case(Ty::Never, Ty::Never)]
#[test_case(Ty::Never, Ty::None)]
#[test_case(Ty::Never, Ty::BuiltinInstance("int"))]
#[test_case(Ty::None, Ty::BooleanLiteral(true))]
#[test_case(Ty::None, Ty::IntLiteral(1))]
#[test_case(Ty::None, Ty::StringLiteral("test"))]
#[test_case(Ty::None, Ty::BytesLiteral("test"))]
#[test_case(Ty::None, Ty::LiteralString)]
#[test_case(Ty::None, Ty::BuiltinInstance("int"))]
#[test_case(Ty::None, Ty::Tuple(vec![Ty::None]))]
#[test_case(Ty::BooleanLiteral(true), Ty::BooleanLiteral(false))]
#[test_case(Ty::BooleanLiteral(true), Ty::Tuple(vec![Ty::None]))]
#[test_case(Ty::BooleanLiteral(true), Ty::IntLiteral(1))]
#[test_case(Ty::BooleanLiteral(false), Ty::IntLiteral(0))]
#[test_case(Ty::IntLiteral(1), Ty::IntLiteral(2))]
#[test_case(Ty::IntLiteral(1), Ty::Tuple(vec![Ty::None]))]
#[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("b"))]
#[test_case(Ty::StringLiteral("a"), Ty::Tuple(vec![Ty::None]))]
#[test_case(Ty::LiteralString, Ty::BytesLiteral("a"))]
#[test_case(Ty::BytesLiteral("a"), Ty::BytesLiteral("b"))]
#[test_case(Ty::BytesLiteral("a"), Ty::Tuple(vec![Ty::None]))]
#[test_case(Ty::BytesLiteral("a"), Ty::StringLiteral("a"))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(3))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(3), Ty::IntLiteral(4)]))]
#[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(1)], neg: vec![]}, Ty::IntLiteral(2))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(1)]), Ty::Tuple(vec![Ty::IntLiteral(2)]))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1)]))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(3)]))]
fn is_disjoint_from(a: Ty, b: Ty) {
let db = setup_db();
let a = a.into_type(&db);
let b = b.into_type(&db);
assert!(a.is_disjoint_from(&db, b));
assert!(b.is_disjoint_from(&db, a));
}
#[test_case(Ty::Any, Ty::BuiltinInstance("int"))]
#[test_case(Ty::None, Ty::None)]
#[test_case(Ty::None, Ty::BuiltinInstance("object"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("int"))]
#[test_case(Ty::BuiltinInstance("str"), Ty::LiteralString)]
#[test_case(Ty::BooleanLiteral(true), Ty::BooleanLiteral(true))]
#[test_case(Ty::BooleanLiteral(false), Ty::BooleanLiteral(false))]
#[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("bool"))]
#[test_case(Ty::BooleanLiteral(true), Ty::BuiltinInstance("int"))]
#[test_case(Ty::IntLiteral(1), Ty::IntLiteral(1))]
#[test_case(Ty::StringLiteral("a"), Ty::StringLiteral("a"))]
#[test_case(Ty::StringLiteral("a"), Ty::LiteralString)]
#[test_case(Ty::StringLiteral("a"), Ty::BuiltinInstance("str"))]
#[test_case(Ty::LiteralString, Ty::LiteralString)]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::IntLiteral(2))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(2), Ty::IntLiteral(3)]))]
#[test_case(Ty::Intersection{pos: vec![Ty::BuiltinInstance("int"), Ty::IntLiteral(2)], neg: vec![]}, Ty::IntLiteral(2))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::IntLiteral(1), Ty::BuiltinInstance("int")]))]
fn is_not_disjoint_from(a: Ty, b: Ty) {
let db = setup_db();
let a = a.into_type(&db);
let b = b.into_type(&db);
assert!(!a.is_disjoint_from(&db, b));
assert!(!b.is_disjoint_from(&db, a));
}
#[test]
fn is_disjoint_from_union_of_class_types() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
"
class A: ...
class B: ...
U = A if flag else B
",
)
.unwrap();
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
let type_a = super::global_symbol_ty(&db, module, "A");
let type_u = super::global_symbol_ty(&db, module, "U");
assert!(type_a.is_class_literal());
assert!(type_u.is_union());
assert!(!type_a.is_disjoint_from(&db, type_u));
}
#[test_case(Ty::None)]
#[test_case(Ty::BoolLiteral(true))]
#[test_case(Ty::BoolLiteral(false))]
#[test_case(Ty::BooleanLiteral(true))]
#[test_case(Ty::BooleanLiteral(false))]
fn is_singleton(from: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_singleton());
}
#[test_case(Ty::None)]
#[test_case(Ty::BooleanLiteral(true))]
#[test_case(Ty::IntLiteral(1))]
#[test_case(Ty::StringLiteral("abc"))]
#[test_case(Ty::BytesLiteral("abc"))]
#[test_case(Ty::Tuple(vec![]))]
#[test_case(Ty::Tuple(vec![Ty::BooleanLiteral(true), Ty::IntLiteral(1)]))]
fn is_single_valued(from: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_single_valued(&db));
}
#[test_case(Ty::Never)]
#[test_case(Ty::Any)]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
#[test_case(Ty::Tuple(vec![Ty::None, Ty::BuiltinInstance("int")]))]
#[test_case(Ty::BuiltinInstance("str"))]
#[test_case(Ty::LiteralString)]
fn is_not_single_valued(from: Ty) {
let db = setup_db();
assert!(!from.into_type(&db).is_single_valued(&db));
}
#[test_case(Ty::Never)]
#[test_case(Ty::IntLiteral(345))]
#[test_case(Ty::BuiltinInstance("str"))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
#[test_case(Ty::Tuple(vec![]))]
#[test_case(Ty::Tuple(vec![Ty::None]))]
#[test_case(Ty::Tuple(vec![Ty::None, Ty::BoolLiteral(true)]))]
#[test_case(Ty::Tuple(vec![Ty::None, Ty::BooleanLiteral(true)]))]
fn is_not_singleton(from: Ty) {
let db = setup_db();
@@ -1738,8 +2162,8 @@ mod tests {
}
#[test_case(Ty::IntLiteral(1), Ty::StringLiteral("1"))]
#[test_case(Ty::BoolLiteral(true), Ty::StringLiteral("True"))]
#[test_case(Ty::BoolLiteral(false), Ty::StringLiteral("False"))]
#[test_case(Ty::BooleanLiteral(true), Ty::StringLiteral("True"))]
#[test_case(Ty::BooleanLiteral(false), Ty::StringLiteral("False"))]
#[test_case(Ty::StringLiteral("ab'cd"), Ty::StringLiteral("ab'cd"))] // no quotes
#[test_case(Ty::LiteralString, Ty::LiteralString)]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]
@@ -1750,8 +2174,8 @@ mod tests {
}
#[test_case(Ty::IntLiteral(1), Ty::StringLiteral("1"))]
#[test_case(Ty::BoolLiteral(true), Ty::StringLiteral("True"))]
#[test_case(Ty::BoolLiteral(false), Ty::StringLiteral("False"))]
#[test_case(Ty::BooleanLiteral(true), Ty::StringLiteral("True"))]
#[test_case(Ty::BooleanLiteral(false), Ty::StringLiteral("False"))]
#[test_case(Ty::StringLiteral("ab'cd"), Ty::StringLiteral("'ab\\'cd'"))] // single quotes
#[test_case(Ty::LiteralString, Ty::LiteralString)]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]

View File

@@ -216,76 +216,145 @@ impl<'db> InnerIntersectionBuilder<'db> {
}
/// Adds a positive type to this intersection.
fn add_positive(&mut self, db: &'db dyn Db, ty: Type<'db>) {
// TODO `Any`/`Unknown`/`Todo` actually should not self-cancel
match ty {
Type::Intersection(inter) => {
let pos = inter.positive(db);
let neg = inter.negative(db);
self.positive.extend(pos.difference(&self.negative));
self.negative.extend(neg.difference(&self.positive));
self.positive.retain(|elem| !neg.contains(elem));
self.negative.retain(|elem| !pos.contains(elem));
fn add_positive(&mut self, db: &'db dyn Db, new_positive: Type<'db>) {
if let Type::Intersection(other) = new_positive {
for pos in other.positive(db) {
self.add_positive(db, *pos);
}
_ => {
if !self.negative.remove(&ty) {
self.positive.insert(ty);
};
for neg in other.negative(db) {
self.add_negative(db, *neg);
}
} else {
// ~Literal[True] & bool = Literal[False]
if let Type::Instance(class_type) = new_positive {
if class_type.is_known(db, KnownClass::Bool) {
if let Some(&Type::BooleanLiteral(value)) = self
.negative
.iter()
.find(|element| element.is_boolean_literal())
{
*self = Self::new();
self.positive.insert(Type::BooleanLiteral(!value));
return;
}
}
}
let mut to_remove = SmallVec::<[usize; 1]>::new();
for (index, existing_positive) in self.positive.iter().enumerate() {
// S & T = S if S <: T
if existing_positive.is_subtype_of(db, new_positive) {
return;
}
// same rule, reverse order
if new_positive.is_subtype_of(db, *existing_positive) {
to_remove.push(index);
}
// A & B = Never if A and B are disjoint
if new_positive.is_disjoint_from(db, *existing_positive) {
*self = Self::new();
self.positive.insert(Type::Never);
return;
}
}
for index in to_remove.iter().rev() {
self.positive.swap_remove_index(*index);
}
let mut to_remove = SmallVec::<[usize; 1]>::new();
for (index, existing_negative) in self.negative.iter().enumerate() {
// S & ~T = Never if S <: T
if new_positive.is_subtype_of(db, *existing_negative) {
*self = Self::new();
self.positive.insert(Type::Never);
return;
}
// A & ~B = A if A and B are disjoint
if existing_negative.is_disjoint_from(db, new_positive) {
to_remove.push(index);
}
}
for index in to_remove.iter().rev() {
self.negative.swap_remove_index(*index);
}
self.positive.insert(new_positive);
}
}
/// Adds a negative type to this intersection.
fn add_negative(&mut self, db: &'db dyn Db, ty: Type<'db>) {
// TODO `Any`/`Unknown`/`Todo` actually should not self-cancel
match ty {
Type::Intersection(intersection) => {
let pos = intersection.negative(db);
let neg = intersection.positive(db);
self.positive.extend(pos.difference(&self.negative));
self.negative.extend(neg.difference(&self.positive));
self.positive.retain(|elem| !neg.contains(elem));
self.negative.retain(|elem| !pos.contains(elem));
fn add_negative(&mut self, db: &'db dyn Db, new_negative: Type<'db>) {
match new_negative {
Type::Intersection(inter) => {
for pos in inter.positive(db) {
self.add_negative(db, *pos);
}
for neg in inter.negative(db) {
self.add_positive(db, *neg);
}
}
Type::Never => {}
Type::Unbound => {}
ty @ (Type::Any | Type::Unknown | Type::Todo) => {
// Adding any of these types to the negative side of an intersection
// is equivalent to adding it to the positive side. We do this to
// simplify the representation.
self.positive.insert(ty);
}
// ~Literal[True] & bool = Literal[False]
Type::BooleanLiteral(bool)
if self
.positive
.iter()
.any(|pos| *pos == KnownClass::Bool.to_instance(db)) =>
{
*self = Self::new();
self.positive.insert(Type::BooleanLiteral(!bool));
}
_ => {
if !self.positive.remove(&ty) {
self.negative.insert(ty);
};
let mut to_remove = SmallVec::<[usize; 1]>::new();
for (index, existing_negative) in self.negative.iter().enumerate() {
// ~S & ~T = ~T if S <: T
if existing_negative.is_subtype_of(db, new_negative) {
to_remove.push(index);
}
// same rule, reverse order
if new_negative.is_subtype_of(db, *existing_negative) {
return;
}
}
for index in to_remove.iter().rev() {
self.negative.swap_remove_index(*index);
}
for existing_positive in &self.positive {
// S & ~T = Never if S <: T
if existing_positive.is_subtype_of(db, new_negative) {
*self = Self::new();
self.positive.insert(Type::Never);
return;
}
// A & ~B = A if A and B are disjoint
if existing_positive.is_disjoint_from(db, new_negative) {
return;
}
}
self.negative.insert(new_negative);
}
}
}
fn simplify(&mut self) {
// TODO this should be generalized based on subtyping, for now we just handle a few cases
// Never is a subtype of all types
if self.positive.contains(&Type::Never) {
self.positive.retain(Type::is_never);
self.negative.clear();
}
fn simplify_unbound(&mut self) {
if self.positive.contains(&Type::Unbound) {
self.positive.retain(Type::is_unbound);
self.negative.clear();
}
// None intersects only with object
for pos in &self.positive {
if let Type::Instance(_) = pos {
// could be `object` type
} else {
self.negative.remove(&Type::None);
break;
}
}
}
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
self.simplify();
self.simplify_unbound();
match (self.positive.len(), self.negative.len()) {
(0, 0) => Type::Never,
(0, 0) => KnownClass::Object.to_instance(db),
(1, 0) => self.positive[0],
_ => {
self.positive.shrink_to_fit();
@@ -302,9 +371,10 @@ mod tests {
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::types::{KnownClass, UnionBuilder};
use crate::types::{KnownClass, StringLiteralType, UnionBuilder};
use crate::ProgramSettings;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use test_case::test_case;
fn setup_db() -> TestDb {
let db = TestDb::new();
@@ -456,6 +526,15 @@ mod tests {
assert_eq!(intersection.neg_vec(&db), &[t0]);
}
#[test]
fn build_intersection_empty_intersection_equals_object() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db).build();
assert_eq!(ty, KnownClass::Object.to_instance(&db));
}
#[test]
fn build_intersection_flatten_positive() {
let db = setup_db();
@@ -473,7 +552,7 @@ mod tests {
.expect_intersection();
assert_eq!(intersection.pos_vec(&db), &[t2, ta]);
assert_eq!(intersection.neg_vec(&db), &[t1]);
assert_eq!(intersection.neg_vec(&db), &[]);
}
#[test]
@@ -481,7 +560,7 @@ mod tests {
let db = setup_db();
let ta = Type::Any;
let t1 = Type::IntLiteral(1);
let t2 = Type::IntLiteral(2);
let t2 = KnownClass::Int.to_instance(&db);
let i0 = IntersectionBuilder::new(&db)
.add_positive(ta)
.add_negative(t1)
@@ -492,8 +571,8 @@ mod tests {
.build()
.expect_intersection();
assert_eq!(intersection.pos_vec(&db), &[t2, t1]);
assert_eq!(intersection.neg_vec(&db), &[ta]);
assert_eq!(intersection.pos_vec(&db), &[ta, t1]);
assert_eq!(intersection.neg_vec(&db), &[]);
}
#[test]
@@ -574,11 +653,269 @@ mod tests {
#[test]
fn build_intersection_simplify_negative_none() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_negative(Type::None)
.add_positive(Type::IntLiteral(1))
.build();
assert_eq!(ty, Type::IntLiteral(1));
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::IntLiteral(1))
.add_negative(Type::None)
.build();
assert_eq!(ty, Type::IntLiteral(1));
}
#[test]
fn build_intersection_simplify_positive_type_and_positive_subtype() {
let db = setup_db();
let t = KnownClass::Str.to_instance(&db);
let s = Type::LiteralString;
let ty = IntersectionBuilder::new(&db)
.add_positive(t)
.add_positive(s)
.build();
assert_eq!(ty, s);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_positive(t)
.build();
assert_eq!(ty, s);
let literal = Type::StringLiteral(StringLiteralType::new(&db, "a"));
let expected = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(literal)
.build();
let ty = IntersectionBuilder::new(&db)
.add_positive(t)
.add_negative(literal)
.add_positive(s)
.build();
assert_eq!(ty, expected);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(literal)
.add_positive(t)
.build();
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_negative_type_and_negative_subtype() {
let db = setup_db();
let t = KnownClass::Str.to_instance(&db);
let s = Type::LiteralString;
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
let ty = IntersectionBuilder::new(&db)
.add_negative(t)
.add_negative(s)
.build();
assert_eq!(ty, expected);
let ty = IntersectionBuilder::new(&db)
.add_negative(s)
.add_negative(t)
.build();
assert_eq!(ty, expected);
let object = KnownClass::Object.to_instance(&db);
let expected = IntersectionBuilder::new(&db)
.add_negative(t)
.add_positive(object)
.build();
let ty = IntersectionBuilder::new(&db)
.add_negative(t)
.add_positive(object)
.add_negative(s)
.build();
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_negative_type_and_multiple_negative_subtypes() {
let db = setup_db();
let s1 = Type::IntLiteral(1);
let s2 = Type::IntLiteral(2);
let t = KnownClass::Int.to_instance(&db);
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
let ty = IntersectionBuilder::new(&db)
.add_negative(s1)
.add_negative(s2)
.add_negative(t)
.build();
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_negative_type_and_positive_subtype() {
let db = setup_db();
let t = KnownClass::Str.to_instance(&db);
let s = Type::LiteralString;
let ty = IntersectionBuilder::new(&db)
.add_negative(t)
.add_positive(s)
.build();
assert_eq!(ty, Type::Never);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(t)
.build();
assert_eq!(ty, Type::Never);
// This should also work in the presence of additional contributions:
let ty = IntersectionBuilder::new(&db)
.add_positive(KnownClass::Object.to_instance(&db))
.add_negative(t)
.add_positive(s)
.build();
assert_eq!(ty, Type::Never);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(Type::StringLiteral(StringLiteralType::new(&db, "a")))
.add_negative(t)
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_disjoint_positive_types() {
let db = setup_db();
let t1 = Type::IntLiteral(1);
let t2 = Type::None;
let ty = IntersectionBuilder::new(&db)
.add_positive(t1)
.add_positive(t2)
.build();
assert_eq!(ty, Type::Never);
// If there are any negative contributions, they should
// be removed too.
let ty = IntersectionBuilder::new(&db)
.add_positive(KnownClass::Str.to_instance(&db))
.add_negative(Type::LiteralString)
.add_positive(t2)
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_disjoint_positive_and_negative_types() {
let db = setup_db();
let t_p = KnownClass::Int.to_instance(&db);
let t_n = Type::StringLiteral(StringLiteralType::new(&db, "t_n"));
let ty = IntersectionBuilder::new(&db)
.add_positive(t_p)
.add_negative(t_n)
.build();
assert_eq!(ty, t_p);
let ty = IntersectionBuilder::new(&db)
.add_negative(t_n)
.add_positive(t_p)
.build();
assert_eq!(ty, t_p);
let int_literal = Type::IntLiteral(1);
let expected = IntersectionBuilder::new(&db)
.add_positive(t_p)
.add_negative(int_literal)
.build();
let ty = IntersectionBuilder::new(&db)
.add_positive(t_p)
.add_negative(int_literal)
.add_negative(t_n)
.build();
assert_eq!(ty, expected);
let ty = IntersectionBuilder::new(&db)
.add_negative(t_n)
.add_negative(int_literal)
.add_positive(t_p)
.build();
assert_eq!(ty, expected);
}
#[test_case(true)]
#[test_case(false)]
fn build_intersection_simplify_split_bool(bool_value: bool) {
let db = setup_db();
let t_bool = KnownClass::Bool.to_instance(&db);
let t_boolean_literal = Type::BooleanLiteral(bool_value);
// We add t_object in various orders (in first or second position) in
// the tests below to ensure that the boolean simplification eliminates
// everything from the intersection, not just `bool`.
let t_object = KnownClass::Object.to_instance(&db);
let ty = IntersectionBuilder::new(&db)
.add_positive(t_object)
.add_positive(t_bool)
.add_negative(t_boolean_literal)
.build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
let ty = IntersectionBuilder::new(&db)
.add_positive(t_bool)
.add_positive(t_object)
.add_negative(t_boolean_literal)
.build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
let ty = IntersectionBuilder::new(&db)
.add_positive(t_object)
.add_negative(t_boolean_literal)
.add_positive(t_bool)
.build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
let ty = IntersectionBuilder::new(&db)
.add_negative(t_boolean_literal)
.add_positive(t_object)
.add_positive(t_bool)
.build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
}
#[test_case(Type::Any)]
#[test_case(Type::Unknown)]
#[test_case(Type::Todo)]
fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) {
let db = setup_db();
let result = IntersectionBuilder::new(&db)
.add_positive(ty)
.add_negative(ty)
.build();
assert_eq!(result, ty);
let result = IntersectionBuilder::new(&db)
.add_negative(ty)
.add_positive(ty)
.build();
assert_eq!(result, ty);
}
}

View File

@@ -34,8 +34,8 @@ impl Display for DisplayType<'_> {
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::Class(_)
| Type::Function(_)
| Type::ClassLiteral(_)
| Type::FunctionLiteral(_)
) {
write!(f, "Literal[{representation}]")
} else {
@@ -69,13 +69,13 @@ impl Display for DisplayRepresentation<'_> {
// `[Type::Todo]`'s display should be explicit that is not a valid display of
// any other type
Type::Todo => f.write_str("@Todo"),
Type::Module(file) => {
Type::ModuleLiteral(file) => {
write!(f, "<module '{:?}'>", file.path(self.db))
}
// TODO functions and classes should display using a fully qualified name
Type::Class(class) => f.write_str(class.name(self.db)),
Type::ClassLiteral(class) => f.write_str(class.name(self.db)),
Type::Instance(class) => f.write_str(class.name(self.db)),
Type::Function(function) => f.write_str(function.name(self.db)),
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
Type::Union(union) => union.display(self.db).fmt(f),
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
Type::IntLiteral(n) => n.fmt(f),
@@ -119,13 +119,13 @@ impl Display for DisplayUnionType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let elements = self.ty.elements(self.db);
// Group literal types by kind.
let mut grouped_literals = FxHashMap::default();
// Group condensed-display types by kind.
let mut grouped_condensed_kinds = FxHashMap::default();
for element in elements {
if let Ok(literal_kind) = LiteralTypeKind::try_from(*element) {
grouped_literals
.entry(literal_kind)
if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) {
grouped_condensed_kinds
.entry(kind)
.or_insert_with(Vec::new)
.push(*element);
}
@@ -134,15 +134,15 @@ impl Display for DisplayUnionType<'_> {
let mut join = f.join(" | ");
for element in elements {
if let Ok(literal_kind) = LiteralTypeKind::try_from(*element) {
let Some(mut literals) = grouped_literals.remove(&literal_kind) else {
if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) {
let Some(mut condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
continue;
};
if literal_kind == LiteralTypeKind::IntLiteral {
literals.sort_unstable_by_key(|ty| ty.expect_int_literal());
if kind == CondensedDisplayTypeKind::Int {
condensed_kind.sort_unstable_by_key(|ty| ty.expect_int_literal());
}
join.entry(&DisplayLiteralGroup {
literals,
literals: condensed_kind,
db: self.db,
});
} else {
@@ -152,7 +152,7 @@ impl Display for DisplayUnionType<'_> {
join.finish()?;
debug_assert!(grouped_literals.is_empty());
debug_assert!(grouped_condensed_kinds.is_empty());
Ok(())
}
@@ -179,25 +179,31 @@ impl Display for DisplayLiteralGroup<'_> {
}
}
/// Enumeration of literal types that are displayed in a "condensed way" inside `Literal` slices.
///
/// For example, `Literal[1] | Literal[2]` is displayed as `"Literal[1, 2]"`.
/// Not all `Literal` types are displayed using `Literal` slices
/// (e.g. it would be inappropriate to display `LiteralString`
/// as `Literal[LiteralString]`).
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
enum LiteralTypeKind {
enum CondensedDisplayTypeKind {
Class,
Function,
IntLiteral,
StringLiteral,
BytesLiteral,
Int,
String,
Bytes,
}
impl TryFrom<Type<'_>> for LiteralTypeKind {
impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
type Error = ();
fn try_from(value: Type<'_>) -> Result<Self, Self::Error> {
match value {
Type::Class(_) => Ok(Self::Class),
Type::Function(_) => Ok(Self::Function),
Type::IntLiteral(_) => Ok(Self::IntLiteral),
Type::StringLiteral(_) => Ok(Self::StringLiteral),
Type::BytesLiteral(_) => Ok(Self::BytesLiteral),
Type::ClassLiteral(_) => Ok(Self::Class),
Type::FunctionLiteral(_) => Ok(Self::Function),
Type::IntLiteral(_) => Ok(Self::Int),
Type::StringLiteral(_) => Ok(Self::String),
Type::BytesLiteral(_) => Ok(Self::Bytes),
_ => Err(()),
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,11 @@ use crate::semantic_index::definition::Definition;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
use crate::semantic_index::symbol_table;
use crate::types::{infer_expression_types, IntersectionBuilder, Type};
use crate::types::{
infer_expression_types, IntersectionBuilder, KnownFunction, Type, UnionBuilder,
};
use crate::Db;
use itertools::Itertools;
use ruff_python_ast as ast;
use rustc_hash::FxHashMap;
use std::sync::Arc;
@@ -59,6 +62,28 @@ fn all_narrowing_constraints_for_expression<'db>(
NarrowingConstraintsBuilder::new(db, Constraint::Expression(expression)).finish()
}
/// Generate a constraint from the *type* of the second argument of an `isinstance` call.
///
/// Example: for `isinstance(…, str)`, we would infer `Type::ClassLiteral(str)` from the
/// second argument, but we need to generate a `Type::Instance(str)` constraint that can
/// be used to narrow down the type of the first argument.
fn generate_isinstance_constraint<'db>(
db: &'db dyn Db,
classinfo: &Type<'db>,
) -> Option<Type<'db>> {
match classinfo {
Type::ClassLiteral(class) => Some(Type::Instance(*class)),
Type::Tuple(tuple) => {
let mut builder = UnionBuilder::new(db);
for element in tuple.elements(db) {
builder = builder.add(generate_isinstance_constraint(db, element)?);
}
Some(builder.build())
}
_ => None,
}
}
type NarrowingConstraints<'db> = FxHashMap<ScopedSymbolId, Type<'db>>;
struct NarrowingConstraintsBuilder<'db> {
@@ -87,10 +112,15 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
}
fn evaluate_expression_constraint(&mut self, expression: Expression<'db>) {
if let ast::Expr::Compare(expr_compare) = expression.node_ref(self.db).node() {
self.add_expr_compare(expr_compare, expression);
match expression.node_ref(self.db).node() {
ast::Expr::Compare(expr_compare) => {
self.add_expr_compare(expr_compare, expression);
}
ast::Expr::Call(expr_call) => {
self.add_expr_call(expr_call, expression);
}
_ => {} // TODO other test expression kinds
}
// TODO other test expression kinds
}
fn evaluate_pattern_constraint(&mut self, pattern: PatternConstraint<'db>) {
@@ -142,19 +172,27 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
ops,
comparators,
} = expr_compare;
if !left.is_name_expr() && comparators.iter().all(|c| !c.is_name_expr()) {
// If none of the comparators are name expressions,
// we have no symbol to narrow down the type of.
return;
}
let scope = self.scope();
let inference = infer_expression_types(self.db, expression);
if let ast::Expr::Name(ast::ExprName {
range: _,
id,
ctx: _,
}) = left.as_ref()
{
// SAFETY: we should always have a symbol for every Name node.
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let scope = self.scope();
let inference = infer_expression_types(self.db, expression);
for (op, comparator) in std::iter::zip(&**ops, &**comparators) {
let comp_ty = inference.expression_ty(comparator.scoped_ast_id(self.db, scope));
let comparator_tuples = std::iter::once(&**left)
.chain(comparators)
.tuple_windows::<(&ruff_python_ast::Expr, &ruff_python_ast::Expr)>();
for (op, (left, right)) in std::iter::zip(&**ops, comparator_tuples) {
if let ast::Expr::Name(ast::ExprName {
range: _,
id,
ctx: _,
}) = left
{
// SAFETY: we should always have a symbol for every Name node.
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let comp_ty = inference.expression_ty(right.scoped_ast_id(self.db, scope));
match op {
ast::CmpOp::IsNot => {
if comp_ty.is_singleton() {
@@ -169,6 +207,14 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
ast::CmpOp::Is => {
self.constraints.insert(symbol, comp_ty);
}
ast::CmpOp::NotEq => {
if comp_ty.is_single_valued(self.db) {
let ty = IntersectionBuilder::new(self.db)
.add_negative(comp_ty)
.build();
self.constraints.insert(symbol, ty);
}
}
_ => {
// TODO other comparison types
}
@@ -177,6 +223,33 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
}
}
fn add_expr_call(&mut self, expr_call: &ast::ExprCall, expression: Expression<'db>) {
let scope = self.scope();
let inference = infer_expression_types(self.db, expression);
if let Some(func_type) = inference
.expression_ty(expr_call.func.scoped_ast_id(self.db, scope))
.into_function_literal_type()
{
if func_type.is_known(self.db, KnownFunction::IsInstance)
&& expr_call.arguments.keywords.is_empty()
{
if let [ast::Expr::Name(ast::ExprName { id, .. }), rhs] = &*expr_call.arguments.args
{
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let rhs_type = inference.expression_ty(rhs.scoped_ast_id(self.db, scope));
// TODO: add support for PEP 604 union types on the right hand side:
// isinstance(x, str | (int | float))
if let Some(constraint) = generate_isinstance_constraint(self.db, &rhs_type) {
self.constraints.insert(symbol, constraint);
}
}
}
}
}
fn add_match_pattern_singleton(
&mut self,
subject: &ast::Expr,

View File

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

View File

@@ -0,0 +1,83 @@
pub(crate) trait PythonSubscript {
type Item;
fn python_subscript(&mut self, index: i64) -> Option<Self::Item>;
}
impl<I, T: DoubleEndedIterator<Item = I>> PythonSubscript for T {
type Item = I;
fn python_subscript(&mut self, index: i64) -> Option<I> {
if index >= 0 {
self.nth(usize::try_from(index).ok()?)
} else {
let nth_rev = usize::try_from(index.checked_neg()?).ok()?.checked_sub(1)?;
self.rev().nth(nth_rev)
}
}
}
#[cfg(test)]
#[allow(clippy::redundant_clone)]
mod tests {
use super::PythonSubscript;
#[test]
fn python_subscript_basic() {
let iter = 'a'..='e';
assert_eq!(iter.clone().python_subscript(0), Some('a'));
assert_eq!(iter.clone().python_subscript(1), Some('b'));
assert_eq!(iter.clone().python_subscript(4), Some('e'));
assert_eq!(iter.clone().python_subscript(5), None);
assert_eq!(iter.clone().python_subscript(-1), Some('e'));
assert_eq!(iter.clone().python_subscript(-2), Some('d'));
assert_eq!(iter.clone().python_subscript(-5), Some('a'));
assert_eq!(iter.clone().python_subscript(-6), None);
}
#[test]
fn python_subscript_empty() {
let iter = 'a'..'a';
assert_eq!(iter.clone().python_subscript(0), None);
assert_eq!(iter.clone().python_subscript(1), None);
assert_eq!(iter.clone().python_subscript(-1), None);
}
#[test]
fn python_subscript_single_element() {
let iter = 'a'..='a';
assert_eq!(iter.clone().python_subscript(0), Some('a'));
assert_eq!(iter.clone().python_subscript(1), None);
assert_eq!(iter.clone().python_subscript(-1), Some('a'));
assert_eq!(iter.clone().python_subscript(-2), None);
}
#[test]
fn python_subscript_uses_full_index_range() {
let iter = 0..=u64::MAX;
assert_eq!(iter.clone().python_subscript(0), Some(0));
assert_eq!(iter.clone().python_subscript(1), Some(1));
assert_eq!(
iter.clone().python_subscript(i64::MAX),
Some(i64::MAX as u64)
);
assert_eq!(iter.clone().python_subscript(-1), Some(u64::MAX));
assert_eq!(iter.clone().python_subscript(-2), Some(u64::MAX - 1));
// i64::MIN is not representable as a positive number, so it is not
// a valid index:
assert_eq!(iter.clone().python_subscript(i64::MIN), None);
// but i64::MIN +1 is:
assert_eq!(
iter.clone().python_subscript(i64::MIN + 1),
Some(2u64.pow(63) + 1)
);
}
}

View File

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

View File

@@ -1,6 +1,8 @@
//! Scheduling, I/O, and API endpoints.
use std::num::NonZeroUsize;
// The new PanicInfoHook name requires MSRV >= 1.82
#[allow(deprecated)]
use std::panic::PanicInfo;
use lsp_server::Message;
@@ -119,6 +121,8 @@ impl Server {
}
pub(crate) fn run(self) -> crate::Result<()> {
// The new PanicInfoHook name requires MSRV >= 1.82
#[allow(deprecated)]
type PanicHook = Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send>;
struct RestorePanicHook {
hook: Option<PanicHook>,

View File

@@ -37,7 +37,7 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
params.cell_text_documents,
)
.with_failure_code(ErrorCode::InternalError)?;
session.open_notebook_document(params.notebook_document.uri.clone(), notebook);
session.open_notebook_document(params.notebook_document.uri, notebook);
match path {
AnySystemPath::System(path) => {

View File

@@ -21,7 +21,7 @@ ruff_text_size = { workspace = true }
anyhow = { workspace = true }
colored = { workspace = true }
once_cell = { workspace = true }
memchr = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }

View File

@@ -34,14 +34,14 @@ syntax, it's just how this README embeds an example mdtest Markdown document.)
See actual example mdtest suites in
[`crates/red_knot_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/resources/mdtest).
> Note: If you use `rstest` to generate a separate test for all Markdown files in a certain directory,
> Note: If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory,
> as with the example in `crates/red_knot_python_semantic/tests/mdtest.rs`,
> you will likely want to also make sure that the crate the tests are in is rebuilt every time a
> Markdown file is added or removed from the directory. See
> [`crates/red_knot_python_semantic/build.rs`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/build.rs)
> for an example of how to do this.
>
> This is because `rstest` generates its tests at build time rather than at runtime.
> This is because these macros generate their tests at build time rather than at runtime.
> Without the `build.rs` file to force a rebuild when a Markdown file is added or removed,
> a new Markdown test suite might not be run unless some other change in the crate caused a rebuild
> following the addition of the new test file.

View File

@@ -35,7 +35,6 @@
//! ```
use crate::db::Db;
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
@@ -45,6 +44,7 @@ use ruff_source_file::{LineIndex, Locator, OneIndexed};
use ruff_text_size::{Ranged, TextRange};
use smallvec::SmallVec;
use std::ops::Deref;
use std::sync::LazyLock;
/// Diagnostic assertion comments in a single embedded file.
#[derive(Debug)]
@@ -76,7 +76,7 @@ impl InlineFileAssertions {
}
fn is_own_line_comment(&self, ranged_assertion: &AssertionWithRange) -> bool {
CommentRanges::is_own_line(ranged_assertion.start(), &self.locator())
CommentRanges::is_own_line(ranged_assertion.start(), self.source.as_str())
}
}
@@ -239,10 +239,10 @@ impl<'a> Deref for LineAssertions<'a> {
}
}
static TYPE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^#\s*revealed:\s*(?<ty_display>.+?)\s*$").unwrap());
static TYPE_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^#\s*revealed:\s*(?<ty_display>.+?)\s*$").unwrap());
static ERROR_RE: Lazy<Regex> = Lazy::new(|| {
static ERROR_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#"^#\s*error:(\s*(?<column>\d+))?(\s*\[(?<rule>.+?)\])?(\s*"(?<message>.+?)")?\s*$"#,
)

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