Compare commits

...

54 Commits

Author SHA1 Message Date
Douglas Creager
81cfa8ea0a tmp arrows 2025-08-05 15:49:14 -04:00
Douglas Creager
6f957475ef distributed union/intersection 2025-08-05 14:45:57 -04:00
Douglas Creager
8235062718 use salsa to memoize 2025-08-05 14:39:15 -04:00
Douglas Creager
b8b4a192c7 explicit return from each arm 2025-08-05 14:28:07 -04:00
Douglas Creager
4dc2ca50ca clippy 2025-08-05 14:27:58 -04:00
Douglas Creager
78e1df037c intersection of atomic types 2025-08-05 14:22:14 -04:00
Douglas Creager
5e163e2a5e into helper 2025-08-05 14:22:00 -04:00
Douglas Creager
530aa2429b norm for atomic types 2025-08-05 09:55:13 -04:00
Douglas Creager
d0719e127b starting on the norm function 2025-08-05 08:37:57 -04:00
Douglas Creager
ce3d84ba1b cap and cup of sets of sets 2025-08-05 08:37:57 -04:00
Douglas Creager
8762a3a868 mention the types from the paper 2025-08-05 08:37:57 -04:00
Douglas Creager
824a5d7d3b adding stuff 2025-08-05 08:37:57 -04:00
Douglas Creager
e2b8b36b7a constraints and sets and sets 2025-08-05 08:37:56 -04:00
David Peter
94947cbf65 [ty] Fix merge base calculation for typing-conformance workflow (#19761)
## Summary

Use `$GITHUB_SHA` (the merged state of `feature` + `main` branch)
instead of `{{ github.event.pull_request.head.sha }}` (just the latest
`feature` commit) for building the "new" version of `ty` in the typing
conformance workflow.

## Test Plan

None.
2025-08-05 14:32:47 +02:00
Alex Waygood
7dccb6a98c [ty] Improve effectiveness of KnownClass fast paths in instance.rs (#19762) 2025-08-05 13:26:14 +01:00
David Peter
948f3f856c [ty] Fix attribute access on TypedDicts (#19758)
## Summary

This PR fixes a few inaccuracies in attribute access on `TypedDict`s. It
also changes the return type of `type(person)` to `type[dict[str,
object]]` if `person: Person` is an inhabitant of a `TypedDict`
`Person`. We still use `type[Person]` as the *meta type* of Person,
however (see reasoning
[here](https://github.com/astral-sh/ruff/pull/19733#discussion_r2253297926)).

## Test Plan

Updated Markdown tests.
2025-08-05 13:59:10 +02:00
Alex Waygood
3af0b31de3 [ty] Speedup the known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version test (#19760) 2025-08-05 11:55:11 +00:00
David Peter
7df7be5c7d [ty] Keep track of type qualifiers in stub declarations without right-hand side (#19756)
## Summary

closes https://github.com/astral-sh/ty/issues/937

## Test Plan

Regression test
2025-08-05 12:07:05 +02:00
Dhruv Manilawala
2d2841e20d [ty] Fix typing repository commit output in CI (#19754)
## Summary

This PR fixes the issue mentioned in
https://github.com/astral-sh/ruff/pull/19736#issuecomment-3151903662
~~but I can't test it without merging it on `main` because GitHub
Actions still pickup the old version of the workflow file.~~ and is
tested by manually triggering the workflow, refer to the comment on this
PR
(https://github.com/astral-sh/ruff/pull/19754#issuecomment-3153894179)
which has the commit hash.
2025-08-05 14:53:55 +05:30
David Peter
14fbc2b167 [ty] New Type variant for TypedDict (#19733)
## Summary

This PR adds a new `Type::TypedDict` variant. Before this PR, we treated
`TypedDict`-based types as dynamic Todo-types, and I originally planned
to make this change a no-op. And we do in fact still treat that new
variant similar to a dynamic type when it comes to type properties such
as assignability and subtyping. But then I somehow tricked myself into
implementing some of the things correctly, so here we are. The two main
behavioral changes are: (1) we now also detect generic `TypedDict`s,
which removes a few false positives in the ecosystem, and (2) we now
support *attribute* access (not key-based indexing!) on these types,
i.e. we infer proper types for something like
`MyTypedDict.__required_keys__`. Nothing exciting yet, but gets the
infrastructure into place.

Note that with this PR, the type of (the type) `MyTypedDict` itself is
still represented as a `Type::ClassLiteral` or `Type::GenericAlias` (in
case `MyTypedDict` is generic). Only inhabitants of `MyTypedDict`
(instances of `dict` at runtime) are represented by `Type::TypedDict`.
We may want to revisit this decision in the future, if this turns out to
be too error-prone. Right now, we need to use `.is_typed_dict(db)` in
all the right places to distinguish between actual (generic) classes and
`TypedDict`s. But so far, it seemed unnecessary to add additional `Type`
variants for these as well.

part of https://github.com/astral-sh/ty/issues/154

## Ecosystem impact

The new diagnostics on `cloud-init` look like true positives to me.

## Test Plan

Updated and new Markdown tests
2025-08-05 11:19:49 +02:00
Shunsuke Shibayama
351121c5c5 [ty] fix incorrect lazy scope narrowing (#19744)
## Summary

This is a follow-up to #19321.

Narrowing constraints introduced in a class scope were not applied even
when they can be applied in lazy nested scopes. This PR fixes so that
they are now applied.
Conversely, there were cases where narrowing constraints were being
applied in places where they should not, so it is also fixed.

## Test Plan

Some TODOs in `narrow/conditionals/nested.md` are now work correctly.
2025-08-04 20:32:08 -07:00
Shunsuke Shibayama
64bcc8db2f [ty] fix lookup order of class variables before they are defined (#19743)
## Summary

This is a follow-up to #19321.

If we try to access a class variable before it is defined, the variable
is looked up in the global scope, rather than in any enclosing scopes.

Closes https://github.com/astral-sh/ty/issues/875.

## Test Plan

New tests in `narrow/conditionals/nested.md`.
2025-08-04 20:21:28 -07:00
Roman Kitaev
b0f01ba514 [flake8-blind-except] Change BLE001 to correctly parse exception tuples (#19747)
## Summary

This PR enhances the `BLE001` rule to correctly detect blind exception
handling in tuple exceptions. Previously, the rule only checked single
exception types, but Python allows catching multiple exceptions using
tuples like `except (Exception, ValueError):`.

## Test Plan

It fails the following (whereas the main branch does not):

```bash
cargo run -p ruff -- check somefile.py --no-cache --select=BLE001
```

```python
# somefile.py

try:
    1/0
except (ValueError, Exception) as e:
    print(e)
```

```
somefile.py:3:21: BLE001 Do not catch blind exception: `Exception`
  |
1 | try:
2 |     1/0
3 | except (ValueError, Exception) as e:
  |                     ^^^^^^^^^ BLE001
4 |     print(e)
  |

Found 1 error.
```
2025-08-04 21:12:45 +00:00
Alex Waygood
3a9341f7be [ty] Remove false positives when subscripting Generic or Protocol with a ParamSpec or TypeVarTuple (#19749) 2025-08-04 21:42:46 +01:00
David Peter
739c94f95a [ty] Support as-patterns in reachability analysis (#19728)
## Summary

Support `as` patterns in reachability analysis:

```py
from typing import assert_never


def f(subject: str | int):
    match subject:
        case int() as x:
            pass
        case str():
            pass
        case _:
            assert_never(subject)  # would previously emit an error
```

Note that we still don't support inferring correct types for the bound
name (`x`).

Closes https://github.com/astral-sh/ty/issues/928

## Test Plan

New Markdown tests
2025-08-04 20:13:50 +02:00
Alex Waygood
af8587eabf [ty] Link directly to typing conformance test suite when commenting the diff (#19736) 2025-08-04 15:51:42 +01:00
Alex Waygood
41207ec901 [ty] Infer type[tuple[int, str]] as the meta-type of tuple[int, str] (#19741) 2025-08-04 13:10:47 +00:00
Alex Waygood
bc6e8b58ce [ty] Return Option<TupleType> from infer_tuple_type_expression (#19735)
## Summary

This PR reduces the virality of some of the `Todo` types in
`infer_tuple_type_expression`. Rather than inferring `Todo`, we instead
infer `tuple[Todo, ...]`. This reflects the fact that whatever the
contents of the slice in a `tuple[]` type expression, we would always
infer some kind of tuple type as the result of the type expression. Any
tuple type should be assignable to `tuple[Todo, ...]`, so this shouldn't
introduce any new false positives; this can be seen in the ecosystem
report.

As a result of the change, we are now able to enforce in the signature
of `Type::infer_tuple_type_expression` that it returns an
`Option<TupleType<'db>>`, which is more strongly typed and expresses
clearly the invariant that a tuple type expression should always be
inferred as a `tuple` type. To enable this, it was necessary to refactor
several `TupleType` constructors in `tuple.rs` so that they return
`Option<TupleType>` rather than `Type`; this means that callers of these
constructor functions are now free to either propagate the
`Option<TupleType<'db>>` or convert it to a `Type<'db>`.

## Test Plan

Mdtests updated.
2025-08-04 13:48:19 +01:00
Micha Reiser
e4d6b54a16 [ty] Fix failing test on windows (#19742) 2025-08-04 14:39:36 +02:00
Micha Reiser
17ee2a28ba [ty] Fix workspace diagnostics being recomputed (#19689) 2025-08-04 13:49:38 +02:00
Leandro Braga
de77b29798 [ty] clear the terminal screen in watch mode (#19712)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-08-04 13:45:37 +02:00
Micha Reiser
f473f6b6e5 [ty] Implement long-polling for workspace diagnsotics (#19670) 2025-08-04 10:26:38 +00:00
Alex Waygood
736c4ab05a Remove myself as a codeowner from some ty crates (#19738) 2025-08-04 10:23:13 +00:00
Micha Reiser
8289432252 [ty] Always refresh diagnostics after a watched files change (#19697) 2025-08-04 12:19:18 +02:00
Micha Reiser
808c94d509 [ty] Implement streaming for workspace diagnostics (#19657) 2025-08-04 09:34:29 +00:00
Micha Reiser
b95d22c08e Don't flag pyrefly pragmas as unused code (ERA001) (#19731) 2025-08-04 10:15:37 +02:00
Micha Reiser
f3e66dd503 Revert "Update NPM Development dependencies" (#19730) 2025-08-04 07:33:58 +00:00
Micha Reiser
6516db7835 [ty] Add progress bar to watch (#19729) 2025-08-04 09:31:13 +02:00
renovate[bot]
03c873765e Update NPM Development dependencies (#19723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-08-04 07:03:55 +00:00
renovate[bot]
c90707875e Update react monorepo to v19.1.1 (#19720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 06:27:11 +00:00
renovate[bot]
8e20e589f1 Update dependency react-resizable-panels to v3.0.4 (#19717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 08:21:56 +02:00
renovate[bot]
113e32b956 Update docker/metadata-action action to v5.8.0 (#19722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 08:21:37 +02:00
renovate[bot]
fdc18eefc3 Update Swatinem/rust-cache action to v2.8.0 (#19725)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [Swatinem/rust-cache](https://redirect.github.com/Swatinem/rust-cache)
| action | minor | `v2.7.8` -> `v2.8.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>Swatinem/rust-cache (Swatinem/rust-cache)</summary>

###
[`v2.8.0`](https://redirect.github.com/Swatinem/rust-cache/releases/tag/v2.8.0)

[Compare
Source](https://redirect.github.com/Swatinem/rust-cache/compare/v2.7.8...v2.8.0)

#### What's Changed

- Add cache-workspace-crates feature by
[@&#8203;jbransen](https://redirect.github.com/jbransen) in
[https://github.com/Swatinem/rust-cache/pull/246](https://redirect.github.com/Swatinem/rust-cache/pull/246)
- Feat: support warpbuild cache provider by
[@&#8203;stegaBOB](https://redirect.github.com/stegaBOB) in
[https://github.com/Swatinem/rust-cache/pull/247](https://redirect.github.com/Swatinem/rust-cache/pull/247)

#### New Contributors

- [@&#8203;jbransen](https://redirect.github.com/jbransen) made their
first contribution in
[https://github.com/Swatinem/rust-cache/pull/246](https://redirect.github.com/Swatinem/rust-cache/pull/246)
- [@&#8203;stegaBOB](https://redirect.github.com/stegaBOB) made their
first contribution in
[https://github.com/Swatinem/rust-cache/pull/247](https://redirect.github.com/Swatinem/rust-cache/pull/247)

**Full Changelog**:
https://github.com/Swatinem/rust-cache/compare/v2.7.8...v2.8.0

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS41MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuNTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 08:38:05 +05:30
renovate[bot]
5f40651ae7 Update Rust crate notify to v8.2.0 (#19724)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [notify](https://redirect.github.com/notify-rs/notify) |
workspace.dependencies | minor | `8.1.0` -> `8.2.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>notify-rs/notify (notify)</summary>

###
[`v8.2.0`](https://redirect.github.com/notify-rs/notify/blob/HEAD/CHANGELOG.md#notify-820-2025-08-03)

[Compare
Source](https://redirect.github.com/notify-rs/notify/compare/notify-8.1.0...notify-8.2.0)

- FEATURE: notify user if inotify's `max_user_watches` has been reached
[#&#8203;698]
- FIX: `INotifyWatcher` ignore events with unknown watch descriptors
(instead of `EventMask::Q_OVERFLOW`) [#&#8203;700]

[#&#8203;698]: https://redirect.github.com/notify-rs/notify/pull/698

[#&#8203;700]: https://redirect.github.com/notify-rs/notify/pull/700

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS41MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuNTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 08:37:47 +05:30
renovate[bot]
ea031a3b39 Update taiki-e/install-action action to v2.57.6 (#19726)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[taiki-e/install-action](https://redirect.github.com/taiki-e/install-action)
| action | minor | `v2.56.19` -> `v2.57.6` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>taiki-e/install-action (taiki-e/install-action)</summary>

###
[`v2.57.6`](https://redirect.github.com/taiki-e/install-action/blob/HEAD/CHANGELOG.md#100---2021-12-30)

[Compare
Source](https://redirect.github.com/taiki-e/install-action/compare/v2.57.5...v2.57.6)

Initial release

[Unreleased]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.6...HEAD

[2.57.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.5...v2.57.6

[2.57.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.4...v2.57.5

[2.57.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.3...v2.57.4

[2.57.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.2...v2.57.3

[2.57.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.1...v2.57.2

[2.57.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.0...v2.57.1

[2.57.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.24...v2.57.0

[2.56.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.23...v2.56.24

[2.56.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.22...v2.56.23

[2.56.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.21...v2.56.22

[2.56.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.20...v2.56.21

[2.56.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.19...v2.56.20

[2.56.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.18...v2.56.19

[2.56.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.17...v2.56.18

[2.56.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.16...v2.56.17

[2.56.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.15...v2.56.16

[2.56.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.14...v2.56.15

[2.56.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.13...v2.56.14

[2.56.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.12...v2.56.13

[2.56.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.11...v2.56.12

[2.56.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.10...v2.56.11

[2.56.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.9...v2.56.10

[2.56.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.8...v2.56.9

[2.56.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.7...v2.56.8

[2.56.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.6...v2.56.7

[2.56.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.5...v2.56.6

[2.56.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.4...v2.56.5

[2.56.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.3...v2.56.4

[2.56.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.2...v2.56.3

[2.56.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.1...v2.56.2

[2.56.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.56.0...v2.56.1

[2.56.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.55.4...v2.56.0

[2.55.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.55.3...v2.55.4

[2.55.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.55.2...v2.55.3

[2.55.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.55.1...v2.55.2

[2.55.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.55.0...v2.55.1

[2.55.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.54.3...v2.55.0

[2.54.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.54.2...v2.54.3

[2.54.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.54.1...v2.54.2

[2.54.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.54.0...v2.54.1

[2.54.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.53.2...v2.54.0

[2.53.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.53.1...v2.53.2

[2.53.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.53.0...v2.53.1

[2.53.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.8...v2.53.0

[2.52.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.7...v2.52.8

[2.52.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.6...v2.52.7

[2.52.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.5...v2.52.6

[2.52.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.4...v2.52.5

[2.52.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.3...v2.52.4

[2.52.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.2...v2.52.3

[2.52.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.1...v2.52.2

[2.52.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.52.0...v2.52.1

[2.52.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.51.3...v2.52.0

[2.51.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.51.2...v2.51.3

[2.51.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.51.1...v2.51.2

[2.51.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.51.0...v2.51.1

[2.51.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.10...v2.51.0

[2.50.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.9...v2.50.10

[2.50.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.8...v2.50.9

[2.50.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.7...v2.50.8

[2.50.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.6...v2.50.7

[2.50.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.5...v2.50.6

[2.50.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.4...v2.50.5

[2.50.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.3...v2.50.4

[2.50.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.2...v2.50.3

[2.50.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.1...v2.50.2

[2.50.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.50.0...v2.50.1

[2.50.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.50...v2.50.0

[2.49.50]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.49...v2.49.50

[2.49.49]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.48...v2.49.49

[2.49.48]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.47...v2.49.48

[2.49.47]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.46...v2.49.47

[2.49.46]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.45...v2.49.46

[2.49.45]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.44...v2.49.45

[2.49.44]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.43...v2.49.44

[2.49.43]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.42...v2.49.43

[2.49.42]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.41...v2.49.42

[2.49.41]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.40...v2.49.41

[2.49.40]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.39...v2.49.40

[2.49.39]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.38...v2.49.39

[2.49.38]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.37...v2.49.38

[2.49.37]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.36...v2.49.37

[2.49.36]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.35...v2.49.36

[2.49.35]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.34...v2.49.35

[2.49.34]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.33...v2.49.34

[2.49.33]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.32...v2.49.33

[2.49.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.31...v2.49.32

[2.49.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.30...v2.49.31

[2.49.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.29...v2.49.30

[2.49.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.28...v2.49.29

[2.49.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.27...v2.49.28

[2.49.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.26...v2.49.27

[2.49.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.25...v2.49.26

[2.49.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.24...v2.49.25

[2.49.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.23...v2.49.24

[2.49.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.22...v2.49.23

[2.49.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.21...v2.49.22

[2.49.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.20...v2.49.21

[2.49.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.19...v2.49.20

[2.49.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.18...v2.49.19

[2.49.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.17...v2.49.18

[2.49.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.16...v2.49.17

[2.49.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.15...v2.49.16

[2.49.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.14...v2.49.15

[2.49.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.13...v2.49.14

[2.49.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.12...v2.49.13

[2.49.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.11...v2.49.12

[2.49.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.10...v2.49.11

[2.49.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.9...v2.49.10

[2.49.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.8...v2.49.9

[2.49.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.7...v2.49.8

[2.49.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.6...v2.49.7

[2.49.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.5...v2.49.6

[2.49.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.4...v2.49.5

[2.49.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.3...v2.49.4

[2.49.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.2...v2.49.3

[2.49.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.1...v2.49.2

[2.49.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.49.0...v2.49.1

[2.49.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.22...v2.49.0

[2.48.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.21...v2.48.22

[2.48.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.20...v2.48.21

[2.48.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.19...v2.48.20

[2.48.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.18...v2.48.19

[2.48.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.17...v2.48.18

[2.48.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.16...v2.48.17

[2.48.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.15...v2.48.16

[2.48.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.14...v2.48.15

[2.48.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.13...v2.48.14

[2.48.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.12...v2.48.13

[2.48.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.11...v2.48.12

[2.48.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.10...v2.48.11

[2.48.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.9...v2.48.10

[2.48.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.8...v2.48.9

[2.48.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.7...v2.48.8

[2.48.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.6...v2.48.7

[2.48.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.5...v2.48.6

[2.48.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.4...v2.48.5

[2.48.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.3...v2.48.4

[2.48.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.2...v2.48.3

[2.48.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.1...v2.48.2

[2.48.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.48.0...v2.48.1

[2.48.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.32...v2.48.0

[2.47.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.31...v2.47.32

[2.47.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.30...v2.47.31

[2.47.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.29...v2.47.30

[2.47.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.28...v2.47.29

[2.47.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.27...v2.47.28

[2.47.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.26...v2.47.27

[2.47.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.25...v2.47.26

[2.47.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.24...v2.47.25

[2.47.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.23...v2.47.24

[2.47.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.22...v2.47.23

[2.47.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.21...v2.47.22

[2.47.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.20...v2.47.21

[2.47.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.19...v2.47.20

[2.47.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.18...v2.47.19

[2.47.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.17...v2.47.18

[2.47.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.16...v2.47.17

[2.47.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.15...v2.47.16

[2.47.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.14...v2.47.15

[2.47.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.13...v2.47.14

[2.47.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.12...v2.47.13

[2.47.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.11...v2.47.12

[2.47.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.10...v2.47.11

[2.47.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.9...v2.47.10

[2.47.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.8...v2.47.9

[2.47.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.7...v2.47.8

[2.47.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.6...v2.47.7

[2.47.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.5...v2.47.6

[2.47.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.4...v2.47.5

[2.47.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.3...v2.47.4

[2.47.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.2...v2.47.3

[2.47.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.1...v2.47.2

[2.47.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.47.0...v2.47.1

[2.47.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.20...v2.47.0

[2.46.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.19...v2.46.20

[2.46.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.18...v2.46.19

[2.46.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.17...v2.46.18

[2.46.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.16...v2.46.17

[2.46.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.15...v2.46.16

[2.46.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.14...v2.46.15

[2.46.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.13...v2.46.14

[2.46.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.12...v2.46.13

[2.46.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.11...v2.46.12

[2.46.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.10...v2.46.11

[2.46.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.9...v2.46.10

[2.46.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.8...v2.46.9

[2.46.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.7...v2.46.8

[2.46.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.6...v2.46.7

[2.46.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.5...v2.46.6

[2.46.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.4...v2.46.5

[2.46.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.3...v2.46.4

[2.46.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.2...v2.46.3

[2.46.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.1...v2.46.2

[2.46.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.46.0...v2.46.1

[2.46.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.15...v2.46.0

[2.45.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.14...v2.45.15

[2.45.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.13...v2.45.14

[2.45.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.12...v2.45.13

[2.45.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.11...v2.45.12

[2.45.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.10...v2.45.11

[2.45.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.9...v2.45.10

[2.45.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.8...v2.45.9

[2.45.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.7...v2.45.8

[2.45.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.6...v2.45.7

[2.45.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.5...v2.45.6

[2.45.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.4...v2.45.5

[2.45.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.3...v2.45.4

[2.45.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.2...v2.45.3

[2.45.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.1...v2.45.2

[2.45.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.45.0...v2.45.1

[2.45.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.72...v2.45.0

[2.44.72]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.71...v2.44.72

[2.44.71]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.70...v2.44.71

[2.44.70]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.69...v2.44.70

[2.44.69]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.68...v2.44.69

[2.44.68]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.67...v2.44.68

[2.44.67]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.66...v2.44.67

[2.44.66]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.65...v2.44.66

[2.44.65]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.64...v2.44.65

[2.44.64]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.63...v2.44.64

[2.44.63]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.62...v2.44.63

[2.44.62]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.61...v2.44.62

[2.44.61]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.60...v2.44.61

[2.44.60]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.59...v2.44.60

[2.44.59]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.58...v2.44.59

[2.44.58]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.57...v2.44.58

[2.44.57]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.56...v2.44.57

[2.44.56]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.55...v2.44.56

[2.44.55]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.54...v2.44.55

[2.44.54]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.53...v2.44.54

[2.44.53]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.52...v2.44.53

[2.44.52]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.51...v2.44.52

[2.44.51]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.50...v2.44.51

[2.44.50]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.49...v2.44.50

[2.44.49]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.48...v2.44.49

[2.44.48]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.47...v2.44.48

[2.44.47]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.46...v2.44.47

[2.44.46]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.45...v2.44.46

[2.44.45]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.44...v2.44.45

[2.44.44]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.43...v2.44.44

[2.44.43]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.42...v2.44.43

[2.44.42]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.41...v2.44.42

[2.44.41]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.40...v2.44.41

[2.44.40]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.39...v2.44.40

[2.44.39]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.38...v2.44.39

[2.44.38]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.37...v2.44.38

[2.44.37]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.36...v2.44.37

[2.44.36]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.35...v2.44.36

[2.44.35]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.34...v2.44.35

[2.44.34]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.33...v2.44.34

[2.44.33]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.32...v2.44.33

[2.44.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.31...v2.44.32

[2.44.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.30...v2.44.31

[2.44.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.29...v2.44.30

[2.44.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.28...v2.44.29

[2.44.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.27...v2.44.28

[2.44.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.26...v2.44.27

[2.44.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.25...v2.44.26

[2.44.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.24...v2.44.25

[2.44.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.23...v2.44.24

[2.44.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.22...v2.44.23

[2.44.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.21...v2.44.22

[2.44.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.20...v2.44.21

[2.44.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.19...v2.44.20

[2.44.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.18...v2.44.19

[2.44.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.17...v2.44.18

[2.44.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.16...v2.44.17

[2.44.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.15...v2.44.16

[2.44.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.14...v2.44.15

[2.44.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.13...v2.44.14

[2.44.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.12...v2.44.13

[2.44.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.11...v2.44.12

[2.44.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.10...v2.44.11

[2.44.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.9...v2.44.10

[2.44.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.8...v2.44.9

[2.44.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.7...v2.44.8

[2.44.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.6...v2.44.7

[2.44.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.5...v2.44.6

[2.44.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.4...v2.44.5

[2.44.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.3...v2.44.4

[2.44.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.2...v2.44.3

[2.44.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.1...v2.44.2

[2.44.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.44.0...v2.44.1

[2.44.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.7...v2.44.0

[2.43.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.6...v2.43.7

[2.43.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.5...v2.43.6

[2.43.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.4...v2.43.5

[2.43.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.3...v2.43.4

[2.43.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.2...v2.43.3

[2.43.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.1...v2.43.2

[2.43.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.43.0...v2.43.1

[2.43.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.42...v2.43.0

[2.42.42]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.41...v2.42.42

[2.42.41]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.40...v2.42.41

[2.42.40]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.39...v2.42.40

[2.42.39]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.38...v2.42.39

[2.42.38]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.37...v2.42.38

[2.42.37]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.36...v2.42.37

[2.42.36]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.35...v2.42.36

[2.42.35]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.34...v2.42.35

[2.42.34]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.33...v2.42.34

[2.42.33]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.32...v2.42.33

[2.42.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.31...v2.42.32

[2.42.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.30...v2.42.31

[2.42.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.29...v2.42.30

[2.42.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.28...v2.42.29

[2.42.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.27...v2.42.28

[2.42.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.26...v2.42.27

[2.42.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.25...v2.42.26

[2.42.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.24...v2.42.25

[2.42.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.23...v2.42.24

[2.42.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.22...v2.42.23

[2.42.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.21...v2.42.22

[2.42.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.20...v2.42.21

[2.42.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.19...v2.42.20

[2.42.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.18...v2.42.19

[2.42.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.17...v2.42.18

[2.42.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.16...v2.42.17

[2.42.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.15...v2.42.16

[2.42.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.14...v2.42.15

[2.42.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.13...v2.42.14

[2.42.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.12...v2.42.13

[2.42.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.11...v2.42.12

[2.42.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.10...v2.42.11

[2.42.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.9...v2.42.10

[2.42.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.8...v2.42.9

[2.42.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.7...v2.42.8

[2.42.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.6...v2.42.7

[2.42.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.5...v2.42.6

[2.42.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.4...v2.42.5

[2.42.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.3...v2.42.4

[2.42.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.2...v2.42.3

[2.42.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.1...v2.42.2

[2.42.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.42.0...v2.42.1

[2.42.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.18...v2.42.0

[2.41.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.17...v2.41.18

[2.41.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.16...v2.41.17

[2.41.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.15...v2.41.16

[2.41.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.14...v2.41.15

[2.41.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.13...v2.41.14

[2.41.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.12...v2.41.13

[2.41.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.11...v2.41.12

[2.41.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.10...v2.41.11

[2.41.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.9...v2.41.10

[2.41.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.8...v2.41.9

[2.41.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.7...v2.41.8

[2.41.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.6...v2.41.7

[2.41.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.5...v2.41.6

[2.41.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.4...v2.41.5

[2.41.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.3...v2.41.4

[2.41.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.2...v2.41.3

[2.41.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.1...v2.41.2

[2.41.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.41.0...v2.41.1

[2.41.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.40.2...v2.41.0

[2.40.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.40.1...v2.40.2

[2.40.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.40.0...v2.40.1

[2.40.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.39.2...v2.40.0

[2.39.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.39.1...v2.39.2

[2.39.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.39.0...v2.39.1

[2.39.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.7...v2.39.0

[2.38.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.6...v2.38.7

[2.38.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.5...v2.38.6

[2.38.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.4...v2.38.5

[2.38.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.3...v2.38.4

[2.38.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.2...v2.38.3

[2.38.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.1...v2.38.2

[2.38.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.38.0...v2.38.1

[2.38.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.37.0...v2.38.0

[2.37.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.36.0...v2.37.0

[2.36.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.35.0...v2.36.0

[2.35.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.34.3...v2.35.0

[2.34.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.34.2...v2.34.3

[2.34.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.34.1...v2.34.2

[2.34.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.34.0...v2.34.1

[2.34.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.36...v2.34.0

[2.33.36]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.35...v2.33.36

[2.33.35]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.34...v2.33.35

[2.33.34]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.33...v2.33.34

[2.33.33]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.32...v2.33.33

[2.33.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.31...v2.33.32

[2.33.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.30...v2.33.31

[2.33.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.29...v2.33.30

[2.33.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.28...v2.33.29

[2.33.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.27...v2.33.28

[2.33.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.26...v2.33.27

[2.33.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.25...v2.33.26

[2.33.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.24...v2.33.25

[2.33.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.23...v2.33.24

[2.33.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.22...v2.33.23

[2.33.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.21...v2.33.22

[2.33.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.20...v2.33.21

[2.33.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.19...v2.33.20

[2.33.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.18...v2.33.19

[2.33.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.17...v2.33.18

[2.33.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.16...v2.33.17

[2.33.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.15...v2.33.16

[2.33.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.14...v2.33.15

[2.33.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.13...v2.33.14

[2.33.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.12...v2.33.13

[2.33.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.11...v2.33.12

[2.33.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.10...v2.33.11

[2.33.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.9...v2.33.10

[2.33.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.8...v2.33.9

[2.33.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.7...v2.33.8

[2.33.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.6...v2.33.7

[2.33.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.5...v2.33.6

[2.33.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.4...v2.33.5

[2.33.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.3...v2.33.4

[2.33.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.2...v2.33.3

[2.33.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.1...v2.33.2

[2.33.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.33.0...v2.33.1

[2.33.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.20...v2.33.0

[2.32.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.19...v2.32.20

[2.32.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.18...v2.32.19

[2.32.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.17...v2.32.18

[2.32.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.16...v2.32.17

[2.32.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.15...v2.32.16

[2.32.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.14...v2.32.15

[2.32.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.13...v2.32.14

[2.32.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.12...v2.32.13

[2.32.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.11...v2.32.12

[2.32.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.10...v2.32.11

[2.32.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.9...v2.32.10

[2.32.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.8...v2.32.9

[2.32.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.7...v2.32.8

[2.32.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.6...v2.32.7

[2.32.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.5...v2.32.6

[2.32.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.4...v2.32.5

[2.32.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.3...v2.32.4

[2.32.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.2...v2.32.3

[2.32.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.1...v2.32.2

[2.32.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.32.0...v2.32.1

[2.32.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.31.3...v2.32.0

[2.31.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.31.2...v2.31.3

[2.31.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.31.1...v2.31.2

[2.31.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.31.0...v2.31.1

[2.31.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.30.0...v2.31.0

[2.30.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.8...v2.30.0

[2.29.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.7...v2.29.8

[2.29.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.6...v2.29.7

[2.29.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.5...v2.29.6

[2.29.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.4...v2.29.5

[2.29.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.3...v2.29.4

[2.29.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.2...v2.29.3

[2.29.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.1...v2.29.2

[2.29.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.29.0...v2.29.1

[2.29.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.16...v2.29.0

[2.28.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.15...v2.28.16

[2.28.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.14...v2.28.15

[2.28.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.13...v2.28.14

[2.28.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.12...v2.28.13

[2.28.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.11...v2.28.12

[2.28.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.10...v2.28.11

[2.28.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.9...v2.28.10

[2.28.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.8...v2.28.9

[2.28.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.7...v2.28.8

[2.28.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.6...v2.28.7

[2.28.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.5...v2.28.6

[2.28.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.4...v2.28.5

[2.28.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.3...v2.28.4

[2.28.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.2...v2.28.3

[2.28.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.1...v2.28.2

[2.28.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.28.0...v2.28.1

[2.28.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.15...v2.28.0

[2.27.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.14...v2.27.15

[2.27.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.13...v2.27.14

[2.27.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.12...v2.27.13

[2.27.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.11...v2.27.12

[2.27.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.10...v2.27.11

[2.27.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.9...v2.27.10

[2.27.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.8...v2.27.9

[2.27.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.7...v2.27.8

[2.27.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.6...v2.27.7

[2.27.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.5...v2.27.6

[2.27.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.4...v2.27.5

[2.27.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.3...v2.27.4

[2.27.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.2...v2.27.3

[2.27.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.1...v2.27.2

[2.27.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.27.0...v2.27.1

[2.27.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.20...v2.27.0

[2.26.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.19...v2.26.20

[2.26.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.18...v2.26.19

[2.26.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.17...v2.26.18

[2.26.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.16...v2.26.17

[2.26.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.15...v2.26.16

[2.26.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.14...v2.26.15

[2.26.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.13...v2.26.14

[2.26.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.12...v2.26.13

[2.26.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.11...v2.26.12

[2.26.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.10...v2.26.11

[2.26.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.9...v2.26.10

[2.26.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.8...v2.26.9

[2.26.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.7...v2.26.8

[2.26.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.6...v2.26.7

[2.26.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.5...v2.26.6

[2.26.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.4...v2.26.5

[2.26.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.3...v2.26.4

[2.26.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.2...v2.26.3

[2.26.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.1...v2.26.2

[2.26.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.26.0...v2.26.1

[2.26.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.11...v2.26.0

[2.25.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.10...v2.25.11

[2.25.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.9...v2.25.10

[2.25.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.8...v2.25.9

[2.25.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.7...v2.25.8

[2.25.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.6...v2.25.7

[2.25.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.5...v2.25.6

[2.25.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.4...v2.25.5

[2.25.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.3...v2.25.4

[2.25.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.2...v2.25.3

[2.25.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.1...v2.25.2

[2.25.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.25.0...v2.25.1

[2.25.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.24.4...v2.25.0

[2.24.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.24.3...v2.24.4

[2.24.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.24.2...v2.24.3

[2.24.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.24.1...v2.24.2

[2.24.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.24.0...v2.24.1

[2.24.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.9...v2.24.0

[2.23.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.8...v2.23.9

[2.23.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.7...v2.23.8

[2.23.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.6...v2.23.7

[2.23.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.5...v2.23.6

[2.23.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.4...v2.23.5

[2.23.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.3...v2.23.4

[2.23.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.2...v2.23.3

[2.23.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.1...v2.23.2

[2.23.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.23.0...v2.23.1

[2.23.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.10...v2.23.0

[2.22.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.9...v2.22.10

[2.22.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.8...v2.22.9

[2.22.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.7...v2.22.8

[2.22.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.6...v2.22.7

[2.22.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.5...v2.22.6

[2.22.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.4...v2.22.5

[2.22.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.3...v2.22.4

[2.22.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.2...v2.22.3

[2.22.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.1...v2.22.2

[2.22.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.22.0...v2.22.1

[2.22.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.27...v2.22.0

[2.21.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.26...v2.21.27

[2.21.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.25...v2.21.26

[2.21.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.24...v2.21.25

[2.21.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.23...v2.21.24

[2.21.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.22...v2.21.23

[2.21.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.21...v2.21.22

[2.21.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.20...v2.21.21

[2.21.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.19...v2.21.20

[2.21.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.18...v2.21.19

[2.21.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.17...v2.21.18

[2.21.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.16...v2.21.17

[2.21.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.15...v2.21.16

[2.21.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.14...v2.21.15

[2.21.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.13...v2.21.14

[2.21.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.12...v2.21.13

[2.21.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.11...v2.21.12

[2.21.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.10...v2.21.11

[2.21.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.9...v2.21.10

[2.21.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.8...v2.21.9

[2.21.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.7...v2.21.8

[2.21.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.6...v2.21.7

[2.21.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.5...v2.21.6

[2.21.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.4...v2.21.5

[2.21.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.3...v2.21.4

[2.21.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.2...v2.21.3

[2.21.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.1...v2.21.2

[2.21.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.21.0...v2.21.1

[2.21.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.17...v2.21.0

[2.20.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.16...v2.20.17

[2.20.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.15...v2.20.16

[2.20.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.14...v2.20.15

[2.20.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.13...v2.20.14

[2.20.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.12...v2.20.13

[2.20.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.11...v2.20.12

[2.20.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.10...v2.20.11

[2.20.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.9...v2.20.10

[2.20.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.8...v2.20.9

[2.20.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.7...v2.20.8

[2.20.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.6...v2.20.7

[2.20.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.5...v2.20.6

[2.20.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.4...v2.20.5

[2.20.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.3...v2.20.4

[2.20.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.2...v2.20.3

[2.20.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.1...v2.20.2

[2.20.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.20.0...v2.20.1

[2.20.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.19.4...v2.20.0

[2.19.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.19.3...v2.19.4

[2.19.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.19.2...v2.19.3

[2.19.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.19.1...v2.19.2

[2.19.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.19.0...v2.19.1

[2.19.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.17...v2.19.0

[2.18.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.16...v2.18.17

[2.18.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.15...v2.18.16

[2.18.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.14...v2.18.15

[2.18.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.13...v2.18.14

[2.18.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.12...v2.18.13

[2.18.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.11...v2.18.12

[2.18.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.10...v2.18.11

[2.18.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.9...v2.18.10

[2.18.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.8...v2.18.9

[2.18.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.7...v2.18.8

[2.18.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.6...v2.18.7

[2.18.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.5...v2.18.6

[2.18.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.4...v2.18.5

[2.18.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.3...v2.18.4

[2.18.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.2...v2.18.3

[2.18.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.1...v2.18.2

[2.18.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.18.0...v2.18.1

[2.18.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.8...v2.18.0

[2.17.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.7...v2.17.8

[2.17.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.6...v2.17.7

[2.17.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.5...v2.17.6

[2.17.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.4...v2.17.5

[2.17.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.3...v2.17.4

[2.17.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.2...v2.17.3

[2.17.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.1...v2.17.2

[2.17.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.17.0...v2.17.1

[2.17.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.5...v2.17.0

[2.16.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.4...v2.16.5

[2.16.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.3...v2.16.4

[2.16.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.2...v2.16.3

[2.16.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.1...v2.16.2

[2.16.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.16.0...v2.16.1

[2.16.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.6...v2.16.0

[2.15.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.5...v2.15.6

[2.15.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.4...v2.15.5

[2.15.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.3...v2.15.4

[2.15.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.2...v2.15.3

[2.15.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.1...v2.15.2

[2.15.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.15.0...v2.15.1

[2.15.0]: https://redirect.github.com/taiki-e/install-action/compar

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS41MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuNTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 08:37:14 +05:30
renovate[bot]
93b64daa4a Update pre-commit hook astral-sh/ruff-pre-commit to v0.12.7 (#19719)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit)
| repository | patch | `v0.12.5` -> `v0.12.7` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

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>astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit)</summary>

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

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

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

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

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

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

Ruff's 0.12.6 release was yanked. See the linked release notes for more
information.

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS41MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuNTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 08:02:49 +05:30
renovate[bot]
74376375e4 Update dependency ruff to v0.12.7 (#19718)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [ruff](https://docs.astral.sh/ruff)
([source](https://redirect.github.com/astral-sh/ruff),
[changelog](https://redirect.github.com/astral-sh/ruff/blob/main/CHANGELOG.md))
| `==0.12.5` -> `==0.12.7` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.12.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.12.5/0.12.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

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

###
[`v0.12.7`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0127)

This is a follow-up release to 0.12.6. Because of an issue in the
package metadata, 0.12.6 failed to publish fully to PyPI and has been
yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The
contents of the 0.12.7 release are identical to 0.12.6, except for the
updated metadata.

###
[`v0.12.6`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0126)

##### Preview features

- \[`flake8-commas`] Add support for trailing comma checks in type
parameter lists (`COM812`, `COM819`)
([#&#8203;19390](https://redirect.github.com/astral-sh/ruff/pull/19390))
- \[`pylint`] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`)
([#&#8203;19387](https://redirect.github.com/astral-sh/ruff/pull/19387))
- \[`ruff`] Offer fixes for `RUF039` in more cases
([#&#8203;19065](https://redirect.github.com/astral-sh/ruff/pull/19065))

##### Bug fixes

- Support `.pyi` files in ruff analyze graph
([#&#8203;19611](https://redirect.github.com/astral-sh/ruff/pull/19611))
- \[`flake8-pyi`] Preserve inline comment in ellipsis removal (`PYI013`)
([#&#8203;19399](https://redirect.github.com/astral-sh/ruff/pull/19399))
- \[`perflint`] Ignore rule if target is `global` or `nonlocal`
(`PERF401`)
([#&#8203;19539](https://redirect.github.com/astral-sh/ruff/pull/19539))
- \[`pyupgrade`] Fix `UP030` to avoid modifying double curly braces in
format strings
([#&#8203;19378](https://redirect.github.com/astral-sh/ruff/pull/19378))
- \[`refurb`] Ignore decorated functions for `FURB118`
([#&#8203;19339](https://redirect.github.com/astral-sh/ruff/pull/19339))
- \[`refurb`] Mark `int` and `bool` cases for `Decimal.from_float` as
safe fixes (`FURB164`)
([#&#8203;19468](https://redirect.github.com/astral-sh/ruff/pull/19468))
- \[`ruff`] Fix `RUF033` for named default expressions
([#&#8203;19115](https://redirect.github.com/astral-sh/ruff/pull/19115))

##### Rule changes

- \[`flake8-blind-except`] Change `BLE001` to permit
`logging.critical(..., exc_info=True)`
([#&#8203;19520](https://redirect.github.com/astral-sh/ruff/pull/19520))

##### Performance

- Add support for specifying minimum dots in detected string imports
([#&#8203;19538](https://redirect.github.com/astral-sh/ruff/pull/19538))

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS41MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuNTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 08:01:53 +05:30
renovate[bot]
30f52d8cf5 Update cargo-bins/cargo-binstall action to v1.14.3 (#19716)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[cargo-bins/cargo-binstall](https://redirect.github.com/cargo-bins/cargo-binstall)
| action | patch | `v1.14.2` -> `v1.14.3` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>cargo-bins/cargo-binstall (cargo-bins/cargo-binstall)</summary>

###
[`v1.14.3`](https://redirect.github.com/cargo-bins/cargo-binstall/releases/tag/v1.14.3)

[Compare
Source](https://redirect.github.com/cargo-bins/cargo-binstall/compare/v1.14.2...v1.14.3)

*Binstall is a tool to fetch and install Rust-based executables as
binaries. It aims to be a drop-in replacement for `cargo install` in
most cases. Install it today with `cargo install cargo-binstall`, from
the binaries below, or if you already have it, upgrade with `cargo
binstall cargo-binstall`.*

##### In this release:

- Fix race condition in target detections
([#&#8203;2238](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2238))

##### Other changes:

- Upgrade dependencies

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS41MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuNTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 08:01:21 +05:30
renovate[bot]
77bc32b9b9 Update Rust crate serde_json to v1.0.142 (#19721)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.141` -> `1.0.142` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.142`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.142)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.141...v1.0.142)

- impl Default for \&Value
([#&#8203;1265](https://redirect.github.com/serde-rs/json/issues/1265),
thanks [@&#8203;aatifsyed](https://redirect.github.com/aatifsyed))

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS41MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuNTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 08:00:58 +05:30
Dan Parizher
1a368b0bf9 [flake8-simplify] Fix raw string handling in SIM905 for embedded quotes (#19591)
## Summary

When splitting triple-quoted, raw strings one has to take care before attempting to make each item have single-quotes.

Fixes #19577

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-08-03 11:31:28 -05:00
Jérémy Scanvic
134435415e Change 'associative' to 'commutative' in docs describing union (#19706)
Thanks for the great tool!

I noticed a small typo [in the
docs](https://docs.astral.sh/ruff/rules/none-not-at-end-of-union/): it's
[commutativity](Commutative_property) that makes the order not matter in
type unions, not
[associativity](https://en.wikipedia.org/wiki/Associative_property)
which is something different.

I make the change in this PR.
2025-08-03 16:30:56 +00:00
cristian64
bc6e105c18 Include column numbers in GitLab output format. (#19708) 2025-08-03 12:37:01 +00:00
Micha Reiser
6bd413df6c [ty] Update salsa (#19710) 2025-08-03 09:18:10 +00:00
Nathaniel Roman
85bd961fd3 [ty] resolve file symlinks in src walk (#19674)
Co-authored-by: Nathaniel Roman <nroman@openai.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-08-01 22:52:04 +02:00
117 changed files with 4737 additions and 912 deletions

6
.github/CODEOWNERS vendored
View File

@@ -19,6 +19,10 @@
# ty
/crates/ty* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ruff_db/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty_project/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty_server/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty_wasm/ @carljm @MichaReiser @sharkdp @dcreager
/scripts/ty_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ty_python_semantic @carljm @AlexWaygood @sharkdp @dcreager

View File

@@ -63,7 +63,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
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
@@ -123,7 +123,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
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
@@ -219,7 +219,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
# ghcr.io prefers index level annotations
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
@@ -266,7 +266,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
with:

View File

@@ -240,11 +240,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-insta
- name: ty mdtests (GitHub annotations)
@@ -298,11 +298,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-insta
- name: "Run tests"
@@ -325,7 +325,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Run tests"
@@ -429,7 +429,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@808dcb1b503398677d089d3216c51ac7cc11e7ab # v1.14.2
uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -682,7 +682,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@808dcb1b503398677d089d3216c51ac7cc11e7ab # v1.14.2
- uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -903,7 +903,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-codspeed
@@ -936,7 +936,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-codspeed

View File

@@ -83,7 +83,7 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
workspaces: "ruff"

View File

@@ -24,6 +24,7 @@ env:
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
CONFORMANCE_SUITE_COMMIT: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
jobs:
typing_conformance:
@@ -40,13 +41,10 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: python/typing
ref: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
ref: ${{ env.CONFORMANCE_SUITE_COMMIT }}
path: typing
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
workspaces: "ruff"
@@ -64,14 +62,13 @@ jobs:
cd ruff
echo "new commit"
git checkout -b new_commit "${{ github.event.pull_request.head.sha }}"
git rev-list --format=%s --max-count=1 new_commit
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
cargo build --release --bin ty
mv target/release/ty ty-new
echo "old commit (merge base)"
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b old_commit "$MERGE_BASE"
echo "old commit (merge base)"
git rev-list --format=%s --max-count=1 old_commit
cargo build --release --bin ty
mv target/release/ty ty-old
@@ -95,6 +92,7 @@ jobs:
fi
echo ${{ github.event.number }} > pr-number
echo "${CONFORMANCE_SUITE_COMMIT}" > conformance-suite-commit
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
@@ -107,3 +105,9 @@ jobs:
with:
name: pr-number
path: pr-number
- name: Upload conformance suite commit
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: conformance-suite-commit
path: conformance-suite-commit

View File

@@ -32,6 +32,14 @@ jobs:
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download typing conformance suite commit
with:
name: conformance-suite-commit
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
if_no_artifact_found: ignore
allow_forks: true
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: "Download typing_conformance results"
id: download-typing_conformance_diff
@@ -61,7 +69,14 @@ jobs:
# subsequent runs
echo '<!-- generated-comment typing_conformance_diagnostics_diff -->' >> comment.txt
echo '## Diagnostic diff on typing conformance tests' >> comment.txt
if [[ -f conformance-suite-commit ]]
then
echo "## Diagnostic diff on [typing conformance tests](https://github.com/python/typing/tree/$(<conformance-suite-commit)/conformance)" >> comment.txt
else
echo "conformance-suite-commit file not found"
echo "## Diagnostic diff on typing conformance tests" >> comment.txt
fi
if [ -s "pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff" ]; then
echo '<details>' >> comment.txt
echo '<summary>Changes were detected when running ty on typing conformance tests</summary>' >> comment.txt

View File

@@ -81,7 +81,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.5
rev: v0.12.7
hooks:
- id: ruff-format
- id: ruff-check

16
Cargo.lock generated
View File

@@ -2050,9 +2050,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "notify"
version = "8.1.0"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.9.1",
"fsevent-sys",
@@ -3441,7 +3441,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa?rev=f3dc2f30f9a250618161e35600a00de7fe744953#f3dc2f30f9a250618161e35600a00de7fe744953"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
dependencies = [
"boxcar",
"compact_str",
@@ -3454,7 +3454,6 @@ dependencies = [
"inventory",
"parking_lot",
"portable-atomic",
"rayon",
"rustc-hash",
"salsa-macro-rules",
"salsa-macros",
@@ -3466,12 +3465,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa?rev=f3dc2f30f9a250618161e35600a00de7fe744953#f3dc2f30f9a250618161e35600a00de7fe744953"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
[[package]]
name = "salsa-macros"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa?rev=f3dc2f30f9a250618161e35600a00de7fe744953#f3dc2f30f9a250618161e35600a00de7fe744953"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
dependencies = [
"proc-macro2",
"quote",
@@ -3568,9 +3567,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.141"
version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
dependencies = [
"itoa",
"memchr",
@@ -4187,6 +4186,7 @@ dependencies = [
"argfile",
"clap",
"clap_complete_command",
"clearscreen",
"colored 3.0.0",
"crossbeam",
"ctrlc",

View File

@@ -141,7 +141,12 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa", rev = "f3dc2f30f9a250618161e35600a00de7fe744953" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d66fe331d546216132ace503512b94d5c68d2c50", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
"inventory",
] }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }

View File

@@ -22,11 +22,17 @@ exit_code: 1
"description": "`os` imported but unused",
"fingerprint": "4dbad37161e65c72",
"location": {
"lines": {
"begin": 1,
"end": 1
},
"path": "input.py"
"path": "input.py",
"positions": {
"begin": {
"column": 8,
"line": 1
},
"end": {
"column": 10,
"line": 1
}
}
},
"severity": "major"
},
@@ -35,11 +41,17 @@ exit_code: 1
"description": "Undefined name `y`",
"fingerprint": "7af59862a085230",
"location": {
"lines": {
"begin": 2,
"end": 2
},
"path": "input.py"
"path": "input.py",
"positions": {
"begin": {
"column": 5,
"line": 2
},
"end": {
"column": 6,
"line": 2
}
}
},
"severity": "major"
},
@@ -48,11 +60,17 @@ exit_code: 1
"description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"fingerprint": "e558cec859bb66e8",
"location": {
"lines": {
"begin": 3,
"end": 3
},
"path": "input.py"
"path": "input.py",
"positions": {
"begin": {
"column": 1,
"line": 3
},
"end": {
"column": 6,
"line": 3
}
}
},
"severity": "major"
}

View File

@@ -89,3 +89,14 @@ print(1)
# ///
#
# Foobar
# Regression tests for https://github.com/astral-sh/ruff/issues/19713
# mypy: ignore-errors
# pyright: ignore-errors
# pyrefly: ignore-errors
# ty: ignore[unresolved-import]
# pyrefly: ignore[unused-import]
print(1)

View File

@@ -162,3 +162,86 @@ except Exception:
exception("An error occurred")
else:
exception("An error occurred")
# Test tuple exceptions
try:
pass
except (Exception,):
pass
try:
pass
except (Exception, ValueError):
pass
try:
pass
except (ValueError, Exception):
pass
try:
pass
except (ValueError, Exception) as e:
print(e)
try:
pass
except (BaseException, TypeError):
pass
try:
pass
except (TypeError, BaseException):
pass
try:
pass
except (Exception, BaseException):
pass
try:
pass
except (BaseException, Exception):
pass
# Test nested tuples
try:
pass
except ((Exception, ValueError), TypeError):
pass
try:
pass
except (ValueError, (BaseException, TypeError)):
pass
# Test valid tuple exceptions (should not trigger)
try:
pass
except (ValueError, TypeError):
pass
try:
pass
except (OSError, FileNotFoundError):
pass
try:
pass
except (OSError, FileNotFoundError) as e:
print(e)
try:
pass
except (Exception, ValueError):
critical("...", exc_info=True)
try:
pass
except (Exception, ValueError):
raise
try:
pass
except (Exception, ValueError) as e:
raise e

View File

@@ -129,4 +129,35 @@ print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))
# https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
r"""simple@example.com
very.common@example.com
FirstName.LastName@EasierReading.org
x@example.com
long.email-address-with-hyphens@and.subdomains.example.com
user.name+tag+sorting@example.com
name/surname@example.com
xample@s.example
" "@example.org
"john..doe"@example.org
mailhost!username@example.org
"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
user%example.com@example.org
user-@example.org
I❤CHOCOLATE@example.com
this\ still\"not\\allowed@example.com
stellyamburrr985@example.com
Abc.123@example.com
user+mailbox/department=shipping@example.com
!#$%&'*+-/=?^_`.{|}~@example.com
"Abc@def"@example.com
"Fred\ Bloggs"@example.com
"Joe.\\Blow"@example.com""".split("\n")
r"""first
'no need' to escape
"swap" quote style
"use' ugly triple quotes""".split("\n")

View File

@@ -61,22 +61,17 @@ impl Serialize for SerializedMessages<'_> {
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
for diagnostic in self.diagnostics {
let start_location = diagnostic.expect_ruff_start_location();
let end_location = diagnostic.expect_ruff_end_location();
let filename = diagnostic.expect_ruff_filename();
let lines = if self.context.is_notebook(&filename) {
let (start_location, end_location) = if self.context.is_notebook(&filename) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
json!({
"begin": 1,
"end": 1
})
Default::default()
} else {
json!({
"begin": start_location.line,
"end": end_location.line
})
(
diagnostic.expect_ruff_start_location(),
diagnostic.expect_ruff_end_location(),
)
};
let path = self.project_dir.as_ref().map_or_else(
@@ -111,8 +106,11 @@ impl Serialize for SerializedMessages<'_> {
"fingerprint": format!("{:x}", message_fingerprint),
"location": {
"path": path,
"lines": lines
}
"positions": {
"begin": start_location,
"end": end_location,
},
},
});
s.serialize_element(&value)?;

View File

@@ -8,11 +8,17 @@ expression: redact_fingerprint(&content)
"description": "`os` imported but unused",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 1,
"end": 1
},
"path": "fib.py"
"path": "fib.py",
"positions": {
"begin": {
"column": 8,
"line": 1
},
"end": {
"column": 10,
"line": 1
}
}
},
"severity": "major"
},
@@ -21,11 +27,17 @@ expression: redact_fingerprint(&content)
"description": "Local variable `x` is assigned to but never used",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 6,
"end": 6
},
"path": "fib.py"
"path": "fib.py",
"positions": {
"begin": {
"column": 5,
"line": 6
},
"end": {
"column": 6,
"line": 6
}
}
},
"severity": "major"
},
@@ -34,11 +46,17 @@ expression: redact_fingerprint(&content)
"description": "Undefined name `a`",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 1,
"end": 1
},
"path": "undef.py"
"path": "undef.py",
"positions": {
"begin": {
"column": 4,
"line": 1
},
"end": {
"column": 5,
"line": 1
}
}
},
"severity": "major"
}

View File

@@ -8,11 +8,17 @@ expression: redact_fingerprint(&content)
"description": "Expected one or more symbol names after import",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 1,
"end": 2
},
"path": "syntax_errors.py"
"path": "syntax_errors.py",
"positions": {
"begin": {
"column": 15,
"line": 1
},
"end": {
"column": 1,
"line": 2
}
}
},
"severity": "major"
},
@@ -21,11 +27,17 @@ expression: redact_fingerprint(&content)
"description": "Expected ')', found newline",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 3,
"end": 4
},
"path": "syntax_errors.py"
"path": "syntax_errors.py",
"positions": {
"begin": {
"column": 12,
"line": 3
},
"end": {
"column": 1,
"line": 4
}
}
},
"severity": "major"
}

View File

@@ -21,6 +21,7 @@ static ALLOWLIST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
(?:
# Case-sensitive
pyright
| pyrefly
| mypy:
| type:\s*ignore
| SPDX-License-Identifier:

View File

@@ -75,6 +75,22 @@ impl Violation for BlindExcept {
}
}
fn contains_blind_exception<'a>(
semantic: &'a SemanticModel,
expr: &'a Expr,
) -> Option<(&'a str, ruff_text_size::TextRange)> {
match expr {
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
.iter()
.find_map(|elt| contains_blind_exception(semantic, elt)),
_ => {
let builtin_exception_type = semantic.resolve_builtin_symbol(expr)?;
matches!(builtin_exception_type, "BaseException" | "Exception")
.then(|| (builtin_exception_type, expr.range()))
}
}
}
/// BLE001
pub(crate) fn blind_except(
checker: &Checker,
@@ -87,12 +103,9 @@ pub(crate) fn blind_except(
};
let semantic = checker.semantic();
let Some(builtin_exception_type) = semantic.resolve_builtin_symbol(type_) else {
let Some((builtin_exception_type, range)) = contains_blind_exception(semantic, type_) else {
return;
};
if !matches!(builtin_exception_type, "BaseException" | "Exception") {
return;
}
// If the exception is re-raised, don't flag an error.
let mut visitor = ReraiseVisitor::new(name);
@@ -110,9 +123,9 @@ pub(crate) fn blind_except(
checker.report_diagnostic(
BlindExcept {
name: builtin_exception_type.to_string(),
name: builtin_exception_type.into(),
},
type_.range(),
range,
);
}

View File

@@ -147,3 +147,93 @@ BLE.py:131:8: BLE001 Do not catch blind exception: `Exception`
| ^^^^^^^^^ BLE001
132 | critical("...", exc_info=None)
|
BLE.py:169:9: BLE001 Do not catch blind exception: `Exception`
|
167 | try:
168 | pass
169 | except (Exception,):
| ^^^^^^^^^ BLE001
170 | pass
|
BLE.py:174:9: BLE001 Do not catch blind exception: `Exception`
|
172 | try:
173 | pass
174 | except (Exception, ValueError):
| ^^^^^^^^^ BLE001
175 | pass
|
BLE.py:179:21: BLE001 Do not catch blind exception: `Exception`
|
177 | try:
178 | pass
179 | except (ValueError, Exception):
| ^^^^^^^^^ BLE001
180 | pass
|
BLE.py:184:21: BLE001 Do not catch blind exception: `Exception`
|
182 | try:
183 | pass
184 | except (ValueError, Exception) as e:
| ^^^^^^^^^ BLE001
185 | print(e)
|
BLE.py:189:9: BLE001 Do not catch blind exception: `BaseException`
|
187 | try:
188 | pass
189 | except (BaseException, TypeError):
| ^^^^^^^^^^^^^ BLE001
190 | pass
|
BLE.py:194:20: BLE001 Do not catch blind exception: `BaseException`
|
192 | try:
193 | pass
194 | except (TypeError, BaseException):
| ^^^^^^^^^^^^^ BLE001
195 | pass
|
BLE.py:199:9: BLE001 Do not catch blind exception: `Exception`
|
197 | try:
198 | pass
199 | except (Exception, BaseException):
| ^^^^^^^^^ BLE001
200 | pass
|
BLE.py:204:9: BLE001 Do not catch blind exception: `BaseException`
|
202 | try:
203 | pass
204 | except (BaseException, Exception):
| ^^^^^^^^^^^^^ BLE001
205 | pass
|
BLE.py:210:10: BLE001 Do not catch blind exception: `Exception`
|
208 | try:
209 | pass
210 | except ((Exception, ValueError), TypeError):
| ^^^^^^^^^ BLE001
211 | pass
|
BLE.py:215:22: BLE001 Do not catch blind exception: `BaseException`
|
213 | try:
214 | pass
215 | except (ValueError, (BaseException, TypeError)):
| ^^^^^^^^^^^^^ BLE001
216 | pass
|

View File

@@ -1,6 +1,7 @@
use std::cmp::Ordering;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::StringFlags;
use ruff_python_ast::{
Expr, ExprCall, ExprContext, ExprList, ExprUnaryOp, StringLiteral, StringLiteralFlags,
StringLiteralValue, UnaryOp, str::TripleQuotes,
@@ -116,26 +117,50 @@ pub(crate) fn split_static_string(
}
}
fn replace_flags(elt: &str, flags: StringLiteralFlags) -> StringLiteralFlags {
// In the ideal case we can wrap the element in _single_ quotes of the same
// style. For example, both of these are okay:
//
// ```python
// """itemA
// itemB
// itemC""".split() # -> ["itemA", "itemB", "itemC"]
// ```
//
// ```python
// r"""itemA
// 'single'quoted
// """.split() # -> [r"itemA",r"'single'quoted'"]
// ```
if !flags.prefix().is_raw() || !elt.contains(flags.quote_style().as_char()) {
flags.with_triple_quotes(TripleQuotes::No)
}
// If we have a raw string containing a quotation mark of the same style,
// then we have to swap the style of quotation marks used
else if !elt.contains(flags.quote_style().opposite().as_char()) {
flags
.with_quote_style(flags.quote_style().opposite())
.with_triple_quotes(TripleQuotes::No)
} else
// If both types of quotes are used in the raw, triple-quoted string, then
// we are forced to either add escapes or keep the triple quotes. We opt for
// the latter.
{
flags
}
}
fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
Expr::List(ExprList {
elts: elts
.iter()
.map(|elt| {
let element_flags = replace_flags(elt, flags);
Expr::from(StringLiteral {
value: Box::from(*elt),
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::dummy(),
// intentionally omit the triple quote flag, if set, to avoid strange
// replacements like
//
// ```python
// """
// itemA
// itemB
// itemC
// """.split() # -> ["""itemA""", """itemB""", """itemC"""]
// ```
flags: flags.with_triple_quotes(TripleQuotes::No),
flags: element_flags,
})
})
.collect(),

View File

@@ -1226,6 +1226,7 @@ SIM905.py:130:7: SIM905 [*] Consider using a list literal instead of `str.split`
130 |+print([" x"])
131 131 | print(" x ".rsplit(maxsplit=0))
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
133 133 |
SIM905.py:131:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
@@ -1244,6 +1245,8 @@ SIM905.py:131:7: SIM905 [*] Consider using a list literal instead of `str.split`
131 |-print(" x ".rsplit(maxsplit=0))
131 |+print([" x"])
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
133 133 |
134 134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
SIM905.py:132:7: SIM905 [*] Consider using a list literal instead of `str.split`
|
@@ -1251,6 +1254,8 @@ SIM905.py:132:7: SIM905 [*] Consider using a list literal instead of `str.split`
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM905
133 |
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
|
= help: Replace with list literal
@@ -1260,3 +1265,88 @@ SIM905.py:132:7: SIM905 [*] Consider using a list literal instead of `str.split`
131 131 | print(" x ".rsplit(maxsplit=0))
132 |-print(" x ".rsplit(sep=None, maxsplit=0))
132 |+print([" x"])
133 133 |
134 134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
135 135 | r"""simple@example.com
SIM905.py:135:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
135 | / r"""simple@example.com
136 | | very.common@example.com
137 | | FirstName.LastName@EasierReading.org
138 | | x@example.com
139 | | long.email-address-with-hyphens@and.subdomains.example.com
140 | | user.name+tag+sorting@example.com
141 | | name/surname@example.com
142 | | xample@s.example
143 | | " "@example.org
144 | | "john..doe"@example.org
145 | | mailhost!username@example.org
146 | | "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
147 | | user%example.com@example.org
148 | | user-@example.org
149 | | I❤CHOCOLATE@example.com
150 | | this\ still\"not\\allowed@example.com
151 | | stellyamburrr985@example.com
152 | | Abc.123@example.com
153 | | user+mailbox/department=shipping@example.com
154 | | !#$%&'*+-/=?^_`.{|}~@example.com
155 | | "Abc@def"@example.com
156 | | "Fred\ Bloggs"@example.com
157 | | "Joe.\\Blow"@example.com""".split("\n")
| |_______________________________________^ SIM905
|
= help: Replace with list literal
Safe fix
132 132 | print(" x ".rsplit(sep=None, maxsplit=0))
133 133 |
134 134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
135 |-r"""simple@example.com
136 |-very.common@example.com
137 |-FirstName.LastName@EasierReading.org
138 |-x@example.com
139 |-long.email-address-with-hyphens@and.subdomains.example.com
140 |-user.name+tag+sorting@example.com
141 |-name/surname@example.com
142 |-xample@s.example
143 |-" "@example.org
144 |-"john..doe"@example.org
145 |-mailhost!username@example.org
146 |-"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
147 |-user%example.com@example.org
148 |-user-@example.org
149 |-I❤CHOCOLATE@example.com
150 |-this\ still\"not\\allowed@example.com
151 |-stellyamburrr985@example.com
152 |-Abc.123@example.com
153 |-user+mailbox/department=shipping@example.com
154 |-!#$%&'*+-/=?^_`.{|}~@example.com
155 |-"Abc@def"@example.com
156 |-"Fred\ Bloggs"@example.com
157 |-"Joe.\\Blow"@example.com""".split("\n")
135 |+[r"simple@example.com", r"very.common@example.com", r"FirstName.LastName@EasierReading.org", r"x@example.com", r"long.email-address-with-hyphens@and.subdomains.example.com", r"user.name+tag+sorting@example.com", r"name/surname@example.com", r"xample@s.example", r'" "@example.org', r'"john..doe"@example.org', r"mailhost!username@example.org", r'"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com', r"user%example.com@example.org", r"user-@example.org", r"I❤CHOCOLATE@example.com", r'this\ still\"not\\allowed@example.com', r"stellyamburrr985@example.com", r"Abc.123@example.com", r"user+mailbox/department=shipping@example.com", r"!#$%&'*+-/=?^_`.{|}~@example.com", r'"Abc@def"@example.com', r'"Fred\ Bloggs"@example.com', r'"Joe.\\Blow"@example.com']
158 136 |
159 137 |
160 138 | r"""first
SIM905.py:160:1: SIM905 [*] Consider using a list literal instead of `str.split`
|
160 | / r"""first
161 | | 'no need' to escape
162 | | "swap" quote style
163 | | "use' ugly triple quotes""".split("\n")
| |_______________________________________^ SIM905
|
= help: Replace with list literal
Safe fix
157 157 | "Joe.\\Blow"@example.com""".split("\n")
158 158 |
159 159 |
160 |-r"""first
161 |-'no need' to escape
162 |-"swap" quote style
163 |-"use' ugly triple quotes""".split("\n")
160 |+[r"first", r"'no need' to escape", r'"swap" quote style', r""""use' ugly triple quotes"""]

View File

@@ -11,7 +11,7 @@ use crate::checkers::ast::Checker;
/// Checks for type annotations where `None` is not at the end of an union.
///
/// ## Why is this bad?
/// Type annotation unions are associative, meaning that the order of the elements
/// Type annotation unions are commutative, meaning that the order of the elements
/// does not matter. The `None` literal represents the absence of a value. For
/// readability, it's preferred to write the more informative type expressions first.
///

View File

@@ -33,6 +33,7 @@ pub(in crate::server) enum BackgroundSchedule {
/// while local tasks have exclusive access and can modify it as they please. Keep in mind that
/// local tasks will **block** the main event loop, so only use local tasks if you **need**
/// mutable state access or you need the absolute lowest latency possible.
#[must_use]
pub(in crate::server) enum Task {
Background(BackgroundTaskBuilder),
Sync(SyncTask),

View File

@@ -25,6 +25,7 @@ anyhow = { workspace = true }
argfile = { workspace = true }
clap = { workspace = true, features = ["wrap_help", "string", "env"] }
clap_complete_command = { workspace = true }
clearscreen = { workspace = true }
colored = { workspace = true }
crossbeam = { workspace = true }
ctrlc = { version = "3.4.4" }

118
crates/ty/docs/rules.md generated
View File

@@ -36,7 +36,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L100)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L99)
</small>
**What it does**
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L144)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L143)
</small>
**What it does**
@@ -88,7 +88,7 @@ f(int) # error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L170)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L169)
</small>
**What it does**
@@ -117,7 +117,7 @@ a = 1
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L195)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L194)
</small>
**What it does**
@@ -147,7 +147,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L221)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L220)
</small>
**What it does**
@@ -177,7 +177,7 @@ class B(A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285)
</small>
**What it does**
@@ -202,7 +202,7 @@ class B(A, A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L307)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L306)
</small>
**What it does**
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L449)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L448)
</small>
**What it does**
@@ -334,7 +334,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L473)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L472)
</small>
**What it does**
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L339)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L338)
</small>
**What it does**
@@ -445,7 +445,7 @@ an atypical memory layout.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L493)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L492)
</small>
**What it does**
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L533)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L532)
</small>
**What it does**
@@ -496,7 +496,7 @@ a: int = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1537)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1536)
</small>
**What it does**
@@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L555)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L554)
</small>
**What it does**
@@ -550,7 +550,7 @@ class A(42): ... # error: [invalid-base]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L606)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L605)
</small>
**What it does**
@@ -575,7 +575,7 @@ with 1:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L627)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L626)
</small>
**What it does**
@@ -602,7 +602,7 @@ a: str
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L649)
</small>
**What it does**
@@ -644,7 +644,7 @@ except ZeroDivisionError:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L686)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L685)
</small>
**What it does**
@@ -675,7 +675,7 @@ class C[U](Generic[T]): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L712)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L711)
</small>
**What it does**
@@ -708,7 +708,7 @@ def f(t: TypeVar("U")): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L761)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L760)
</small>
**What it does**
@@ -740,7 +740,7 @@ class B(metaclass=f): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L787)
</small>
**What it does**
@@ -788,7 +788,7 @@ def foo(x: int) -> int: ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L831)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L830)
</small>
**What it does**
@@ -812,7 +812,7 @@ def f(a: int = ''): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L421)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L420)
</small>
**What it does**
@@ -844,7 +844,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L851)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L850)
</small>
Checks for `raise` statements that raise non-exceptions or use invalid
@@ -891,7 +891,7 @@ def g():
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L514)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L513)
</small>
**What it does**
@@ -914,7 +914,7 @@ def func() -> int:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L894)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L893)
</small>
**What it does**
@@ -968,7 +968,7 @@ TODO #14889
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L740)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739)
</small>
**What it does**
@@ -993,7 +993,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L933)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L932)
</small>
**What it does**
@@ -1021,7 +1021,7 @@ TYPE_CHECKING = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L957)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956)
</small>
**What it does**
@@ -1049,7 +1049,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1009)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1008)
</small>
**What it does**
@@ -1081,7 +1081,7 @@ f(10) # Error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L980)
</small>
**What it does**
@@ -1113,7 +1113,7 @@ class C:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1037)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1036)
</small>
**What it does**
@@ -1146,7 +1146,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1066)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1065)
</small>
**What it does**
@@ -1169,7 +1169,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1084)
</small>
**What it does**
@@ -1196,7 +1196,7 @@ func("string") # error: [no-matching-overload]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1108)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1107)
</small>
**What it does**
@@ -1218,7 +1218,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1126)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1125)
</small>
**What it does**
@@ -1242,7 +1242,7 @@ for i in 34: # TypeError: 'int' object is not iterable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1177)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176)
</small>
**What it does**
@@ -1296,7 +1296,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1513)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1512)
</small>
**What it does**
@@ -1324,7 +1324,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1268)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1267)
</small>
**What it does**
@@ -1351,7 +1351,7 @@ class B(A): ... # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1313)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312)
</small>
**What it does**
@@ -1376,7 +1376,7 @@ f("foo") # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1291)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290)
</small>
**What it does**
@@ -1402,7 +1402,7 @@ def _(x: int):
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1334)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1333)
</small>
**What it does**
@@ -1446,7 +1446,7 @@ class A:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1391)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1390)
</small>
**What it does**
@@ -1471,7 +1471,7 @@ f(x=1, y=2) # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411)
</small>
**What it does**
@@ -1497,7 +1497,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1434)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1433)
</small>
**What it does**
@@ -1520,7 +1520,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1452)
</small>
**What it does**
@@ -1543,7 +1543,7 @@ print(x) # NameError: name 'x' is not defined
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1146)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1145)
</small>
**What it does**
@@ -1578,7 +1578,7 @@ b1 < b2 < b1 # exception raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1472)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471)
</small>
**What it does**
@@ -1604,7 +1604,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1494)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1493)
</small>
**What it does**
@@ -1627,7 +1627,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L265)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L264)
</small>
**What it does**
@@ -1680,7 +1680,7 @@ a = 20 / 0 # type: ignore
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1197)
</small>
**What it does**
@@ -1706,7 +1706,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L118)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L117)
</small>
**What it does**
@@ -1736,7 +1736,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1220)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1219)
</small>
**What it does**
@@ -1766,7 +1766,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1565)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564)
</small>
**What it does**
@@ -1791,7 +1791,7 @@ cast(int, f()) # Redundant
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1373)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1372)
</small>
**What it does**
@@ -1842,7 +1842,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1586)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1585)
</small>
**What it does**
@@ -1896,7 +1896,7 @@ def g():
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L573)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L572)
</small>
**What it does**
@@ -1933,7 +1933,7 @@ class D(C): ... # error: [unsupported-base]
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L247)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L246)
</small>
**What it does**
@@ -1955,7 +1955,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1245)
</small>
**What it does**

View File

@@ -22,12 +22,13 @@ use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use rayon::ThreadPoolBuilder;
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
use ruff_db::files::File;
use ruff_db::max_parallelism;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
use salsa::Database;
use ty_project::metadata::options::ProjectOptionsOverrides;
use ty_project::watch::ProjectWatcher;
use ty_project::{Db, watch};
use ty_project::{CollectReporter, Db, watch};
use ty_project::{ProjectDatabase, ProjectMetadata};
use ty_server::run_server;
@@ -238,11 +239,6 @@ impl MainLoop {
})?;
self.watcher = Some(ProjectWatcher::new(watcher, db));
// Do not show progress bars with `--watch`, indicatif does not seem to
// handle cancelling independent progress bars very well.
// TODO(zanieb): We can probably use `MultiProgress` to handle this case in the future.
self.printer = self.printer.with_no_progress();
self.run(db)?;
Ok(ExitStatus::Success)
@@ -265,6 +261,9 @@ impl MainLoop {
let mut revision = 0u64;
while let Ok(message) = self.receiver.recv() {
if self.watcher.is_some() {
Printer::clear_screen()?;
}
match message {
MainLoopMessage::CheckWorkspace => {
let db = db.clone();
@@ -273,9 +272,13 @@ impl MainLoop {
// Spawn a new task that checks the project. This needs to be done in a separate thread
// to prevent blocking the main loop here.
rayon::spawn(move || {
let mut reporter = IndicatifReporter::from(self.printer);
let bar = reporter.bar.clone();
match salsa::Cancelled::catch(|| {
let mut reporter = IndicatifReporter::from(self.printer);
db.check_with_reporter(&mut reporter)
db.check_with_reporter(&mut reporter);
reporter.bar.finish();
reporter.collector.into_sorted(&db)
}) {
Ok(result) => {
// Send the result back to the main loop for printing.
@@ -284,6 +287,7 @@ impl MainLoop {
.unwrap();
}
Err(cancelled) => {
bar.finish_and_clear();
tracing::debug!("Check has been cancelled: {cancelled:?}");
}
}
@@ -380,9 +384,7 @@ impl MainLoop {
}
MainLoopMessage::Exit => {
// Cancel any pending queries and wait for them to complete.
// TODO: Don't use Salsa internal APIs
// [Zulip-Thread](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries)
let _ = db.zalsa_mut();
db.trigger_cancellation();
return Ok(ExitStatus::Success);
}
}
@@ -395,54 +397,52 @@ impl MainLoop {
}
/// A progress reporter for `ty check`.
enum IndicatifReporter {
/// A constructed reporter that is not yet ready, contains the target for the progress bar.
Pending(indicatif::ProgressDrawTarget),
struct IndicatifReporter {
collector: CollectReporter,
/// A reporter that is ready, containing a progress bar to report to.
///
/// Initialization of the bar is deferred to [`ty_project::ProgressReporter::set_files`] so we
/// do not initialize the bar too early as it may take a while to collect the number of files to
/// process and we don't want to display an empty "0/0" bar.
Initialized(indicatif::ProgressBar),
bar: indicatif::ProgressBar,
printer: Printer,
}
impl From<Printer> for IndicatifReporter {
fn from(printer: Printer) -> Self {
Self::Pending(printer.progress_target())
Self {
bar: indicatif::ProgressBar::hidden(),
collector: CollectReporter::default(),
printer,
}
}
}
impl ty_project::ProgressReporter for IndicatifReporter {
fn set_files(&mut self, files: usize) {
let target = match std::mem::replace(
self,
IndicatifReporter::Pending(indicatif::ProgressDrawTarget::hidden()),
) {
Self::Pending(target) => target,
Self::Initialized(_) => panic!("The progress reporter should only be initialized once"),
};
self.collector.set_files(files);
let bar = indicatif::ProgressBar::with_draw_target(Some(files as u64), target);
bar.set_style(
self.bar.set_length(files as u64);
self.bar.set_message("Checking");
self.bar.set_style(
indicatif::ProgressStyle::with_template(
"{msg:8.dim} {bar:60.green/dim} {pos}/{len} files",
)
.unwrap()
.progress_chars("--"),
);
bar.set_message("Checking");
*self = Self::Initialized(bar);
self.bar.set_draw_target(self.printer.progress_target());
}
fn report_file(&self, _file: &ruff_db::files::File) {
match self {
IndicatifReporter::Initialized(progress_bar) => {
progress_bar.inc(1);
}
IndicatifReporter::Pending(_) => {
panic!("`report_file` called before `set_files`")
}
}
fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
self.collector.report_checked_file(db, file, diagnostics);
self.bar.inc(1);
}
fn report_diagnostics(&mut self, db: &dyn Db, diagnostics: Vec<Diagnostic>) {
self.collector.report_diagnostics(db, diagnostics);
}
}

View File

@@ -1,5 +1,6 @@
use std::io::StdoutLock;
use anyhow::Result;
use indicatif::ProgressDrawTarget;
use crate::logging::VerbosityLevel;
@@ -11,14 +12,6 @@ pub(crate) struct Printer {
}
impl Printer {
#[must_use]
pub(crate) fn with_no_progress(self) -> Self {
Self {
verbosity: self.verbosity,
no_progress: true,
}
}
#[must_use]
pub(crate) fn with_verbosity(self, verbosity: VerbosityLevel) -> Self {
Self {
@@ -109,6 +102,11 @@ impl Printer {
pub(crate) fn stream_for_details(self) -> Stdout {
self.stdout_general()
}
pub(crate) fn clear_screen() -> Result<()> {
clearscreen::clear()?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@@ -724,3 +724,231 @@ fn invalid_exclude_pattern() -> anyhow::Result<()> {
Ok(())
}
/// Test that ty works correctly with Bazel's symlinked file structure
#[test]
#[cfg(unix)]
fn bazel_symlinked_files() -> anyhow::Result<()> {
let case = CliTest::with_files([
// Original source files in the project
(
"main.py",
r#"
import library
result = library.process_data()
print(undefined_var) # error: unresolved-reference
"#,
),
(
"library.py",
r#"
def process_data():
return missing_value # error: unresolved-reference
"#,
),
// Another source file that won't be symlinked
(
"other.py",
r#"
print(other_undefined) # error: unresolved-reference
"#,
),
])?;
// Create Bazel-style symlinks pointing to the actual source files
// Bazel typically creates symlinks in bazel-out/k8-fastbuild/bin/ that point to actual sources
std::fs::create_dir_all(case.project_dir.join("bazel-out/k8-fastbuild/bin"))?;
// Use absolute paths to ensure the symlinks work correctly
case.write_symlink(
case.project_dir.join("main.py"),
"bazel-out/k8-fastbuild/bin/main.py",
)?;
case.write_symlink(
case.project_dir.join("library.py"),
"bazel-out/k8-fastbuild/bin/library.py",
)?;
// Change to the bazel-out directory and run ty from there
// The symlinks should be followed and errors should be found
assert_cmd_snapshot!(case.command().current_dir(case.project_dir.join("bazel-out/k8-fastbuild/bin")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `missing_value` used when not defined
--> library.py:3:12
|
2 | def process_data():
3 | return missing_value # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
error[unresolved-reference]: Name `undefined_var` used when not defined
--> main.py:5:7
|
4 | result = library.process_data()
5 | print(undefined_var) # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Test that when checking a specific symlinked file from the bazel-out directory, it works correctly
assert_cmd_snapshot!(case.command().current_dir(case.project_dir.join("bazel-out/k8-fastbuild/bin")).arg("main.py"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `undefined_var` used when not defined
--> main.py:5:7
|
4 | result = library.process_data()
5 | print(undefined_var) # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// Test that exclude patterns match on symlink source names, not target names
#[test]
#[cfg(unix)]
fn exclude_symlink_source_not_target() -> anyhow::Result<()> {
let case = CliTest::with_files([
// Target files with generic names
(
"src/module.py",
r#"
def process():
return undefined_var # error: unresolved-reference
"#,
),
(
"src/utils.py",
r#"
def helper():
return missing_value # error: unresolved-reference
"#,
),
(
"regular.py",
r#"
print(regular_undefined) # error: unresolved-reference
"#,
),
])?;
// Create symlinks with names that differ from their targets
// This simulates build systems that rename files during symlinking
case.write_symlink("src/module.py", "generated_module.py")?;
case.write_symlink("src/utils.py", "generated_utils.py")?;
// Exclude pattern should match on the symlink name (generated_*), not the target name
assert_cmd_snapshot!(case.command().arg("--exclude").arg("generated_*.py"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `regular_undefined` used when not defined
--> regular.py:2:7
|
2 | print(regular_undefined) # error: unresolved-reference
| ^^^^^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
error[unresolved-reference]: Name `undefined_var` used when not defined
--> src/module.py:3:12
|
2 | def process():
3 | return undefined_var # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
error[unresolved-reference]: Name `missing_value` used when not defined
--> src/utils.py:3:12
|
2 | def helper():
3 | return missing_value # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 3 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Exclude pattern on target path should not affect symlinks with different names
assert_cmd_snapshot!(case.command().arg("--exclude").arg("src/*.py"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `undefined_var` used when not defined
--> generated_module.py:3:12
|
2 | def process():
3 | return undefined_var # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
error[unresolved-reference]: Name `missing_value` used when not defined
--> generated_utils.py:3:12
|
2 | def helper():
3 | return missing_value # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
error[unresolved-reference]: Name `regular_undefined` used when not defined
--> regular.py:2:7
|
2 | print(regular_undefined) # error: unresolved-reference
| ^^^^^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 3 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Test that explicitly passing a symlink always checks it, even if excluded
assert_cmd_snapshot!(case.command().arg("--exclude").arg("generated_*.py").arg("generated_module.py"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `undefined_var` used when not defined
--> generated_module.py:3:12
|
2 | def process():
3 | return undefined_var # error: unresolved-reference
| ^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}

View File

@@ -863,10 +863,18 @@ fn overrides_unknown_rules() -> anyhow::Result<()> {
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> main.py:2:5
|
2 | y = 4 / 0
| ^^^^^
|
info: rule `division-by-zero` was selected in the configuration file
warning[unknown-rule]: Unknown lint rule `division-by-zer`
--> pyproject.toml:10:1
|
@@ -876,14 +884,6 @@ fn overrides_unknown_rules() -> anyhow::Result<()> {
| ^^^^^^^^^^^^^^^
|
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> main.py:2:5
|
2 | y = 4 / 0
| ^^^^^
|
info: rule `division-by-zero` was selected in the configuration file
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> tests/test_main.py:2:5
|
@@ -896,7 +896,7 @@ fn overrides_unknown_rules() -> anyhow::Result<()> {
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
"###);
Ok(())
}

View File

@@ -1233,28 +1233,28 @@ quux.<CURSOR>
baz :: Unknown | Literal[3]
foo :: Unknown | Literal[1]
__annotations__ :: dict[str, Any]
__class__ :: type
__delattr__ :: bound method object.__delattr__(name: str, /) -> None
__class__ :: type[Quux]
__delattr__ :: bound method Quux.__delattr__(name: str, /) -> None
__dict__ :: dict[str, Any]
__dir__ :: bound method object.__dir__() -> Iterable[str]
__dir__ :: bound method Quux.__dir__() -> Iterable[str]
__doc__ :: str | None
__eq__ :: bound method object.__eq__(value: object, /) -> bool
__format__ :: bound method object.__format__(format_spec: str, /) -> str
__getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
__getstate__ :: bound method object.__getstate__() -> object
__hash__ :: bound method object.__hash__() -> int
__eq__ :: bound method Quux.__eq__(value: object, /) -> bool
__format__ :: bound method Quux.__format__(format_spec: str, /) -> str
__getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any
__getstate__ :: bound method Quux.__getstate__() -> object
__hash__ :: bound method Quux.__hash__() -> int
__init__ :: bound method Quux.__init__() -> Unknown
__init_subclass__ :: bound method object.__init_subclass__() -> None
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method object.__ne__(value: object, /) -> bool
__new__ :: bound method object.__new__() -> Self@object
__reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method object.__repr__() -> str
__setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
__sizeof__ :: bound method object.__sizeof__() -> int
__str__ :: bound method object.__str__() -> str
__subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: bound method Quux.__new__() -> Self@object
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method Quux.__repr__() -> str
__setattr__ :: bound method Quux.__setattr__(name: str, value: Any, /) -> None
__sizeof__ :: bound method Quux.__sizeof__() -> int
__str__ :: bound method Quux.__str__() -> str
__subclasshook__ :: bound method type[Quux].__subclasshook__(subclass: type, /) -> bool
");
}
@@ -1278,28 +1278,28 @@ quux.b<CURSOR>
baz :: Unknown | Literal[3]
foo :: Unknown | Literal[1]
__annotations__ :: dict[str, Any]
__class__ :: type
__delattr__ :: bound method object.__delattr__(name: str, /) -> None
__class__ :: type[Quux]
__delattr__ :: bound method Quux.__delattr__(name: str, /) -> None
__dict__ :: dict[str, Any]
__dir__ :: bound method object.__dir__() -> Iterable[str]
__dir__ :: bound method Quux.__dir__() -> Iterable[str]
__doc__ :: str | None
__eq__ :: bound method object.__eq__(value: object, /) -> bool
__format__ :: bound method object.__format__(format_spec: str, /) -> str
__getattribute__ :: bound method object.__getattribute__(name: str, /) -> Any
__getstate__ :: bound method object.__getstate__() -> object
__hash__ :: bound method object.__hash__() -> int
__eq__ :: bound method Quux.__eq__(value: object, /) -> bool
__format__ :: bound method Quux.__format__(format_spec: str, /) -> str
__getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any
__getstate__ :: bound method Quux.__getstate__() -> object
__hash__ :: bound method Quux.__hash__() -> int
__init__ :: bound method Quux.__init__() -> Unknown
__init_subclass__ :: bound method object.__init_subclass__() -> None
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method object.__ne__(value: object, /) -> bool
__new__ :: bound method object.__new__() -> Self@object
__reduce__ :: bound method object.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method object.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method object.__repr__() -> str
__setattr__ :: bound method object.__setattr__(name: str, value: Any, /) -> None
__sizeof__ :: bound method object.__sizeof__() -> int
__str__ :: bound method object.__str__() -> str
__subclasshook__ :: bound method type.__subclasshook__(subclass: type, /) -> bool
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: bound method Quux.__new__() -> Self@object
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method Quux.__repr__() -> str
__setattr__ :: bound method Quux.__setattr__(name: str, value: Any, /) -> None
__sizeof__ :: bound method Quux.__sizeof__() -> int
__str__ :: bound method Quux.__str__() -> str
__subclasshook__ :: bound method type[Quux].__subclasshook__(subclass: type, /) -> bool
");
}

View File

@@ -5,15 +5,14 @@ use std::{cmp, fmt};
pub use self::changes::ChangeResult;
use crate::metadata::settings::file_settings;
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
use crate::{CollectReporter, DEFAULT_LINT_REGISTRY};
use crate::{ProgressReporter, Project, ProjectMetadata};
use ruff_db::Db as SourceDb;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, Files};
use ruff_db::system::System;
use ruff_db::vendored::VendoredFileSystem;
use salsa::plumbing::ZalsaDatabase;
use salsa::{Event, Setter};
use salsa::{Database, Event, Setter};
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{Db as SemanticDb, Program};
@@ -34,7 +33,7 @@ pub struct ProjectDatabase {
// or the "trick" to get a mutable `Arc` in `Self::system_mut` is no longer guaranteed to work.
system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
// IMPORTANT: This field must be the last because we use `zalsa_mut` (drops all other storage references)
// IMPORTANT: This field must be the last because we use `trigger_cancellation` (drops all other storage references)
// to drop all other references to the database, which gives us exclusive access to other `Arc`s stored on this db.
// However, for this to work it's important that the `storage` is dropped AFTER any `Arc` that
// we try to mutably borrow using `Arc::get_mut` (like `system`).
@@ -87,7 +86,9 @@ impl ProjectDatabase {
///
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
pub fn check(&self) -> Vec<Diagnostic> {
self.project().check(self, &mut DummyReporter)
let mut collector = CollectReporter::default();
self.project().check(self, &mut collector);
collector.into_sorted(self)
}
/// Checks the files in the project and its dependencies, using the given reporter.
@@ -95,8 +96,8 @@ impl ProjectDatabase {
/// Use [`set_check_mode`] to update the check mode.
///
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
self.project().check(self, reporter)
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) {
self.project().check(self, reporter);
}
#[tracing::instrument(level = "debug", skip(self))]
@@ -114,12 +115,11 @@ impl ProjectDatabase {
///
/// WARNING: Triggers a new revision, canceling other database handles. This can lead to deadlock.
pub fn system_mut(&mut self) -> &mut dyn System {
// TODO: Use a more official method to cancel other queries.
// https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries
let _ = self.zalsa_mut();
self.trigger_cancellation();
Arc::get_mut(&mut self.system)
.expect("ref count should be 1 because `zalsa_mut` drops all other DB references.")
Arc::get_mut(&mut self.system).expect(
"ref count should be 1 because `trigger_cancellation` drops all other DB references.",
)
}
/// Returns a [`SalsaMemoryDump`] that can be use to dump Salsa memory usage information

View File

@@ -58,17 +58,16 @@ impl IndexedFiles {
///
/// The changes are automatically written back to the database once the view is dropped.
pub(super) fn indexed_mut(db: &mut dyn Db, project: Project) -> Option<IndexedMut> {
// Calling `zalsa_mut` cancels all pending salsa queries. This ensures that there are no pending
// reads to the file set.
// TODO: Use a non-internal API instead https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries
let _ = db.as_dyn_database_mut().zalsa_mut();
// Calling `trigger_cancellation` cancels all pending salsa queries. This ensures that there are no pending
// reads to the file set (this `db` is the only alive db).
db.trigger_cancellation();
// Replace the state with lazy. The `IndexedMut` guard restores the state
// to `State::Indexed` or sets a new `PackageFiles` when it gets dropped to ensure the state
// is restored to how it has been before replacing the value.
//
// It isn't necessary to hold on to the lock after this point:
// * The above call to `zalsa_mut` guarantees that there's exactly **one** DB reference.
// * The above call to `trigger_cancellation` guarantees that there's exactly **one** DB reference.
// * `Indexed` has a `'db` lifetime, and this method requires a `&mut db`.
// This means that there can't be any pending reference to `Indexed` because Rust
// doesn't allow borrowing `db` as mutable (to call this method) and immutable (`Indexed<'db>`) at the same time.

View File

@@ -127,17 +127,46 @@ pub trait ProgressReporter: Send + Sync {
/// Initialize the reporter with the number of files.
fn set_files(&mut self, files: usize);
/// Report the completion of a given file.
fn report_file(&self, file: &File);
/// Report the completion of checking a given file along with its diagnostics.
fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]);
/// Reports settings or IO related diagnostics. The diagnostics
/// can belong to different files or no file at all.
/// But it's never a file for which [`Self::report_checked_file`] gets called.
fn report_diagnostics(&mut self, db: &dyn Db, diagnostics: Vec<Diagnostic>);
}
/// A no-op implementation of [`ProgressReporter`].
/// Reporter that collects all diagnostics into a `Vec`.
#[derive(Default)]
pub struct DummyReporter;
pub struct CollectReporter(std::sync::Mutex<Vec<Diagnostic>>);
impl ProgressReporter for DummyReporter {
impl CollectReporter {
pub fn into_sorted(self, db: &dyn Db) -> Vec<Diagnostic> {
let mut diagnostics = self.0.into_inner().unwrap();
diagnostics.sort_by(|left, right| {
left.rendering_sort_key(db)
.cmp(&right.rendering_sort_key(db))
});
diagnostics
}
}
impl ProgressReporter for CollectReporter {
fn set_files(&mut self, _files: usize) {}
fn report_file(&self, _file: &File) {}
fn report_checked_file(&self, _db: &dyn Db, _file: File, diagnostics: &[Diagnostic]) {
if diagnostics.is_empty() {
return;
}
self.0
.lock()
.unwrap()
.extend(diagnostics.iter().map(Clone::clone));
}
fn report_diagnostics(&mut self, _db: &dyn Db, diagnostics: Vec<Diagnostic>) {
self.0.get_mut().unwrap().extend(diagnostics);
}
}
#[salsa::tracked]
@@ -225,11 +254,7 @@ impl Project {
}
/// Checks the project and its dependencies according to the project's check mode.
pub(crate) fn check(
self,
db: &ProjectDatabase,
reporter: &mut dyn ProgressReporter,
) -> Vec<Diagnostic> {
pub(crate) fn check(self, db: &ProjectDatabase, reporter: &mut dyn ProgressReporter) {
let project_span = tracing::debug_span!("Project::check");
let _span = project_span.enter();
@@ -239,12 +264,11 @@ impl Project {
name = self.name(db)
);
let mut diagnostics: Vec<Diagnostic> = Vec::new();
diagnostics.extend(
self.settings_diagnostics(db)
.iter()
.map(OptionDiagnostic::to_diagnostic),
);
let mut diagnostics: Vec<Diagnostic> = self
.settings_diagnostics(db)
.iter()
.map(OptionDiagnostic::to_diagnostic)
.collect();
let files = ProjectFiles::new(db, self);
reporter.set_files(files.len());
@@ -256,19 +280,19 @@ impl Project {
.map(IOErrorDiagnostic::to_diagnostic),
);
reporter.report_diagnostics(db, diagnostics);
let open_files = self.open_files(db);
let check_start = ruff_db::Instant::now();
let file_diagnostics = std::sync::Mutex::new(vec![]);
{
let db = db.clone();
let file_diagnostics = &file_diagnostics;
let project_span = &project_span;
let reporter = &reporter;
rayon::scope(move |scope| {
for file in &files {
let db = db.clone();
let reporter = &*reporter;
scope.spawn(move |_| {
let check_file_span =
tracing::debug_span!(parent: project_span, "check_file", ?file);
@@ -276,10 +300,7 @@ impl Project {
match check_file_impl(&db, file) {
Ok(diagnostics) => {
file_diagnostics
.lock()
.unwrap()
.extend(diagnostics.iter().map(Clone::clone));
reporter.report_checked_file(&db, file, diagnostics);
// This is outside `check_file_impl` to avoid that opening or closing
// a file invalidates the `check_file_impl` query of every file!
@@ -295,28 +316,22 @@ impl Project {
}
}
Err(io_error) => {
file_diagnostics.lock().unwrap().push(io_error.clone());
reporter.report_checked_file(
&db,
file,
std::slice::from_ref(io_error),
);
}
}
reporter.report_file(&file);
});
}
});
}
};
tracing::debug!(
"Checking all files took {:.3}s",
check_start.elapsed().as_secs_f64(),
);
let mut file_diagnostics = file_diagnostics.into_inner().unwrap();
file_diagnostics.sort_by(|left, right| {
left.rendering_sort_key(db)
.cmp(&right.rendering_sort_key(db))
});
diagnostics.extend(file_diagnostics);
diagnostics
}
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {

View File

@@ -173,21 +173,30 @@ impl<'a> ProjectFilesWalker<'a> {
Ok(entry) => {
// Skip excluded directories unless they were explicitly passed to the walker
// (which is the case passed to `ty check <paths>`).
if entry.file_type().is_directory() && entry.depth() > 0 {
return match self.filter.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown) {
IncludeResult::Included => WalkState::Continue,
IncludeResult::Excluded => {
tracing::debug!("Skipping directory '{path}' because it is excluded by a default or `src.exclude` pattern", path=entry.path());
WalkState::Skip
},
IncludeResult::NotIncluded => {
tracing::debug!("Skipping directory `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI", path=entry.path());
WalkState::Skip
},
};
}
if entry.file_type().is_file() {
if entry.file_type().is_directory() {
if entry.depth() > 0 {
let directory_included = self
.filter
.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown);
return match directory_included {
IncludeResult::Included => WalkState::Continue,
IncludeResult::Excluded => {
tracing::debug!(
"Skipping directory '{path}' because it is excluded by a default or `src.exclude` pattern",
path=entry.path()
);
WalkState::Skip
},
IncludeResult::NotIncluded => {
tracing::debug!(
"Skipping directory `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI",
path=entry.path()
);
WalkState::Skip
},
};
}
} else {
// Ignore any non python files to avoid creating too many entries in `Files`.
if entry
.path()
@@ -201,14 +210,23 @@ impl<'a> ProjectFilesWalker<'a> {
// For all files, except the ones that were explicitly passed to the walker (CLI),
// check if they're included in the project.
if entry.depth() > 0 {
match self.filter.is_file_included(entry.path(), GlobFilterCheckMode::TopDown) {
match self
.filter
.is_file_included(entry.path(), GlobFilterCheckMode::TopDown)
{
IncludeResult::Included => {},
IncludeResult::Excluded => {
tracing::debug!("Ignoring file `{path}` because it is excluded by a default or `src.exclude` pattern.", path=entry.path());
tracing::debug!(
"Ignoring file `{path}` because it is excluded by a default or `src.exclude` pattern.",
path=entry.path()
);
return WalkState::Continue;
},
IncludeResult::NotIncluded => {
tracing::debug!("Ignoring file `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI.", path=entry.path());
tracing::debug!(
"Ignoring file `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI.",
path=entry.path()
);
return WalkState::Continue;
},
}

View File

@@ -18,5 +18,5 @@ def append_int(*args: *Ts) -> tuple[*Ts, int]:
return (*args, 1)
# TODO should be tuple[Literal[True], Literal["a"], int]
reveal_type(append_int(True, "a")) # revealed: @Todo(PEP 646)
reveal_type(append_int(True, "a")) # revealed: tuple[@Todo(PEP 646), ...]
```

View File

@@ -17,6 +17,7 @@ Alias: TypeAlias = int
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
return args
def g() -> TypeGuard[int]: ...
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:

View File

@@ -59,7 +59,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
reveal_type(e) # revealed: tuple[str, ...]
reveal_type(f) # revealed: tuple[str, *tuple[int, ...], bytes]
reveal_type(g) # revealed: @Todo(PEP 646)
reveal_type(g) # revealed: tuple[@Todo(PEP 646), ...]
reveal_type(h) # revealed: tuple[list[int], list[int]]
reveal_type(i) # revealed: tuple[str | int, str | int]

View File

@@ -1916,7 +1916,7 @@ d = True
reveal_type(d.__class__) # revealed: <class 'bool'>
e = (42, 42)
reveal_type(e.__class__) # revealed: <class 'tuple[Literal[42], Literal[42]]'>
reveal_type(e.__class__) # revealed: type[tuple[Literal[42], Literal[42]]]
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
reveal_type(a.__class__) # revealed: type[int]

View File

@@ -350,6 +350,25 @@ def _(target: None | Foo):
reveal_type(y) # revealed: Literal[1, 3]
```
## `as` patterns
```py
def _(target: int | str):
y = 1
match target:
case 1 as x:
y = 2
reveal_type(x) # revealed: @Todo(`match` pattern definition types)
case "foo" as x:
y = 3
reveal_type(x) # revealed: @Todo(`match` pattern definition types)
case _:
y = 4
reveal_type(y) # revealed: Literal[2, 3, 4]
```
## Guard with object that implements `__bool__` incorrectly
```py

View File

@@ -338,3 +338,32 @@ def no_invalid_return_diagnostic_here_either[T](x: A[T]) -> ASub[T]:
# is null and void (and therefore we don't emit a diagnostic)
return x
```
## More `match` pattern types
### `as` patterns
```py
from typing import assert_never
def as_pattern_exhaustive(subject: int | str):
match subject:
case int() as x:
pass
case str() as y:
pass
case _:
no_diagnostic_here
assert_never(subject)
def as_pattern_non_exhaustive(subject: int | str):
match subject:
case int() as x:
pass
case _:
this_should_be_an_error # error: [unresolved-reference]
# this diagnostic is correct: the inferred type of `subject` is `str`
assert_never(subject) # error: [type-assertion-failure]
```

View File

@@ -208,6 +208,36 @@ static_assert(has_member(Answer, "YES"))
static_assert(has_member(Answer, "__members__"))
```
### TypedDicts
```py
from ty_extensions import has_member, static_assert
from typing import TypedDict
class Person(TypedDict):
name: str
age: int | None
static_assert(not has_member(Person, "name"))
static_assert(has_member(Person, "keys"))
static_assert(has_member(Person, "__total__"))
def _(person: Person):
static_assert(not has_member(person, "name"))
static_assert(not has_member(person, "__total__"))
static_assert(has_member(person, "keys"))
# type(person) is `dict` at runtime, so `__total__` is not available:
static_assert(not has_member(type(person), "name"))
static_assert(not has_member(type(person), "__total__"))
static_assert(has_member(type(person), "keys"))
def _(t_person: type[Person]):
static_assert(not has_member(t_person, "name"))
static_assert(has_member(t_person, "__total__"))
static_assert(has_member(t_person, "keys"))
```
### Unions
For unions, `ide_support::all_members` only returns members that are available on all elements of

View File

@@ -182,16 +182,19 @@ class ContextManagerThatMightNotRunToCompletion:
with ContextManagerThatMightNotRunToCompletion() as L:
U = ...
match 42:
def get_object() -> object:
pass
match get_object():
case {"something": M}:
...
case [*N]:
...
case [O]:
...
case P | Q: # error: [invalid-syntax] "name capture `P` makes remaining patterns unreachable"
case I(foo=R):
...
case object(foo=R):
case P | Q:
...
match 56:

View File

@@ -346,7 +346,10 @@ l: list[str | Literal[1] | None] = [None]
def f(x: str | Literal[1] | None):
class C:
if x is not None: # TODO: should be an unresolved-reference error
# If we try to access a variable in a class before it has been defined,
# the lookup will fall back to global.
# error: [unresolved-reference]
if x is not None:
def _():
if x != 1:
reveal_type(x) # revealed: str | None
@@ -360,11 +363,12 @@ def f(x: str | Literal[1] | None):
x = None
def _():
# No narrowing is performed on unresolved references.
# error: [unresolved-reference]
if x is not None:
def _():
if x != 1:
reveal_type(x) # revealed: Never
reveal_type(x) # revealed: None
x = None
def f(const: str | Literal[1] | None):
@@ -372,8 +376,7 @@ def f(const: str | Literal[1] | None):
if const is not None:
def _():
if const != 1:
# TODO: should be `str`
reveal_type(const) # revealed: str | None
reveal_type(const) # revealed: str
class D:
if const != 1:

View File

@@ -262,3 +262,28 @@ def f():
global __file__ # allowed, implicit global
global int # error: [unresolved-global] "Invalid global declaration of `int`: `int` has no declarations or bindings in the global scope"
```
## References to variables before they are defined within a class scope are considered global
If we try to access a variable in a class before it has been defined, the lookup will fall back to
global.
```py
x: str = "a"
def f(x: int, y: int):
class C:
reveal_type(x) # revealed: int
class D:
x = None
reveal_type(x) # revealed: None
class E:
reveal_type(x) # revealed: str
x = None
# error: [unresolved-reference]
reveal_type(y) # revealed: Unknown
y = None
```

View File

@@ -119,6 +119,28 @@ def _(empty: EmptyTupleSubclass, single_element: SingleElementTupleSubclass, mix
mixed.__class__()
```
## Meta-type of tuple instances
The type `tuple[str, int]` does not only have exact instances of `tuple` as its inhabitants: its
inhabitants also include any instances of subclasses of `tuple[str, int]`. As such, the meta-type of
`tuple[str, int]` should be `type[tuple[str, int]]` rather than `<class 'tuple[str, int]'>`. The
former accurately reflects the fact that given an instance of `tuple[str, int]`, we do not know
exactly what the `__class__` of that instance will be: we only know that it will be a subclass of
`tuple[str, int]`. The latter would be incorrectly precise: it would imply that all instances of
`tuple[str, int]` have the runtime object `tuple` as their `__class__`, which isn't true.
```toml
[environment]
python-version = "3.11"
```
```py
def f(x: tuple[int, ...], y: tuple[str, str], z: tuple[int, *tuple[str, ...], bytes]):
reveal_type(type(x)) # revealed: type[tuple[int, ...]]
reveal_type(type(y)) # revealed: type[tuple[str, str]]
reveal_type(type(z)) # revealed: type[tuple[int, *tuple[str, ...], bytes]]
```
## Subtyping relationships
The type `tuple[S1, S2]` is a subtype of `tuple[T1, T2]` if and only if `S1` is a subtype of `T1`

View File

@@ -1052,8 +1052,7 @@ class FooLegacy(Generic[T]):
class Bar[T, **P]:
def __call__(self): ...
# TODO: should not error
class BarLegacy(Generic[T, P]): # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
class BarLegacy(Generic[T, P]):
def __call__(self): ...
static_assert(is_assignable_to(Foo, Callable[..., Any]))
@@ -1064,9 +1063,7 @@ static_assert(is_assignable_to(BarLegacy, Callable[..., Any]))
class Spam[T]: ...
class SpamLegacy(Generic[T]): ...
class Eggs[T, **P]: ...
# TODO: should not error
class EggsLegacy(Generic[T, P]): ... # error: [invalid-argument-type] "`ParamSpec` is not a valid argument to `Generic`"
class EggsLegacy(Generic[T, P]): ...
static_assert(not is_assignable_to(Spam, Callable[..., Any]))
static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any]))

View File

@@ -205,3 +205,54 @@ reveal_type(bool(AmbiguousEnum2.YES)) # revealed: bool
reveal_type(bool(CustomLenEnum.NO)) # revealed: bool
reveal_type(bool(CustomLenEnum.YES)) # revealed: bool
```
## TypedDict
It may be feasible to infer `Literal[True]` for some `TypedDict` types, if `{}` can definitely be
excluded as a possible value. We currently do not attempt to do this.
If `{}` is the *only* possible value, we could infer `Literal[False]`. This might only be possible
if something like <https://peps.python.org/pep-0728> is accepted, a `TypedDict` has no defined
items, and `closed=True` is used.
```py
from typing_extensions import TypedDict, Literal, NotRequired
class Normal(TypedDict):
a: str
b: int
def _(n: Normal) -> None:
# Could be `Literal[True]`
reveal_type(bool(n)) # revealed: bool
class OnlyFalsyItems(TypedDict):
wrong: Literal[False]
def _(n: OnlyFalsyItems) -> None:
# Could be `Literal[True]` (it does not matter if all items are falsy)
reveal_type(bool(n)) # revealed: bool
class Empty(TypedDict):
pass
def _(e: Empty) -> None:
# This should be `bool`. `Literal[False]` would be wrong, as `Empty` can be subclassed.
reveal_type(bool(e)) # revealed: bool
class AllKeysOptional(TypedDict, total=False):
a: str
b: int
def _(a: AllKeysOptional) -> None:
# This should be `bool`. `Literal[True]` would be wrong as `{}` is a valid value.
reveal_type(bool(a)) # revealed: bool
class AllKeysNotRequired(TypedDict):
a: NotRequired[str]
b: NotRequired[int]
def _(a: AllKeysNotRequired) -> None:
# This should be `bool`. `Literal[True]` would be wrong as `{}` is a valid value.
reveal_type(bool(a)) # revealed: bool
```

View File

@@ -38,6 +38,29 @@ c.d = 2
c.e = 2
```
## From stubs
This is a regression test for a bug where we did not properly keep track of type qualifiers when
accessed from stub files.
`module.pyi`:
```pyi
from typing import ClassVar
class C:
a: ClassVar[int]
```
`main.py`:
```py
from module import C
c = C()
c.a = 2 # error: [invalid-attribute-access]
```
## Conflicting type qualifiers
We currently ignore conflicting qualifiers and simply union them, which is more conservative than

View File

@@ -60,6 +60,8 @@ Assignments to keys are also validated:
```py
# TODO: this should be an error
alice["name"] = None
# TODO: this should be an error
bob["name"] = None
```
Assignments to non-existing keys are disallowed:
@@ -67,6 +69,8 @@ Assignments to non-existing keys are disallowed:
```py
# TODO: this should be an error
alice["extra"] = True
# TODO: this should be an error
bob["extra"] = True
```
## Structural assignability
@@ -123,7 +127,7 @@ dangerous(alice)
reveal_type(alice["name"]) # revealed: Unknown
```
## Types of keys and values
## Methods on `TypedDict`
```py
from typing import TypedDict
@@ -133,14 +137,19 @@ class Person(TypedDict):
age: int | None
def _(p: Person) -> None:
reveal_type(p.keys()) # revealed: @Todo(Support for `TypedDict`)
reveal_type(p.values()) # revealed: @Todo(Support for `TypedDict`)
reveal_type(p.keys()) # revealed: dict_keys[str, object]
reveal_type(p.values()) # revealed: dict_values[str, object]
reveal_type(p.setdefault("name", "Alice")) # revealed: @Todo(Support for `TypedDict`)
reveal_type(p.get("name")) # revealed: @Todo(Support for `TypedDict`)
reveal_type(p.get("name", "Unknown")) # revealed: @Todo(Support for `TypedDict`)
```
## Unlike normal classes
`TypedDict` types are not like normal classes. The "attributes" can not be accessed. Neither on the
class itself, nor on inhabitants of the type defined by the class:
`TypedDict` types do not act like normal classes. For example, calling `type(..)` on an inhabitant
of a `TypedDict` type will return `dict`:
```py
from typing import TypedDict
@@ -149,11 +158,28 @@ class Person(TypedDict):
name: str
age: int | None
# TODO: this should be an error
def _(p: Person) -> None:
reveal_type(type(p)) # revealed: <class 'dict[str, object]'>
reveal_type(p.__class__) # revealed: <class 'dict[str, object]'>
```
Also, the "attributes" on the class definition can not be accessed. Neither on the class itself, nor
on inhabitants of the type defined by the class:
```py
# error: [unresolved-attribute] "Type `<class 'Person'>` has no attribute `name`"
Person.name
# TODO: this should be an error
Person(name="Alice", age=30).name
def _(P: type[Person]):
# error: [unresolved-attribute] "Type `type[Person]` has no attribute `name`"
P.name
def _(p: Person) -> None:
# error: [unresolved-attribute] "Type `Person` has no attribute `name`"
p.name
type(p).name # error: [unresolved-attribute] "Type `<class 'dict[str, object]'>` has no attribute `name`"
```
## Special properties
@@ -167,9 +193,39 @@ class Person(TypedDict):
name: str
age: int | None
reveal_type(Person.__total__) # revealed: @Todo(Support for `TypedDict`)
reveal_type(Person.__required_keys__) # revealed: @Todo(Support for `TypedDict`)
reveal_type(Person.__optional_keys__) # revealed: @Todo(Support for `TypedDict`)
reveal_type(Person.__total__) # revealed: bool
reveal_type(Person.__required_keys__) # revealed: frozenset[str]
reveal_type(Person.__optional_keys__) # revealed: frozenset[str]
```
These attributes can not be accessed on inhabitants:
```py
def _(person: Person) -> None:
person.__total__ # error: [unresolved-attribute]
person.__required_keys__ # error: [unresolved-attribute]
person.__optional_keys__ # error: [unresolved-attribute]
```
Also, they can not be accessed on `type(person)`, as that would be `dict` at runtime:
```py
def _(person: Person) -> None:
type(person).__total__ # error: [unresolved-attribute]
type(person).__required_keys__ # error: [unresolved-attribute]
type(person).__optional_keys__ # error: [unresolved-attribute]
```
But they *can* be accessed on `type[Person]`, because this function would accept the class object
`Person` as an argument:
```py
def accepts_typed_dict_class(t_person: type[Person]) -> None:
reveal_type(t_person.__total__) # revealed: bool
reveal_type(t_person.__required_keys__) # revealed: frozenset[str]
reveal_type(t_person.__optional_keys__) # revealed: frozenset[str]
accepts_typed_dict_class(Person)
```
## Subclassing
@@ -272,6 +328,9 @@ msg = Message(id=1, content="Hello")
OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True)
reveal_type(Message.__required_keys__) # revealed: @Todo(Support for `TypedDict`)
# TODO: this should be an error
msg.content
```
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html

View File

@@ -356,7 +356,9 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
for nested_symbol in self.place_tables[popped_scope_id].symbols() {
// For the same reason, symbols declared as nonlocal or global are not recorded.
// Also, if the enclosing scope allows its members to be modified from elsewhere, the snapshot will not be recorded.
if self.scopes[enclosing_scope_id].visibility().is_public() {
// (In the case of class scopes, class variables can be modified from elsewhere, but this has no effect in nested scopes,
// as class variables are not visible to them)
if self.scopes[enclosing_scope_id].kind().is_module() {
continue;
}
@@ -838,6 +840,13 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
.collect();
PatternPredicateKind::Or(predicates)
}
ast::Pattern::MatchAs(pattern) => PatternPredicateKind::As(
pattern
.pattern
.as_ref()
.map(|p| Box::new(self.predicate_kind(p))),
pattern.name.as_ref().map(|name| name.id.clone()),
),
_ => PatternPredicateKind::Unsupported,
}
}

View File

@@ -9,7 +9,7 @@
use ruff_db::files::File;
use ruff_index::{Idx, IndexVec};
use ruff_python_ast::Singleton;
use ruff_python_ast::{Singleton, name::Name};
use crate::db::Db;
use crate::semantic_index::expression::Expression;
@@ -136,6 +136,7 @@ pub(crate) enum PatternPredicateKind<'db> {
Value(Expression<'db>),
Or(Vec<PatternPredicateKind<'db>>),
Class(Expression<'db>, ClassPatternKind),
As(Option<Box<PatternPredicateKind<'db>>>, Option<Name>),
Unsupported,
}

View File

@@ -340,6 +340,10 @@ fn pattern_kind_to_type<'db>(db: &'db dyn Db, kind: &PatternPredicateKind<'db>)
PatternPredicateKind::Or(predicates) => {
UnionType::from_elements(db, predicates.iter().map(|p| pattern_kind_to_type(db, p)))
}
PatternPredicateKind::As(pattern, _) => pattern
.as_deref()
.map(|p| pattern_kind_to_type(db, p))
.unwrap_or_else(|| Type::object(db)),
PatternPredicateKind::Unsupported => Type::Never,
}
}
@@ -761,6 +765,10 @@ impl ReachabilityConstraints {
}
})
}
PatternPredicateKind::As(pattern, _) => pattern
.as_deref()
.map(|p| Self::analyze_single_pattern_predicate_kind(db, p, subject_ty))
.unwrap_or(Truthiness::AlwaysTrue),
PatternPredicateKind::Unsupported => Truthiness::Ambiguous,
}
}

View File

@@ -251,6 +251,10 @@ impl ScopeKind {
matches!(self, ScopeKind::Class)
}
pub(crate) const fn is_module(self) -> bool {
matches!(self, ScopeKind::Module)
}
pub(crate) const fn is_type_parameter(self) -> bool {
matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias)
}

View File

@@ -36,6 +36,7 @@ bitflags! {
const IS_DECLARED = 1 << 2;
const MARKED_GLOBAL = 1 << 3;
const MARKED_NONLOCAL = 1 << 4;
/// true if the symbol is assigned more than once, or if it is assigned even though it is already in use
const IS_REASSIGNED = 1 << 5;
}
}
@@ -92,7 +93,7 @@ impl Symbol {
}
pub(super) fn mark_bound(&mut self) {
if self.is_bound() {
if self.is_bound() || self.is_used() {
self.insert_flags(SymbolFlags::IS_REASSIGNED);
}

View File

@@ -223,7 +223,8 @@ impl<'db> Completion<'db> {
Type::NominalInstance(_)
| Type::PropertyInstance(_)
| Type::Tuple(_)
| Type::BoundSuper(_) => CompletionKind::Struct,
| Type::BoundSuper(_)
| Type::TypedDict(_) => CompletionKind::Struct,
Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::TypeIs(_)

View File

@@ -90,6 +90,7 @@ mod string_annotation;
mod subclass_of;
mod tuple;
mod type_ordering;
mod unification;
mod unpacker;
mod visitor;
@@ -606,6 +607,8 @@ pub enum Type<'db> {
BoundSuper(BoundSuperType<'db>),
/// A subtype of `bool` that allows narrowing in both positive and negative cases.
TypeIs(TypeIsType<'db>),
/// A type that represents an inhabitant of a `TypedDict`.
TypedDict(TypedDictType<'db>),
}
#[salsa::tracked]
@@ -667,6 +670,10 @@ impl<'db> Type<'db> {
matches!(self, Type::Dynamic(_))
}
pub(crate) const fn is_typed_dict(&self) -> bool {
matches!(self, Type::TypedDict(..))
}
/// Returns the top materialization (or upper bound materialization) of this type, which is the
/// most general form of the type that is fully static.
#[must_use]
@@ -780,6 +787,10 @@ impl<'db> Type<'db> {
Type::TypeIs(type_is) => {
type_is.with_type(db, type_is.return_type(db).materialize(db, variance))
}
Type::TypedDict(_) => {
// TODO: Materialization of gradual TypedDicts
*self
}
}
}
@@ -790,6 +801,13 @@ impl<'db> Type<'db> {
}
}
pub const fn into_subclass_of(self) -> Option<SubclassOfType<'db>> {
match self {
Type::SubclassOf(subclass_of) => Some(subclass_of),
_ => None,
}
}
#[track_caller]
pub fn expect_class_literal(self) -> ClassLiteral<'db> {
self.into_class_literal()
@@ -1073,6 +1091,12 @@ impl<'db> Type<'db> {
// Always normalize single-member enums to their class instance (`Literal[Single.VALUE]` => `Single`)
enum_literal.enum_class_instance(db)
}
Type::TypedDict(_) => {
// TODO: Normalize TypedDicts
self
}
Type::LiteralString
| Type::AlwaysFalsy
| Type::AlwaysTruthy
@@ -1121,7 +1145,8 @@ impl<'db> Type<'db> {
| Type::AlwaysTruthy
| Type::PropertyInstance(_)
// might inherit `Any`, but subtyping is still reflexive
| Type::ClassLiteral(_) => true,
| Type::ClassLiteral(_)
=> true,
Type::Dynamic(_)
| Type::NominalInstance(_)
| Type::ProtocolInstance(_)
@@ -1133,7 +1158,8 @@ impl<'db> Type<'db> {
| Type::Tuple(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::TypeIs(_) => false,
| Type::TypeIs(_)
| Type::TypedDict(_) => false,
}
}
@@ -1196,7 +1222,8 @@ impl<'db> Type<'db> {
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::Tuple(_)
| Type::TypeIs(_) => None,
| Type::TypeIs(_)
| Type::TypedDict(_) => None,
// TODO
Type::MethodWrapper(_)
@@ -1290,6 +1317,11 @@ impl<'db> Type<'db> {
field.default_type(db).has_relation_to(db, right, relation)
}
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
// TODO: Implement assignability and subtyping for TypedDict
relation.is_assignability()
}
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
@@ -1754,6 +1786,11 @@ impl<'db> Type<'db> {
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => false,
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
// TODO: Implement disjointness for TypedDict
false
}
// A typevar is never disjoint from itself, since all occurrences of the typevar must
// be specialized to the same type. (This is an important difference between typevars
// and `Any`!) Different typevars might be disjoint, depending on their bounds and
@@ -2367,6 +2404,7 @@ impl<'db> Type<'db> {
}
Type::AlwaysTruthy | Type::AlwaysFalsy => false,
Type::TypeIs(type_is) => type_is.is_bound(db),
Type::TypedDict(_) => false,
}
}
@@ -2455,7 +2493,8 @@ impl<'db> Type<'db> {
| Type::Callable(_)
| Type::PropertyInstance(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_) => false,
| Type::DataclassTransformer(_)
| Type::TypedDict(_) => false,
}
}
@@ -2495,6 +2534,10 @@ impl<'db> Type<'db> {
Type::Dynamic(_) | Type::Never => Some(Place::bound(self).into()),
Type::ClassLiteral(class) if class.is_typed_dict(db) => {
Some(class.typed_dict_member(db, None, name, policy))
}
Type::ClassLiteral(class) => {
match (class.known(db), name) {
(Some(KnownClass::FunctionType), "__get__") => Some(
@@ -2524,6 +2567,10 @@ impl<'db> Type<'db> {
}
}
Type::GenericAlias(alias) if alias.is_typed_dict(db) => {
Some(alias.origin(db).typed_dict_member(db, None, name, policy))
}
Type::GenericAlias(alias) => {
Some(ClassType::from(*alias).class_member(db, name, policy))
}
@@ -2575,7 +2622,8 @@ impl<'db> Type<'db> {
| Type::NominalInstance(_)
| Type::ProtocolInstance(_)
| Type::PropertyInstance(_)
| Type::TypeIs(_) => None,
| Type::TypeIs(_)
| Type::TypedDict(_) => None,
}
}
@@ -2732,6 +2780,8 @@ impl<'db> Type<'db> {
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => {
Place::Unbound.into()
}
Type::TypedDict(_) => Place::Unbound.into(),
}
}
@@ -3063,7 +3113,7 @@ impl<'db> Type<'db> {
) -> PlaceAndQualifiers<'db> {
tracing::trace!("member_lookup_with_policy: {}.{}", self.display(db), name);
if name == "__class__" {
return Place::bound(self.to_meta_type(db)).into();
return Place::bound(self.dunder_class(db)).into();
}
let name_str = name.as_str();
@@ -3221,7 +3271,8 @@ impl<'db> Type<'db> {
| Type::FunctionLiteral(..)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::TypeIs(..) => {
| Type::TypeIs(..)
| Type::TypedDict(_) => {
let fallback = self.instance_member(db, name_str);
let result = self.invoke_descriptor_protocol(
@@ -3279,6 +3330,12 @@ impl<'db> Type<'db> {
.into()
};
if result.is_class_var() && self.is_typed_dict() {
// `ClassVar`s on `TypedDictFallback` can not be accessed on inhabitants of `SomeTypedDict`.
// They can only be accessed on `SomeTypedDict` directly.
return Place::Unbound.into();
}
match result {
member @ PlaceAndQualifiers {
place: Place::Type(_, Boundness::Bound),
@@ -3507,6 +3564,12 @@ impl<'db> Type<'db> {
| Type::LiteralString
| Type::TypeIs(_) => Truthiness::Ambiguous,
Type::TypedDict(_) => {
// TODO: We could do better here, but it's unclear if this is important.
// See existing `TypedDict`-related tests in `truthiness.md`
Truthiness::Ambiguous
}
Type::FunctionLiteral(_)
| Type::BoundMethod(_)
| Type::WrapperDescriptor(_)
@@ -3801,7 +3864,7 @@ impl<'db> Type<'db> {
db,
[
KnownClass::Str.to_instance(db),
TupleType::homogeneous(db, KnownClass::Str.to_instance(db)),
Type::homogeneous_tuple(db, KnownClass::Str.to_instance(db)),
],
)),
Parameter::positional_only(Some(Name::new_static("start")))
@@ -4114,7 +4177,7 @@ impl<'db> Type<'db> {
Parameter::positional_only(Some(Name::new_static("name")))
.with_annotated_type(str_instance),
Parameter::positional_only(Some(Name::new_static("bases")))
.with_annotated_type(TupleType::homogeneous(
.with_annotated_type(Type::homogeneous_tuple(
db,
type_instance,
)),
@@ -4304,7 +4367,7 @@ impl<'db> Type<'db> {
.with_annotated_type(Type::any())
.type_form(),
Parameter::keyword_only(Name::new_static("type_params"))
.with_annotated_type(TupleType::homogeneous(
.with_annotated_type(Type::homogeneous_tuple(
db,
UnionType::from_elements(
db,
@@ -4315,7 +4378,7 @@ impl<'db> Type<'db> {
],
),
))
.with_default_type(TupleType::empty(db)),
.with_default_type(Type::empty_tuple(db)),
]),
None,
),
@@ -4401,7 +4464,7 @@ impl<'db> Type<'db> {
CallableBinding::from_overloads(
self,
[
Signature::new(Parameters::empty(), Some(TupleType::empty(db))),
Signature::new(Parameters::empty(), Some(Type::empty_tuple(db))),
Signature::new(
Parameters::new([Parameter::positional_only(Some(
Name::new_static("iterable"),
@@ -4409,7 +4472,7 @@ impl<'db> Type<'db> {
.with_annotated_type(
KnownClass::Iterable.to_specialized_instance(db, [object]),
)]),
Some(TupleType::homogeneous(db, object)),
Some(Type::homogeneous_tuple(db, object)),
),
],
)
@@ -4545,7 +4608,8 @@ impl<'db> Type<'db> {
| Type::Tuple(_)
| Type::BoundSuper(_)
| Type::ModuleLiteral(_)
| Type::TypeIs(_) => CallableBinding::not_callable(self).into(),
| Type::TypeIs(_)
| Type::TypedDict(_) => CallableBinding::not_callable(self).into(),
}
}
@@ -5160,7 +5224,8 @@ impl<'db> Type<'db> {
| Type::BoundSuper(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::TypeIs(_) => None,
| Type::TypeIs(_)
| Type::TypedDict(_) => None,
}
}
@@ -5199,10 +5264,16 @@ impl<'db> Type<'db> {
KnownClass::Float.to_instance(db),
],
),
_ if class.is_typed_dict(db) => {
Type::TypedDict(TypedDictType::new(db, ClassType::NonGeneric(*class)))
}
_ => Type::instance(db, class.default_specialization(db)),
};
Ok(ty)
}
Type::GenericAlias(alias) if alias.is_typed_dict(db) => Ok(Type::TypedDict(
TypedDictType::new(db, ClassType::from(*alias)),
)),
Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))),
Type::SubclassOf(_)
@@ -5228,7 +5299,8 @@ impl<'db> Type<'db> {
| Type::BoundSuper(_)
| Type::ProtocolInstance(_)
| Type::PropertyInstance(_)
| Type::TypeIs(_) => Err(InvalidTypeExpressionError {
| Type::TypeIs(_)
| Type::TypedDict(_) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec_inline![
InvalidTypeExpression::InvalidType(*self, scope_id)
],
@@ -5267,7 +5339,7 @@ impl<'db> Type<'db> {
// We treat `typing.Type` exactly the same as `builtins.type`:
SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)),
SpecialFormType::Tuple => Ok(TupleType::homogeneous(db, Type::unknown())),
SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())),
// Legacy `typing` aliases
SpecialFormType::List => Ok(KnownClass::List.to_instance(db)),
@@ -5458,7 +5530,7 @@ impl<'db> Type<'db> {
Type::Union(UnionType::new(db, elements))
};
TupleType::from_elements(
Type::heterogeneous_tuple(
db,
[
Type::IntLiteral(python_version.major.into()),
@@ -5472,6 +5544,9 @@ impl<'db> Type<'db> {
/// Given a type that is assumed to represent an instance of a class,
/// return a type that represents that class itself.
///
/// Note: the return type of `type(obj)` is subtly different from this.
/// See `Self::dunder_class` for more details.
#[must_use]
pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> {
match self {
@@ -5496,10 +5571,8 @@ impl<'db> Type<'db> {
Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
Type::Tuple(tuple) => tuple
.to_class_type(db)
.map(Type::from)
.unwrap_or_else(Type::unknown),
.to_subclass_of(db)
.unwrap_or_else(SubclassOfType::subclass_of_unknown),
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => KnownClass::Type.to_instance(db),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db),
@@ -5532,9 +5605,27 @@ impl<'db> Type<'db> {
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db),
Type::ProtocolInstance(protocol) => protocol.to_meta_type(db),
Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class(db)),
}
}
/// Get the type of the `__class__` attribute of this type.
///
/// For most types, this is equivalent to the meta type of this type. For `TypedDict` types,
/// this returns `type[dict[str, object]]` instead, because inhabitants of a `TypedDict` are
/// instances of `dict` at runtime.
#[must_use]
pub fn dunder_class(self, db: &'db dyn Db) -> Type<'db> {
if self.is_typed_dict() {
return KnownClass::Dict
.to_specialized_class_type(db, [KnownClass::Str.to_instance(db), Type::object(db)])
.map(Type::from)
.unwrap_or_else(Type::unknown);
}
self.to_meta_type(db)
}
#[must_use]
pub fn apply_optional_specialization(
self,
@@ -5632,6 +5723,10 @@ impl<'db> Type<'db> {
Type::GenericAlias(generic.apply_type_mapping(db, type_mapping))
}
Type::TypedDict(typed_dict) => {
Type::TypedDict(typed_dict.apply_type_mapping(db, type_mapping))
}
Type::SubclassOf(subclass_of) => Type::SubclassOf(
subclass_of.apply_type_mapping(db, type_mapping),
),
@@ -5791,7 +5886,8 @@ impl<'db> Type<'db> {
| Type::EnumLiteral(_)
| Type::BoundSuper(_)
| Type::SpecialForm(_)
| Type::KnownInstance(_) => {}
| Type::KnownInstance(_)
| Type::TypedDict(_) => {}
}
}
@@ -5906,6 +6002,10 @@ impl<'db> Type<'db> {
Protocol::Synthesized(_) => None,
},
Type::TypedDict(typed_dict) => {
Some(TypeDefinition::Class(typed_dict.defining_class(db).definition(db)))
}
Self::Union(_) | Self::Intersection(_) => None,
// These types have no definition
@@ -6250,9 +6350,6 @@ pub enum DynamicType {
/// A special Todo-variant for type aliases declared using `typing.TypeAlias`.
/// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions.
TodoTypeAlias,
/// A special Todo-variant for classes inheriting from `TypedDict`.
/// A temporary variant to avoid false positives while we wait for full support.
TodoTypedDict,
}
impl DynamicType {
@@ -6284,13 +6381,6 @@ impl std::fmt::Display for DynamicType {
f.write_str("@Todo")
}
}
DynamicType::TodoTypedDict => {
if cfg!(debug_assertions) {
f.write_str("@Todo(Support for `TypedDict`)")
} else {
f.write_str("@Todo")
}
}
}
}
}
@@ -8581,6 +8671,21 @@ pub(super) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>
}
impl<'db> IntersectionType<'db> {
/// Create an intersection from a list of elements
/// (which may be eagerly simplified into a different variant of [`Type`] altogether).
pub fn from_elements<I, T>(db: &'db dyn Db, elements: I) -> Type<'db>
where
I: IntoIterator<Item = T>,
T: Into<Type<'db>>,
{
elements
.into_iter()
.fold(IntersectionBuilder::new(db), |builder, element| {
builder.add_positive(element.into())
})
.build()
}
/// Return a new `IntersectionType` instance with the positive and negative types sorted
/// according to a canonical ordering, and other normalizations applied to each element as applicable.
///
@@ -8745,6 +8850,10 @@ impl<'db> IntersectionType<'db> {
self.positive(db).iter().copied()
}
pub fn iter_negative(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {
self.negative(db).iter().copied()
}
pub fn has_one_element(&self, db: &'db dyn Db) -> bool {
(self.positive(db).len() + self.negative(db).len()) == 1
}
@@ -8825,6 +8934,40 @@ impl<'db> EnumLiteralType<'db> {
}
}
/// Type that represents the set of all inhabitants (`dict` instances) that conform to
/// a given `TypedDict` schema.
#[salsa::interned(debug)]
#[derive(PartialOrd, Ord)]
pub struct TypedDictType<'db> {
/// A reference to the class (inheriting from `typing.TypedDict`) that specifies the
/// schema of this `TypedDict`.
defining_class: ClassType<'db>,
}
// The Salsa heap is tracked separately.
impl get_size2::GetSize for TypedDictType<'_> {}
impl<'db> TypedDictType<'db> {
pub(crate) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
) -> Self {
Self::new(
db,
self.defining_class(db).apply_type_mapping(db, type_mapping),
)
}
}
fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
db: &'db dyn Db,
typed_dict: TypedDictType<'db>,
visitor: &mut V,
) {
visitor.visit_type(db, typed_dict.defining_class(db).into());
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum BoundSuperError<'db> {
InvalidPivotClassType {

View File

@@ -6,7 +6,7 @@ use ruff_python_ast as ast;
use crate::Db;
use crate::types::KnownClass;
use crate::types::enums::enum_member_literals;
use crate::types::tuple::{TupleLength, TupleSpec, TupleType};
use crate::types::tuple::{TupleLength, TupleSpec};
use super::Type;
@@ -246,7 +246,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
}
})
.multi_cartesian_product()
.map(|types| TupleType::from_elements(db, types))
.map(|types| Type::heterogeneous_tuple(db, types))
.collect::<Vec<_>>();
if expanded.len() == 1 {
// There are no elements in the tuple type that can be expanded.
@@ -306,17 +306,17 @@ mod tests {
let false_ty = Type::BooleanLiteral(false);
// Empty tuple
let empty_tuple = TupleType::empty(&db);
let empty_tuple = Type::empty_tuple(&db);
let expanded = expand_type(&db, empty_tuple);
assert!(expanded.is_none());
// None of the elements can be expanded.
let tuple_type1 = TupleType::from_elements(&db, [int_ty, str_ty]);
let tuple_type1 = Type::heterogeneous_tuple(&db, [int_ty, str_ty]);
let expanded = expand_type(&db, tuple_type1);
assert!(expanded.is_none());
// All elements can be expanded.
let tuple_type2 = TupleType::from_elements(
let tuple_type2 = Type::heterogeneous_tuple(
&db,
[
bool_ty,
@@ -324,18 +324,18 @@ mod tests {
],
);
let expected_types = [
TupleType::from_elements(&db, [true_ty, int_ty]),
TupleType::from_elements(&db, [true_ty, str_ty]),
TupleType::from_elements(&db, [true_ty, bytes_ty]),
TupleType::from_elements(&db, [false_ty, int_ty]),
TupleType::from_elements(&db, [false_ty, str_ty]),
TupleType::from_elements(&db, [false_ty, bytes_ty]),
Type::heterogeneous_tuple(&db, [true_ty, int_ty]),
Type::heterogeneous_tuple(&db, [true_ty, str_ty]),
Type::heterogeneous_tuple(&db, [true_ty, bytes_ty]),
Type::heterogeneous_tuple(&db, [false_ty, int_ty]),
Type::heterogeneous_tuple(&db, [false_ty, str_ty]),
Type::heterogeneous_tuple(&db, [false_ty, bytes_ty]),
];
let expanded = expand_type(&db, tuple_type2).unwrap();
assert_eq!(expanded, expected_types);
// Mixed set of elements where some can be expanded while others cannot be.
let tuple_type3 = TupleType::from_elements(
let tuple_type3 = Type::heterogeneous_tuple(
&db,
[
bool_ty,
@@ -345,21 +345,21 @@ mod tests {
],
);
let expected_types = [
TupleType::from_elements(&db, [true_ty, int_ty, str_ty, str_ty]),
TupleType::from_elements(&db, [true_ty, int_ty, bytes_ty, str_ty]),
TupleType::from_elements(&db, [false_ty, int_ty, str_ty, str_ty]),
TupleType::from_elements(&db, [false_ty, int_ty, bytes_ty, str_ty]),
Type::heterogeneous_tuple(&db, [true_ty, int_ty, str_ty, str_ty]),
Type::heterogeneous_tuple(&db, [true_ty, int_ty, bytes_ty, str_ty]),
Type::heterogeneous_tuple(&db, [false_ty, int_ty, str_ty, str_ty]),
Type::heterogeneous_tuple(&db, [false_ty, int_ty, bytes_ty, str_ty]),
];
let expanded = expand_type(&db, tuple_type3).unwrap();
assert_eq!(expanded, expected_types);
// Variable-length tuples are not expanded.
let variable_length_tuple = TupleType::mixed(
let variable_length_tuple = Type::tuple(TupleType::mixed(
&db,
[bool_ty],
int_ty,
[UnionType::from_elements(&db, [str_ty, bytes_ty]), str_ty],
);
));
let expanded = expand_type(&db, variable_length_tuple);
assert!(expanded.is_none());
}

View File

@@ -390,9 +390,9 @@ impl<'db> Bindings<'db> {
);
}
Some("__constraints__") => {
overload.set_return_type(TupleType::from_elements(
overload.set_return_type(Type::heterogeneous_tuple(
db,
typevar.constraints(db).into_iter().flatten().copied(),
typevar.constraints(db).into_iter().flatten(),
));
}
Some("__default__") => {
@@ -674,7 +674,7 @@ impl<'db> Bindings<'db> {
Some(names) => {
let mut names = names.iter().collect::<Vec<_>>();
names.sort();
TupleType::from_elements(
Type::heterogeneous_tuple(
db,
names.iter().map(|name| {
Type::string_literal(db, name.as_str())
@@ -694,7 +694,7 @@ impl<'db> Bindings<'db> {
let return_ty = match ty {
Type::ClassLiteral(class) => {
if let Some(metadata) = enums::enum_metadata(db, *class) {
TupleType::from_elements(
Type::heterogeneous_tuple(
db,
metadata
.members
@@ -714,7 +714,7 @@ impl<'db> Bindings<'db> {
Some(KnownFunction::AllMembers) => {
if let [Some(ty)] = overload.parameter_types() {
overload.set_return_type(TupleType::from_elements(
overload.set_return_type(Type::heterogeneous_tuple(
db,
ide_support::all_members(db, *ty)
.into_iter()
@@ -1010,7 +1010,7 @@ impl<'db> Bindings<'db> {
Some(KnownClass::Type) if overload_index == 0 => {
if let [Some(arg)] = overload.parameter_types() {
overload.set_return_type(arg.to_meta_type(db));
overload.set_return_type(arg.dunder_class(db));
}
}
@@ -1458,7 +1458,7 @@ impl<'db> CallableBinding<'db> {
}
let top_materialized_argument_type =
TupleType::from_elements(db, top_materialized_argument_types);
Type::heterogeneous_tuple(db, top_materialized_argument_types);
// A flag to indicate whether we've found the overload that makes the remaining overloads
// unmatched for the given argument types.
@@ -1494,7 +1494,7 @@ impl<'db> CallableBinding<'db> {
parameter_types.push(UnionType::from_elements(db, current_parameter_types));
}
if top_materialized_argument_type
.is_assignable_to(db, TupleType::from_elements(db, parameter_types))
.is_assignable_to(db, Type::heterogeneous_tuple(db, parameter_types))
{
filter_remaining_overloads = true;
}

View File

@@ -20,12 +20,12 @@ use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::generics::{GenericContext, Specialization, walk_specialization};
use crate::types::infer::nearest_enclosing_class;
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::tuple::TupleSpec;
use crate::types::{
BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams,
DeprecatedInstance, DynamicType, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation,
DeprecatedInstance, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation,
TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type,
infer_definition_types,
infer_definition_types, todo_type,
};
use crate::{
Db, FxIndexMap, FxOrderSet, Program,
@@ -112,6 +112,21 @@ fn try_mro_cycle_initial<'db>(
))
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_typed_dict_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &bool,
_count: u32,
_self: ClassLiteral<'db>,
) -> salsa::CycleRecoveryAction<bool> {
salsa::CycleRecoveryAction::Iterate
}
#[allow(clippy::unnecessary_wraps)]
fn is_typed_dict_cycle_initial<'db>(_db: &'db dyn Db, _self: ClassLiteral<'db>) -> bool {
false
}
fn try_metaclass_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &Result<(Type<'db>, Option<DataclassTransformerParams>), MetaclassError<'db>>,
@@ -140,6 +155,8 @@ pub(crate) enum CodeGeneratorKind {
DataclassLike,
/// Classes inheriting from `typing.NamedTuple`
NamedTuple,
/// Classes inheriting from `typing.TypedDict`
TypedDict,
}
impl CodeGeneratorKind {
@@ -148,6 +165,8 @@ impl CodeGeneratorKind {
Some(CodeGeneratorKind::DataclassLike)
} else if CodeGeneratorKind::NamedTuple.matches(db, class) {
Some(CodeGeneratorKind::NamedTuple)
} else if CodeGeneratorKind::TypedDict.matches(db, class) {
Some(CodeGeneratorKind::TypedDict)
} else {
None
}
@@ -165,6 +184,7 @@ impl CodeGeneratorKind {
base.into_class_literal()
.is_some_and(|c| c.is_known(db, KnownClass::NamedTuple))
}),
Self::TypedDict => class.is_typed_dict(db),
}
}
}
@@ -238,6 +258,10 @@ impl<'db> GenericAlias<'db> {
// look in `self.tuple`.
self.specialization(db).find_legacy_typevars(db, typevars);
}
pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
self.origin(db).is_typed_dict(db)
}
}
impl<'db> From<GenericAlias<'db>> for Type<'db> {
@@ -268,6 +292,10 @@ pub enum ClassType<'db> {
#[salsa::tracked]
impl<'db> ClassType<'db> {
pub(super) const fn is_not_generic(self) -> bool {
matches!(self, Self::NonGeneric(_))
}
pub(super) fn normalized_impl(
self,
db: &'db dyn Db,
@@ -419,15 +447,6 @@ impl<'db> ClassType<'db> {
other: Self,
relation: TypeRelation,
) -> bool {
// TODO: remove this branch once we have proper support for TypedDicts.
if self.is_known(db, KnownClass::Dict)
&& other
.iter_mro(db)
.any(|b| matches!(b, ClassBase::Dynamic(DynamicType::TodoTypedDict)))
{
return true;
}
self.iter_mro(db).any(|base| {
match base {
ClassBase::Dynamic(_) => match relation {
@@ -451,6 +470,11 @@ impl<'db> ClassType<'db> {
(ClassType::Generic(_), ClassType::NonGeneric(_))
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
},
ClassBase::TypedDict => {
// TODO: Implement subclassing and assignability for TypedDicts.
true
}
}
})
}
@@ -778,7 +802,7 @@ impl<'db> ClassType<'db> {
overload_signatures.push(synthesize_getitem_overload_signature(
KnownClass::Slice.to_instance(db),
TupleType::homogeneous(db, all_elements_unioned),
Type::homogeneous_tuple(db, all_elements_unioned),
));
let getitem_signature =
@@ -841,7 +865,8 @@ impl<'db> ClassType<'db> {
// - an unspecialized tuple
// - a tuple with no minimum length
if specialization.is_none_or(|spec| spec.tuple(db).len().minimum() == 0) {
iterable_parameter = iterable_parameter.with_default_type(TupleType::empty(db));
iterable_parameter =
iterable_parameter.with_default_type(Type::empty_tuple(db));
}
let parameters = Parameters::new([
@@ -868,6 +893,11 @@ impl<'db> ClassType<'db> {
/// See [`Type::instance_member`] for more details.
pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
let (class_literal, specialization) = self.class_literal(db);
if class_literal.is_typed_dict(db) {
return Place::Unbound.into();
}
class_literal
.instance_member(db, specialization, name)
.map_type(|ty| ty.apply_optional_specialization(db, specialization))
@@ -1452,6 +1482,22 @@ impl<'db> ClassLiteral<'db> {
.contains(&ClassBase::Class(other))
}
/// Return `true` if this class constitutes a typed dict specification (inherits from
/// `typing.TypedDict`, either directly or indirectly).
#[salsa::tracked(
cycle_fn=is_typed_dict_cycle_recover,
cycle_initial=is_typed_dict_cycle_initial,
heap_size=get_size2::heap_size
)]
pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
if let Some(known) = self.known(db) {
return known.is_typed_dict_subclass();
}
self.iter_mro(db, None)
.any(|base| matches!(base, ClassBase::TypedDict))
}
/// Return the explicit `metaclass` of this class, if one is defined.
///
/// ## Note
@@ -1533,7 +1579,7 @@ impl<'db> ClassLiteral<'db> {
}
} else {
let name = Type::string_literal(db, self.name(db));
let bases = TupleType::from_elements(db, self.explicit_bases(db).iter().copied());
let bases = Type::heterogeneous_tuple(db, self.explicit_bases(db));
let namespace = KnownClass::Dict
.to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()]);
@@ -1624,8 +1670,8 @@ impl<'db> ClassLiteral<'db> {
policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
if name == "__mro__" {
let tuple_elements = self.iter_mro(db, specialization).map(Type::from);
return Place::bound(TupleType::from_elements(db, tuple_elements)).into();
let tuple_elements = self.iter_mro(db, specialization);
return Place::bound(Type::heterogeneous_tuple(db, tuple_elements)).into();
}
self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization))
@@ -1687,6 +1733,12 @@ impl<'db> ClassLiteral<'db> {
)
});
}
ClassBase::TypedDict => {
return KnownClass::TypedDictFallback
.to_class_literal(db)
.find_name_in_mro_with_policy(db, name, policy)
.expect("Will return Some() when called on class literal");
}
}
if lookup_result.is_ok() {
break;
@@ -1787,7 +1839,7 @@ impl<'db> ClassLiteral<'db> {
/// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or
/// a synthesized `__new__` method for a `NamedTuple`.
fn own_synthesized_member(
pub(super) fn own_synthesized_member(
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
@@ -1971,10 +2023,118 @@ impl<'db> ClassLiteral<'db> {
}
None
}
(CodeGeneratorKind::TypedDict, "__setitem__") => {
// TODO: synthesize a set of overloads with precise types
let signature = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key"))),
Parameter::positional_only(Some(Name::new_static("value"))),
]),
Some(Type::none(db)),
);
Some(CallableType::function_like(db, signature))
}
(CodeGeneratorKind::TypedDict, "__getitem__") => {
// TODO: synthesize a set of overloads with precise types
let signature = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key"))),
]),
Some(todo_type!("Support for `TypedDict`")),
);
Some(CallableType::function_like(db, signature))
}
(CodeGeneratorKind::TypedDict, "get") => {
// TODO: synthesize a set of overloads with precise types
let signature = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key"))),
Parameter::positional_only(Some(Name::new_static("default")))
.with_default_type(Type::unknown()),
]),
Some(todo_type!("Support for `TypedDict`")),
);
Some(CallableType::function_like(db, signature))
}
(CodeGeneratorKind::TypedDict, "pop") => {
// TODO: synthesize a set of overloads with precise types.
// Required keys should be forbidden to be popped.
let signature = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key"))),
Parameter::positional_only(Some(Name::new_static("default")))
.with_default_type(Type::unknown()),
]),
Some(todo_type!("Support for `TypedDict`")),
);
Some(CallableType::function_like(db, signature))
}
(CodeGeneratorKind::TypedDict, "setdefault") => {
// TODO: synthesize a set of overloads with precise types
let signature = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key"))),
Parameter::positional_only(Some(Name::new_static("default"))),
]),
Some(todo_type!("Support for `TypedDict`")),
);
Some(CallableType::function_like(db, signature))
}
(CodeGeneratorKind::TypedDict, "update") => {
// TODO: synthesize a set of overloads with precise types
let signature = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::variadic(Name::new_static("args")),
Parameter::keyword_variadic(Name::new_static("kwargs")),
]),
Some(Type::none(db)),
);
Some(CallableType::function_like(db, signature))
}
_ => None,
}
}
/// Member lookup for classes that inherit from `typing.TypedDict`.
///
/// This is implemented as a separate method because the item definitions on a `TypedDict`-based
/// class are *not* accessible as class members. Instead, this mostly defers to `TypedDictFallback`,
/// unless `name` corresponds to one of the specialized synthetic members like `__getitem__`.
pub(crate) fn typed_dict_member(
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
name: &str,
policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
if let Some(member) = self.own_synthesized_member(db, specialization, name) {
Place::bound(member).into()
} else {
KnownClass::TypedDictFallback
.to_class_literal(db)
.find_name_in_mro_with_policy(db, name, policy)
.expect("`find_name_in_mro_with_policy` will return `Some()` when called on class literal")
}
}
/// Returns a list of all annotated attributes defined in this class, or any of its superclasses.
///
/// See [`ClassLiteral::own_fields`] for more details.
@@ -2105,6 +2265,10 @@ impl<'db> ClassLiteral<'db> {
specialization: Option<Specialization<'db>>,
name: &str,
) -> PlaceAndQualifiers<'db> {
if self.is_typed_dict(db) {
return Place::Unbound.into();
}
let mut union = UnionBuilder::new(db);
let mut union_qualifiers = TypeQualifiers::empty();
@@ -2142,6 +2306,9 @@ impl<'db> ClassLiteral<'db> {
union = union.add(ty);
}
}
ClassBase::TypedDict => {
return Place::bound(todo_type!("Support for `TypedDict`")).into();
}
}
}
@@ -2857,6 +3024,7 @@ pub enum KnownClass {
InitVar,
// _typeshed._type_checker_internals
NamedTupleFallback,
TypedDictFallback,
}
impl KnownClass {
@@ -2953,7 +3121,8 @@ impl KnownClass {
| Self::Field
| Self::KwOnly
| Self::InitVar
| Self::NamedTupleFallback => Truthiness::Ambiguous,
| Self::NamedTupleFallback
| Self::TypedDictFallback => Truthiness::Ambiguous,
}
}
@@ -3035,6 +3204,7 @@ impl KnownClass {
| Self::SupportsIndex
| Self::NamedTuple
| Self::NamedTupleFallback
| Self::TypedDictFallback
| Self::Counter
| Self::DefaultDict
| Self::OrderedDict
@@ -3118,7 +3288,85 @@ impl KnownClass {
| KnownClass::Field
| KnownClass::KwOnly
| KnownClass::InitVar
| KnownClass::NamedTupleFallback => false,
| KnownClass::NamedTupleFallback
| KnownClass::TypedDictFallback => false,
}
}
/// Return `true` if this class is a (true) subclass of `typing.TypedDict`.
pub(crate) const fn is_typed_dict_subclass(self) -> bool {
match self {
KnownClass::Bool
| KnownClass::Object
| KnownClass::Bytes
| KnownClass::Bytearray
| KnownClass::Type
| KnownClass::Int
| KnownClass::Float
| KnownClass::Complex
| KnownClass::Str
| KnownClass::List
| KnownClass::Tuple
| KnownClass::Set
| KnownClass::FrozenSet
| KnownClass::Dict
| KnownClass::Slice
| KnownClass::Property
| KnownClass::BaseException
| KnownClass::Exception
| KnownClass::BaseExceptionGroup
| KnownClass::ExceptionGroup
| KnownClass::Staticmethod
| KnownClass::Classmethod
| KnownClass::Awaitable
| KnownClass::Generator
| KnownClass::Deprecated
| KnownClass::Super
| KnownClass::Enum
| KnownClass::EnumType
| KnownClass::Auto
| KnownClass::Member
| KnownClass::Nonmember
| KnownClass::ABCMeta
| KnownClass::GenericAlias
| KnownClass::ModuleType
| KnownClass::FunctionType
| KnownClass::MethodType
| KnownClass::MethodWrapperType
| KnownClass::WrapperDescriptorType
| KnownClass::UnionType
| KnownClass::GeneratorType
| KnownClass::AsyncGeneratorType
| KnownClass::CoroutineType
| KnownClass::NoneType
| KnownClass::Any
| KnownClass::StdlibAlias
| KnownClass::SpecialForm
| KnownClass::TypeVar
| KnownClass::ParamSpec
| KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs
| KnownClass::TypeVarTuple
| KnownClass::TypeAliasType
| KnownClass::NoDefaultType
| KnownClass::NamedTuple
| KnownClass::NewType
| KnownClass::SupportsIndex
| KnownClass::Iterable
| KnownClass::Iterator
| KnownClass::ChainMap
| KnownClass::Counter
| KnownClass::DefaultDict
| KnownClass::Deque
| KnownClass::OrderedDict
| KnownClass::VersionInfo
| KnownClass::EllipsisType
| KnownClass::NotImplementedType
| KnownClass::Field
| KnownClass::KwOnly
| KnownClass::InitVar
| KnownClass::NamedTupleFallback
| KnownClass::TypedDictFallback => false,
}
}
@@ -3206,7 +3454,8 @@ impl KnownClass {
| Self::Field
| Self::KwOnly
| Self::InitVar
| Self::NamedTupleFallback => false,
| Self::NamedTupleFallback
| Self::TypedDictFallback => false,
}
}
@@ -3302,6 +3551,7 @@ impl KnownClass {
Self::KwOnly => "KW_ONLY",
Self::InitVar => "InitVar",
Self::NamedTupleFallback => "NamedTupleFallback",
Self::TypedDictFallback => "TypedDictFallback",
}
}
@@ -3558,7 +3808,7 @@ impl KnownClass {
| Self::Deque
| Self::OrderedDict => KnownModule::Collections,
Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses,
Self::NamedTupleFallback => KnownModule::TypeCheckerInternals,
Self::NamedTupleFallback | Self::TypedDictFallback => KnownModule::TypeCheckerInternals,
}
}
@@ -3635,7 +3885,8 @@ impl KnownClass {
| Self::InitVar
| Self::Iterable
| Self::Iterator
| Self::NamedTupleFallback => false,
| Self::NamedTupleFallback
| Self::TypedDictFallback => false,
}
}
@@ -3714,7 +3965,8 @@ impl KnownClass {
| Self::InitVar
| Self::Iterable
| Self::Iterator
| Self::NamedTupleFallback => false,
| Self::NamedTupleFallback
| Self::TypedDictFallback => false,
}
}
@@ -3804,6 +4056,7 @@ impl KnownClass {
"KW_ONLY" => Self::KwOnly,
"InitVar" => Self::InitVar,
"NamedTupleFallback" => Self::NamedTupleFallback,
"TypedDictFallback" => Self::TypedDictFallback,
_ => return None,
};
@@ -3868,6 +4121,7 @@ impl KnownClass {
| Self::KwOnly
| Self::InitVar
| Self::NamedTupleFallback
| Self::TypedDictFallback
| Self::Awaitable
| Self::Generator => module == self.canonical_module(db),
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
@@ -4422,22 +4676,41 @@ mod tests {
fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() {
let mut db = setup_db();
for class in KnownClass::iter() {
let version_added = match class {
KnownClass::UnionType => PythonVersion::PY310,
KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => PythonVersion::PY311,
KnownClass::GenericAlias => PythonVersion::PY39,
KnownClass::KwOnly => PythonVersion::PY310,
KnownClass::Member | KnownClass::Nonmember => PythonVersion::PY311,
_ => PythonVersion::PY37,
};
// First, collect the `KnownClass` variants
// and sort them according to the version they were added in.
// This makes the test far faster as it minimizes the number of times
// we need to change the Python version in the loop.
let mut classes: Vec<(KnownClass, PythonVersion)> = KnownClass::iter()
.map(|class| {
let version_added = match class {
KnownClass::UnionType => PythonVersion::PY310,
KnownClass::BaseExceptionGroup | KnownClass::ExceptionGroup => {
PythonVersion::PY311
}
KnownClass::GenericAlias => PythonVersion::PY39,
KnownClass::KwOnly => PythonVersion::PY310,
KnownClass::Member | KnownClass::Nonmember => PythonVersion::PY311,
_ => PythonVersion::PY37,
};
(class, version_added)
})
.collect();
Program::get(&db)
.set_python_version_with_source(&mut db)
.to(PythonVersionWithSource {
version: version_added,
source: PythonVersionSource::default(),
});
classes.sort_unstable_by_key(|(_, version)| *version);
let program = Program::get(&db);
let mut current_version = program.python_version(&db);
for (class, version_added) in classes {
if version_added != current_version {
program
.set_python_version_with_source(&mut db)
.to(PythonVersionWithSource {
version: version_added,
source: PythonVersionSource::default(),
});
current_version = version_added;
}
assert_ne!(
class.to_instance(&db),

View File

@@ -24,6 +24,7 @@ pub enum ClassBase<'db> {
/// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`,
/// `Protocol[T]`, or bare `Protocol`.
Generic,
TypedDict,
}
impl<'db> ClassBase<'db> {
@@ -39,7 +40,7 @@ impl<'db> ClassBase<'db> {
match self {
Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()),
Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)),
Self::Protocol | Self::Generic => self,
Self::Protocol | Self::Generic | Self::TypedDict => self,
}
}
@@ -51,11 +52,11 @@ impl<'db> ClassBase<'db> {
ClassBase::Dynamic(
DynamicType::Todo(_)
| DynamicType::TodoPEP695ParamSpec
| DynamicType::TodoTypeAlias
| DynamicType::TodoTypedDict,
| DynamicType::TodoTypeAlias,
) => "@Todo",
ClassBase::Protocol => "Protocol",
ClassBase::Generic => "Generic",
ClassBase::TypedDict => "TypedDict",
}
}
@@ -158,7 +159,8 @@ impl<'db> ClassBase<'db> {
| Type::ProtocolInstance(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy
| Type::TypeIs(_) => None,
| Type::TypeIs(_)
| Type::TypedDict(_) => None,
Type::KnownInstance(known_instance) => match known_instance {
KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic),
@@ -234,7 +236,7 @@ impl<'db> ClassBase<'db> {
SpecialFormType::OrderedDict => {
Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db))
}
SpecialFormType::TypedDict => Some(Self::Dynamic(DynamicType::TodoTypedDict)),
SpecialFormType::TypedDict => Some(Self::TypedDict),
SpecialFormType::Callable => {
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
}
@@ -245,14 +247,14 @@ impl<'db> ClassBase<'db> {
pub(super) fn into_class(self) -> Option<ClassType<'db>> {
match self {
Self::Class(class) => Some(class),
Self::Dynamic(_) | Self::Generic | Self::Protocol => None,
Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => None,
}
}
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
match self {
Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)),
Self::Dynamic(_) | Self::Generic | Self::Protocol => self,
Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => self,
}
}
@@ -276,7 +278,10 @@ impl<'db> ClassBase<'db> {
.try_mro(db, specialization)
.is_err_and(MroError::is_cycle)
}
ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::Protocol => false,
ClassBase::Dynamic(_)
| ClassBase::Generic
| ClassBase::Protocol
| ClassBase::TypedDict => false,
}
}
@@ -288,7 +293,9 @@ impl<'db> ClassBase<'db> {
) -> impl Iterator<Item = ClassBase<'db>> {
match self {
ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic),
ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self),
ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::TypedDict => {
ClassBaseMroIterator::length_2(db, self)
}
ClassBase::Class(class) => {
ClassBaseMroIterator::from_class(db, class, additional_specialization)
}
@@ -309,6 +316,7 @@ impl<'db> From<ClassBase<'db>> for Type<'db> {
ClassBase::Class(class) => class.into(),
ClassBase::Protocol => Type::SpecialForm(SpecialFormType::Protocol),
ClassBase::Generic => Type::SpecialForm(SpecialFormType::Generic),
ClassBase::TypedDict => Type::SpecialForm(SpecialFormType::TypedDict),
}
}
}

View File

@@ -15,7 +15,6 @@ use crate::types::string_annotation::{
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::tuple::TupleType;
use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
use crate::util::diagnostics::format_enumeration;
use crate::{Db, FxIndexMap, Module, ModuleName, Program, declare_lint};
@@ -2430,7 +2429,7 @@ pub(crate) fn report_invalid_or_unsupported_base(
return;
}
let tuple_of_types = TupleType::homogeneous(db, instance_of_type);
let tuple_of_types = Type::homogeneous_tuple(db, instance_of_type);
let explain_mro_entries = |diagnostic: &mut LintDiagnosticGuard| {
diagnostic.info(

View File

@@ -234,6 +234,9 @@ impl Display for DisplayRepresentation<'_> {
}
f.write_str("]")
}
Type::TypedDict(typed_dict) => {
f.write_str(typed_dict.defining_class(self.db).name(self.db))
}
}
}
}

View File

@@ -975,7 +975,8 @@ fn is_instance_truthiness<'db>(
| Type::TypeIs(..)
| Type::Callable(..)
| Type::Dynamic(..)
| Type::Never => {
| Type::Never
| Type::TypedDict(_) => {
// We could probably try to infer more precise types in some of these cases, but it's unclear
// if it's worth the effort.
Truthiness::Ambiguous

View File

@@ -242,7 +242,7 @@ impl<'db> GenericContext<'db> {
/// Returns a tuple type of the typevars introduced by this generic context.
pub(crate) fn as_tuple(self, db: &'db dyn Db) -> Type<'db> {
TupleType::from_elements(
Type::heterogeneous_tuple(
db,
self.variables(db)
.iter()

View File

@@ -95,7 +95,19 @@ impl<'db> AllMembers<'db> {
Type::NominalInstance(instance) => {
let (class_literal, _specialization) = instance.class.class_literal(db);
self.extend_with_instance_members(db, class_literal);
self.extend_with_instance_members(db, ty, class_literal);
}
Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => {
self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db));
}
Type::GenericAlias(generic_alias) if generic_alias.is_typed_dict(db) => {
self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db));
}
Type::SubclassOf(subclass_of_type) if subclass_of_type.is_typed_dict(db) => {
self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db));
}
Type::ClassLiteral(class_literal) => {
@@ -143,6 +155,11 @@ impl<'db> AllMembers<'db> {
Type::ClassLiteral(class_literal) => {
self.extend_with_class_members(db, ty, class_literal);
}
Type::SubclassOf(subclass_of) => {
if let Some(class) = subclass_of.subclass_of().into_class() {
self.extend_with_class_members(db, ty, class.class_literal(db).0);
}
}
Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db);
self.extend_with_class_members(db, ty, class_literal);
@@ -150,6 +167,18 @@ impl<'db> AllMembers<'db> {
_ => {}
},
Type::TypedDict(_) => {
if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) {
self.extend_with_class_members(db, ty, class_literal);
}
if let Type::ClassLiteral(class) =
KnownClass::TypedDictFallback.to_class_literal(db)
{
self.extend_with_instance_members(db, ty, class);
}
}
Type::ModuleLiteral(literal) => {
self.extend_with_type(db, KnownClass::ModuleType.to_instance(db));
let module = literal.module(db);
@@ -260,13 +289,17 @@ impl<'db> AllMembers<'db> {
}
}
fn extend_with_instance_members(&mut self, db: &'db dyn Db, class_literal: ClassLiteral<'db>) {
fn extend_with_instance_members(
&mut self,
db: &'db dyn Db,
ty: Type<'db>,
class_literal: ClassLiteral<'db>,
) {
for parent in class_literal
.iter_mro(db, None)
.filter_map(ClassBase::into_class)
.map(|class| class.class_literal(db).0)
{
let parent_instance = Type::instance(db, parent.default_specialization(db));
let class_body_scope = parent.body_scope(db);
let file = class_body_scope.file(db);
let index = semantic_index(db, file);
@@ -276,7 +309,7 @@ impl<'db> AllMembers<'db> {
let Some(name) = place_expr.as_instance_attribute() else {
continue;
};
let result = parent_instance.member(db, name.as_str());
let result = ty.member(db, name.as_str());
let Some(ty) = result.place.ignore_possibly_unbound() else {
continue;
};
@@ -293,7 +326,7 @@ impl<'db> AllMembers<'db> {
// member, e.g., `SomeClass.__delattr__` is not a bound
// method, but `instance_of_SomeClass.__delattr__` is.
for Member { name, .. } in all_declarations_and_bindings(db, class_body_scope) {
let result = parent_instance.member(db, name.as_str());
let result = ty.member(db, name.as_str());
let Some(ty) = result.place.ignore_possibly_unbound() else {
continue;
};

View File

@@ -694,7 +694,7 @@ enum IntersectionOn {
#[derive(Debug, Clone, PartialEq, Eq)]
enum DeclaredAndInferredType<'db> {
/// We know that both the declared and inferred types are the same.
AreTheSame(Type<'db>),
AreTheSame(TypeAndQualifiers<'db>),
/// Declared and inferred types might be different, we need to check assignability.
MightBeDifferent {
declared_ty: TypeAndQualifiers<'db>,
@@ -702,6 +702,12 @@ enum DeclaredAndInferredType<'db> {
},
}
impl<'db> DeclaredAndInferredType<'db> {
fn are_the_same_type(ty: Type<'db>) -> Self {
Self::AreTheSame(ty.into())
}
}
/// Builder to infer all types in a region.
///
/// A builder is used by creating it with [`new()`](TypeInferenceBuilder::new), and then calling
@@ -2132,7 +2138,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
let (declared_ty, inferred_ty) = match *declared_and_inferred_ty {
DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty),
DeclaredAndInferredType::AreTheSame(type_and_qualifiers) => {
(type_and_qualifiers, type_and_qualifiers.inner_type())
}
DeclaredAndInferredType::MightBeDifferent {
declared_ty,
inferred_ty,
@@ -2191,7 +2199,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
node,
definition,
&DeclaredAndInferredType::AreTheSame(Type::unknown()),
&DeclaredAndInferredType::are_the_same_type(Type::unknown()),
);
}
@@ -2658,7 +2666,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
function.into(),
definition,
&DeclaredAndInferredType::AreTheSame(inferred_ty),
&DeclaredAndInferredType::are_the_same_type(inferred_ty),
);
}
@@ -2818,7 +2826,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.as_ref()
.is_some_and(|d| d.is_ellipsis_literal_expr())
{
DeclaredAndInferredType::AreTheSame(declared_ty)
DeclaredAndInferredType::are_the_same_type(declared_ty)
} else {
if let Some(builder) = self
.context
@@ -2831,10 +2839,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
declared_ty.display(self.db())
));
}
DeclaredAndInferredType::AreTheSame(declared_ty)
DeclaredAndInferredType::are_the_same_type(declared_ty)
}
} else {
DeclaredAndInferredType::AreTheSame(declared_ty)
DeclaredAndInferredType::are_the_same_type(declared_ty)
};
self.add_declaration_with_binding(
parameter.into(),
@@ -2868,19 +2876,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
todo_type!("PEP 646")
} else {
let annotated_type = self.file_expression_type(annotation);
TupleType::homogeneous(self.db(), annotated_type)
Type::homogeneous_tuple(self.db(), annotated_type)
};
self.add_declaration_with_binding(
parameter.into(),
definition,
&DeclaredAndInferredType::AreTheSame(ty),
&DeclaredAndInferredType::are_the_same_type(ty),
);
} else {
self.add_binding(
parameter.into(),
definition,
TupleType::homogeneous(self.db(), Type::unknown()),
Type::homogeneous_tuple(self.db(), Type::unknown()),
);
}
}
@@ -2906,7 +2914,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
parameter.into(),
definition,
&DeclaredAndInferredType::AreTheSame(ty),
&DeclaredAndInferredType::are_the_same_type(ty),
);
} else {
self.add_binding(
@@ -3004,7 +3012,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
class_node.into(),
definition,
&DeclaredAndInferredType::AreTheSame(class_ty),
&DeclaredAndInferredType::are_the_same_type(class_ty),
);
// if there are type parameters, then the keywords and bases are within that scope
@@ -3078,7 +3086,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
type_alias.into(),
definition,
&DeclaredAndInferredType::AreTheSame(type_alias_ty),
&DeclaredAndInferredType::are_the_same_type(type_alias_ty),
);
}
@@ -3293,7 +3301,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
)
} else if node_ty.is_assignable_to(
self.db(),
TupleType::homogeneous(self.db(), type_base_exception),
Type::homogeneous_tuple(self.db(), type_base_exception),
) {
extract_tuple_specialization(self.db(), node_ty)
.unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db()))
@@ -3303,7 +3311,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.db(),
[
type_base_exception,
TupleType::homogeneous(self.db(), type_base_exception),
Type::homogeneous_tuple(self.db(), type_base_exception),
],
),
) {
@@ -3433,7 +3441,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
node.into(),
definition,
&DeclaredAndInferredType::AreTheSame(ty),
&DeclaredAndInferredType::are_the_same_type(ty),
);
}
@@ -3453,7 +3461,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
node.into(),
definition,
&DeclaredAndInferredType::AreTheSame(pep_695_todo),
&DeclaredAndInferredType::are_the_same_type(pep_695_todo),
);
}
@@ -3473,7 +3481,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
node.into(),
definition,
&DeclaredAndInferredType::AreTheSame(pep_695_todo),
&DeclaredAndInferredType::are_the_same_type(pep_695_todo),
);
}
@@ -3584,7 +3592,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_nested_match_pattern(&mut self, pattern: &ast::Pattern) {
match pattern {
ast::Pattern::MatchValue(match_value) => {
self.infer_expression(&match_value.value);
self.infer_maybe_standalone_expression(&match_value.value);
}
ast::Pattern::MatchSequence(match_sequence) => {
for pattern in &match_sequence.patterns {
@@ -3619,7 +3627,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
for keyword in &arguments.keywords {
self.infer_nested_match_pattern(&keyword.pattern);
}
self.infer_expression(cls);
self.infer_maybe_standalone_expression(cls);
}
ast::Pattern::MatchAs(match_as) => {
if let Some(pattern) = &match_as.pattern {
@@ -3906,7 +3914,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| Type::TypeVar(..)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::TypeIs(_) => {
| Type::TypeIs(_)
| Type::TypedDict(_) => {
// First, try to call the `__setattr__` dunder method. If this is present/defined, overrides
// assigning the attributed by the normal mechanism.
let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy(
@@ -4557,7 +4566,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
target.into(),
definition,
&DeclaredAndInferredType::AreTheSame(declared.inner_type()),
&DeclaredAndInferredType::AreTheSame(declared),
);
} else {
self.add_declaration(target.into(), definition, declared);
@@ -4875,7 +4884,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
alias.into(),
definition,
&DeclaredAndInferredType::AreTheSame(binding_ty),
&DeclaredAndInferredType::are_the_same_type(binding_ty),
);
}
@@ -5124,7 +5133,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
alias.into(),
definition,
&DeclaredAndInferredType::AreTheSame(submodule_type),
&DeclaredAndInferredType::are_the_same_type(submodule_type),
);
return;
}
@@ -5611,7 +5620,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// consuming the whole iterator).
let element_types: Vec<_> = elts.iter().map(|elt| self.infer_expression(elt)).collect();
TupleType::from_elements(self.db(), element_types)
Type::heterogeneous_tuple(self.db(), element_types)
}
fn infer_list_expression(&mut self, list: &ast::ExprList) -> Type<'db> {
@@ -6069,7 +6078,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// the `try_call` path below.
// TODO: it should be possible to move these special cases into the `try_call_constructor`
// path instead, or even remove some entirely once we support overloads fully.
if !matches!(
let has_special_cased_constructor = matches!(
class.known(self.db()),
Some(
KnownClass::Bool
@@ -6083,20 +6092,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| KnownClass::TypeAliasType
| KnownClass::Deprecated
)
)
// Constructor calls to `tuple` and subclasses of `tuple` are handled in `Type::Bindings`,
// but constructor calls to `tuple[int]`, `tuple[int, ...]`, `tuple[int, *tuple[str, ...]]` (etc.)
// are handled by the default constructor-call logic (we synthesize a `__new__` method for them
// in `ClassType::own_class_member()`).
&& (callable_type.is_generic_alias() || !class.is_known(self.db(), KnownClass::Tuple))
) || (
// Constructor calls to `tuple` and subclasses of `tuple` are handled in `Type::Bindings`,
// but constructor calls to `tuple[int]`, `tuple[int, ...]`, `tuple[int, *tuple[str, ...]]` (etc.)
// are handled by the default constructor-call logic (we synthesize a `__new__` method for them
// in `ClassType::own_class_member()`).
class.is_known(self.db(), KnownClass::Tuple) && class.is_not_generic()
);
// temporary special-casing for all subclasses of `enum.Enum`
// until we support the functional syntax for creating enum classes
&& KnownClass::Enum
.to_class_literal(self.db())
.to_class_type(self.db())
.is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class))
if !has_special_cased_constructor
&& KnownClass::Enum
.to_class_literal(self.db())
.to_class_type(self.db())
.is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class))
{
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
self.infer_argument_types(arguments, &mut call_arguments, &argument_forms);
@@ -6555,6 +6565,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Some(use_id) = use_id {
constraint_keys.push((file_scope_id, ConstraintKey::UseId(use_id)));
}
let local_is_unbound = local_scope_place.is_unbound();
let place = PlaceAndQualifiers::from(local_scope_place).or_fall_back_to(db, || {
let has_bindings_in_this_scope = match place_table.place_id(place_expr) {
@@ -6573,7 +6584,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut is_nonlocal_binding = false;
if let Some(symbol) = place_expr.as_symbol() {
if let Some(symbol_id) = place_table.symbol_id(symbol.name()) {
if self.skip_non_global_scopes(file_scope_id, symbol_id) {
// If we try to access a variable in a class before it has been defined,
// the lookup will fall back to global.
let fallback_to_global = local_is_unbound
&& has_bindings_in_this_scope
&& scope.node(db).scope_kind().is_class();
if self.skip_non_global_scopes(file_scope_id, symbol_id) || fallback_to_global {
return global_symbol(self.db(), self.file(), symbol.name()).map_type(
|ty| {
self.narrow_place_with_applicable_constraints(
@@ -7142,7 +7158,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| Type::Tuple(_)
| Type::BoundSuper(_)
| Type::TypeVar(_)
| Type::TypeIs(_),
| Type::TypeIs(_)
| Type::TypedDict(_),
) => {
let unary_dunder_method = match op {
ast::UnaryOp::Invert => "__invert__",
@@ -7267,8 +7284,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
todo @ Type::Dynamic(
DynamicType::Todo(_)
| DynamicType::TodoPEP695ParamSpec
| DynamicType::TodoTypeAlias
| DynamicType::TodoTypedDict,
| DynamicType::TodoTypeAlias,
),
_,
_,
@@ -7278,8 +7294,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
todo @ Type::Dynamic(
DynamicType::Todo(_)
| DynamicType::TodoPEP695ParamSpec
| DynamicType::TodoTypeAlias
| DynamicType::TodoTypedDict,
| DynamicType::TodoTypeAlias,
),
_,
) => Some(todo),
@@ -7467,7 +7482,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| Type::Tuple(_)
| Type::BoundSuper(_)
| Type::TypeVar(_)
| Type::TypeIs(_),
| Type::TypeIs(_)
| Type::TypedDict(_),
Type::FunctionLiteral(_)
| Type::BooleanLiteral(_)
| Type::Callable(..)
@@ -7496,7 +7512,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| Type::Tuple(_)
| Type::BoundSuper(_)
| Type::TypeVar(_)
| Type::TypeIs(_),
| Type::TypeIs(_)
| Type::TypedDict(_),
op,
) => {
// We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from
@@ -8478,6 +8495,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
let tuple_generic_alias = |db: &'db dyn Db, tuple: Option<TupleType<'db>>| {
let tuple =
tuple.unwrap_or_else(|| TupleType::homogeneous(db, Type::unknown()).unwrap());
tuple
.to_class_type(db)
.map(Type::from)
.unwrap_or_else(Type::unknown)
};
// HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the
// subscript inference logic and treat this as an explicit specialization.
// TODO: Move this logic into a custom callable, and update `find_name_in_mro` to return
@@ -8486,9 +8512,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// special cases, too.
if let Type::ClassLiteral(class) = value_ty {
if class.is_tuple(self.db()) {
return self
.infer_tuple_type_expression(slice)
.to_meta_type(self.db());
return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice));
}
if let Some(generic_context) = class.generic_context(self.db()) {
return self.infer_explicit_class_specialization(
@@ -8500,9 +8524,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty {
return self
.infer_tuple_type_expression(slice)
.to_meta_type(self.db());
return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice));
}
let slice_ty = self.infer_expression(slice);
@@ -8525,7 +8547,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
self.store_expression_type(
slice_node,
TupleType::from_elements(self.db(), arguments.iter_types()),
Type::heterogeneous_tuple(self.db(), arguments.iter_types()),
);
arguments
}
@@ -8557,7 +8579,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_subscript_expression_types(
&self,
value_node: &ast::Expr,
value_node: &'ast ast::Expr,
value_ty: Type<'db>,
slice_ty: Type<'db>,
expr_context: ExprContext,
@@ -8617,7 +8639,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
};
if let Ok(new_elements) = tuple.py_slice(db, start, stop, step) {
TupleType::from_elements(db, new_elements)
Type::heterogeneous_tuple(db, new_elements)
} else {
report_slice_step_size_zero(context, value_node.into());
Type::unknown()
@@ -8726,7 +8748,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.map(|context| {
Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context))
})
.unwrap_or_else(Type::unknown),
.unwrap_or_else(GenericContextError::into_type),
// TODO: emit a diagnostic
TupleSpec::Variable(_) => Type::unknown(),
})
@@ -8739,7 +8761,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
LegacyGenericBase::Protocol,
)
.map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context)))
.unwrap_or_else(Type::unknown),
.unwrap_or_else(GenericContextError::into_type),
),
(Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)), _) => {
@@ -8758,7 +8780,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.map(|context| {
Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context))
})
.unwrap_or_else(Type::unknown),
.unwrap_or_else(GenericContextError::into_type),
// TODO: emit a diagnostic
TupleSpec::Variable(_) => Type::unknown(),
})
@@ -8771,7 +8793,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
LegacyGenericBase::Generic,
)
.map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context)))
.unwrap_or_else(Type::unknown),
.unwrap_or_else(GenericContextError::into_type),
),
(Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_)), _) => {
@@ -8940,11 +8962,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
value_node: &ast::Expr,
typevars: &[Type<'db>],
origin: LegacyGenericBase,
) -> Option<GenericContext<'db>> {
let typevars: Option<FxOrderSet<_>> = typevars
) -> Result<GenericContext<'db>, GenericContextError> {
let typevars: Result<FxOrderSet<_>, GenericContextError> = typevars
.iter()
.map(|typevar| match typevar {
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Some(*typevar),
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Ok(*typevar),
Type::NominalInstance(NominalInstanceType { class, .. })
if matches!(
class.known(self.db()),
Some(KnownClass::TypeVarTuple | KnownClass::ParamSpec)
) =>
{
Err(GenericContextError::NotYetSupported)
}
_ => {
if let Some(builder) =
self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node)
@@ -8954,7 +8984,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
typevar.display(self.db()),
));
}
None
Err(GenericContextError::InvalidArgument)
}
})
.collect();
@@ -9644,7 +9674,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let hinted_type = if list.len() == 1 {
KnownClass::List.to_specialized_instance(db, inner_types)
} else {
TupleType::from_elements(db, inner_types)
Type::heterogeneous_tuple(db, inner_types)
};
diagnostic.set_primary_message(format_args!(
@@ -9676,7 +9706,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
Type::Dynamic(DynamicType::Todo(_) | DynamicType::Unknown)
)
}) {
let hinted_type = TupleType::from_elements(self.db(), inner_types);
let hinted_type = Type::heterogeneous_tuple(self.db(), inner_types);
diagnostic.set_primary_message(format_args!(
"Did you mean `{}`?",
hinted_type.display(self.db()),
@@ -9895,7 +9925,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
) -> Type<'db> {
match value_ty {
Type::ClassLiteral(class_literal) => match class_literal.known(self.db()) {
Some(KnownClass::Tuple) => self.infer_tuple_type_expression(slice),
Some(KnownClass::Tuple) => Type::tuple(self.infer_tuple_type_expression(slice)),
Some(KnownClass::Type) => self.infer_subclass_of_type_expression(slice),
_ => self.infer_subscript_type_expression(subscript, value_ty),
},
@@ -9920,7 +9950,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
/// Given the slice of a `tuple[]` annotation, return the type that the annotation represents
fn infer_tuple_type_expression(&mut self, tuple_slice: &ast::Expr) -> Type<'db> {
fn infer_tuple_type_expression(&mut self, tuple_slice: &ast::Expr) -> Option<TupleType<'db>> {
/// In most cases, if a subelement of the tuple is inferred as `Todo`,
/// we should only infer `Todo` for that specific subelement.
/// Certain specific AST nodes can however change the meaning of the entire tuple,
@@ -9961,7 +9991,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_expression(ellipsis);
let result =
TupleType::homogeneous(self.db(), self.infer_type_expression(element));
self.store_expression_type(tuple_slice, result);
self.store_expression_type(tuple_slice, Type::tuple(result));
return result;
}
@@ -9988,15 +10018,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
let ty = if return_todo {
todo_type!("PEP 646")
TupleType::homogeneous(self.db(), todo_type!("PEP 646"))
} else {
Type::tuple(TupleType::new(self.db(), element_types))
TupleType::new(self.db(), element_types)
};
// Here, we store the type for the inner `int, str` tuple-expression,
// while the type for the outer `tuple[int, str]` slice-expression is
// stored in the surrounding `infer_type_expression` call:
self.store_expression_type(tuple_slice, ty);
self.store_expression_type(tuple_slice, Type::tuple(ty));
ty
}
@@ -10004,7 +10034,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let single_element_ty = self.infer_type_expression(single_element);
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty, self)
{
todo_type!("PEP 646")
TupleType::homogeneous(self.db(), todo_type!("PEP 646"))
} else {
TupleType::from_elements(self.db(), std::iter::once(single_element_ty))
}
@@ -10668,7 +10698,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
Type::unknown()
}
SpecialFormType::Type => self.infer_subclass_of_type_expression(arguments_slice),
SpecialFormType::Tuple => self.infer_tuple_type_expression(arguments_slice),
SpecialFormType::Tuple => {
Type::tuple(self.infer_tuple_type_expression(arguments_slice))
}
SpecialFormType::Generic | SpecialFormType::Protocol => {
self.infer_expression(arguments_slice);
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
@@ -10852,6 +10884,24 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GenericContextError {
/// It's invalid to subscript `Generic` or `Protocol` with this type
InvalidArgument,
/// It's valid to subscribe `Generic` or `Protocol` with this type,
/// but the type is not yet supported.
NotYetSupported,
}
impl GenericContextError {
const fn into_type<'db>(self) -> Type<'db> {
match self {
GenericContextError::InvalidArgument => Type::unknown(),
GenericContextError::NotYetSupported => todo_type!("ParamSpecs and TypeVarTuples"),
}
}
}
/// The deferred state of a specific expression in an inference region.
#[derive(Default, Debug, Clone, Copy)]
enum DeferredExpressionState {

View File

@@ -19,7 +19,7 @@ impl<'db> Type<'db> {
match (class, class.known(db)) {
(_, Some(KnownClass::Any)) => Self::Dynamic(DynamicType::Any),
(ClassType::NonGeneric(_), Some(KnownClass::Tuple)) => {
TupleType::homogeneous(db, Type::unknown())
Type::tuple(TupleType::homogeneous(db, Type::unknown()))
}
(ClassType::Generic(alias), Some(KnownClass::Tuple)) => {
Self::tuple(TupleType::new(db, alias.specialization(db).tuple(db)))
@@ -125,15 +125,17 @@ impl<'db> NominalInstanceType<'db> {
}
pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool {
self.class.known(db).is_some_and(KnownClass::is_singleton)
|| is_single_member_enum(db, self.class.class_literal(db).0)
self.class
.known(db)
.map(KnownClass::is_singleton)
.unwrap_or_else(|| is_single_member_enum(db, self.class.class_literal(db).0))
}
pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool {
self.class
.known(db)
.is_some_and(KnownClass::is_single_valued)
|| is_single_member_enum(db, self.class.class_literal(db).0)
.map(KnownClass::is_single_valued)
.unwrap_or_else(|| is_single_member_enum(db, self.class.class_literal(db).0))
}
pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> {

View File

@@ -269,7 +269,10 @@ impl<'db> Mro<'db> {
continue;
}
match base {
ClassBase::Class(_) | ClassBase::Generic | ClassBase::Protocol => {
ClassBase::Class(_)
| ClassBase::Generic
| ClassBase::Protocol
| ClassBase::TypedDict => {
errors.push(DuplicateBaseError {
duplicate_base: base,
first_index: *first_index,

View File

@@ -256,7 +256,8 @@ impl ClassInfoConstraintFunction {
| Type::KnownInstance(_)
| Type::TypeIs(_)
| Type::WrapperDescriptor(_)
| Type::DataclassTransformer(_) => None,
| Type::DataclassTransformer(_)
| Type::TypedDict(_) => None,
}
}
}
@@ -410,6 +411,9 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
PatternPredicateKind::Or(predicates) => {
self.evaluate_match_pattern_or(subject, predicates, is_positive)
}
PatternPredicateKind::As(pattern, _) => pattern
.as_deref()
.and_then(|p| self.evaluate_pattern_predicate_kind(p, subject, is_positive)),
PatternPredicateKind::Unsupported => None,
}
}

View File

@@ -195,13 +195,13 @@ impl Ty {
}
Ty::FixedLengthTuple(tys) => {
let elements = tys.into_iter().map(|ty| ty.into_type(db));
TupleType::from_elements(db, elements)
Type::heterogeneous_tuple(db, elements)
}
Ty::VariableLengthTuple(prefix, variable, suffix) => {
let prefix = prefix.into_iter().map(|ty| ty.into_type(db));
let variable = variable.into_type(db);
let suffix = suffix.into_iter().map(|ty| ty.into_type(db));
TupleType::mixed(db, prefix, variable, suffix)
Type::tuple(TupleType::mixed(db, prefix, variable, suffix))
}
Ty::SubclassOfAny => SubclassOfType::subclass_of_any(),
Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from(

View File

@@ -196,6 +196,12 @@ impl<'db> SubclassOfType<'db> {
SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type),
}
}
pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
self.subclass_of
.into_class()
.is_some_and(|class| class.class_literal(db).0.is_typed_dict(db))
}
}
/// An enumeration of the different kinds of `type[]` types that a [`SubclassOfType`] can represent:

View File

@@ -22,8 +22,8 @@ use std::hash::Hash;
use itertools::{Either, EitherOrBoth, Itertools};
use crate::types::Truthiness;
use crate::types::class::{ClassType, KnownClass};
use crate::types::{SubclassOfType, Truthiness};
use crate::types::{
Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, TypeVarVariance,
UnionBuilder, UnionType, cyclic::PairVisitor,
@@ -151,6 +151,25 @@ impl<'db> Type<'db> {
};
Self::Tuple(tuple)
}
pub(crate) fn homogeneous_tuple(db: &'db dyn Db, element: Type<'db>) -> Self {
Type::tuple(TupleType::homogeneous(db, element))
}
pub(crate) fn heterogeneous_tuple<I, T>(db: &'db dyn Db, elements: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Type<'db>>,
{
Type::tuple(TupleType::from_elements(
db,
elements.into_iter().map(Into::into),
))
}
pub(crate) fn empty_tuple(db: &'db dyn Db) -> Self {
Type::Tuple(TupleType::empty(db))
}
}
impl<'db> TupleType<'db> {
@@ -180,18 +199,16 @@ impl<'db> TupleType<'db> {
Some(TupleType::new_internal(db, tuple_key))
}
pub(crate) fn empty(db: &'db dyn Db) -> Type<'db> {
Type::tuple(TupleType::new(
db,
TupleSpec::from(FixedLengthTuple::empty()),
))
pub(crate) fn empty(db: &'db dyn Db) -> Self {
TupleType::new(db, TupleSpec::from(FixedLengthTuple::empty()))
.expect("TupleType::new() should always return `Some` for an empty `TupleSpec`")
}
pub(crate) fn from_elements(
db: &'db dyn Db,
types: impl IntoIterator<Item = Type<'db>>,
) -> Type<'db> {
Type::tuple(TupleType::new(db, TupleSpec::from_elements(types)))
) -> Option<Self> {
TupleType::new(db, TupleSpec::from_elements(types))
}
#[cfg(test)]
@@ -200,15 +217,12 @@ impl<'db> TupleType<'db> {
prefix: impl IntoIterator<Item = Type<'db>>,
variable: Type<'db>,
suffix: impl IntoIterator<Item = Type<'db>>,
) -> Type<'db> {
Type::tuple(TupleType::new(
db,
VariableLengthTuple::mixed(prefix, variable, suffix),
))
) -> Option<Self> {
TupleType::new(db, VariableLengthTuple::mixed(prefix, variable, suffix))
}
pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Type<'db> {
Type::tuple(TupleType::new(db, TupleSpec::homogeneous(element)))
pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Option<Self> {
TupleType::new(db, TupleSpec::homogeneous(element))
}
pub(crate) fn to_class_type(self, db: &'db dyn Db) -> Option<ClassType<'db>> {
@@ -224,6 +238,11 @@ impl<'db> TupleType<'db> {
})
}
pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Option<Type<'db>> {
self.to_class_type(db)
.map(|class| SubclassOfType::from(db, class))
}
/// Return a normalized version of `self`.
///
/// See [`Type::normalized`] for more details.

View File

@@ -166,6 +166,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(ClassBase::Generic, _) => Ordering::Less,
(_, ClassBase::Generic) => Ordering::Greater,
(ClassBase::TypedDict, _) => Ordering::Less,
(_, ClassBase::TypedDict) => Ordering::Greater,
(ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => {
dynamic_elements_ordering(left, right)
}
@@ -234,6 +237,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
unreachable!("Two equal, normalized intersections should share the same Salsa ID")
}
(Type::TypedDict(left), Type::TypedDict(right)) => left.cmp(right),
(Type::TypedDict(_), _) => Ordering::Less,
(_, Type::TypedDict(_)) => Ordering::Greater,
}
}
@@ -257,9 +264,6 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
(DynamicType::TodoTypeAlias, _) => Ordering::Less,
(_, DynamicType::TodoTypeAlias) => Ordering::Greater,
(DynamicType::TodoTypedDict, _) => Ordering::Less,
(_, DynamicType::TodoTypedDict) => Ordering::Greater,
}
}

View File

@@ -0,0 +1,438 @@
//! Implements the _tallying_ algorithm from [[POPL2015][]], which finds the unification of a
//! set of subtyping constraints.
//!
//! [POPL2015]: https://doi.org/10.1145/2676726.2676991
// XXX
#![allow(dead_code)]
use crate::Db;
use crate::types::{
IntersectionBuilder, IntersectionType, Type, TypeVarInstance, UnionBuilder, UnionType,
};
/// A constraint that the type `s` must be a subtype of the type `t`. Tallying will find all
/// substitutions of any type variables in `s` and `t` that ensure that this subtyping holds.
#[derive(Clone, Copy, Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)]
pub(crate) struct Constraint<'db> {
pub(crate) lower: Type<'db>,
pub(crate) typevar: TypeVarInstance<'db>,
pub(crate) upper: Type<'db>,
}
impl<'db> Constraint<'db> {
/// Returns whether this constraint subsumes `other` — if it constrains the same typevar and
/// has tighter bounds.
fn subsumes(self, db: &'db dyn Db, other: Self) -> bool {
self.typevar == other.typevar
&& other.lower.is_assignable_to(db, self.lower)
&& self.upper.is_assignable_to(db, other.upper)
}
/// Merges another constraint into this one. Panics if the two constraints have different
/// typevars.
fn merge(&mut self, db: &'db dyn Db, other: Self) {
debug_assert!(self.typevar == other.typevar);
self.lower = UnionType::from_elements(db, [self.lower, other.lower]);
self.upper = IntersectionType::from_elements(db, [self.upper, other.upper]);
}
}
/// A set of merged constraints. We guarantee that no constraint in the set subsumes another, and
/// that no two constraints in the set have the same typevar.
///
/// This is denoted _C_ in [[POPL2015][]].
///
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
#[derive(Clone, Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)]
pub(crate) struct ConstraintSet<'db> {
constraints: Vec<Constraint<'db>>,
}
impl<'db> ConstraintSet<'db> {
/// Returns an empty constraint set
fn empty() -> Self {
Self {
constraints: vec![],
}
}
/// Returns a singleton constraint set.
pub(crate) fn singleton(constraint: Constraint<'db>) -> Self {
Self {
constraints: vec![constraint],
}
}
/// Adds a new constraint to this set, ensuring that no constraint in the set subsumes another,
/// and that no two constraints in the set have the same typevar.
fn add(&mut self, db: &'db dyn Db, constraint: Constraint<'db>) {
for existing in &mut self.constraints {
if constraint.typevar == existing.typevar {
existing.merge(db, constraint);
return;
}
}
self.constraints.push(constraint);
}
/// Combines two constraint sets, merging any constraints that share the same typevar.
fn combine(&mut self, db: &'db dyn Db, other: &Self) {
for constraint in &other.constraints {
self.add(db, *constraint);
}
}
/// Returns whether this constraint set subsumes `other` — if every constraint in `other` is
/// subsumed by some constraint in `self`.
fn subsumes(&self, db: &'db dyn Db, other: &Self) -> bool {
other.constraints.iter().all(|other_constraint| {
self.constraints
.iter()
.any(|self_constraint| self_constraint.subsumes(db, *other_constraint))
})
}
}
/// A set of constraint sets.
///
/// This is denoted _𝒮_ in [[POPL2015][]].
///
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
#[derive(Clone, Debug, Eq, PartialEq, salsa::Update, get_size2::GetSize)]
pub(crate) struct ConstraintSetSet<'db> {
sets: Vec<ConstraintSet<'db>>,
}
impl<'db> ConstraintSetSet<'db> {
/// Returns the set of constraint sets that is unsolvable.
pub(crate) fn never() -> Self {
Self { sets: vec![] }
}
/// Returns a singleton set of constraint sets.
pub(crate) fn singleton(constraint_set: ConstraintSet<'db>) -> Self {
Self {
sets: vec![constraint_set],
}
}
/// Returns the set of constraint sets that is always satisfied.
pub(crate) fn always() -> Self {
Self::singleton(ConstraintSet::empty())
}
/// Adds a new constraint set to this set, ensuring that no constraint set in the set subsumes
/// another.
fn add(&mut self, db: &'db dyn Db, constraint_set: ConstraintSet<'db>) {
for existing in &mut self.sets {
// If there is an existing constraint set that subsumes (or is subsumed by) the new
// one, we want to keep the _subsumed_ constraint set.
if constraint_set.subsumes(db, existing) {
return;
} else if existing.subsumes(db, &constraint_set) {
*existing = constraint_set;
return;
}
}
self.sets.push(constraint_set);
}
/// Intersects two sets of constraint sets.
///
/// This is the ⊓ operator from [[POPL2015][]], Definition 3.5.
///
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
fn intersect(&self, db: &'db dyn Db, other: &Self) -> Self {
let mut result = Self::never();
for self_set in &self.sets {
for other_set in &other.sets {
let mut new_set = self_set.clone();
new_set.combine(db, other_set);
result.add(db, new_set);
}
}
result
}
/// Union two sets of constraint sets.
///
/// This is the ⊔ operator from [[POPL2015][]], Definition 3.5.
///
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
fn union(&mut self, db: &'db dyn Db, other: Self) {
for set in other.sets {
self.add(db, set);
}
}
/// Calculate the distributed intersection of an iterator of sets of constraint sets.
fn distributed_intersection(db: &'db dyn Db, sets: impl IntoIterator<Item = Self>) -> Self {
sets.into_iter()
.fold(Self::always(), |sets, element| element.intersect(db, &sets))
}
/// Calculate the distributed union of an iterator of sets of constraint sets.
fn distributed_union(db: &'db dyn Db, sets: impl IntoIterator<Item = Self>) -> Self {
let mut result = Self::never();
for set in sets {
result.union(db, set);
}
result
}
}
impl<'db> From<Constraint<'db>> for ConstraintSetSet<'db> {
fn from(constraint: Constraint<'db>) -> ConstraintSetSet<'db> {
ConstraintSetSet::singleton(ConstraintSet::singleton(constraint))
}
}
impl<'db> From<ConstraintSet<'db>> for ConstraintSetSet<'db> {
fn from(constraint_set: ConstraintSet<'db>) -> ConstraintSetSet<'db> {
ConstraintSetSet::singleton(constraint_set)
}
}
/// Returns a set of normalized constraint sets that is equivalent to the constraint that `ty` is
/// (a subtype of) `Never`.
///
/// This is a combination of the "Constraint normalization" and "Constraint merging" steps from
/// [[POPL2015][]], §3.2.1.
///
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
pub(crate) fn normalized_constraints_from_type<'db>(
db: &'db dyn Db,
ty: Type<'db>,
) -> ConstraintSetSet<'db> {
normalized_constraints_from_type_inner(db, ty, ())
}
#[salsa::tracked(
cycle_fn=normalized_constraints_from_type_recover,
cycle_initial=normalized_constraints_from_type_initial,
heap_size=get_size2::heap_size,
)]
fn normalized_constraints_from_type_inner<'db>(
db: &'db dyn Db,
ty: Type<'db>,
_unused: (),
) -> ConstraintSetSet<'db> {
match ty {
// These atomic types are always inhabited by at least one value, and can
// therefore never be a subtype of `Never`.
Type::AlwaysFalsy
| Type::AlwaysTruthy
| Type::Never
| Type::LiteralString
| Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::EnumLiteral(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::WrapperDescriptor(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
| Type::SpecialForm(_) => ConstraintSetSet::never(),
Type::Union(union) => {
// Figure 3, step 6
// A union is a subtype of Never only if every element is.
ConstraintSetSet::distributed_union(
db,
(union.iter(db)).map(|element| normalized_constraints_from_type(db, *element)),
)
}
Type::Intersection(intersection) => {
// Figure 3, step 2
// If an intersection contains any (positive or negative) top-level type
// variables, extract out and isolate the smallest one (according to our
// arbitrary ordering).
let smallest_positive = find_smallest_typevar(intersection.iter_positive(db));
let smallest_negative = find_smallest_typevar(intersection.iter_negative(db));
let constraint = match (smallest_positive, smallest_negative) {
(Some(typevar), None) => Some(Constraint {
lower: Type::Never,
typevar,
upper: remove_positive_typevar(db, intersection, typevar).negate(db),
}),
(Some(typevar), Some(negative)) if typevar < negative => Some(Constraint {
lower: Type::Never,
typevar,
upper: remove_positive_typevar(db, intersection, typevar).negate(db),
}),
(_, Some(typevar)) => Some(Constraint {
lower: remove_negative_typevar(db, intersection, typevar),
typevar,
upper: Type::object(db),
}),
(None, None) => None,
};
if let Some(constraint) = constraint {
return constraint.into();
}
// Figure 3, step 3
// If all (positive and negative) elements of the intersection are atomic
// types (and therefore cannot contain any typevars), fall back on an
// assignability check: if the intersection of the positive elements is
// assignable to the union of the negative elements, then the overall
// intersection is empty.
let mut all_atomic = true;
let mut positive_atomic = IntersectionBuilder::new(db);
let mut negative_atomic = UnionBuilder::new(db);
for positive in intersection.iter_positive(db) {
if !all_atomic {
break;
}
if !positive.is_atomic() {
all_atomic = false;
break;
}
positive_atomic = positive_atomic.add_positive(positive);
}
for negative in intersection.iter_negative(db) {
if !all_atomic {
break;
}
if !negative.is_atomic() {
all_atomic = false;
break;
}
negative_atomic = negative_atomic.add(negative);
}
if all_atomic {
let positive_atomic = positive_atomic.build();
let negative_atomic = negative_atomic.build();
if positive_atomic.is_assignable_to(db, negative_atomic) {
return ConstraintSetSet::always();
} else {
return ConstraintSetSet::never();
};
}
// Figure 3, step 4
// If all (positive and negative) elements of the intersection are callable, decompose
// the corresponding arrow type.
let positive_callable: Option<Vec<_>> = (intersection.iter_positive(db))
.map(|ty| ty.into_callable(db))
.collect();
let negative_callable: Option<Vec<_>> = (intersection.iter_negative(db))
.map(|ty| ty.into_callable(db))
.collect();
if let (Some(positive), Some(negative)) = (positive_callable, negative_callable) {
ConstraintSetSet::distributed_union(
db,
negative.into_iter().map(|negative| {
let norm_negative = normalized_constraints_from_type(db);
}),
)
}
// TODO: Do we need to handle non-uniform intersections? For now, assume
// the intersection is not empty.
ConstraintSetSet::never()
}
Type::TypeVar(typevar) => {
// Figure 3, step 2
// (special case where P' = {typevar}, and P = N = N' = ø)
let constraint = Constraint {
lower: Type::Never,
typevar,
upper: Type::object(db),
};
constraint.into()
}
_ => todo!(),
}
}
fn normalized_constraints_from_type_recover<'db>(
_db: &'db dyn Db,
_value: &ConstraintSetSet<'db>,
_count: u32,
_ty: Type<'db>,
_unused: (),
) -> salsa::CycleRecoveryAction<ConstraintSetSet<'db>> {
salsa::CycleRecoveryAction::Iterate
}
fn normalized_constraints_from_type_initial<'db>(
_db: &'db dyn Db,
_ty: Type<'db>,
_unused: (),
) -> ConstraintSetSet<'db> {
// Figure 3, step 1: The coinductive base case, for if we encounter this type again recursively
// while we are in the middle of calculating a result for it.
ConstraintSetSet::always()
}
/// Returns the “smallest” top-level typevar in an iterator of types.
///
/// “Smallest” is with respect to the ≼ total order mentioned in [[POPL2015][]], §3.2.1. “Any will
/// do”, so we just compare Salsa IDs.
///
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
fn find_smallest_typevar<'db>(
types: impl IntoIterator<Item = Type<'db>>,
) -> Option<TypeVarInstance<'db>> {
types
.into_iter()
.filter_map(|ty| match ty {
Type::TypeVar(typevar) => Some(typevar),
_ => None,
})
.min()
}
/// Removes a top-level positive typevar from an intersection.
///
/// This is the `single` function from [[POPL2015][]], §3.2.1, for the `k ∈ P'` case.
///
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
fn remove_positive_typevar<'db>(
db: &'db dyn Db,
intersection: IntersectionType<'db>,
typevar: TypeVarInstance<'db>,
) -> Type<'db> {
let mut builder = IntersectionBuilder::new(db);
for positive in intersection.iter_positive(db) {
if positive != Type::TypeVar(typevar) {
builder = builder.add_positive(positive);
}
}
for negative in intersection.iter_negative(db) {
builder = builder.add_negative(negative);
}
builder.build()
}
/// Removes a top-level negative typevar from an intersection.
///
/// This is the `single` function from [[POPL2015][]], §3.2.1, for the `k ∈ N'` case.
///
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
fn remove_negative_typevar<'db>(
db: &'db dyn Db,
intersection: IntersectionType<'db>,
typevar: TypeVarInstance<'db>,
) -> Type<'db> {
let mut builder = IntersectionBuilder::new(db);
for positive in intersection.iter_positive(db) {
builder = builder.add_positive(positive);
}
for negative in intersection.iter_negative(db) {
if negative != Type::TypeVar(typevar) {
builder = builder.add_negative(negative);
}
}
builder.build()
}

View File

@@ -4,7 +4,7 @@ use crate::{
BoundMethodType, BoundSuperType, CallableType, GenericAlias, IntersectionType,
KnownInstanceType, MethodWrapperKind, NominalInstanceType, PropertyInstanceType,
ProtocolInstanceType, SubclassOfType, Type, TypeAliasType, TypeIsType, TypeVarInstance,
UnionType,
TypedDictType, UnionType,
class::walk_generic_alias,
function::{FunctionType, walk_function_type},
instance::{walk_nominal_instance_type, walk_protocol_instance_type},
@@ -12,7 +12,8 @@ use crate::{
tuple::{TupleType, walk_tuple_type},
walk_bound_method_type, walk_bound_super_type, walk_callable_type, walk_intersection_type,
walk_known_instance_type, walk_method_wrapper_type, walk_property_instance_type,
walk_type_alias_type, walk_type_var_type, walk_typeis_type, walk_union,
walk_type_alias_type, walk_type_var_type, walk_typed_dict_type, walk_typeis_type,
walk_union,
},
};
@@ -107,6 +108,10 @@ pub(crate) trait TypeVisitor<'db> {
fn visit_type_alias_type(&mut self, db: &'db dyn Db, type_alias: TypeAliasType<'db>) {
walk_type_alias_type(db, type_alias, self);
}
fn visit_typed_dict_type(&mut self, db: &'db dyn Db, typed_dict: TypedDictType<'db>) {
walk_typed_dict_type(db, typed_dict, self);
}
}
/// Enumeration of types that may contain other types, such as unions, intersections, and generics.
@@ -128,6 +133,7 @@ enum NonAtomicType<'db> {
TypeIs(TypeIsType<'db>),
TypeVar(TypeVarInstance<'db>),
ProtocolInstance(ProtocolInstanceType<'db>),
TypedDict(TypedDictType<'db>),
}
enum TypeKind<'db> {
@@ -190,10 +196,19 @@ impl<'db> From<Type<'db>> for TypeKind<'db> {
}
Type::TypeVar(type_var) => TypeKind::NonAtomic(NonAtomicType::TypeVar(type_var)),
Type::TypeIs(type_is) => TypeKind::NonAtomic(NonAtomicType::TypeIs(type_is)),
Type::TypedDict(typed_dict) => {
TypeKind::NonAtomic(NonAtomicType::TypedDict(typed_dict))
}
}
}
}
impl Type<'_> {
pub(crate) fn is_atomic(self) -> bool {
matches!(TypeKind::from(self), TypeKind::Atomic)
}
}
fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>(
db: &'db dyn Db,
non_atomic_type: NonAtomicType<'db>,
@@ -226,6 +241,7 @@ fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>(
NonAtomicType::ProtocolInstance(protocol) => {
visitor.visit_protocol_instance_type(db, protocol);
}
NonAtomicType::TypedDict(typed_dict) => visitor.visit_typed_dict_type(db, typed_dict),
}
}

View File

@@ -5,7 +5,7 @@ use lsp_server::Connection;
use ruff_db::system::{OsSystem, SystemPathBuf};
pub use crate::logging::{LogLevel, init_logging};
pub use crate::server::Server;
pub use crate::server::{PartialWorkspaceProgress, PartialWorkspaceProgressParams, Server};
pub use crate::session::{ClientOptions, DiagnosticMode};
pub use document::{NotebookDocument, PositionEncoding, TextDocument};
pub(crate) use session::{DocumentQuery, Session};
@@ -47,7 +47,7 @@ pub fn run_server() -> anyhow::Result<()> {
// This is to complement the `LSPSystem` if the document is not available in the index.
let fallback_system = Arc::new(OsSystem::new(cwd));
let server_result = Server::new(worker_threads, connection, fallback_system, true)
let server_result = Server::new(worker_threads, connection, fallback_system, false)
.context("Failed to start server")?
.run();

View File

@@ -29,10 +29,10 @@ pub(crate) use main_loop::{
Action, ConnectionSender, Event, MainLoopReceiver, MainLoopSender, SendRequest,
};
pub(crate) type Result<T> = std::result::Result<T, api::Error>;
pub use api::{PartialWorkspaceProgress, PartialWorkspaceProgressParams};
pub struct Server {
connection: Connection,
client_capabilities: ClientCapabilities,
worker_threads: NonZeroUsize,
main_loop_receiver: MainLoopReceiver,
main_loop_sender: MainLoopSender,
@@ -44,7 +44,7 @@ impl Server {
worker_threads: NonZeroUsize,
connection: Connection,
native_system: Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>,
initialize_logging: bool,
in_test: bool,
) -> crate::Result<Self> {
let (id, init_value) = connection.initialize_start()?;
let init_params: InitializeParams = serde_json::from_value(init_value)?;
@@ -81,7 +81,7 @@ impl Server {
let (main_loop_sender, main_loop_receiver) = crossbeam::channel::bounded(32);
let client = Client::new(main_loop_sender.clone(), connection.sender.clone());
if initialize_logging {
if !in_test {
crate::logging::init_logging(
global_options.tracing.log_level.unwrap_or_default(),
global_options.tracing.log_file.as_deref(),
@@ -160,8 +160,8 @@ impl Server {
global_options,
workspaces,
native_system,
in_test,
)?,
client_capabilities,
})
}

View File

@@ -19,6 +19,7 @@ use self::traits::{NotificationHandler, RequestHandler};
use super::{Result, schedule::BackgroundSchedule};
use crate::session::client::Client;
pub(crate) use diagnostics::publish_settings_diagnostics;
pub use requests::{PartialWorkspaceProgress, PartialWorkspaceProgressParams};
use ruff_db::panic::PanicError;
/// Processes a request from the client to the server.
@@ -218,13 +219,11 @@ where
return;
}
let result = ruff_db::panic::catch_unwind(|| {
if let Err(error) = ruff_db::panic::catch_unwind(|| {
let snapshot = snapshot;
R::run(snapshot.0, client, params)
});
if let Some(response) = request_result_to_response::<R>(&id, client, result, retry) {
respond::<R>(&id, response, client);
R::handle_request(&id, snapshot.0, client, params);
}) {
panic_response::<R>(&id, client, &error, retry);
}
})
}))
@@ -287,58 +286,50 @@ where
return;
}
let result = ruff_db::panic::catch_unwind(|| {
R::run_with_snapshot(&db, snapshot, client, params)
});
if let Some(response) = request_result_to_response::<R>(&id, client, result, retry) {
respond::<R>(&id, response, client);
if let Err(error) = ruff_db::panic::catch_unwind(|| {
R::handle_request(&id, &db, snapshot, client, params);
}) {
panic_response::<R>(&id, client, &error, retry);
}
})
}))
}
fn request_result_to_response<R>(
fn panic_response<R>(
id: &RequestId,
client: &Client,
result: std::result::Result<
Result<<<R as RequestHandler>::RequestType as Request>::Result>,
PanicError,
>,
error: &PanicError,
request: Option<lsp_server::Request>,
) -> Option<Result<<<R as RequestHandler>::RequestType as Request>::Result>>
where
) where
R: traits::RetriableRequestHandler,
{
match result {
Ok(response) => Some(response),
Err(error) => {
// Check if the request was canceled due to some modifications to the salsa database.
if error.payload.downcast_ref::<salsa::Cancelled>().is_some() {
// If the query supports retry, re-queue the request.
// The query is still likely to succeed if the user modified any other document.
if let Some(request) = request {
tracing::trace!(
"request id={} method={} was cancelled by salsa, re-queueing for retry",
request.id,
request.method
);
client.retry(request);
} else {
tracing::trace!(
"request id={} was cancelled by salsa, sending content modified",
id
);
respond_silent_error(id.clone(), client, R::salsa_cancellation_error());
}
None
} else {
Some(Err(Error {
code: lsp_server::ErrorCode::InternalError,
error: anyhow!("request handler {error}"),
}))
}
// Check if the request was canceled due to some modifications to the salsa database.
if error.payload.downcast_ref::<salsa::Cancelled>().is_some() {
// If the query supports retry, re-queue the request.
// The query is still likely to succeed if the user modified any other document.
if let Some(request) = request {
tracing::trace!(
"request id={} method={} was cancelled by salsa, re-queueing for retry",
request.id,
request.method
);
client.retry(request);
} else {
tracing::trace!(
"request id={} was cancelled by salsa, sending content modified",
id
);
respond_silent_error(id.clone(), client, R::salsa_cancellation_error());
}
} else {
respond::<R>(
id,
Err(Error {
code: lsp_server::ErrorCode::InternalError,
error: anyhow!("request handler {error}"),
}),
client,
);
}
}
@@ -351,7 +342,13 @@ fn sync_notification_task<N: traits::SyncNotificationHandler>(
if let Err(err) = N::run(session, client, params) {
tracing::error!("An error occurred while running {id}: {err}");
client.show_error_message("ty encountered a problem. Check the logs for more details.");
return;
}
// If there's a pending workspace diagnostic long-polling request,
// resume it, but only if the session revision changed (e.g. because some document changed).
session.resume_suspended_workspace_diagnostic_request(client);
}))
}

View File

@@ -26,17 +26,29 @@ pub(super) struct Diagnostics<'a> {
}
impl Diagnostics<'_> {
pub(super) fn result_id_from_hash(diagnostics: &[ruff_db::diagnostic::Diagnostic]) -> String {
/// Computes the result ID for `diagnostics`.
///
/// Returns `None` if there are no diagnostics.
pub(super) fn result_id_from_hash(
diagnostics: &[ruff_db::diagnostic::Diagnostic],
) -> Option<String> {
if diagnostics.is_empty() {
return None;
}
// Generate result ID based on raw diagnostic content only
let mut hasher = DefaultHasher::new();
// Hash the length first to ensure different numbers of diagnostics produce different hashes
diagnostics.hash(&mut hasher);
format!("{:x}", hasher.finish())
Some(format!("{:x}", hasher.finish()))
}
pub(super) fn result_id(&self) -> String {
/// Computes the result ID for the diagnostics.
///
/// Returns `None` if there are no diagnostics.
pub(super) fn result_id(&self) -> Option<String> {
Self::result_id_from_hash(&self.items)
}

View File

@@ -83,33 +83,27 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
return Ok(());
}
let mut project_changed = false;
for (root, changes) in events_by_db {
tracing::debug!("Applying changes to `{root}`");
let result = session.apply_changes(&AnySystemPath::System(root.clone()), changes);
session.apply_changes(&AnySystemPath::System(root.clone()), changes);
publish_settings_diagnostics(session, client, root);
project_changed |= result.project_changed();
}
let client_capabilities = session.client_capabilities();
if project_changed {
if client_capabilities.supports_workspace_diagnostic_refresh() {
client.send_request::<types::request::WorkspaceDiagnosticRefresh>(
session,
(),
|_, ()| {},
);
} else {
for key in session.text_document_keys() {
publish_diagnostics(session, &key, client);
}
if client_capabilities.supports_workspace_diagnostic_refresh() {
client.send_request::<types::request::WorkspaceDiagnosticRefresh>(
session,
(),
|_, ()| {},
);
} else {
for key in session.text_document_keys() {
publish_diagnostics(session, &key, client);
}
// TODO: always publish diagnostics for notebook files (since they don't use pull diagnostics)
}
// TODO: always publish diagnostics for notebook files (since they don't use pull diagnostics)
if client_capabilities.supports_inlay_hint_refresh() {
client.send_request::<types::request::InlayHintRefreshRequest>(session, (), |_, ()| {});

View File

@@ -33,3 +33,5 @@ pub(super) use shutdown::ShutdownHandler;
pub(super) use signature_help::SignatureHelpRequestHandler;
pub(super) use workspace_diagnostic::WorkspaceDiagnosticRequestHandler;
pub(super) use workspace_symbols::WorkspaceSymbolRequestHandler;
pub use workspace_diagnostic::{PartialWorkspaceProgress, PartialWorkspaceProgressParams};

View File

@@ -28,7 +28,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: CompletionParams,
) -> crate::server::Result<Option<CompletionResponse>> {

View File

@@ -29,11 +29,11 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: DocumentDiagnosticParams,
) -> Result<DocumentDiagnosticReportResult> {
let diagnostics = compute_diagnostics(db, &snapshot);
let diagnostics = compute_diagnostics(db, snapshot);
let Some(diagnostics) = diagnostics else {
return Ok(DocumentDiagnosticReportResult::Report(
@@ -43,23 +43,26 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
let result_id = diagnostics.result_id();
let report = if params.previous_result_id.as_deref() == Some(&result_id) {
DocumentDiagnosticReport::Unchanged(RelatedUnchangedDocumentDiagnosticReport {
related_documents: None,
unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport {
result_id,
},
})
} else {
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: Some(result_id),
// SAFETY: Pull diagnostic requests are only called for text documents, not for
// notebook documents.
items: diagnostics.to_lsp_diagnostics(db).expect_text_document(),
},
})
let report = match result_id {
Some(new_id) if Some(&new_id) == params.previous_result_id.as_ref() => {
DocumentDiagnosticReport::Unchanged(RelatedUnchangedDocumentDiagnosticReport {
related_documents: None,
unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport {
result_id: new_id,
},
})
}
new_id => {
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: new_id,
// SAFETY: Pull diagnostic requests are only called for text documents, not for
// notebook documents.
items: diagnostics.to_lsp_diagnostics(db).expect_text_document(),
},
})
}
};
Ok(DocumentDiagnosticReportResult::Report(report))

View File

@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: DocumentHighlightParams,
) -> crate::server::Result<Option<Vec<DocumentHighlight>>> {

View File

@@ -28,7 +28,7 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: DocumentSymbolParams,
) -> crate::server::Result<Option<lsp_types::DocumentSymbolResponse>> {

View File

@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: GotoDeclarationParams,
) -> crate::server::Result<Option<GotoDefinitionResponse>> {

View File

@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for GotoDefinitionRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: GotoDefinitionParams,
) -> crate::server::Result<Option<GotoDefinitionResponse>> {

View File

@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for ReferencesRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: ReferenceParams,
) -> crate::server::Result<Option<Vec<Location>>> {

View File

@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: GotoTypeDefinitionParams,
) -> crate::server::Result<Option<GotoDefinitionResponse>> {

View File

@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: HoverParams,
) -> crate::server::Result<Option<lsp_types::Hover>> {

View File

@@ -25,7 +25,7 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: InlayHintParams,
) -> crate::server::Result<Option<Vec<lsp_types::InlayHint>>> {

View File

@@ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: SelectionRangeParams,
) -> crate::server::Result<Option<Vec<LspSelectionRange>>> {

View File

@@ -22,7 +22,7 @@ impl BackgroundDocumentRequestHandler for SemanticTokensRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
_params: SemanticTokensParams,
) -> crate::server::Result<Option<SemanticTokensResult>> {

View File

@@ -24,7 +24,7 @@ impl BackgroundDocumentRequestHandler for SemanticTokensRangeRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: SemanticTokensRangeParams,
) -> crate::server::Result<Option<SemanticTokensRangeResult>> {

View File

@@ -2,6 +2,8 @@ use crate::Session;
use crate::server::api::traits::{RequestHandler, SyncRequestHandler};
use crate::session::client::Client;
use lsp_types::{WorkspaceDiagnosticReport, WorkspaceDiagnosticReportResult};
pub(crate) struct ShutdownHandler;
impl RequestHandler for ShutdownHandler {
@@ -9,9 +11,23 @@ impl RequestHandler for ShutdownHandler {
}
impl SyncRequestHandler for ShutdownHandler {
fn run(session: &mut Session, _client: &Client, _params: ()) -> crate::server::Result<()> {
tracing::debug!("Received shutdown request, waiting for shutdown notification");
fn run(session: &mut Session, client: &Client, _params: ()) -> crate::server::Result<()> {
tracing::debug!("Received shutdown request, waiting for exit notification");
// Respond to any pending workspace diagnostic requests
if let Some(suspended_workspace_request) =
session.take_suspended_workspace_diagnostic_request()
{
client.respond(
&suspended_workspace_request.id,
Ok(WorkspaceDiagnosticReportResult::Report(
WorkspaceDiagnosticReport::default(),
)),
);
}
session.set_shutdown_requested(true);
Ok(())
}
}

View File

@@ -28,7 +28,7 @@ impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler {
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
snapshot: &DocumentSnapshot,
_client: &Client,
params: SignatureHelpParams,
) -> crate::server::Result<Option<SignatureHelp>> {

View File

@@ -1,25 +1,96 @@
use lsp_types::request::WorkspaceDiagnosticRequest;
use lsp_types::{
FullDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport, Url,
WorkspaceDiagnosticParams, WorkspaceDiagnosticReport, WorkspaceDiagnosticReportResult,
WorkspaceDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport,
WorkspaceUnchangedDocumentDiagnosticReport,
};
use ruff_db::files::File;
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use ty_project::ProgressReporter;
use crate::server::Result;
use crate::PositionEncoding;
use crate::server::api::diagnostics::{Diagnostics, to_lsp_diagnostic};
use crate::server::api::traits::{
BackgroundRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::server::lazy_work_done_progress::LazyWorkDoneProgress;
use crate::session::SessionSnapshot;
use crate::server::{Action, Result};
use crate::session::client::Client;
use crate::system::file_to_url;
use crate::session::index::Index;
use crate::session::{SessionSnapshot, SuspendedWorkspaceDiagnosticRequest};
use crate::system::{AnySystemPath, file_to_url};
use lsp_server::RequestId;
use lsp_types::request::WorkspaceDiagnosticRequest;
use lsp_types::{
FullDocumentDiagnosticReport, PreviousResultId, ProgressToken,
UnchangedDocumentDiagnosticReport, Url, WorkspaceDiagnosticParams, WorkspaceDiagnosticReport,
WorkspaceDiagnosticReportPartialResult, WorkspaceDiagnosticReportResult,
WorkspaceDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport,
WorkspaceUnchangedDocumentDiagnosticReport, notification::Notification,
};
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::File;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
use ty_project::{Db, ProgressReporter};
/// Handler for [Workspace diagnostics](workspace-diagnostics)
///
/// Workspace diagnostics are special in many ways compared to other request handlers.
/// This is mostly due to the fact that computing them is expensive. Because of that,
/// the LSP supports multiple optimizations of which we all make use:
///
/// ## Partial results
///
/// Many clients support partial results. They allow a server
/// to send multiple responses (in the form of `$/progress` notifications) for
/// the same request. We use partial results to stream the results for
/// changed files. This has the obvious benefit is that users
/// don't need to wait for the entire check to complete before they see any diagnostics.
/// The other benefit of "chunking" the work also helps client to incrementally
/// update (and repaint) the diagnostics instead of all at once.
/// We did see lags in VS code for projects with 10k+ diagnostics before implementing
/// this improvement.
///
/// ## Result IDs
///
/// The server can compute a result id for every file which the client
/// sends back in the next pull or workspace diagnostic request. The way we use
/// the result id is that we compute a fingerprint of the file's diagnostics (a hash)
/// and compare it with the result id sent by the server. We know that
/// the diagnostics for a file are unchanged (the client still has the most recent review)
/// if the ids compare equal.
///
/// Result IDs are also useful to identify files for which ty no longer emits
/// any diagnostics. For example, file A contained a syntax error that has now been fixed
/// by the user. The client will send us a result id for file A but we won't match it with
/// any new diagnostics because all errors in the file were fixed. The fact that we can't
/// match up the result ID tells us that we need to clear the diagnostics on the client
/// side by sending an empty diagnostic report (report without any diagnostics). We'll set the
/// result id to `None` so that the client stops sending us a result id for this file.
///
/// Sending unchanged instead of the full diagnostics for files that haven't changed
/// helps reduce the data that's sent from the server to the client and it also enables long-polling
/// (see the next section).
///
/// ## Long polling
///
/// As of today (1st of August 2025), VS code's LSP client automatically schedules a
/// workspace diagnostic request every two seconds because it doesn't know *when* to pull
/// for new workspace diagnostics (it doesn't know what actions invalidate the diagnostics).
/// However, running the workspace diagnostics every two seconds is wasting a lot of CPU cycles (and battery life as a result)
/// if the user's only browsing the project (it requires ty to iterate over all files).
/// That's why we implement long polling (as recommended in the LSP) for workspace diagnostics.
///
/// The basic idea of long-polling is that the server doesn't respond if there are no diagnostics
/// or all diagnostics are unchanged. Instead, the server keeps the request open (it doesn't respond)
/// and only responses when the diagnostics change. This puts the server in full control of when
/// to recheck a workspace and a client can simply wait for the response to come in.
///
/// One challenge with long polling for ty's server architecture is that we can't just keep
/// the background thread running because holding on to the [`ProjectDatabase`] references
/// prevents notifications from acquiring the exclusive db lock (or the long polling background thread
/// panics if a notification tries to do so). What we do instead is that this request handler
/// doesn't send a response if there are no diagnostics or all are unchanged and it
/// sets a "[snapshot](SuspendedWorkspaceDiagnosticRequest)" of the workspace diagnostic request on the [`Session`].
/// The second part to this is in the notification request handling. ty retries the
/// suspended workspace diagnostic request (if any) after every notification if the notification
/// changed the [`Session`]'s state.
///
/// [workspace-diagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_diagnostic)
pub(crate) struct WorkspaceDiagnosticRequestHandler;
impl RequestHandler for WorkspaceDiagnosticRequestHandler {
@@ -28,7 +99,7 @@ impl RequestHandler for WorkspaceDiagnosticRequestHandler {
impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
fn run(
snapshot: SessionSnapshot,
snapshot: &SessionSnapshot,
client: &Client,
params: WorkspaceDiagnosticParams,
) -> Result<WorkspaceDiagnosticReportResult> {
@@ -41,12 +112,12 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
));
}
// Create a map of previous result IDs for efficient lookup
let mut previous_results: BTreeMap<_, _> = params
.previous_result_ids
.into_iter()
.map(|prev| (prev.uri, prev.value))
.collect();
let writer = ResponseWriter::new(
params.partial_result_params.partial_result_token,
params.previous_result_ids,
snapshot,
client,
);
// Use the work done progress token from the client request, if provided
// Note: neither VS Code nor Zed currently support this,
@@ -58,103 +129,57 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
"Checking",
snapshot.resolved_client_capabilities(),
);
// Collect all diagnostics from all projects with their database references
let mut items = Vec::new();
let mut reporter = WorkspaceDiagnosticsProgressReporter::new(work_done_progress, writer);
for db in snapshot.projects() {
let diagnostics = db.check_with_reporter(
&mut WorkspaceDiagnosticsProgressReporter::new(work_done_progress.clone()),
);
db.check_with_reporter(&mut reporter);
}
// Group diagnostics by URL
let mut diagnostics_by_url: BTreeMap<Url, Vec<_>> = BTreeMap::default();
Ok(reporter.into_final_report())
}
for diagnostic in diagnostics {
if let Some(span) = diagnostic.primary_span() {
let file = span.expect_ty_file();
let Some(url) = file_to_url(db, file) else {
tracing::debug!("Failed to convert file to URL at {}", file.path(db));
continue;
};
diagnostics_by_url.entry(url).or_default().push(diagnostic);
}
}
fn handle_request(
id: &RequestId,
snapshot: SessionSnapshot,
client: &Client,
params: WorkspaceDiagnosticParams,
) {
let result = Self::run(&snapshot, client, params.clone());
items.reserve(diagnostics_by_url.len());
// Test if this is a no-op result, in which case we should long-poll the request and
// only respond once some diagnostics have changed to get the latest result ids.
//
// Bulk response: This the simple case. Simply test if all diagnostics are unchanged (or empty)
// Streaming: This trickier but follows the same principle.
// * If the server sent any partial results, then `result` is a `Partial` result (in which
// case we shouldn't do any long polling because some diagnostics changed).
// * If this is a full report, then check if all items are unchanged (or empty), the same as for
// the non-streaming case.
if let Ok(WorkspaceDiagnosticReportResult::Report(full)) = &result {
let all_unchanged = full
.items
.iter()
.all(|item| matches!(item, WorkspaceDocumentDiagnosticReport::Unchanged(_)));
// Convert to workspace diagnostic report format
for (url, file_diagnostics) in diagnostics_by_url {
let version = index
.key_from_url(url.clone())
.ok()
.and_then(|key| index.make_document_ref(key).ok())
.map(|doc| i64::from(doc.version()));
let result_id = Diagnostics::result_id_from_hash(&file_diagnostics);
if all_unchanged {
tracing::debug!(
"Suspending workspace diagnostic request, all diagnostics are unchanged or the project has no diagnostics"
);
// Check if this file's diagnostics have changed since the previous request
if let Some(previous_result_id) = previous_results.remove(&url) {
if previous_result_id == result_id {
// Diagnostics haven't changed, return unchanged report
items.push(WorkspaceDocumentDiagnosticReport::Unchanged(
WorkspaceUnchangedDocumentDiagnosticReport {
uri: url,
version,
unchanged_document_diagnostic_report:
UnchangedDocumentDiagnosticReport { result_id },
},
));
continue;
}
}
// Convert diagnostics to LSP format
let lsp_diagnostics = file_diagnostics
.into_iter()
.map(|diagnostic| {
to_lsp_diagnostic(db, &diagnostic, snapshot.position_encoding())
})
.collect::<Vec<_>>();
// Diagnostics have changed or this is the first request, return full report
items.push(WorkspaceDocumentDiagnosticReport::Full(
WorkspaceFullDocumentDiagnosticReport {
uri: url,
version,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: Some(result_id),
items: lsp_diagnostics,
},
client.queue_action(Action::SuspendWorkspaceDiagnostics(Box::new(
SuspendedWorkspaceDiagnosticRequest {
id: id.clone(),
params: serde_json::to_value(&params).unwrap(),
revision: snapshot.revision(),
},
));
)));
// Don't respond, keep the request open (long polling).
return;
}
}
// Handle files that had diagnostics in previous request but no longer have any
// Any remaining entries in previous_results are files that were fixed
for (previous_url, _previous_result_id) in previous_results {
// This file had diagnostics before but doesn't now, so we need to report it as having no diagnostics
let version = index
.key_from_url(previous_url.clone())
.ok()
.and_then(|key| index.make_document_ref(key).ok())
.map(|doc| i64::from(doc.version()));
items.push(WorkspaceDocumentDiagnosticReport::Full(
WorkspaceFullDocumentDiagnosticReport {
uri: previous_url,
version,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: None, // No result ID needed for empty diagnostics
items: vec![], // No diagnostics
},
},
));
}
Ok(WorkspaceDiagnosticReportResult::Report(
WorkspaceDiagnosticReport { items },
))
client.respond(id, result);
}
}
@@ -171,21 +196,32 @@ impl RetriableRequestHandler for WorkspaceDiagnosticRequestHandler {
}
}
struct WorkspaceDiagnosticsProgressReporter {
/// ty progress reporter that streams the diagnostics to the client
/// and sends progress reports (checking X/Y files).
///
/// Diagnostics are only streamed if the client sends a partial result token.
struct WorkspaceDiagnosticsProgressReporter<'a> {
total_files: usize,
checked_files: AtomicUsize,
work_done: LazyWorkDoneProgress,
response: std::sync::Mutex<ResponseWriter<'a>>,
}
impl WorkspaceDiagnosticsProgressReporter {
fn new(work_done: LazyWorkDoneProgress) -> Self {
impl<'a> WorkspaceDiagnosticsProgressReporter<'a> {
fn new(work_done: LazyWorkDoneProgress, response: ResponseWriter<'a>) -> Self {
Self {
total_files: 0,
checked_files: AtomicUsize::new(0),
work_done,
response: std::sync::Mutex::new(response),
}
}
fn into_final_report(self) -> WorkspaceDiagnosticReportResult {
let writer = self.response.into_inner().unwrap();
writer.into_final_report()
}
fn report_progress(&self) {
let checked = self.checked_files.load(Ordering::Relaxed);
let total = self.total_files;
@@ -207,18 +243,371 @@ impl WorkspaceDiagnosticsProgressReporter {
}
}
impl ProgressReporter for WorkspaceDiagnosticsProgressReporter {
impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> {
fn set_files(&mut self, files: usize) {
self.total_files += files;
self.report_progress();
}
fn report_file(&self, _file: &File) {
fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
let checked = self.checked_files.fetch_add(1, Ordering::Relaxed) + 1;
if checked % 10 == 0 || checked == self.total_files {
// Report progress every 10 files or when all files are checked
if checked % 100 == 0 || checked == self.total_files {
// Report progress every 100 files or when all files are checked
self.report_progress();
}
// Another thread might have panicked at this point because of a salsa cancellation which
// poisoned the result. If the response is poisoned, just don't report and wait for our thread
// to unwind with a salsa cancellation next.
let Ok(mut response) = self.response.lock() else {
return;
};
// Don't report empty diagnostics. We clear previous diagnostics in `into_response`
// which also handles the case where a file no longer has diagnostics because
// it's no longer part of the project.
if !diagnostics.is_empty() {
response.write_diagnostics_for_file(db, file, diagnostics);
}
response.maybe_flush();
}
fn report_diagnostics(&mut self, db: &dyn Db, diagnostics: Vec<Diagnostic>) {
let mut by_file: BTreeMap<File, Vec<Diagnostic>> = BTreeMap::new();
for diagnostic in diagnostics {
if let Some(file) = diagnostic.primary_span().map(|span| span.expect_ty_file()) {
by_file.entry(file).or_default().push(diagnostic);
} else {
tracing::debug!(
"Ignoring diagnostic without a file: {diagnostic}",
diagnostic = diagnostic.primary_message()
);
}
}
let response = self.response.get_mut().unwrap();
for (file, diagnostics) in by_file {
response.write_diagnostics_for_file(db, file, &diagnostics);
}
response.maybe_flush();
}
}
#[derive(Debug)]
struct ResponseWriter<'a> {
mode: ReportingMode,
index: &'a Index,
position_encoding: PositionEncoding,
// It's important that we use `AnySystemPath` over `Url` here because
// `file_to_url` isn't guaranteed to return the exact same URL as the one provided
// by the client.
previous_result_ids: FxHashMap<AnySystemPath, (Url, String)>,
}
impl<'a> ResponseWriter<'a> {
fn new(
partial_result_token: Option<ProgressToken>,
previous_result_ids: Vec<PreviousResultId>,
snapshot: &'a SessionSnapshot,
client: &Client,
) -> Self {
let index = snapshot.index();
let position_encoding = snapshot.position_encoding();
let mode = if let Some(token) = partial_result_token {
ReportingMode::Streaming(Streaming {
first: true,
client: client.clone(),
token,
is_test: snapshot.in_test(),
last_flush: Instant::now(),
changed: Vec::new(),
unchanged: Vec::with_capacity(previous_result_ids.len()),
})
} else {
ReportingMode::Bulk(Vec::new())
};
let previous_result_ids = previous_result_ids
.into_iter()
.filter_map(|prev| {
Some((
AnySystemPath::try_from_url(&prev.uri).ok()?,
(prev.uri, prev.value),
))
})
.collect();
Self {
mode,
index,
position_encoding,
previous_result_ids,
}
}
fn write_diagnostics_for_file(&mut self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
let Some(url) = file_to_url(db, file) else {
tracing::debug!("Failed to convert file path to URL at {}", file.path(db));
return;
};
let version = self
.index
.key_from_url(url.clone())
.ok()
.and_then(|key| self.index.make_document_ref(key).ok())
.map(|doc| i64::from(doc.version()));
let result_id = Diagnostics::result_id_from_hash(diagnostics);
let previous_result_id = AnySystemPath::try_from_url(&url)
.ok()
.and_then(|path| self.previous_result_ids.remove(&path))
.map(|(_url, id)| id);
let report = match result_id {
Some(new_id) if Some(&new_id) == previous_result_id.as_ref() => {
WorkspaceDocumentDiagnosticReport::Unchanged(
WorkspaceUnchangedDocumentDiagnosticReport {
uri: url,
version,
unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport {
result_id: new_id,
},
},
)
}
new_id => {
let lsp_diagnostics = diagnostics
.iter()
.map(|diagnostic| to_lsp_diagnostic(db, diagnostic, self.position_encoding))
.collect::<Vec<_>>();
WorkspaceDocumentDiagnosticReport::Full(WorkspaceFullDocumentDiagnosticReport {
uri: url,
version,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: new_id,
items: lsp_diagnostics,
},
})
}
};
self.write_report(report);
}
fn write_report(&mut self, report: WorkspaceDocumentDiagnosticReport) {
match &mut self.mode {
ReportingMode::Streaming(streaming) => {
streaming.write_report(report);
}
ReportingMode::Bulk(all) => {
all.push(report);
}
}
}
/// Flush any pending reports if streaming diagnostics.
///
/// Note: The flush is throttled when streaming.
fn maybe_flush(&mut self) {
match &mut self.mode {
ReportingMode::Streaming(streaming) => streaming.maybe_flush(),
ReportingMode::Bulk(_) => {}
}
}
/// Creates the final response after all files have been processed.
///
/// The result can be a partial or full report depending on whether the server's streaming
/// diagnostics and if it already sent some diagnostics.
fn into_final_report(mut self) -> WorkspaceDiagnosticReportResult {
let mut items = Vec::new();
// Handle files that had diagnostics in previous request but no longer have any
// Any remaining entries in previous_results are files that were fixed
for (previous_url, previous_result_id) in self.previous_result_ids.into_values() {
// This file had diagnostics before but doesn't now, so we need to report it as having no diagnostics
let version = self
.index
.key_from_url(previous_url.clone())
.ok()
.and_then(|key| self.index.make_document_ref(key).ok())
.map(|doc| i64::from(doc.version()));
let new_result_id = Diagnostics::result_id_from_hash(&[]);
let report = match new_result_id {
Some(new_id) if new_id == previous_result_id => {
WorkspaceDocumentDiagnosticReport::Unchanged(
WorkspaceUnchangedDocumentDiagnosticReport {
uri: previous_url,
version,
unchanged_document_diagnostic_report:
UnchangedDocumentDiagnosticReport { result_id: new_id },
},
)
}
new_id => {
WorkspaceDocumentDiagnosticReport::Full(WorkspaceFullDocumentDiagnosticReport {
uri: previous_url,
version,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: new_id,
items: vec![], // No diagnostics
},
})
}
};
items.push(report);
}
match &mut self.mode {
ReportingMode::Streaming(streaming) => {
items.extend(
std::mem::take(&mut streaming.changed)
.into_iter()
.map(WorkspaceDocumentDiagnosticReport::Full),
);
items.extend(
std::mem::take(&mut streaming.unchanged)
.into_iter()
.map(WorkspaceDocumentDiagnosticReport::Unchanged),
);
}
ReportingMode::Bulk(all) => {
all.extend(items);
items = std::mem::take(all);
}
}
self.mode.create_result(items)
}
}
#[derive(Debug)]
enum ReportingMode {
/// Streams the diagnostics to the client as they are computed (file by file).
/// Requires that the client provides a partial result token.
Streaming(Streaming),
/// For clients that don't support streaming diagnostics. Collects all workspace
/// diagnostics and sends them in the `workspace/diagnostic` response.
Bulk(Vec<WorkspaceDocumentDiagnosticReport>),
}
impl ReportingMode {
fn create_result(
&mut self,
items: Vec<WorkspaceDocumentDiagnosticReport>,
) -> WorkspaceDiagnosticReportResult {
match self {
ReportingMode::Streaming(streaming) => streaming.create_result(items),
ReportingMode::Bulk(..) => {
WorkspaceDiagnosticReportResult::Report(WorkspaceDiagnosticReport { items })
}
}
}
}
#[derive(Debug)]
struct Streaming {
first: bool,
client: Client,
/// The partial result token.
token: ProgressToken,
/// Throttles the flush reports to not happen more than once every 100ms.
last_flush: Instant,
is_test: bool,
/// The reports for files with changed diagnostics.
/// The implementation uses batching to avoid too many
/// requests for large projects (can slow down the entire
/// analysis).
changed: Vec<WorkspaceFullDocumentDiagnosticReport>,
/// All the unchanged reports. Don't stream them,
/// since nothing has changed.
unchanged: Vec<WorkspaceUnchangedDocumentDiagnosticReport>,
}
impl Streaming {
fn write_report(&mut self, report: WorkspaceDocumentDiagnosticReport) {
match report {
WorkspaceDocumentDiagnosticReport::Full(full) => {
self.changed.push(full);
}
WorkspaceDocumentDiagnosticReport::Unchanged(unchanged) => {
self.unchanged.push(unchanged);
}
}
}
fn maybe_flush(&mut self) {
if self.changed.is_empty() {
return;
}
// Flush every ~50ms or whenever we have two items and this is a test run.
let should_flush = if self.is_test {
self.changed.len() >= 2
} else {
self.last_flush.elapsed().as_millis() >= 50
};
if !should_flush {
return;
}
let items = self
.changed
.drain(..)
.map(WorkspaceDocumentDiagnosticReport::Full)
.collect();
let report = self.create_result(items);
self.client
.send_notification::<PartialWorkspaceProgress>(PartialWorkspaceProgressParams {
token: self.token.clone(),
value: report,
});
self.last_flush = Instant::now();
}
fn create_result(
&mut self,
items: Vec<WorkspaceDocumentDiagnosticReport>,
) -> WorkspaceDiagnosticReportResult {
// As per the LSP spec:
// > partial result: The first literal send need to be a WorkspaceDiagnosticReport followed
// > by `n` WorkspaceDiagnosticReportPartialResult literals defined as follows:
if self.first {
self.first = false;
WorkspaceDiagnosticReportResult::Report(WorkspaceDiagnosticReport { items })
} else {
WorkspaceDiagnosticReportResult::Partial(WorkspaceDiagnosticReportPartialResult {
items,
})
}
}
}
/// The `$/progress` notification for partial workspace diagnostics.
///
/// This type is missing in `lsp_types`. That's why we define it here.
pub struct PartialWorkspaceProgress;
impl Notification for PartialWorkspaceProgress {
type Params = PartialWorkspaceProgressParams;
const METHOD: &'static str = "$/progress";
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct PartialWorkspaceProgressParams {
pub token: ProgressToken,
pub value: WorkspaceDiagnosticReportResult,
}

View File

@@ -19,7 +19,7 @@ impl RequestHandler for WorkspaceSymbolRequestHandler {
impl BackgroundRequestHandler for WorkspaceSymbolRequestHandler {
fn run(
snapshot: SessionSnapshot,
snapshot: &SessionSnapshot,
_client: &Client,
params: WorkspaceSymbolParams,
) -> crate::server::Result<Option<WorkspaceSymbolResponse>> {

View File

@@ -33,10 +33,10 @@
//! See the `./requests` and `./notifications` directories for concrete implementations of these
//! traits in action.
use std::borrow::Cow;
use crate::session::client::Client;
use crate::session::{DocumentSnapshot, Session, SessionSnapshot};
use lsp_server::RequestId;
use std::borrow::Cow;
use lsp_types::Url;
use lsp_types::notification::Notification;
@@ -91,12 +91,36 @@ pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler {
params: &<<Self as RequestHandler>::RequestType as Request>::Params,
) -> Cow<Url>;
/// Processes the request parameters and returns the LSP request result.
///
/// This is the main method that handlers implement. It takes the request parameters
/// from the client and computes the appropriate response data for the LSP request.
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: &DocumentSnapshot,
client: &Client,
params: <<Self as RequestHandler>::RequestType as Request>::Params,
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
/// Handles the entire request lifecycle and sends the response to the client.
///
/// It allows handlers to customize how the server sends the response to the client.
fn handle_request(
id: &RequestId,
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
client: &Client,
params: <<Self as RequestHandler>::RequestType as Request>::Params,
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
) {
let result = Self::run_with_snapshot(db, &snapshot, client, params);
if let Err(err) = &result {
tracing::error!("An error occurred with request ID {id}: {err}");
client.show_error_message("ty encountered a problem. Check the logs for more details.");
}
client.respond(id, result);
}
}
/// A request handler that can be run on a background thread.
@@ -106,11 +130,34 @@ pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler {
/// operations that require access to the entire session state, such as fetching workspace
/// diagnostics.
pub(super) trait BackgroundRequestHandler: RetriableRequestHandler {
/// Processes the request parameters and returns the LSP request result.
///
/// This is the main method that handlers implement. It takes the request parameters
/// from the client and computes the appropriate response data for the LSP request.
fn run(
snapshot: SessionSnapshot,
snapshot: &SessionSnapshot,
client: &Client,
params: <<Self as RequestHandler>::RequestType as Request>::Params,
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
/// Handles the request lifecycle and sends the response to the client.
///
/// It allows handlers to customize how the server sends the response to the client.
fn handle_request(
id: &RequestId,
snapshot: SessionSnapshot,
client: &Client,
params: <<Self as RequestHandler>::RequestType as Request>::Params,
) {
let result = Self::run(&snapshot, client, params);
if let Err(err) = &result {
tracing::error!("An error occurred with request ID {id}: {err}");
client.show_error_message("ty encountered a problem. Check the logs for more details.");
}
client.respond(id, result);
}
}
/// A supertrait for any server notification handler.

View File

@@ -1,7 +1,7 @@
use crate::server::schedule::Scheduler;
use crate::server::{Server, api};
use crate::session::ClientOptions;
use crate::session::client::{Client, ClientResponseHandler};
use crate::session::{ClientOptions, SuspendedWorkspaceDiagnosticRequest};
use anyhow::anyhow;
use crossbeam::select;
use lsp_server::Message;
@@ -49,7 +49,8 @@ impl Server {
if self.session.is_shutdown_requested() {
tracing::warn!(
"Received request after server shutdown was requested, discarding"
"Received request `{}` after server shutdown was requested, discarding",
&req.method
);
client.respond_err(
req.id,
@@ -130,7 +131,8 @@ impl Server {
.incoming()
.is_pending(&request.id)
{
api::request(request);
let task = api::request(request);
scheduler.dispatch(task, &mut self.session, client);
} else {
tracing::debug!(
"Request {}/{} was cancelled, not retrying",
@@ -142,6 +144,13 @@ impl Server {
Action::SendRequest(request) => client.send_request_raw(&self.session, request),
Action::SuspendWorkspaceDiagnostics(suspended_request) => {
self.session.set_suspended_workspace_diagnostics_request(
*suspended_request,
&client,
);
}
Action::InitializeWorkspaces(workspaces_with_options) => {
self.session
.initialize_workspaces(workspaces_with_options, &client);
@@ -227,11 +236,9 @@ impl Server {
);
let fs_watcher = self
.client_capabilities
.workspace
.as_ref()
.and_then(|workspace| workspace.did_change_watched_files?.dynamic_registration)
.unwrap_or_default();
.session
.client_capabilities()
.supports_did_change_watched_files_dynamic_registration();
if fs_watcher {
let registration = lsp_types::Registration {
@@ -306,6 +313,8 @@ pub(crate) enum Action {
/// Send a request from the server to the client.
SendRequest(SendRequest),
SuspendWorkspaceDiagnostics(Box<SuspendedWorkspaceDiagnosticRequest>),
/// Initialize the workspace after the server received
/// the options from the client.
InitializeWorkspaces(Vec<(Url, ClientOptions)>),

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