Compare commits

...

61 Commits

Author SHA1 Message Date
David Peter
e609da0c72 Remove it from ClassType 2025-11-26 14:55:59 +01:00
David Peter
7818186fcc GenericAliasInstance 2025-11-26 14:19:10 +01:00
David Peter
54c88b599d Store binding context 2025-11-26 12:02:48 +01:00
David Peter
8ed96b04e4 Cleanup 2025-11-26 09:04:44 +01:00
David Peter
0a2536736b Better diagnostic message 2025-11-25 14:54:04 +01:00
David Peter
6aaa9d784a Fix problem with np.array related to type[T] 2025-11-25 12:04:41 +01:00
David Peter
d85469e94c Store definition in instance types 2025-11-25 09:56:21 +01:00
David Peter
f184132d69 Fix value-position specializations 2025-11-25 08:57:30 +01:00
David Peter
96c491099f Rename 2025-11-25 08:57:30 +01:00
David Peter
c1e6ecccc0 Patch panics for stringified annotations for now 2025-11-25 08:57:30 +01:00
David Peter
343c6b6287 Handle PEP 613 aliases as well 2025-11-25 08:57:30 +01:00
David Peter
f40ab81093 Handle attribute expressions as well 2025-11-25 08:57:30 +01:00
David Peter
eee6f25f2e Use assignment definition as typevar binding context 2025-11-25 08:57:30 +01:00
David Peter
013d43a2dd [ty] Generic implicit types aliases 2025-11-25 08:57:30 +01:00
Shunsuke Shibayama
dd15656deb [ty] fix ty playground initialization and vite optimization issues (#21471)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-11-25 07:42:56 +00:00
Alex Waygood
adf095e889 [ty] Extend Liskov checks to also cover classmethods and staticmethods (#21598)
## Summary

Building on https://github.com/astral-sh/ruff/pull/21436.

There's nothing conceptually more complicated about this, it just
requires its own set of tests and its own subdiagnostic hint.

I also uncovered another inconsistency between mypy/pyright/pyrefly,
which is fun. In this case, I suggest we go with pyright's behaviour.

## Test Plan

mdtests/snapshots
2025-11-24 23:14:06 +00:00
Alex Waygood
bfd65c4215 Dogfood ty on the scripts directory (#21617)
## Summary

This PR sets up CI jobs to run ty from the `main` branch on the files
and subdirectories in our `scripts` directory

## Test Plan

Both these commands pass for me locally:
- `uv run --project=./scripts cargo run -p ty check --project=./scripts`
- `uv run --project=./scripts/ty_benchmark cargo run -p ty check
--project=./scripts/ty_benchmark`
2025-11-24 23:13:44 +00:00
Jack O'Connor
0631e72187 [ty] support generic aliases in type[...], like type[C[int]] (#21552)
Closes https://github.com/astral-sh/ty/issues/1101.
2025-11-24 13:56:42 -08:00
Alex Waygood
bab688b76c [ty] Retain the function-like-ness of Callable types when binding self (#21614)
## Summary

For something like this:

```py
from typing import Callable

def my_lossy_decorator(fn: Callable[..., int]) -> Callable[..., int]:
    return fn

class MyClass:
    @my_lossy_decorator
    def method(self) -> int:
        return 42
```

we will currently infer the type of `MyClass.method` as a function-like
`Callable`, but we will infer the type of `MyClass().method` as a
`Callable` that is _not_ function-like. That's because a `CallableType`
currently "forgets" whether it was function-like or not during the
`bound_self` transformation:


a57e291311/crates/ty_python_semantic/src/types.rs (L10985-L10987)

This seems incorrect, and it's quite different to what we do when
binding the `self` parameter of `FunctionLiteral` types: `BoundMethod`
types are all seen as subtypes of function-like `Callable` supertypes --
here's `BoundMethodType::into_callable_type`:


a57e291311/crates/ty_python_semantic/src/types.rs (L10844-L10860)

The bug here is also causing lots of false positives in the ecosystem
report on https://github.com/astral-sh/ruff/pull/21611: a decorated
method on a subclass is currently not seen as validly overriding an
undecorated method with the same signature on a superclass, because the
undecorated superclass method is seen as function-like after binding
`self` whereas the decorated subclass method is not.

Fixing the bug required adding a new API in `protocol_class.rs`, because
it turns out that for our purposes in protocol subtyping/assignability,
we really do want a callable type to forget its function-like-ness when
binding `self`.

I initially tried out this change without changing anything in
`protocol_class.rs`. However, it resulted in many ecosystem false
positives and new false positives on the typing conformance test suite.
This is because it would mean that no protocol with a `__call__` method
would ever be seen as a subtype of a `Callable` type, since the
`__call__` method on the protocol would be seen as being function-like
whereas the `Callable` type would not be seen as function-like.

## Test Plan

Added an mdtest that fails on `main`
2025-11-24 21:14:03 +00:00
Douglas Creager
7e277667d1 [ty] Distinguish "unconstrained" from "constrained to any type" (#21539)
Before, we would collapse any constraint of the form `Never ≤ T ≤
object` down to the "always true" constraint set. This is correct in
terms of BDD semantics, but loses information, since "not constraining a
typevar at all" is different than "constraining a typevar to take on any
type". Once we get to specialization inference, we should fall back on
the typevar's default for the former, but not for the latter.

This is much easier to support now that we have a sequent map, since we
need to treat `¬(Never ≤ T ≤ object)` as being impossible, and prune it
when we walk through BDD paths, just like we do for other impossible
combinations.
2025-11-24 15:23:09 -05:00
Alex Waygood
d379f3826f Disable ty workspace diagnostics for VSCode users (#21620) 2025-11-24 20:06:09 +00:00
Matthew Mckee
6f9265d78d [ty] Double click to insert inlay hint (#21600)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

Resolves
https://github.com/astral-sh/ty/issues/317#issuecomment-3567398107.

I can't get the auto import working great.

I haven't added many places where we specify that the type display is
invalid syntax.

## Test Plan

Nothing yet
2025-11-24 19:48:30 +00:00
Alex Waygood
0c6d652b5f [ty] Switch the error code from unresolved-attribute to possibly-missing-attribute for submodules that may not be available (#21618) 2025-11-24 19:15:45 +00:00
Douglas Creager
03fe560164 [ty] Substitute for typing.Self when checking protocol members (#21569)
This patch updates our protocol assignability checks to substitute for
any occurrences of `typing.Self` in method signatures, replacing it with
the class being checked for assignability against the protocol.

This requires a new helper method on signatures, `apply_self`, which
substitutes occurrences of `typing.Self` _without_ binding the `self`
parameter.

We also update the `try_upcast_to_callable` method. Before, it would
return a `Type`, since certain types upcast to a _union_ of callables,
not to a single callable. However, even in that case, we know that every
element of the union is a callable. We now return a vector of
`CallableType`. (Actually a smallvec to handle the most common case of a
single callable; and wrapped in a new type so that we can provide helper
methods.) If there is more than one element in the result, it represents
a union of callables. This lets callers get at the `CallableType`
instances in a more type-safe way. (This makes it easier for our
protocol checking code to call the new `apply_self` helper.) We also
provide an `into_type` method so that callers that really do want a
`Type` can get the original result easily.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-11-24 14:05:09 -05:00
Andrew Gallant
68343e7edf [ty] Don't suggest things that aren't subclasses of BaseException after raise
This only applies to items that have a type associated with them. That
is, things that are already in scope. For items that don't have a type
associated with them (i.e., suggestions from auto-import), we still
suggest them since we can't know if they're appropriate or not. It's not
quite clear on how best to improve here for the auto-import case. (Short
of, say, asking for the type of each such symbol. But the performance
implications of that aren't known yet.)

Note that because of auto-import, we were still suggesting
`NotImplemented` even though astral-sh/ty#1262 specifically cites it as
the motivating example that we *shouldn't* suggest. This was occuring
because auto-import was including symbols from the `builtins` module,
even though those are actually already in scope. So this PR also gets
rid of those suggestions from auto-import.

Overall, this means that, at least, `raise NotImpl` won't suggest
`NotImplemented`.

Fixes astral-sh/ty#1262
2025-11-24 12:55:30 -05:00
Alex Waygood
a57e291311 [ty] Add hint about resolved Python version when a user attempts to import a member added on a newer version (#21615)
## Summary

Fixes https://github.com/astral-sh/ty/issues/1620. #20909 added hints if
you do something like this and your Python version is set to 3.10 or
lower:

```py
import typing

typing.LiteralString
```

And we also have hints if you try to do something like this and your
Python version is set too low:

```py
from stdlib_module import new_submodule
```

But we don't currently have any subdiagnostic hint if you do something
like _this_ and your Python version is set too low:

```py
from typing import LiteralString
```

This PR adds that hint!

## Test Plan

snapshots

---------

Co-authored-by: Aria Desires <aria.desires@gmail.com>
2025-11-24 15:12:01 +00:00
Micha Reiser
f317a71682 Use release commit for actions/checkout (#21610) 2025-11-24 09:24:23 +01:00
Alex Waygood
35bfcff24d [ty] Add failing mdtest for known Protocol panic (#21594)
## Summary

This PR adds a failing mdtest for the panic in
https://github.com/astral-sh/ty/issues/1587. The added snippet currently
panics with this query stacktrace:

```
error[panic]: Panicked at /Users/alexw/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/17bc55d/src/function/execute.rs:321:21 when checking `/Users/alexw/dev/ruff/foo.py`: `ClassLiteral < 'db >::explicit_bases_(Id(4c09)): execute: too many cycle iterations`
info: This indicates a bug in ty.
info: If you could open an issue at https://github.com/astral-sh/ty/issues/new?title=%5Bpanic%5D, we'd be very appreciative!
info: Platform: macos aarch64
info: Version: ruff/0.14.5+105 (d24c891a4 2025-11-22)
info: Args: ["target/debug/ty", "check", "foo.py", "--python-version=3.14"]
info: run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information
info: query stacktrace:
   0: cached_protocol_interface(Id(6805))
             at crates/ty_python_semantic/src/types/protocol_class.rs:795
   1: is_equivalent_to_object_inner(Id(8003))
             at crates/ty_python_semantic/src/types/instance.rs:667
   2: infer_deferred_types(Id(1406))
             at crates/ty_python_semantic/src/types/infer.rs:140
             cycle heads: infer_definition_types(Id(140b)) -> iteration = 200, TypeVarInstance < 'db >::lazy_bound_(Id(5802)) -> iteration = 200
   3: TypeVarInstance < 'db >::lazy_bound_(Id(5803))
             at crates/ty_python_semantic/src/types.rs:8827
   4: infer_definition_types(Id(140c))
             at crates/ty_python_semantic/src/types/infer.rs:94
   5: infer_deferred_types(Id(1405))
             at crates/ty_python_semantic/src/types/infer.rs:140
   6: TypeVarInstance < 'db >::lazy_bound_(Id(5802))
             at crates/ty_python_semantic/src/types.rs:8827
   7: infer_definition_types(Id(140b))
             at crates/ty_python_semantic/src/types/infer.rs:94
   8: infer_scope_types(Id(1000))
             at crates/ty_python_semantic/src/types/infer.rs:70
   9: check_file_impl(Id(c00))
             at crates/ty_project/src/lib.rs:535
```

It's not totally clear to me how to fix this or to what extent it might
be a bug in our `Protocol` internals rather than a bug in our `TypeVar`
internals. (It's sort of interesting that we're trying to evaluate the
upper bound of any `TypeVar`s here!) @carljm suggested that it would be
a good idea to add a failing mdtest in the meantime to document the
panic, which I agree with.

## Test Plan

I verified that we panic on this snippet, and that the test fails if I
remove the `expect-panic` assertion or if I change the asserted error
message.

I experimented with ways of minimizing the snippet further, but I think
any further minimization takes the snippet further away from something a
user would actually be likely to write -- so I think is probably
counterproductive. The failing test added in this PR isn't unreasonable
code at the end of the day; I've seen Python like it in the wild.
2025-11-24 08:21:15 +00:00
Dan Parizher
474b00568a [parser] Fix panic when parsing IPython escape command expressions (#21480)
## Summary

Fixes a panic when parsing IPython escape commands with `Help` kind
(`?`) in expression contexts. The parser now reports an error instead of
panicking.

Fixes #21465.

## Problem

The parser panicked with `unreachable!()` in
`parse_ipython_escape_command_expression` when encountering escape
commands with `Help` kind (`?`) in expression contexts, where only
`Magic` (`%`) and `Shell` (`!`) are allowed.

## Approach

Replaced the `unreachable!()` panic with error handling that adds a
`ParseErrorType::OtherError` and continues parsing, returning a valid
AST node with the error attached.

## Test Plan

Added `test_ipython_escape_command_in_with_statement` and
`test_ipython_help_escape_command_as_expression` to verify the fix.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-11-24 05:40:27 +00:00
Dhruv Manilawala
3b23d3c041 Fix cargo shear in CI (#21609)
Tested using `cargo build` and `cargo build --release`.
2025-11-24 05:35:34 +00:00
renovate[bot]
3f4875313f Update actions/checkout digest to c2d88d3 (#21601)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| actions/checkout | action | digest | `ff7abcd` -> `c2d88d3` |

---

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

---

### 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:eyJjcmVhdGVkSW5WZXIiOiI0Mi4xNi4xIiwidXBkYXRlZEluVmVyIjoiNDIuMTYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 05:27:54 +00:00
renovate[bot]
8327f262ff Update dependency ruff to v0.14.6 (#21603)
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.14.5` -> `==0.14.6` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.14.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.14.5/0.14.6?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.14.6`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0146)

[Compare
Source](https://redirect.github.com/astral-sh/ruff/compare/0.14.5...0.14.6)

Released on 2025-11-21.

##### Preview features

- \[`flake8-bandit`] Support new PySNMP API paths (`S508`, `S509`)
([#&#8203;21374](https://redirect.github.com/astral-sh/ruff/pull/21374))

##### Bug fixes

- Adjust own-line comment placement between branches
([#&#8203;21185](https://redirect.github.com/astral-sh/ruff/pull/21185))
- Avoid syntax error when formatting attribute expressions with outer
parentheses, parenthesized value, and trailing comment on value
([#&#8203;20418](https://redirect.github.com/astral-sh/ruff/pull/20418))
- Fix panic when formatting comments in unary expressions
([#&#8203;21501](https://redirect.github.com/astral-sh/ruff/pull/21501))
- Respect `fmt: skip` for compound statements on a single line
([#&#8203;20633](https://redirect.github.com/astral-sh/ruff/pull/20633))
- \[`refurb`] Fix `FURB103` autofix
([#&#8203;21454](https://redirect.github.com/astral-sh/ruff/pull/21454))
- \[`ruff`] Fix false positive for complex conversion specifiers in
`logging-eager-conversion` (`RUF065`)
([#&#8203;21464](https://redirect.github.com/astral-sh/ruff/pull/21464))

##### Rule changes

- \[`ruff`] Avoid false positive on `ClassVar` reassignment (`RUF012`)
([#&#8203;21478](https://redirect.github.com/astral-sh/ruff/pull/21478))

##### CLI

- Render hyperlinks for lint errors
([#&#8203;21514](https://redirect.github.com/astral-sh/ruff/pull/21514))
- Add a `ruff analyze` option to skip over imports in `TYPE_CHECKING`
blocks
([#&#8203;21472](https://redirect.github.com/astral-sh/ruff/pull/21472))

##### Documentation

- Limit `eglot-format` hook to eglot-managed Python buffers
([#&#8203;21459](https://redirect.github.com/astral-sh/ruff/pull/21459))
- Mention `force-exclude` in "Configuration > Python file discovery"
([#&#8203;21500](https://redirect.github.com/astral-sh/ruff/pull/21500))

##### Contributors

- [@&#8203;ntBre](https://redirect.github.com/ntBre)
- [@&#8203;dylwil3](https://redirect.github.com/dylwil3)
- [@&#8203;gauthsvenkat](https://redirect.github.com/gauthsvenkat)
- [@&#8203;MichaReiser](https://redirect.github.com/MichaReiser)
- [@&#8203;thamer](https://redirect.github.com/thamer)
- [@&#8203;Ruchir28](https://redirect.github.com/Ruchir28)
- [@&#8203;thejcannon](https://redirect.github.com/thejcannon)
- [@&#8203;danparizher](https://redirect.github.com/danparizher)
- [@&#8203;chirizxc](https://redirect.github.com/chirizxc)

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 10:47:43 +05:30
renovate[bot]
8c4a9d8808 Update astral-sh/setup-uv action to v7.1.4 (#21602)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | patch | `v7.1.3` -> `v7.1.4` |

---

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

---

### Release Notes

<details>
<summary>astral-sh/setup-uv (astral-sh/setup-uv)</summary>

###
[`v7.1.4`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v7.1.4):
🌈 Fix libuv closing bug on Windows

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v7.1.3...v7.1.4)

##### Changes

This release fixes the bug `Assertion failed: !(handle->flags &
UV_HANDLE_CLOSING)` on Windows runners

##### 🐛 Bug fixes

- Wait 50ms before exit to fix libuv bug
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;689](https://redirect.github.com/astral-sh/setup-uv/issues/689))

##### 🧰 Maintenance

- chore: update known checksums for 0.9.10
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;681](https://redirect.github.com/astral-sh/setup-uv/issues/681))
- chore: update known checksums for 0.9.9
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;679](https://redirect.github.com/astral-sh/setup-uv/issues/679))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 10:47:23 +05:30
renovate[bot]
907e7f7705 Update Rust crate clap to v4.5.53 (#21604)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) |
workspace.dependencies | patch | `4.5.51` -> `4.5.53` |

---

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

---

### Release Notes

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

###
[`v4.5.53`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4553---2025-11-19)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.52...v4.5.53)

##### Features

- Add `default_values_if`, `default_values_ifs`

###
[`v4.5.52`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4552---2025-11-17)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.51...v4.5.52)

##### Fixes

- Don't panic when `args_conflicts_with_subcommands` conflicts with an
`ArgGroup`

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 10:47:05 +05:30
renovate[bot]
7e8915d76e Update taiki-e/install-action action to v2.62.56 (#21608)
This PR contains the following updates:

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

---

> [!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.62.56`](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.62.55...v2.62.56)

Initial release

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

[2.62.56]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.55...v2.62.56

[2.62.55]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.54...v2.62.55

[2.62.54]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.53...v2.62.54

[2.62.53]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.52...v2.62.53

[2.62.52]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.51...v2.62.52

[2.62.51]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.50...v2.62.51

[2.62.50]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.49...v2.62.50

[2.62.49]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.48...v2.62.49

[2.62.48]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.47...v2.62.48

[2.62.47]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.46...v2.62.47

[2.62.46]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.45...v2.62.46

[2.62.45]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.44...v2.62.45

[2.62.44]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.43...v2.62.44

[2.62.43]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.42...v2.62.43

[2.62.42]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.41...v2.62.42

[2.62.41]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.40...v2.62.41

[2.62.40]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.39...v2.62.40

[2.62.39]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.38...v2.62.39

[2.62.38]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.37...v2.62.38

[2.62.37]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.36...v2.62.37

[2.62.36]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.35...v2.62.36

[2.62.35]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.34...v2.62.35

[2.62.34]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.33...v2.62.34

[2.62.33]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.32...v2.62.33

[2.62.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.31...v2.62.32

[2.62.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.30...v2.62.31

[2.62.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.29...v2.62.30

[2.62.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.28...v2.62.29

[2.62.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.27...v2.62.28

[2.62.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.26...v2.62.27

[2.62.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.25...v2.62.26

[2.62.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.24...v2.62.25

[2.62.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.23...v2.62.24

[2.62.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.22...v2.62.23

[2.62.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.21...v2.62.22

[2.62.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.20...v2.62.21

[2.62.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.19...v2.62.20

[2.62.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.18...v2.62.19

[2.62.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.17...v2.62.18

[2.62.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.16...v2.62.17

[2.62.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.15...v2.62.16

[2.62.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.14...v2.62.15

[2.62.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.13...v2.62.14

[2.62.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.12...v2.62.13

[2.62.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.11...v2.62.12

[2.62.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.10...v2.62.11

[2.62.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.9...v2.62.10

[2.62.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.8...v2.62.9

[2.62.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.7...v2.62.8

[2.62.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.6...v2.62.7

[2.62.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.5...v2.62.6

[2.62.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.4...v2.62.5

[2.62.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.3...v2.62.4

[2.62.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.2...v2.62.3

[2.62.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.1...v2.62.2

[2.62.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.62.0...v2.62.1

[2.62.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.13...v2.62.0

[2.61.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.12...v2.61.13

[2.61.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.11...v2.61.12

[2.61.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.10...v2.61.11

[2.61.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.9...v2.61.10

[2.61.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.8...v2.61.9

[2.61.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.7...v2.61.8

[2.61.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.6...v2.61.7

[2.61.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.5...v2.61.6

[2.61.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.4...v2.61.5

[2.61.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.3...v2.61.4

[2.61.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.2...v2.61.3

[2.61.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.1...v2.61.2

[2.61.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.61.0...v2.61.1

[2.61.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.60.0...v2.61.0

[2.60.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.59.1...v2.60.0

[2.59.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.59.0...v2.59.1

[2.59.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.33...v2.59.0

[2.58.33]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.32...v2.58.33

[2.58.32]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.31...v2.58.32

[2.58.31]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.30...v2.58.31

[2.58.30]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.29...v2.58.30

[2.58.29]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.28...v2.58.29

[2.58.28]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.27...v2.58.28

[2.58.27]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.26...v2.58.27

[2.58.26]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.25...v2.58.26

[2.58.25]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.24...v2.58.25

[2.58.24]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.23...v2.58.24

[2.58.23]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.22...v2.58.23

[2.58.22]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.21...v2.58.22

[2.58.21]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.20...v2.58.21

[2.58.20]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.19...v2.58.20

[2.58.19]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.18...v2.58.19

[2.58.18]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.17...v2.58.18

[2.58.17]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.16...v2.58.17

[2.58.16]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.15...v2.58.16

[2.58.15]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.14...v2.58.15

[2.58.14]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.13...v2.58.14

[2.58.13]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.12...v2.58.13

[2.58.12]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.11...v2.58.12

[2.58.11]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.10...v2.58.11

[2.58.10]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.9...v2.58.10

[2.58.9]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.8...v2.58.9

[2.58.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.7...v2.58.8

[2.58.7]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.6...v2.58.7

[2.58.6]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.5...v2.58.6

[2.58.5]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.4...v2.58.5

[2.58.4]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.3...v2.58.4

[2.58.3]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.2...v2.58.3

[2.58.2]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.1...v2.58.2

[2.58.1]:
https://redirect.github.com/taiki-e/install-action/compare/v2.58.0...v2.58.1

[2.58.0]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.8...v2.58.0

[2.57.8]:
https://redirect.github.com/taiki-e/install-action/compare/v2.57.7...v2.57.8

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

[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.gith

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 10:44:22 +05:30
renovate[bot]
680297aef8 Update Rust crate hashbrown to v0.16.1 (#21605)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [hashbrown](https://redirect.github.com/rust-lang/hashbrown) |
workspace.dependencies | patch | `0.16.0` -> `0.16.1` |

---

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

---

### Release Notes

<details>
<summary>rust-lang/hashbrown (hashbrown)</summary>

###
[`v0.16.1`](https://redirect.github.com/rust-lang/hashbrown/blob/HEAD/CHANGELOG.md#0161---2025-11-20)

[Compare
Source](https://redirect.github.com/rust-lang/hashbrown/compare/v0.16.0...v0.16.1)

##### Added

- Added `HashTable` methods related to the raw bucket index
([#&#8203;657](https://redirect.github.com/rust-lang/hashbrown/issues/657))
- Added `VacantEntryRef::insert_with_key`
([#&#8203;579](https://redirect.github.com/rust-lang/hashbrown/issues/579))

##### Changed

- Removed specialization for `Copy` types
([#&#8203;662](https://redirect.github.com/rust-lang/hashbrown/issues/662))
- The `get_many_mut` family of methods have been renamed to
`get_disjoint_mut`
to match the standard library. The old names are still present for now,
but
deprecated.
([#&#8203;648](https://redirect.github.com/rust-lang/hashbrown/issues/648))
- Recognize and use over-sized allocations when using custom allocators.
([#&#8203;523](https://redirect.github.com/rust-lang/hashbrown/issues/523))
- Depend on `serde_core` instead of `serde`.
([#&#8203;649](https://redirect.github.com/rust-lang/hashbrown/issues/649))
- Optimized `collect` on rayon parallel iterators.
([#&#8203;652](https://redirect.github.com/rust-lang/hashbrown/issues/652))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 10:43:56 +05:30
renovate[bot]
314a6e58ed Update Rust crate indexmap to v2.12.1 (#21606)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [indexmap](https://redirect.github.com/indexmap-rs/indexmap) |
workspace.dependencies | patch | `2.12.0` -> `2.12.1` |

---

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

---

### Release Notes

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

###
[`v2.12.1`](https://redirect.github.com/indexmap-rs/indexmap/blob/HEAD/RELEASES.md#2121-2025-11-20)

[Compare
Source](https://redirect.github.com/indexmap-rs/indexmap/compare/2.12.0...2.12.1)

- Simplified a lot of internals using `hashbrown`'s new bucket API.

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 10:43:03 +05:30
renovate[bot]
918fc2c773 Update Rust crate syn to v2.0.111 (#21607)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [syn](https://redirect.github.com/dtolnay/syn) |
workspace.dependencies | patch | `2.0.110` -> `2.0.111` |

---

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

---

### Release Notes

<details>
<summary>dtolnay/syn (syn)</summary>

###
[`v2.0.111`](https://redirect.github.com/dtolnay/syn/releases/tag/2.0.111)

[Compare
Source](https://redirect.github.com/dtolnay/syn/compare/2.0.110...2.0.111)

- Allow first argument of `braced!`, `bracketed!`, `parenthesized!` to
be an otherwise unused variable
([#&#8203;1946](https://redirect.github.com/dtolnay/syn/issues/1946))

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 10:42:44 +05:30
Alex Waygood
e642874cf1 [ty] Check method definitions on subclasses for Liskov violations (#21436) 2025-11-23 18:08:15 +00:00
Micha Reiser
aec225d825 [ty] Fix panic for unclosed string literal in type annotation position (#21592) 2025-11-23 16:51:58 +01:00
Micha Reiser
d24c891a4b [ty] Fix rendering of unused suppression diagnostic (#21580) 2025-11-22 18:42:56 +01:00
Aria Desires
859f9ec21a [ty] Improve lsp handling of hover/goto on imports (#21572)
* Fixes https://github.com/astral-sh/ty/issues/1011
* Also fixes the fact that we didn't handle `.x` properly *at all* in
hover/goto

It turns out all of our import handling completely ignored the `level`
(number of relative `.`'s) in a `from ..x.y import z` statement. It was
nice seeing how much my understanding of `ty` has improved -- previously
this would have all been opaque to me but now it was just, completely
glaring and blatant.

Fixing this required refactoring all the import code to take the
importing file into consideration. I ended up refactoring a bunch of
code to pass around/require `SemanticModel` more, as it's the natural
API for resolving this kind of import (it actually had an API for this
that was just... dead code, whoops!).
2025-11-22 11:06:16 -05:00
Alex Waygood
3410041b4c [ty] Improve diagnostics when a submodule is not available as an attribute on a module-literal type (#21561) 2025-11-22 14:07:48 +00:00
Alex Waygood
f2ce5e561a [ty] Improve concise diagnostics for invalid exceptions when a user catches a tuple of objects (#21578) 2025-11-22 13:46:46 +00:00
Carl Meyer
f495c6d4ae [ty] upgrade salsa (#21575) 2025-11-22 11:46:57 +01:00
Aria Desires
768bb24cdf [ty] make implicit submodule imports re-exported (#21573)
Thus they work in `.pyi` files

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

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-11-21 17:42:11 -08:00
Prakhar Pratyush
492d676736 [flake8-implicit-str-concat] Avoid invalid fix generated by autofix (ISC003) (#21517)
## Summary

As reported in #19757:
While attempting ISC003 autofix for an expression with explicit string
concatenation, with either operand being a string literal that wraps
across multiple lines (in parentheses) - it resulted in generating a fix
which caused runtime error.

Example:
```
_ = "abc" + (
    "def"
    "ghi"
)
```
was being auto-fixed to:
```
_ = "abc" (
    "def"
    "ghi"
)
```
which raised `TypeError: 'str' object is not callable`

This commit makes changes to just report diagnostic - no autofix in such
cases.

Fixes #19757.

## Test Plan
Added example scenarios in
`crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC.py`.

Signed-off-by: Prakhar Pratyush <prakhar1144@gmail.com>
2025-11-21 17:22:35 -08:00
Dan Parizher
ddc1417f22 [pylint] Fix suppression for empty dict without tuple key annotation (PLE1141) (#21290)
## Summary

Fixes the PLE1141 (`dict-iter-missing-items`) rule to allow fixes for
empty dictionaries unless they have type annotations indicating 2-tuple
keys. Previously, the fix was incorrectly suppressed for all empty dicts
due to vacuous truth in the `all()` function.

Fixes #21289

## Problem Analysis

The `is_dict_key_tuple_with_two_elements` function was designed to
suppress the fix when a dictionary's keys are all 2-tuples, as unpacking
tuple keys directly would change runtime behavior.

However, for empty dictionaries, `iter_keys()` returns an empty
iterator, and `all()` on an empty iterator returns `true` (vacuous
truth). This caused the function to incorrectly suppress fixes for empty
dicts, even when there was no indication that future keys would be
2-tuples.

## Approach

1. **Detect empty dictionaries**: Added a check to identify when a dict
literal has no keys.

2. **Handle annotated empty dicts**: For empty dicts with type
annotations:
- Parse the annotation to check if it's `dict[tuple[T1, T2], ...]` where
the tuple has exactly 2 elements
- Support both PEP 484 (`typing.Dict`, `typing.Tuple`) and PEP 585
(`dict`, `tuple`) syntax
   - If tuple keys are detected, suppress the fix (correct behavior)
   - Otherwise, allow the fix

3. **Handle unannotated empty dicts**: For empty dicts without
annotations, allow the fix since there's no indication that keys will be
2-tuples.

4. **Preserve existing behavior**: For non-empty dicts, the original
logic is unchanged - check if all existing keys are 2-tuples.

The implementation includes helper functions:
- `is_annotation_dict_with_tuple_keys()`: Checks if a type annotation
specifies dict with tuple keys
- `is_tuple_type_with_two_elements()`: Checks if a type expression
represents a 2-tuple

Test cases were added to verify:
- Empty dict without annotation triggers the error
- Empty dict with `dict[tuple[int, str], bool]` suppresses the error
- Empty dict with `dict[str, int]` triggers the error
- Existing tests remain unchanged

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-11-21 22:07:18 +00:00
Ibraheem Ahmed
040aa7463b [ty] Narrow type context during literal promotion in generic class constructors (#21574)
## Summary

Resolves https://github.com/astral-sh/ty/issues/1603.
2025-11-21 17:05:32 -05:00
chiri
09d457aa52 [pylint] Fix PLR1708 false positives on nested functions (#21177)
## Summary

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

## Test Plan

`cargo nextest run pylint`
2025-11-21 15:41:22 -05:00
Andrew Gallant
438ef334d3 [ty] Fix subtraction overflow bug
PR #21549 introduced a subtle overflow bug that seemed impossible, but
can empirically happen. This PR fixes it by saturating to zero.

I did try to write a regression test for this, but couldn't manage it.
Instead, I'll attach before-and-after screen recordings.
2025-11-21 15:07:37 -05:00
Douglas Creager
6cc502781f [ty] Remove brittle constraint set reveal tests (#21568)
These were added to try to make it clearer that assignability checks
will eventually return more detailed answers than true or false.
However, the constraint set display rendering is still more brittle than
I'd like it to be, and it's more trouble than it's worth to keep them
updated with semantically identically but textually different edits. The
`static_assert`s are sufficient to check correctness, and we can always
add `reveal_type` when needed for further debugging.
2025-11-21 13:57:55 -05:00
Mikko Leppänen
e2a1d1a8eb [ruff] Catch more dummy variable uses (RUF052) (#19799)
## Summary

Extends the `used-dummy-variable` rule
([RUF052](https://docs.astral.sh/ruff/rules/used-dummy-variable/)) to
detect dummy variables that are used within list comprehensions, dict
comprehensions, set comprehensions, and generator expressions, not just
regular for loops and function assignments.

### Problem

Previously, RUF052 only flagged dummy variables (variables with leading
underscores) that were used in function scopes via assignments or
regular for loops. It missed cases where dummy variables were used
within comprehensions:

```python
def example():
    my_list = [{"foo": 1}, {"foo": 2}]
    
    # These were not detected before:
    [_item["foo"] for _item in my_list]  # Should warn: _item is used
    {_item["key"]: _item["val"] for _item in my_list}  # Should warn: _item is used
    (_item["foo"] for _item in my_list)  # Should warn: _item is used
```

### Solution

- Extended scope checking to include all generator scopes () with any
(list/dict/set comprehensions and generator expressions)
`ScopeKind::Generator``GeneratorKind`
- Added support for bindings, which cover loop variables in both regular
for loops and comprehensions `BindingKind::LoopVar`
- Refactored the scope validation logic for better readability with a
descriptive variable `is_allowed_scope`



[ISSUE](https://github.com/astral-sh/ruff/issues/19732)

## Test Plan

```bash
cargo test
```

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-11-21 12:57:02 -05:00
Aria Desires
040b482cf7 [ty] Use the same snapshot handling as other tests (#21564)
Fixes https://github.com/astral-sh/ty/issues/1605
2025-11-21 17:48:01 +00:00
RasmusNygren
03dfbf21eb [ty] suppress autocomplete suggestions during variable binding (#21549)
Statements such as `def foo(p<CURSOR>`,
`def foo[T<CURSOR>` and `for foo<CURSOR>`
should not generate any suggestions as these
cases are introducing new names.

If it's not possible to determine that suggestions should be omitted
using token matching in an easy way, we turn
to traversing the AST to determine the context.

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

Fixes https://github.com/astral-sh/ty/issues/1563

It keeps using the existing token matching pattern for the easy cases
(nothing typed and most recent token is a definition token) and
fallbacks to AST traveral for the slightly more difficult cases where
token matching becomes difficult and error prone.

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
New test cases and sanity-checking in the ty playground
<!-- How was it tested? -->
2025-11-21 12:06:46 -05:00
Remo Senekowitsch
e3c78d8203 Set severity for non-rule diagnostics (#21559) 2025-11-21 17:42:35 +01:00
Aria Desires
a9b3caf181 [ty] Add with_type convenience to display code (#21563)
Code is much more readable.
2025-11-21 16:36:22 +00:00
Aria Desires
629258241f [ty] Implement docstring rendering to markdown (#21550)
## Summary

This introduces a very bad and naive
python-docstring-flavoured-reStructuredText to github-flavor-markdown
translator. The main goal is to try to preserve a lot of the formatting
and plaintext, progressively enhance the content when we find things we
know about, and escape the text when we find things that might get
corrupt.

Previously I'd broken this out into rendering each different format, but
with this approach you don't really need to?

## Test Plan

Lots of snapshot tests, also messed around in some random stdlib
modules.
2025-11-21 10:47:38 -05:00
Alex Waygood
762c44527e [ty] Reduce indentation of TypeInferenceBuilder::infer_attribute_load (#21560) 2025-11-21 14:12:39 +00:00
Brent Westbrook
59c6cb521d Bump 0.14.6 (#21558) 2025-11-21 09:00:56 -05:00
Alex Waygood
54dba15088 [ty] Improve debug messages when imports fail (#21555) 2025-11-21 13:45:57 +00:00
147 changed files with 8373 additions and 2063 deletions

View File

@@ -261,15 +261,15 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
enable-cache: "true"
- name: ty mdtests (GitHub annotations)
@@ -284,6 +284,10 @@ jobs:
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
- name: Dogfood ty on py-fuzzer
run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
- name: Dogfood ty on the scripts directory
run: uv run --project=./scripts cargo run -p ty check --project=./scripts
- name: Dogfood ty on ty_benchmark
run: uv run --project=./scripts/ty_benchmark cargo run -p ty check --project=./scripts/ty_benchmark
# Check for broken links in the documentation.
- run: cargo doc --all --no-deps
env:
@@ -319,11 +323,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
enable-cache: "true"
- name: "Run tests"
@@ -352,11 +356,11 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
enable-cache: "true"
- name: "Run tests"
@@ -462,7 +466,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
shared-key: ruff-linux-debug
@@ -497,7 +501,7 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -532,7 +536,7 @@ jobs:
ref: ${{ github.event.pull_request.base.ref }}
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
python-version: ${{ env.PYTHON_VERSION }}
activate-environment: true
@@ -638,7 +642,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -697,7 +701,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -748,7 +752,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -792,7 +796,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
python-version: 3.13
activate-environment: true
@@ -947,13 +951,13 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
with:
tool: cargo-codspeed
@@ -987,13 +991,13 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
with:
tool: cargo-codspeed
@@ -1027,13 +1031,13 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
with:
tool: cargo-codspeed

View File

@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"

View File

@@ -43,7 +43,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
@@ -81,7 +81,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
pattern: wheels-*

View File

@@ -60,7 +60,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
with:
persist-credentials: false
submodules: recursive
@@ -123,7 +123,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
with:
persist-credentials: false
submodules: recursive
@@ -174,7 +174,7 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
with:
persist-credentials: false
submodules: recursive
@@ -250,7 +250,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
with:
persist-credentials: false
submodules: recursive

View File

@@ -77,7 +77,7 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: Sync typeshed stubs
run: |
rm -rf "ruff/${VENDORED_TYPESHED}"
@@ -131,7 +131,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -170,7 +170,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -207,12 +207,12 @@ jobs:
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
if: ${{ success() }}
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
with:
tool: cargo-nextest
- name: "Install cargo insta"
if: ${{ success() }}
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
with:
tool: cargo-insta
- name: Update snapshots

View File

@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact

View File

@@ -5,5 +5,6 @@
"rust-analyzer.check.command": "clippy",
"search.exclude": {
"**/*.snap": true
}
},
"ty.diagnosticMode": "openFilesOnly"
}

View File

@@ -1,5 +1,48 @@
# Changelog
## 0.14.6
Released on 2025-11-21.
### Preview features
- \[`flake8-bandit`\] Support new PySNMP API paths (`S508`, `S509`) ([#21374](https://github.com/astral-sh/ruff/pull/21374))
### Bug fixes
- Adjust own-line comment placement between branches ([#21185](https://github.com/astral-sh/ruff/pull/21185))
- Avoid syntax error when formatting attribute expressions with outer parentheses, parenthesized value, and trailing comment on value ([#20418](https://github.com/astral-sh/ruff/pull/20418))
- Fix panic when formatting comments in unary expressions ([#21501](https://github.com/astral-sh/ruff/pull/21501))
- Respect `fmt: skip` for compound statements on a single line ([#20633](https://github.com/astral-sh/ruff/pull/20633))
- \[`refurb`\] Fix `FURB103` autofix ([#21454](https://github.com/astral-sh/ruff/pull/21454))
- \[`ruff`\] Fix false positive for complex conversion specifiers in `logging-eager-conversion` (`RUF065`) ([#21464](https://github.com/astral-sh/ruff/pull/21464))
### Rule changes
- \[`ruff`\] Avoid false positive on `ClassVar` reassignment (`RUF012`) ([#21478](https://github.com/astral-sh/ruff/pull/21478))
### CLI
- Render hyperlinks for lint errors ([#21514](https://github.com/astral-sh/ruff/pull/21514))
- Add a `ruff analyze` option to skip over imports in `TYPE_CHECKING` blocks ([#21472](https://github.com/astral-sh/ruff/pull/21472))
### Documentation
- Limit `eglot-format` hook to eglot-managed Python buffers ([#21459](https://github.com/astral-sh/ruff/pull/21459))
- Mention `force-exclude` in "Configuration > Python file discovery" ([#21500](https://github.com/astral-sh/ruff/pull/21500))
### Contributors
- [@ntBre](https://github.com/ntBre)
- [@dylwil3](https://github.com/dylwil3)
- [@gauthsvenkat](https://github.com/gauthsvenkat)
- [@MichaReiser](https://github.com/MichaReiser)
- [@thamer](https://github.com/thamer)
- [@Ruchir28](https://github.com/Ruchir28)
- [@thejcannon](https://github.com/thejcannon)
- [@danparizher](https://github.com/danparizher)
- [@chirizxc](https://github.com/chirizxc)
## 0.14.5
Released on 2025-11-13.

52
Cargo.lock generated
View File

@@ -442,9 +442,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.51"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [
"clap_builder",
"clap_derive",
@@ -452,9 +452,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.51"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [
"anstream",
"anstyle",
@@ -1016,7 +1016,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1108,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1255,7 +1255,7 @@ checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af"
dependencies = [
"compact_str",
"get-size-derive2",
"hashbrown 0.16.0",
"hashbrown 0.16.1",
"indexmap",
"smallvec",
]
@@ -1353,9 +1353,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"equivalent",
]
@@ -1564,12 +1564,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.12.0"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
dependencies = [
"equivalent",
"hashbrown 0.16.0",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
@@ -1763,7 +1763,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2859,7 +2859,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.5"
version = "0.14.6"
dependencies = [
"anyhow",
"argfile",
@@ -3117,7 +3117,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.5"
version = "0.14.6"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3127,7 +3127,7 @@ dependencies = [
"fern",
"glob",
"globset",
"hashbrown 0.16.0",
"hashbrown 0.16.1",
"imperative",
"insta",
"is-macro",
@@ -3472,7 +3472,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.5"
version = "0.14.6"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3570,7 +3570,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3588,7 +3588,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
dependencies = [
"boxcar",
"compact_str",
@@ -3612,12 +3612,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
dependencies = [
"proc-macro2",
"quote",
@@ -3935,9 +3935,9 @@ checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
[[package]]
name = "syn"
version = "2.0.110"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
@@ -3971,7 +3971,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4462,7 +4462,7 @@ dependencies = [
"drop_bomb",
"get-size2",
"glob",
"hashbrown 0.16.0",
"hashbrown 0.16.1",
"indexmap",
"indoc",
"insta",
@@ -5020,7 +5020,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]

View File

@@ -146,7 +146,7 @@ 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.git", rev = "a885bb4c4c192741b8a17418fef81a71e33d111e", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "17bc55d699565e5a1cb1bd42363b905af2f9f3e7", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.14.5"
version = "0.14.6"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -59,8 +59,6 @@ divan = { workspace = true, optional = true }
anyhow = { workspace = true }
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true }
criterion = { workspace = true, default-features = false, optional = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
@@ -88,3 +86,7 @@ mimalloc = { workspace = true }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dev-dependencies]
tikv-jemallocator = { workspace = true }
[dev-dependencies]
rustc-hash = { workspace = true }
rayon = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.14.5"
version = "0.14.6"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -208,3 +208,17 @@ _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
_ = f"b {t"abc" \
t"def"} g"
# Explicit concatenation with either operand being
# a string literal that wraps across multiple lines (in parentheses)
# reports diagnostic - no autofix.
# See https://github.com/astral-sh/ruff/issues/19757
_ = "abc" + (
"def"
"ghi"
)
_ = (
"abc"
"def"
) + "ghi"

View File

@@ -30,3 +30,23 @@ for a, b in d_tuple:
pass
for a, b in d_tuple_annotated:
pass
# Empty dict cases
empty_dict = {}
empty_dict["x"] = 1
for k, v in empty_dict:
pass
empty_dict_annotated_tuple_keys: dict[tuple[int, str], bool] = {}
for k, v in empty_dict_annotated_tuple_keys:
pass
empty_dict_unannotated = {}
empty_dict_unannotated[("x", "y")] = True
for k, v in empty_dict_unannotated:
pass
empty_dict_annotated_str_keys: dict[str, int] = {}
empty_dict_annotated_str_keys["x"] = 1
for k, v in empty_dict_annotated_str_keys:
pass

View File

@@ -129,3 +129,26 @@ def generator_with_lambda():
yield 1
func = lambda x: x # Just a regular lambda
yield 2
# See: https://github.com/astral-sh/ruff/issues/21162
def foo():
def g():
yield 1
raise StopIteration # Should not trigger
def foo():
def g():
raise StopIteration # Should not trigger
yield 1
# https://github.com/astral-sh/ruff/pull/21177#pullrequestreview-3430209718
def foo():
yield 1
class C:
raise StopIteration # Should trigger
yield C
# https://github.com/astral-sh/ruff/pull/21177#discussion_r2539702728
def foo():
raise StopIteration((yield 1)) # Should trigger

View File

@@ -0,0 +1,109 @@
# Correct usage in loop and comprehension
def process_data():
return 42
def test_correct_dummy_usage():
my_list = [{"foo": 1}, {"foo": 2}]
# Should NOT detect - dummy variable is not used
[process_data() for _ in my_list] # OK: `_` is ignored by rule
# Should NOT detect - dummy variable is not used
[item["foo"] for item in my_list] # OK: not a dummy variable name
# Should NOT detect - dummy variable is not used
[42 for _unused in my_list] # OK: `_unused` is not accessed
# Regular For Loops
def test_for_loops():
my_list = [{"foo": 1}, {"foo": 2}]
# Should detect used dummy variable
for _item in my_list:
print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
# Should detect used dummy variable
for _index, _value in enumerate(my_list):
result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
# List Comprehensions
def test_list_comprehensions():
my_list = [{"foo": 1}, {"foo": 2}]
# Should detect used dummy variable
result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
# Should detect used dummy variable in nested comprehension
nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
# RUF052: Both `_item` and `_sublist` are accessed
# Should detect with conditions
filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
# RUF052: Local dummy variable `_item` is accessed
# Dict Comprehensions
def test_dict_comprehensions():
my_list = [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
# Should detect used dummy variable
result = {_item["key"]: _item["value"] for _item in my_list}
# RUF052: Local dummy variable `_item` is accessed
# Should detect with enumerate
indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
# RUF052: Both `_index` and `_item` are accessed
# Should detect in nested dict comprehension
nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
for _outer, sublist in enumerate([my_list])}
# RUF052: `_outer`, `_inner` are accessed
# Set Comprehensions
def test_set_comprehensions():
my_list = [{"foo": 1}, {"foo": 2}, {"foo": 1}] # Note: duplicate values
# Should detect used dummy variable
unique_values = {_item["foo"] for _item in my_list}
# RUF052: Local dummy variable `_item` is accessed
# Should detect with conditions
filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
# RUF052: Local dummy variable `_item` is accessed
# Should detect with complex expression
processed = {_item["foo"] * 2 for _item in my_list}
# RUF052: Local dummy variable `_item` is accessed
# Generator Expressions
def test_generator_expressions():
my_list = [{"foo": 1}, {"foo": 2}]
# Should detect used dummy variable
gen = (_item["foo"] for _item in my_list)
# RUF052: Local dummy variable `_item` is accessed
# Should detect when passed to function
total = sum(_item["foo"] for _item in my_list)
# RUF052: Local dummy variable `_item` is accessed
# Should detect with multiple generators
pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
# RUF052: Both `_x` and `_y` are accessed
# Should detect in nested generator
nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
# RUF052: `_inner` and `_sublist` are accessed
# Complex Examples with Multiple Comprehension Types
def test_mixed_comprehensions():
data = [{"items": [1, 2, 3]}, {"items": [4, 5, 6]}]
# Should detect in mixed comprehensions
result = [
{_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
for _record in data
]
# RUF052: `_key`, `_val`, and `_record` are all accessed
# Should detect in generator passed to list constructor
gen_list = list(_item["items"][0] for _item in data)
# RUF052: Local dummy variable `_item` is accessed

View File

@@ -131,6 +131,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::GeneratorReturnFromIterMethod) {
flake8_pyi::rules::bad_generator_return_type(function_def, checker);
}
if checker.is_rule_enabled(Rule::StopIterationReturn) {
pylint::rules::stop_iteration_return(checker, function_def);
}
if checker.source_type.is_stub() {
if checker.is_rule_enabled(Rule::StrOrReprDefinedInStub) {
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
@@ -950,9 +953,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::MisplacedBareRaise) {
pylint::rules::misplaced_bare_raise(checker, raise);
}
if checker.is_rule_enabled(Rule::StopIterationReturn) {
pylint::rules::stop_iteration_return(checker, raise);
}
}
Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => {
if checker.is_rule_enabled(Rule::GlobalStatement) {

View File

@@ -1,12 +1,12 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_trivia::is_python_whitespace;
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::AlwaysFixableViolation;
use crate::checkers::ast::Checker;
use crate::{Edit, Fix};
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for string literals that are explicitly concatenated (using the
@@ -36,14 +36,16 @@ use crate::{Edit, Fix};
#[violation_metadata(stable_since = "v0.0.201")]
pub(crate) struct ExplicitStringConcatenation;
impl AlwaysFixableViolation for ExplicitStringConcatenation {
impl Violation for ExplicitStringConcatenation {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"Explicitly concatenated string should be implicitly concatenated".to_string()
}
fn fix_title(&self) -> String {
"Remove redundant '+' operator to implicitly concatenate".to_string()
fn fix_title(&self) -> Option<String> {
Some("Remove redundant '+' operator to implicitly concatenate".to_string())
}
}
@@ -82,9 +84,27 @@ pub(crate) fn explicit(checker: &Checker, expr: &Expr) {
.locator()
.contains_line_break(TextRange::new(left.end(), right.start()))
{
checker
.report_diagnostic(ExplicitStringConcatenation, expr.range())
.set_fix(generate_fix(checker, bin_op));
let mut diagnostic =
checker.report_diagnostic(ExplicitStringConcatenation, expr.range());
let is_parenthesized = |expr: &Expr| {
parenthesized_range(
expr.into(),
bin_op.into(),
checker.comment_ranges(),
checker.source(),
)
.is_some()
};
// If either `left` or `right` is parenthesized, generating
// a fix would be too involved. Just report the diagnostic.
// Currently, attempting `generate_fix` would result in
// an invalid code. See: #19757
if is_parenthesized(left) || is_parenthesized(right) {
return;
}
diagnostic.set_fix(generate_fix(checker, bin_op));
}
}
}

View File

@@ -357,3 +357,33 @@ help: Remove redundant '+' operator to implicitly concatenate
203 | )
204 |
205 | # nested examples with both t and f-strings
ISC003 Explicitly concatenated string should be implicitly concatenated
--> ISC.py:216:5
|
214 | # reports diagnostic - no autofix.
215 | # See https://github.com/astral-sh/ruff/issues/19757
216 | _ = "abc" + (
| _____^
217 | | "def"
218 | | "ghi"
219 | | )
| |_^
220 |
221 | _ = (
|
help: Remove redundant '+' operator to implicitly concatenate
ISC003 Explicitly concatenated string should be implicitly concatenated
--> ISC.py:221:5
|
219 | )
220 |
221 | _ = (
| _____^
222 | | "abc"
223 | | "def"
224 | | ) + "ghi"
| |_________^
|
help: Remove redundant '+' operator to implicitly concatenate

View File

@@ -89,3 +89,24 @@ ISC002 Implicitly concatenated string literals over multiple lines
209 | | t"def"} g"
| |__________^
|
ISC002 Implicitly concatenated string literals over multiple lines
--> ISC.py:217:5
|
215 | # See https://github.com/astral-sh/ruff/issues/19757
216 | _ = "abc" + (
217 | / "def"
218 | | "ghi"
| |_________^
219 | )
|
ISC002 Implicitly concatenated string literals over multiple lines
--> ISC.py:222:5
|
221 | _ = (
222 | / "abc"
223 | | "def"
| |_________^
224 | ) + "ghi"
|

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{Expr, Stmt};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::analyze::typing::is_dict;
@@ -108,15 +108,77 @@ fn is_dict_key_tuple_with_two_elements(binding: &Binding, semantic: &SemanticMod
return false;
};
let Stmt::Assign(assign_stmt) = statement else {
let (value, annotation) = match statement {
Stmt::Assign(assign_stmt) => (assign_stmt.value.as_ref(), None),
Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value),
annotation,
..
}) => (value.as_ref(), Some(annotation.as_ref())),
_ => return false,
};
let Expr::Dict(dict_expr) = value else {
return false;
};
let Expr::Dict(dict_expr) = &*assign_stmt.value else {
return false;
};
// Check if dict is empty
let is_empty = dict_expr.is_empty();
if is_empty {
// For empty dicts, check type annotation
return annotation
.is_some_and(|annotation| is_annotation_dict_with_tuple_keys(annotation, semantic));
}
// For non-empty dicts, check if all keys are 2-tuples
dict_expr
.iter_keys()
.all(|key| matches!(key, Some(Expr::Tuple(tuple)) if tuple.len() == 2))
}
/// Returns true if the annotation is `dict[tuple[T1, T2], ...]` where tuple has exactly 2 elements.
fn is_annotation_dict_with_tuple_keys(annotation: &Expr, semantic: &SemanticModel) -> bool {
// Check if it's a subscript: dict[...]
let Expr::Subscript(subscript) = annotation else {
return false;
};
// Check if it's dict or typing.Dict
if !semantic.match_builtin_expr(subscript.value.as_ref(), "dict")
&& !semantic.match_typing_expr(subscript.value.as_ref(), "Dict")
{
return false;
}
// Extract the slice (should be a tuple: (key_type, value_type))
let Expr::Tuple(tuple) = subscript.slice.as_ref() else {
return false;
};
// dict[K, V] format - check if K is tuple with 2 elements
if let [key, _value] = tuple.elts.as_slice() {
return is_tuple_type_with_two_elements(key, semantic);
}
false
}
/// Returns true if the expression represents a tuple type with exactly 2 elements.
fn is_tuple_type_with_two_elements(expr: &Expr, semantic: &SemanticModel) -> bool {
// Handle tuple[...] subscript
if let Expr::Subscript(subscript) = expr {
// Check if it's tuple or typing.Tuple
if semantic.match_builtin_expr(subscript.value.as_ref(), "tuple")
|| semantic.match_typing_expr(subscript.value.as_ref(), "Tuple")
{
// Check the slice - tuple[T1, T2]
if let Expr::Tuple(tuple_slice) = subscript.slice.as_ref() {
return tuple_slice.elts.len() == 2;
}
return false;
}
}
false
}

View File

@@ -1,6 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
use ruff_python_ast::{
self as ast,
helpers::map_callable,
visitor::{Visitor, walk_expr, walk_stmt},
};
use ruff_text_size::Ranged;
use crate::Violation;
@@ -50,65 +53,54 @@ impl Violation for StopIterationReturn {
}
/// PLR1708
pub(crate) fn stop_iteration_return(checker: &Checker, raise_stmt: &ast::StmtRaise) {
// Fast-path: only continue if this is `raise StopIteration` (with or without args)
let Some(exc) = &raise_stmt.exc else {
return;
pub(crate) fn stop_iteration_return(checker: &Checker, function_def: &ast::StmtFunctionDef) {
let mut analyzer = GeneratorAnalyzer {
checker,
has_yield: false,
stop_iteration_raises: Vec::new(),
};
let is_stop_iteration = match exc.as_ref() {
ast::Expr::Call(ast::ExprCall { func, .. }) => {
checker.semantic().match_builtin_expr(func, "StopIteration")
analyzer.visit_body(&function_def.body);
if analyzer.has_yield {
for raise_stmt in analyzer.stop_iteration_raises {
checker.report_diagnostic(StopIterationReturn, raise_stmt.range());
}
expr => checker.semantic().match_builtin_expr(expr, "StopIteration"),
};
if !is_stop_iteration {
return;
}
// Now check the (more expensive) generator context
if !in_generator_context(checker) {
return;
}
checker.report_diagnostic(StopIterationReturn, raise_stmt.range());
}
/// Returns true if we're inside a function that contains any `yield`/`yield from`.
fn in_generator_context(checker: &Checker) -> bool {
for scope in checker.semantic().current_scopes() {
if let ruff_python_semantic::ScopeKind::Function(function_def) = scope.kind {
if contains_yield_statement(&function_def.body) {
return true;
struct GeneratorAnalyzer<'a, 'b> {
checker: &'a Checker<'b>,
has_yield: bool,
stop_iteration_raises: Vec<&'a ast::StmtRaise>,
}
impl<'a> Visitor<'a> for GeneratorAnalyzer<'a, '_> {
fn visit_stmt(&mut self, stmt: &'a ast::Stmt) {
match stmt {
ast::Stmt::FunctionDef(_) => {}
ast::Stmt::Raise(raise @ ast::StmtRaise { exc: Some(exc), .. }) => {
if self
.checker
.semantic()
.match_builtin_expr(map_callable(exc), "StopIteration")
{
self.stop_iteration_raises.push(raise);
}
walk_stmt(self, stmt);
}
_ => walk_stmt(self, stmt),
}
}
false
}
/// Check if a statement list contains any yield statements
fn contains_yield_statement(body: &[ast::Stmt]) -> bool {
struct YieldFinder {
found: bool,
}
impl Visitor<'_> for YieldFinder {
fn visit_expr(&mut self, expr: &ast::Expr) {
if matches!(expr, ast::Expr::Yield(_) | ast::Expr::YieldFrom(_)) {
self.found = true;
} else {
fn visit_expr(&mut self, expr: &'a ast::Expr) {
match expr {
ast::Expr::Lambda(_) => {}
ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) => {
self.has_yield = true;
walk_expr(self, expr);
}
_ => walk_expr(self, expr),
}
}
let mut finder = YieldFinder { found: false };
for stmt in body {
walk_stmt(&mut finder, stmt);
if finder.found {
return true;
}
}
false
}

View File

@@ -39,3 +39,61 @@ help: Add a call to `.items()`
18 |
19 |
note: This is an unsafe fix and may change runtime behavior
PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
--> dict_iter_missing_items.py:37:13
|
35 | empty_dict = {}
36 | empty_dict["x"] = 1
37 | for k, v in empty_dict:
| ^^^^^^^^^^
38 | pass
|
help: Add a call to `.items()`
34 | # Empty dict cases
35 | empty_dict = {}
36 | empty_dict["x"] = 1
- for k, v in empty_dict:
37 + for k, v in empty_dict.items():
38 | pass
39 |
40 | empty_dict_annotated_tuple_keys: dict[tuple[int, str], bool] = {}
note: This is an unsafe fix and may change runtime behavior
PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
--> dict_iter_missing_items.py:46:13
|
44 | empty_dict_unannotated = {}
45 | empty_dict_unannotated[("x", "y")] = True
46 | for k, v in empty_dict_unannotated:
| ^^^^^^^^^^^^^^^^^^^^^^
47 | pass
|
help: Add a call to `.items()`
43 |
44 | empty_dict_unannotated = {}
45 | empty_dict_unannotated[("x", "y")] = True
- for k, v in empty_dict_unannotated:
46 + for k, v in empty_dict_unannotated.items():
47 | pass
48 |
49 | empty_dict_annotated_str_keys: dict[str, int] = {}
note: This is an unsafe fix and may change runtime behavior
PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
--> dict_iter_missing_items.py:51:13
|
49 | empty_dict_annotated_str_keys: dict[str, int] = {}
50 | empty_dict_annotated_str_keys["x"] = 1
51 | for k, v in empty_dict_annotated_str_keys:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 | pass
|
help: Add a call to `.items()`
48 |
49 | empty_dict_annotated_str_keys: dict[str, int] = {}
50 | empty_dict_annotated_str_keys["x"] = 1
- for k, v in empty_dict_annotated_str_keys:
51 + for k, v in empty_dict_annotated_str_keys.items():
52 | pass
note: This is an unsafe fix and may change runtime behavior

View File

@@ -107,3 +107,24 @@ PLR1708 Explicit `raise StopIteration` in generator
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:149:9
|
147 | yield 1
148 | class C:
149 | raise StopIteration # Should trigger
| ^^^^^^^^^^^^^^^^^^^
150 | yield C
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:154:5
|
152 | # https://github.com/astral-sh/ruff/pull/21177#discussion_r2539702728
153 | def foo():
154 | raise StopIteration((yield 1)) # Should trigger
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use `return` instead

View File

@@ -97,7 +97,8 @@ mod tests {
#[test_case(Rule::MapIntVersionParsing, Path::new("RUF048_1.py"))]
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
#[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"))]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_1.py"))]
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
#[test_case(Rule::FalsyDictGetFallback, Path::new("RUF056.py"))]
#[test_case(Rule::UnnecessaryRound, Path::new("RUF057.py"))]
@@ -621,8 +622,8 @@ mod tests {
Ok(())
}
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"^_+", 1)]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"), r"", 2)]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"), r"^_+", 1)]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"), r"", 2)]
fn custom_regexp_preset(
rule_code: Rule,
path: &Path,

View File

@@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::is_dunder;
use ruff_python_semantic::{Binding, BindingId};
use ruff_python_semantic::{Binding, BindingId, BindingKind, ScopeKind};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_text_size::Ranged;
@@ -111,7 +111,7 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
return;
}
// We only emit the lint on variables defined via assignments.
// We only emit the lint on local variables.
//
// ## Why not also emit the lint on function parameters?
//
@@ -127,8 +127,30 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
// autofixing the diagnostic for assignments. See:
// - <https://github.com/astral-sh/ruff/issues/14790>
// - <https://github.com/astral-sh/ruff/issues/14799>
if !binding.kind.is_assignment() {
return;
match binding.kind {
BindingKind::Annotation
| BindingKind::Argument
| BindingKind::NamedExprAssignment
| BindingKind::Assignment
| BindingKind::LoopVar
| BindingKind::WithItemVar
| BindingKind::BoundException
| BindingKind::UnboundException(_) => {}
BindingKind::TypeParam
| BindingKind::Global(_)
| BindingKind::Nonlocal(_, _)
| BindingKind::Builtin
| BindingKind::ClassDefinition(_)
| BindingKind::FunctionDefinition(_)
| BindingKind::Export(_)
| BindingKind::FutureImport
| BindingKind::Import(_)
| BindingKind::FromImport(_)
| BindingKind::SubmoduleImport(_)
| BindingKind::Deletion
| BindingKind::ConditionalDeletion(_)
| BindingKind::DunderClassCell => return,
}
// This excludes `global` and `nonlocal` variables.
@@ -138,9 +160,12 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding, binding_
let semantic = checker.semantic();
// Only variables defined in function scopes
// Only variables defined in function and generator scopes
let scope = &semantic.scopes[binding.scope];
if !scope.kind.is_function() {
if !matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::Generator { .. }
) {
return;
}

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF052 [*] Local dummy variable `_var` is accessed
--> RUF052.py:92:9
--> RUF052_0.py:92:9
|
90 | class Class_:
91 | def fun(self):
@@ -24,7 +24,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_list` is accessed
--> RUF052.py:99:5
--> RUF052_0.py:99:5
|
98 | def fun():
99 | _list = "built-in" # [RUF052]
@@ -45,7 +45,7 @@ help: Prefer using trailing underscores to avoid shadowing a built-in
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052.py:106:5
--> RUF052_0.py:106:5
|
104 | def fun():
105 | global x
@@ -67,7 +67,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052.py:113:5
--> RUF052_0.py:113:5
|
111 | def bar():
112 | nonlocal x
@@ -90,7 +90,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052.py:120:5
--> RUF052_0.py:120:5
|
118 | def fun():
119 | x = "local"
@@ -112,7 +112,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052.py:128:5
--> RUF052_0.py:128:5
|
127 | def unfixables():
128 | _GLOBAL_1 = "foo"
@@ -123,7 +123,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052.py:136:5
--> RUF052_0.py:136:5
|
135 | # unfixable because the rename would shadow a local variable
136 | _local = "local3" # [RUF052]
@@ -133,7 +133,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052.py:140:9
--> RUF052_0.py:140:9
|
139 | def nested():
140 | _GLOBAL_1 = "foo"
@@ -144,7 +144,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052.py:145:9
--> RUF052_0.py:145:9
|
144 | # unfixable because the rename would shadow a variable from the outer function
145 | _local = "local4"
@@ -154,7 +154,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 [*] Local dummy variable `_P` is accessed
--> RUF052.py:153:5
--> RUF052_0.py:153:5
|
151 | from collections import namedtuple
152 |
@@ -184,7 +184,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_T` is accessed
--> RUF052.py:154:5
--> RUF052_0.py:154:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -213,7 +213,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT` is accessed
--> RUF052.py:155:5
--> RUF052_0.py:155:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -242,7 +242,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_E` is accessed
--> RUF052.py:156:5
--> RUF052_0.py:156:5
|
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
155 | _NT = NamedTuple("_NT", [("foo", int)])
@@ -270,7 +270,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT2` is accessed
--> RUF052.py:157:5
--> RUF052_0.py:157:5
|
155 | _NT = NamedTuple("_NT", [("foo", int)])
156 | _E = Enum("_E", ["a", "b", "c"])
@@ -297,7 +297,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT3` is accessed
--> RUF052.py:158:5
--> RUF052_0.py:158:5
|
156 | _E = Enum("_E", ["a", "b", "c"])
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
@@ -323,7 +323,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_DynamicClass` is accessed
--> RUF052.py:159:5
--> RUF052_0.py:159:5
|
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
@@ -347,7 +347,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NotADynamicClass` is accessed
--> RUF052.py:160:5
--> RUF052_0.py:160:5
|
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
159 | _DynamicClass = type("_DynamicClass", (), {})
@@ -371,7 +371,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_dummy_var` is accessed
--> RUF052.py:182:5
--> RUF052_0.py:182:5
|
181 | def foo():
182 | _dummy_var = 42
@@ -396,7 +396,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 Local dummy variable `_dummy_var` is accessed
--> RUF052.py:192:5
--> RUF052_0.py:192:5
|
190 | # Unfixable because both possible candidates for the new name are shadowed
191 | # in the scope of one of the references to the variable

View File

@@ -0,0 +1,494 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:21:9
|
20 | # Should detect used dummy variable
21 | for _item in my_list:
| ^^^^^
22 | print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
18 | my_list = [{"foo": 1}, {"foo": 2}]
19 |
20 | # Should detect used dummy variable
- for _item in my_list:
- print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
21 + for item in my_list:
22 + print(item["foo"]) # RUF052: Local dummy variable `_item` is accessed
23 |
24 | # Should detect used dummy variable
25 | for _index, _value in enumerate(my_list):
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_index` is accessed
--> RUF052_1.py:25:9
|
24 | # Should detect used dummy variable
25 | for _index, _value in enumerate(my_list):
| ^^^^^^
26 | result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
help: Remove leading underscores
22 | print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
23 |
24 | # Should detect used dummy variable
- for _index, _value in enumerate(my_list):
- result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
25 + for index, _value in enumerate(my_list):
26 + result = index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
27 |
28 | # List Comprehensions
29 | def test_list_comprehensions():
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_value` is accessed
--> RUF052_1.py:25:17
|
24 | # Should detect used dummy variable
25 | for _index, _value in enumerate(my_list):
| ^^^^^^
26 | result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
|
help: Remove leading underscores
22 | print(_item["foo"]) # RUF052: Local dummy variable `_item` is accessed
23 |
24 | # Should detect used dummy variable
- for _index, _value in enumerate(my_list):
- result = _index + _value["foo"] # RUF052: Both `_index` and `_value` are accessed
25 + for _index, value in enumerate(my_list):
26 + result = _index + value["foo"] # RUF052: Both `_index` and `_value` are accessed
27 |
28 | # List Comprehensions
29 | def test_list_comprehensions():
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:33:32
|
32 | # Should detect used dummy variable
33 | result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
| ^^^^^
34 |
35 | # Should detect used dummy variable in nested comprehension
|
help: Remove leading underscores
30 | my_list = [{"foo": 1}, {"foo": 2}]
31 |
32 | # Should detect used dummy variable
- result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
33 + result = [item["foo"] for item in my_list] # RUF052: Local dummy variable `_item` is accessed
34 |
35 | # Should detect used dummy variable in nested comprehension
36 | nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:36:33
|
35 | # Should detect used dummy variable in nested comprehension
36 | nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
| ^^^^^
37 | # RUF052: Both `_item` and `_sublist` are accessed
|
help: Remove leading underscores
33 | result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
34 |
35 | # Should detect used dummy variable in nested comprehension
- nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
36 + nested = [[item["foo"] for item in _sublist] for _sublist in [my_list, my_list]]
37 | # RUF052: Both `_item` and `_sublist` are accessed
38 |
39 | # Should detect with conditions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_sublist` is accessed
--> RUF052_1.py:36:56
|
35 | # Should detect used dummy variable in nested comprehension
36 | nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
| ^^^^^^^^
37 | # RUF052: Both `_item` and `_sublist` are accessed
|
help: Remove leading underscores
33 | result = [_item["foo"] for _item in my_list] # RUF052: Local dummy variable `_item` is accessed
34 |
35 | # Should detect used dummy variable in nested comprehension
- nested = [[_item["foo"] for _item in _sublist] for _sublist in [my_list, my_list]]
36 + nested = [[_item["foo"] for _item in sublist] for sublist in [my_list, my_list]]
37 | # RUF052: Both `_item` and `_sublist` are accessed
38 |
39 | # Should detect with conditions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:40:34
|
39 | # Should detect with conditions
40 | filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
| ^^^^^
41 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
37 | # RUF052: Both `_item` and `_sublist` are accessed
38 |
39 | # Should detect with conditions
- filtered = [_item["foo"] for _item in my_list if _item["foo"] > 0]
40 + filtered = [item["foo"] for item in my_list if item["foo"] > 0]
41 | # RUF052: Local dummy variable `_item` is accessed
42 |
43 | # Dict Comprehensions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:48:48
|
47 | # Should detect used dummy variable
48 | result = {_item["key"]: _item["value"] for _item in my_list}
| ^^^^^
49 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
45 | my_list = [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
46 |
47 | # Should detect used dummy variable
- result = {_item["key"]: _item["value"] for _item in my_list}
48 + result = {item["key"]: item["value"] for item in my_list}
49 | # RUF052: Local dummy variable `_item` is accessed
50 |
51 | # Should detect with enumerate
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_index` is accessed
--> RUF052_1.py:52:43
|
51 | # Should detect with enumerate
52 | indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
| ^^^^^^
53 | # RUF052: Both `_index` and `_item` are accessed
|
help: Remove leading underscores
49 | # RUF052: Local dummy variable `_item` is accessed
50 |
51 | # Should detect with enumerate
- indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
52 + indexed = {index: _item["value"] for index, _item in enumerate(my_list)}
53 | # RUF052: Both `_index` and `_item` are accessed
54 |
55 | # Should detect in nested dict comprehension
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:52:51
|
51 | # Should detect with enumerate
52 | indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
| ^^^^^
53 | # RUF052: Both `_index` and `_item` are accessed
|
help: Remove leading underscores
49 | # RUF052: Local dummy variable `_item` is accessed
50 |
51 | # Should detect with enumerate
- indexed = {_index: _item["value"] for _index, _item in enumerate(my_list)}
52 + indexed = {_index: item["value"] for _index, item in enumerate(my_list)}
53 | # RUF052: Both `_index` and `_item` are accessed
54 |
55 | # Should detect in nested dict comprehension
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_inner` is accessed
--> RUF052_1.py:56:59
|
55 | # Should detect in nested dict comprehension
56 | nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
| ^^^^^^
57 | for _outer, sublist in enumerate([my_list])}
58 | # RUF052: `_outer`, `_inner` are accessed
|
help: Remove leading underscores
53 | # RUF052: Both `_index` and `_item` are accessed
54 |
55 | # Should detect in nested dict comprehension
- nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
56 + nested = {_outer: {inner["key"]: inner["value"] for inner in sublist}
57 | for _outer, sublist in enumerate([my_list])}
58 | # RUF052: `_outer`, `_inner` are accessed
59 |
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_outer` is accessed
--> RUF052_1.py:57:19
|
55 | # Should detect in nested dict comprehension
56 | nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
57 | for _outer, sublist in enumerate([my_list])}
| ^^^^^^
58 | # RUF052: `_outer`, `_inner` are accessed
|
help: Remove leading underscores
53 | # RUF052: Both `_index` and `_item` are accessed
54 |
55 | # Should detect in nested dict comprehension
- nested = {_outer: {_inner["key"]: _inner["value"] for _inner in sublist}
- for _outer, sublist in enumerate([my_list])}
56 + nested = {outer: {_inner["key"]: _inner["value"] for _inner in sublist}
57 + for outer, sublist in enumerate([my_list])}
58 | # RUF052: `_outer`, `_inner` are accessed
59 |
60 | # Set Comprehensions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:65:39
|
64 | # Should detect used dummy variable
65 | unique_values = {_item["foo"] for _item in my_list}
| ^^^^^
66 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
62 | my_list = [{"foo": 1}, {"foo": 2}, {"foo": 1}] # Note: duplicate values
63 |
64 | # Should detect used dummy variable
- unique_values = {_item["foo"] for _item in my_list}
65 + unique_values = {item["foo"] for item in my_list}
66 | # RUF052: Local dummy variable `_item` is accessed
67 |
68 | # Should detect with conditions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:69:38
|
68 | # Should detect with conditions
69 | filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
| ^^^^^
70 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
66 | # RUF052: Local dummy variable `_item` is accessed
67 |
68 | # Should detect with conditions
- filtered_set = {_item["foo"] for _item in my_list if _item["foo"] > 0}
69 + filtered_set = {item["foo"] for item in my_list if item["foo"] > 0}
70 | # RUF052: Local dummy variable `_item` is accessed
71 |
72 | # Should detect with complex expression
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:73:39
|
72 | # Should detect with complex expression
73 | processed = {_item["foo"] * 2 for _item in my_list}
| ^^^^^
74 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
70 | # RUF052: Local dummy variable `_item` is accessed
71 |
72 | # Should detect with complex expression
- processed = {_item["foo"] * 2 for _item in my_list}
73 + processed = {item["foo"] * 2 for item in my_list}
74 | # RUF052: Local dummy variable `_item` is accessed
75 |
76 | # Generator Expressions
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:81:29
|
80 | # Should detect used dummy variable
81 | gen = (_item["foo"] for _item in my_list)
| ^^^^^
82 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
78 | my_list = [{"foo": 1}, {"foo": 2}]
79 |
80 | # Should detect used dummy variable
- gen = (_item["foo"] for _item in my_list)
81 + gen = (item["foo"] for item in my_list)
82 | # RUF052: Local dummy variable `_item` is accessed
83 |
84 | # Should detect when passed to function
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:85:34
|
84 | # Should detect when passed to function
85 | total = sum(_item["foo"] for _item in my_list)
| ^^^^^
86 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
82 | # RUF052: Local dummy variable `_item` is accessed
83 |
84 | # Should detect when passed to function
- total = sum(_item["foo"] for _item in my_list)
85 + total = sum(item["foo"] for item in my_list)
86 | # RUF052: Local dummy variable `_item` is accessed
87 |
88 | # Should detect with multiple generators
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052_1.py:89:27
|
88 | # Should detect with multiple generators
89 | pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
| ^^
90 | # RUF052: Both `_x` and `_y` are accessed
|
help: Remove leading underscores
86 | # RUF052: Local dummy variable `_item` is accessed
87 |
88 | # Should detect with multiple generators
- pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
89 + pairs = ((x, _y) for x in range(3) for _y in range(3) if x != _y)
90 | # RUF052: Both `_x` and `_y` are accessed
91 |
92 | # Should detect in nested generator
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_y` is accessed
--> RUF052_1.py:89:46
|
88 | # Should detect with multiple generators
89 | pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
| ^^
90 | # RUF052: Both `_x` and `_y` are accessed
|
help: Remove leading underscores
86 | # RUF052: Local dummy variable `_item` is accessed
87 |
88 | # Should detect with multiple generators
- pairs = ((_x, _y) for _x in range(3) for _y in range(3) if _x != _y)
89 + pairs = ((_x, y) for _x in range(3) for y in range(3) if _x != y)
90 | # RUF052: Both `_x` and `_y` are accessed
91 |
92 | # Should detect in nested generator
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_inner` is accessed
--> RUF052_1.py:93:41
|
92 | # Should detect in nested generator
93 | nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
| ^^^^^^
94 | # RUF052: `_inner` and `_sublist` are accessed
|
help: Remove leading underscores
90 | # RUF052: Both `_x` and `_y` are accessed
91 |
92 | # Should detect in nested generator
- nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
93 + nested_gen = (sum(inner["foo"] for inner in sublist) for _sublist in [my_list] for sublist in _sublist)
94 | # RUF052: `_inner` and `_sublist` are accessed
95 |
96 | # Complex Examples with Multiple Comprehension Types
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_sublist` is accessed
--> RUF052_1.py:93:64
|
92 | # Should detect in nested generator
93 | nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
| ^^^^^^^^
94 | # RUF052: `_inner` and `_sublist` are accessed
|
help: Prefer using trailing underscores to avoid shadowing a variable
90 | # RUF052: Both `_x` and `_y` are accessed
91 |
92 | # Should detect in nested generator
- nested_gen = (sum(_inner["foo"] for _inner in sublist) for _sublist in [my_list] for sublist in _sublist)
93 + nested_gen = (sum(_inner["foo"] for _inner in sublist) for sublist_ in [my_list] for sublist in sublist_)
94 | # RUF052: `_inner` and `_sublist` are accessed
95 |
96 | # Complex Examples with Multiple Comprehension Types
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_val` is accessed
--> RUF052_1.py:102:30
|
100 | # Should detect in mixed comprehensions
101 | result = [
102 | {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
| ^^^^
103 | for _record in data
104 | ]
|
help: Remove leading underscores
99 |
100 | # Should detect in mixed comprehensions
101 | result = [
- {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
102 + {_key: [val * 2 for val in _record["items"]] for _key in ["doubled"]}
103 | for _record in data
104 | ]
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_key` is accessed
--> RUF052_1.py:102:60
|
100 | # Should detect in mixed comprehensions
101 | result = [
102 | {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
| ^^^^
103 | for _record in data
104 | ]
|
help: Remove leading underscores
99 |
100 | # Should detect in mixed comprehensions
101 | result = [
- {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
102 + {key: [_val * 2 for _val in _record["items"]] for key in ["doubled"]}
103 | for _record in data
104 | ]
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_record` is accessed
--> RUF052_1.py:103:13
|
101 | result = [
102 | {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
103 | for _record in data
| ^^^^^^^
104 | ]
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
|
help: Remove leading underscores
99 |
100 | # Should detect in mixed comprehensions
101 | result = [
- {_key: [_val * 2 for _val in _record["items"]] for _key in ["doubled"]}
- for _record in data
102 + {_key: [_val * 2 for _val in record["items"]] for _key in ["doubled"]}
103 + for record in data
104 | ]
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
106 |
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_item` is accessed
--> RUF052_1.py:108:43
|
107 | # Should detect in generator passed to list constructor
108 | gen_list = list(_item["items"][0] for _item in data)
| ^^^^^
109 | # RUF052: Local dummy variable `_item` is accessed
|
help: Remove leading underscores
105 | # RUF052: `_key`, `_val`, and `_record` are all accessed
106 |
107 | # Should detect in generator passed to list constructor
- gen_list = list(_item["items"][0] for _item in data)
108 + gen_list = list(item["items"][0] for item in data)
109 | # RUF052: Local dummy variable `_item` is accessed
note: This is an unsafe fix and may change runtime behavior

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF052 [*] Local dummy variable `_var` is accessed
--> RUF052.py:92:9
--> RUF052_0.py:92:9
|
90 | class Class_:
91 | def fun(self):
@@ -24,7 +24,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_list` is accessed
--> RUF052.py:99:5
--> RUF052_0.py:99:5
|
98 | def fun():
99 | _list = "built-in" # [RUF052]
@@ -45,7 +45,7 @@ help: Prefer using trailing underscores to avoid shadowing a built-in
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052.py:106:5
--> RUF052_0.py:106:5
|
104 | def fun():
105 | global x
@@ -67,7 +67,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052.py:113:5
--> RUF052_0.py:113:5
|
111 | def bar():
112 | nonlocal x
@@ -90,7 +90,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_x` is accessed
--> RUF052.py:120:5
--> RUF052_0.py:120:5
|
118 | def fun():
119 | x = "local"
@@ -112,7 +112,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052.py:128:5
--> RUF052_0.py:128:5
|
127 | def unfixables():
128 | _GLOBAL_1 = "foo"
@@ -123,7 +123,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052.py:136:5
--> RUF052_0.py:136:5
|
135 | # unfixable because the rename would shadow a local variable
136 | _local = "local3" # [RUF052]
@@ -133,7 +133,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052.py:140:9
--> RUF052_0.py:140:9
|
139 | def nested():
140 | _GLOBAL_1 = "foo"
@@ -144,7 +144,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052.py:145:9
--> RUF052_0.py:145:9
|
144 | # unfixable because the rename would shadow a variable from the outer function
145 | _local = "local4"
@@ -154,7 +154,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 [*] Local dummy variable `_P` is accessed
--> RUF052.py:153:5
--> RUF052_0.py:153:5
|
151 | from collections import namedtuple
152 |
@@ -184,7 +184,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_T` is accessed
--> RUF052.py:154:5
--> RUF052_0.py:154:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -213,7 +213,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT` is accessed
--> RUF052.py:155:5
--> RUF052_0.py:155:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -242,7 +242,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_E` is accessed
--> RUF052.py:156:5
--> RUF052_0.py:156:5
|
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
155 | _NT = NamedTuple("_NT", [("foo", int)])
@@ -270,7 +270,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT2` is accessed
--> RUF052.py:157:5
--> RUF052_0.py:157:5
|
155 | _NT = NamedTuple("_NT", [("foo", int)])
156 | _E = Enum("_E", ["a", "b", "c"])
@@ -297,7 +297,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NT3` is accessed
--> RUF052.py:158:5
--> RUF052_0.py:158:5
|
156 | _E = Enum("_E", ["a", "b", "c"])
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
@@ -323,7 +323,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_DynamicClass` is accessed
--> RUF052.py:159:5
--> RUF052_0.py:159:5
|
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
@@ -347,7 +347,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_NotADynamicClass` is accessed
--> RUF052.py:160:5
--> RUF052_0.py:160:5
|
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
159 | _DynamicClass = type("_DynamicClass", (), {})
@@ -371,7 +371,7 @@ help: Remove leading underscores
note: This is an unsafe fix and may change runtime behavior
RUF052 [*] Local dummy variable `_dummy_var` is accessed
--> RUF052.py:182:5
--> RUF052_0.py:182:5
|
181 | def foo():
182 | _dummy_var = 42
@@ -396,7 +396,7 @@ help: Prefer using trailing underscores to avoid shadowing a variable
note: This is an unsafe fix and may change runtime behavior
RUF052 Local dummy variable `_dummy_var` is accessed
--> RUF052.py:192:5
--> RUF052_0.py:192:5
|
190 | # Unfixable because both possible candidates for the new name are shadowed
191 | # in the scope of one of the references to the variable

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF052 Local dummy variable `_var` is accessed
--> RUF052.py:92:9
--> RUF052_0.py:92:9
|
90 | class Class_:
91 | def fun(self):
@@ -13,7 +13,7 @@ RUF052 Local dummy variable `_var` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_list` is accessed
--> RUF052.py:99:5
--> RUF052_0.py:99:5
|
98 | def fun():
99 | _list = "built-in" # [RUF052]
@@ -23,7 +23,7 @@ RUF052 Local dummy variable `_list` is accessed
help: Prefer using trailing underscores to avoid shadowing a built-in
RUF052 Local dummy variable `_x` is accessed
--> RUF052.py:106:5
--> RUF052_0.py:106:5
|
104 | def fun():
105 | global x
@@ -34,7 +34,7 @@ RUF052 Local dummy variable `_x` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `x` is accessed
--> RUF052.py:110:3
--> RUF052_0.py:110:3
|
109 | def foo():
110 | x = "outer"
@@ -44,7 +44,7 @@ RUF052 Local dummy variable `x` is accessed
|
RUF052 Local dummy variable `_x` is accessed
--> RUF052.py:113:5
--> RUF052_0.py:113:5
|
111 | def bar():
112 | nonlocal x
@@ -56,7 +56,7 @@ RUF052 Local dummy variable `_x` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_x` is accessed
--> RUF052.py:120:5
--> RUF052_0.py:120:5
|
118 | def fun():
119 | x = "local"
@@ -67,7 +67,7 @@ RUF052 Local dummy variable `_x` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052.py:128:5
--> RUF052_0.py:128:5
|
127 | def unfixables():
128 | _GLOBAL_1 = "foo"
@@ -78,7 +78,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052.py:136:5
--> RUF052_0.py:136:5
|
135 | # unfixable because the rename would shadow a local variable
136 | _local = "local3" # [RUF052]
@@ -88,7 +88,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_GLOBAL_1` is accessed
--> RUF052.py:140:9
--> RUF052_0.py:140:9
|
139 | def nested():
140 | _GLOBAL_1 = "foo"
@@ -99,7 +99,7 @@ RUF052 Local dummy variable `_GLOBAL_1` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_local` is accessed
--> RUF052.py:145:9
--> RUF052_0.py:145:9
|
144 | # unfixable because the rename would shadow a variable from the outer function
145 | _local = "local4"
@@ -109,7 +109,7 @@ RUF052 Local dummy variable `_local` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_P` is accessed
--> RUF052.py:153:5
--> RUF052_0.py:153:5
|
151 | from collections import namedtuple
152 |
@@ -121,7 +121,7 @@ RUF052 Local dummy variable `_P` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_T` is accessed
--> RUF052.py:154:5
--> RUF052_0.py:154:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -132,7 +132,7 @@ RUF052 Local dummy variable `_T` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_NT` is accessed
--> RUF052.py:155:5
--> RUF052_0.py:155:5
|
153 | _P = ParamSpec("_P")
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
@@ -144,7 +144,7 @@ RUF052 Local dummy variable `_NT` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_E` is accessed
--> RUF052.py:156:5
--> RUF052_0.py:156:5
|
154 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
155 | _NT = NamedTuple("_NT", [("foo", int)])
@@ -156,7 +156,7 @@ RUF052 Local dummy variable `_E` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_NT2` is accessed
--> RUF052.py:157:5
--> RUF052_0.py:157:5
|
155 | _NT = NamedTuple("_NT", [("foo", int)])
156 | _E = Enum("_E", ["a", "b", "c"])
@@ -168,7 +168,7 @@ RUF052 Local dummy variable `_NT2` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_NT3` is accessed
--> RUF052.py:158:5
--> RUF052_0.py:158:5
|
156 | _E = Enum("_E", ["a", "b", "c"])
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
@@ -180,7 +180,7 @@ RUF052 Local dummy variable `_NT3` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_DynamicClass` is accessed
--> RUF052.py:159:5
--> RUF052_0.py:159:5
|
157 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
@@ -191,7 +191,7 @@ RUF052 Local dummy variable `_DynamicClass` is accessed
help: Remove leading underscores
RUF052 Local dummy variable `_NotADynamicClass` is accessed
--> RUF052.py:160:5
--> RUF052_0.py:160:5
|
158 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
159 | _DynamicClass = type("_DynamicClass", (), {})
@@ -202,8 +202,18 @@ RUF052 Local dummy variable `_NotADynamicClass` is accessed
|
help: Remove leading underscores
RUF052 Local dummy variable `other` is accessed
--> RUF052_0.py:177:13
|
175 | return
176 | _seen.add(self)
177 | for other in self.connected:
| ^^^^^
178 | other.recurse(_seen=_seen)
|
RUF052 Local dummy variable `_dummy_var` is accessed
--> RUF052.py:182:5
--> RUF052_0.py:182:5
|
181 | def foo():
182 | _dummy_var = 42
@@ -214,7 +224,7 @@ RUF052 Local dummy variable `_dummy_var` is accessed
help: Prefer using trailing underscores to avoid shadowing a variable
RUF052 Local dummy variable `_dummy_var` is accessed
--> RUF052.py:192:5
--> RUF052_0.py:192:5
|
190 | # Unfixable because both possible candidates for the new name are shadowed
191 | # in the scope of one of the references to the variable

View File

@@ -773,10 +773,14 @@ pub trait StringFlags: Copy {
}
/// The total length of the string's closer.
/// This is always equal to `self.quote_len()`,
/// but is provided here for symmetry with the `opener_len()` method.
/// This is always equal to `self.quote_len()`, except when the string is unclosed,
/// in which case the length is zero.
fn closer_len(self) -> TextSize {
self.quote_len()
if self.is_unclosed() {
TextSize::default()
} else {
self.quote_len()
}
}
fn as_any_string_flags(self) -> AnyStringFlags {

View File

@@ -0,0 +1,3 @@
# parse_options: {"mode": "ipython"}
with (a, ?b)
?

View File

@@ -0,0 +1,3 @@
# parse_options: {"mode": "ipython"}
with (a, ?b
?

View File

@@ -0,0 +1,4 @@
# parse_options: {"mode": "ipython"}
with a, ?b
?
x = 1

View File

@@ -1115,7 +1115,27 @@ impl RecoveryContextKind {
TokenKind::Colon => Some(ListTerminatorKind::ErrorRecovery),
_ => None,
},
WithItemKind::Unparenthesized | WithItemKind::ParenthesizedExpression => p
// test_err ipython_help_escape_command_error_recovery_1
// # parse_options: {"mode": "ipython"}
// with (a, ?b)
// ?
// test_err ipython_help_escape_command_error_recovery_2
// # parse_options: {"mode": "ipython"}
// with (a, ?b
// ?
// test_err ipython_help_escape_command_error_recovery_3
// # parse_options: {"mode": "ipython"}
// with a, ?b
// ?
// x = 1
WithItemKind::Unparenthesized => matches!(
p.current_token_kind(),
TokenKind::Colon | TokenKind::Newline
)
.then_some(ListTerminatorKind::Regular),
WithItemKind::ParenthesizedExpression => p
.at(TokenKind::Colon)
.then_some(ListTerminatorKind::Regular),
},

View File

@@ -0,0 +1,84 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/ipython_help_escape_command_error_recovery_1.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..52,
body: [
With(
StmtWith {
node_index: NodeIndex(None),
range: 37..49,
is_async: false,
items: [
WithItem {
range: 43..44,
node_index: NodeIndex(None),
context_expr: Name(
ExprName {
node_index: NodeIndex(None),
range: 43..44,
id: Name("a"),
ctx: Load,
},
),
optional_vars: None,
},
WithItem {
range: 47..48,
node_index: NodeIndex(None),
context_expr: Name(
ExprName {
node_index: NodeIndex(None),
range: 47..48,
id: Name("b"),
ctx: Load,
},
),
optional_vars: None,
},
],
body: [],
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
node_index: NodeIndex(None),
range: 50..51,
kind: Help,
value: "",
},
),
],
},
)
```
## Errors
|
1 | # parse_options: {"mode": "ipython"}
2 | with (a, ?b)
| ^ Syntax Error: Expected `,`, found `?`
3 | ?
|
|
1 | # parse_options: {"mode": "ipython"}
2 | with (a, ?b)
| ^ Syntax Error: Expected `:`, found newline
3 | ?
|
|
1 | # parse_options: {"mode": "ipython"}
2 | with (a, ?b)
3 | ?
| ^ Syntax Error: Expected an indented block after `with` statement
|

View File

@@ -0,0 +1,80 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/ipython_help_escape_command_error_recovery_2.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..51,
body: [
With(
StmtWith {
node_index: NodeIndex(None),
range: 37..51,
is_async: false,
items: [
WithItem {
range: 42..51,
node_index: NodeIndex(None),
context_expr: Tuple(
ExprTuple {
node_index: NodeIndex(None),
range: 42..51,
elts: [
Name(
ExprName {
node_index: NodeIndex(None),
range: 43..44,
id: Name("a"),
ctx: Load,
},
),
Name(
ExprName {
node_index: NodeIndex(None),
range: 47..48,
id: Name("b"),
ctx: Load,
},
),
],
ctx: Load,
parenthesized: true,
},
),
optional_vars: None,
},
],
body: [],
},
),
],
},
)
```
## Errors
|
1 | # parse_options: {"mode": "ipython"}
2 | with (a, ?b
| ^ Syntax Error: Expected an expression or a ')'
3 | ?
|
|
1 | # parse_options: {"mode": "ipython"}
2 | with (a, ?b
3 | ?
| ^ Syntax Error: Expected `,`, found `?`
|
|
2 | with (a, ?b
3 | ?
| ^ Syntax Error: unexpected EOF while parsing
|

View File

@@ -0,0 +1,112 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/ipython_help_escape_command_error_recovery_3.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..56,
body: [
With(
StmtWith {
node_index: NodeIndex(None),
range: 37..47,
is_async: false,
items: [
WithItem {
range: 42..43,
node_index: NodeIndex(None),
context_expr: Name(
ExprName {
node_index: NodeIndex(None),
range: 42..43,
id: Name("a"),
ctx: Load,
},
),
optional_vars: None,
},
WithItem {
range: 46..47,
node_index: NodeIndex(None),
context_expr: Name(
ExprName {
node_index: NodeIndex(None),
range: 46..47,
id: Name("b"),
ctx: Load,
},
),
optional_vars: None,
},
],
body: [],
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
node_index: NodeIndex(None),
range: 48..49,
kind: Help,
value: "",
},
),
Assign(
StmtAssign {
node_index: NodeIndex(None),
range: 50..55,
targets: [
Name(
ExprName {
node_index: NodeIndex(None),
range: 50..51,
id: Name("x"),
ctx: Store,
},
),
],
value: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 54..55,
value: Int(
1,
),
},
),
},
),
],
},
)
```
## Errors
|
1 | # parse_options: {"mode": "ipython"}
2 | with a, ?b
| ^ Syntax Error: Expected `,`, found `?`
3 | ?
4 | x = 1
|
|
1 | # parse_options: {"mode": "ipython"}
2 | with a, ?b
| ^ Syntax Error: Expected `:`, found newline
3 | ?
4 | x = 1
|
|
1 | # parse_options: {"mode": "ipython"}
2 | with a, ?b
3 | ?
| ^ Syntax Error: Expected an indented block after `with` statement
4 | x = 1
|

View File

@@ -283,24 +283,27 @@ fn to_lsp_diagnostic(
range = diagnostic_range.to_range(source_kind.source_code(), index, encoding);
}
let (severity, tags, code) = if let Some(code) = code {
let code = code.to_string();
(
Some(severity(&code)),
tags(diagnostic),
Some(lsp_types::NumberOrString::String(code)),
)
let (severity, code) = if let Some(code) = code {
(severity(code), code.to_string())
} else {
(None, None, None)
(
match diagnostic.severity() {
ruff_db::diagnostic::Severity::Info => lsp_types::DiagnosticSeverity::INFORMATION,
ruff_db::diagnostic::Severity::Warning => lsp_types::DiagnosticSeverity::WARNING,
ruff_db::diagnostic::Severity::Error => lsp_types::DiagnosticSeverity::ERROR,
ruff_db::diagnostic::Severity::Fatal => lsp_types::DiagnosticSeverity::ERROR,
},
diagnostic.id().to_string(),
)
};
(
cell,
lsp_types::Diagnostic {
range,
severity,
tags,
code,
severity: Some(severity),
tags: tags(diagnostic),
code: Some(lsp_types::NumberOrString::String(code)),
code_description: diagnostic.documentation_url().and_then(|url| {
Some(lsp_types::CodeDescription {
href: lsp_types::Url::parse(url).ok()?,

View File

@@ -106,6 +106,25 @@ impl TextSize {
pub fn checked_sub(self, rhs: TextSize) -> Option<TextSize> {
self.raw.checked_sub(rhs.raw).map(|raw| TextSize { raw })
}
/// Saturating addition. Returns maximum `TextSize` if overflow occurred.
#[inline]
#[must_use]
pub fn saturating_add(self, rhs: TextSize) -> TextSize {
TextSize {
raw: self.raw.saturating_add(rhs.raw),
}
}
/// Saturating subtraction. Returns minimum `TextSize` if overflow
/// occurred.
#[inline]
#[must_use]
pub fn saturating_sub(self, rhs: TextSize) -> TextSize {
TextSize {
raw: self.raw.saturating_sub(rhs.raw),
}
}
}
impl From<u32> for TextSize {

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.14.5"
version = "0.14.6"
publish = false
authors = { workspace = true }
edition = { workspace = true }

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

@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L121" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L127" target="_blank">View source</a>
</small>
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L165" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L171" target="_blank">View source</a>
</small>
@@ -95,7 +95,7 @@ f(int) # error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L191" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L197" target="_blank">View source</a>
</small>
@@ -126,7 +126,7 @@ a = 1
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L216" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L222" target="_blank">View source</a>
</small>
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L242" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L248" target="_blank">View source</a>
</small>
@@ -190,7 +190,7 @@ class B(A): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L307" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L313" target="_blank">View source</a>
</small>
@@ -217,7 +217,7 @@ class B(A, A): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L328" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L334" target="_blank">View source</a>
</small>
@@ -329,7 +329,7 @@ def test(): -> "Literal[5]":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L532" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L538" target="_blank">View source</a>
</small>
@@ -359,7 +359,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L556" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L562" target="_blank">View source</a>
</small>
@@ -385,7 +385,7 @@ t[3] # IndexError: tuple index out of range
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L360" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L366" target="_blank">View source</a>
</small>
@@ -474,7 +474,7 @@ an atypical memory layout.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L610" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L616" target="_blank">View source</a>
</small>
@@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L656" target="_blank">View source</a>
</small>
@@ -529,7 +529,7 @@ a: int = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1809" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1815" target="_blank">View source</a>
</small>
@@ -563,7 +563,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L672" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L678" target="_blank">View source</a>
</small>
@@ -599,7 +599,7 @@ asyncio.run(main())
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L708" target="_blank">View source</a>
</small>
@@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L753" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L759" target="_blank">View source</a>
</small>
@@ -650,7 +650,7 @@ with 1:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L774" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L780" target="_blank">View source</a>
</small>
@@ -679,7 +679,7 @@ a: str
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L797" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L803" target="_blank">View source</a>
</small>
@@ -723,7 +723,7 @@ except ZeroDivisionError:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L833" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L839" target="_blank">View source</a>
</small>
@@ -756,7 +756,7 @@ class C[U](Generic[T]): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L577" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L583" target="_blank">View source</a>
</small>
@@ -795,7 +795,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L859" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L865" target="_blank">View source</a>
</small>
@@ -830,7 +830,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L962" target="_blank">View source</a>
</small>
@@ -858,13 +858,99 @@ class B(metaclass=f): ...
- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
## `invalid-method-override`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1943" target="_blank">View source</a>
</small>
**What it does**
Detects method overrides that violate the [Liskov Substitution Principle] ("LSP").
The LSP states that an instance of a subtype should be substitutable for an instance of its supertype.
Applied to Python, this means:
1. All argument combinations a superclass method accepts
must also be accepted by an overriding subclass method.
2. The return type of an overriding subclass method must be a subtype
of the return type of the superclass method.
**Why is this bad?**
Violating the Liskov Substitution Principle will lead to many of ty's assumptions and
inferences being incorrect, which will mean that it will fail to catch many possible
type errors in your code.
**Example**
```python
class Super:
def method(self, x) -> int:
return 42
class Sub(Super):
# Liskov violation: `str` is not a subtype of `int`,
# but the supertype method promises to return an `int`.
def method(self, x) -> str: # error: [invalid-override]
return "foo"
def accepts_super(s: Super) -> int:
return s.method(x=42)
accepts_super(Sub()) # The result of this call is a string, but ty will infer
# it to be an `int` due to the violation of the Liskov Substitution Principle.
class Sub2(Super):
# Liskov violation: the superclass method can be called with a `x=`
# keyword argument, but the subclass method does not accept it.
def method(self, y) -> int: # error: [invalid-override]
return 42
accepts_super(Sub2()) # TypeError at runtime: method() got an unexpected keyword argument 'x'
# ty cannot catch this error due to the violation of the Liskov Substitution Principle.
```
**Common issues**
**Why does ty complain about my `__eq__` method?**
`__eq__` and `__ne__` methods in Python are generally expected to accept arbitrary
objects as their second argument, for example:
```python
class A:
x: int
def __eq__(self, other: object) -> bool:
# gracefully handle an object of an unexpected type
# without raising an exception
if not isinstance(other, A):
return False
return self.x == other.x
```
If `A.__eq__` here were annotated as only accepting `A` instances for its second argument,
it would imply that you wouldn't be able to use `==` between instances of `A` and
instances of unrelated classes without an exception possibly being raised. While some
classes in Python do indeed behave this way, the strongly held convention is that it should
be avoided wherever possible. As part of this check, therefore, ty enforces that `__eq__`
and `__ne__` methods accept `object` as their second argument.
[Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle
## `invalid-named-tuple`
<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L512" target="_blank">View source</a>
</small>
@@ -896,7 +982,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L932" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L938" target="_blank">View source</a>
</small>
@@ -926,7 +1012,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L983" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L989" target="_blank">View source</a>
</small>
@@ -976,7 +1062,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1082" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1088" target="_blank">View source</a>
</small>
@@ -1002,7 +1088,7 @@ def f(a: int = ''): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L887" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L893" target="_blank">View source</a>
</small>
@@ -1033,7 +1119,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L442" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L448" target="_blank">View source</a>
</small>
@@ -1067,7 +1153,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1102" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1108" target="_blank">View source</a>
</small>
@@ -1116,7 +1202,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L631" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L637" target="_blank">View source</a>
</small>
@@ -1141,7 +1227,7 @@ def func() -> int:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1145" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1151" target="_blank">View source</a>
</small>
@@ -1199,7 +1285,7 @@ TODO #14889
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L911" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L917" target="_blank">View source</a>
</small>
@@ -1226,7 +1312,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1184" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1190" target="_blank">View source</a>
</small>
@@ -1256,7 +1342,7 @@ TYPE_CHECKING = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1208" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1214" target="_blank">View source</a>
</small>
@@ -1286,7 +1372,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1260" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1266" target="_blank">View source</a>
</small>
@@ -1320,7 +1406,7 @@ f(10) # Error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1232" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1238" target="_blank">View source</a>
</small>
@@ -1354,7 +1440,7 @@ class C:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1288" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1294" target="_blank">View source</a>
</small>
@@ -1389,7 +1475,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1317" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323" target="_blank">View source</a>
</small>
@@ -1414,7 +1500,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1910" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1916" target="_blank">View source</a>
</small>
@@ -1447,7 +1533,7 @@ alice["age"] # KeyError
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1336" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1342" target="_blank">View source</a>
</small>
@@ -1476,7 +1562,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1359" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1365" target="_blank">View source</a>
</small>
@@ -1500,7 +1586,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1377" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1383" target="_blank">View source</a>
</small>
@@ -1526,7 +1612,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1428" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1434" target="_blank">View source</a>
</small>
@@ -1553,7 +1639,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1663" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1669" target="_blank">View source</a>
</small>
@@ -1611,7 +1697,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1785" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1791" target="_blank">View source</a>
</small>
@@ -1641,7 +1727,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1519" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1525" target="_blank">View source</a>
</small>
@@ -1670,7 +1756,7 @@ class B(A): ... # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1570" target="_blank">View source</a>
</small>
@@ -1697,7 +1783,7 @@ f("foo") # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1542" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1548" target="_blank">View source</a>
</small>
@@ -1725,7 +1811,7 @@ def _(x: int):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1585" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1591" target="_blank">View source</a>
</small>
@@ -1771,7 +1857,7 @@ class A:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1642" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1648" target="_blank">View source</a>
</small>
@@ -1798,7 +1884,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1684" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1690" target="_blank">View source</a>
</small>
@@ -1826,7 +1912,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1706" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1712" target="_blank">View source</a>
</small>
@@ -1851,7 +1937,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1725" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1731" target="_blank">View source</a>
</small>
@@ -1876,7 +1962,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1397" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1403" target="_blank">View source</a>
</small>
@@ -1913,7 +1999,7 @@ b1 < b2 < b1 # exception raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1744" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1750" target="_blank">View source</a>
</small>
@@ -1941,7 +2027,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1766" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1772" target="_blank">View source</a>
</small>
@@ -1966,7 +2052,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L471" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L477" target="_blank">View source</a>
</small>
@@ -2007,7 +2093,7 @@ class SubProto(BaseProto, Protocol):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.16">0.0.1-alpha.16</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L292" target="_blank">View source</a>
</small>
@@ -2095,7 +2181,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1449" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1455" target="_blank">View source</a>
</small>
@@ -2123,7 +2209,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L139" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L145" target="_blank">View source</a>
</small>
@@ -2155,7 +2241,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1477" target="_blank">View source</a>
</small>
@@ -2187,7 +2273,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1837" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1843" target="_blank">View source</a>
</small>
@@ -2214,7 +2300,7 @@ cast(int, f()) # Redundant
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1624" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1630" target="_blank">View source</a>
</small>
@@ -2238,7 +2324,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1858" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1864" target="_blank">View source</a>
</small>
@@ -2296,7 +2382,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L720" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L726" target="_blank">View source</a>
</small>
@@ -2335,7 +2421,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1026" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1032" target="_blank">View source</a>
</small>
@@ -2398,7 +2484,7 @@ def foo(x: int | str) -> int | str:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L268" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L274" target="_blank">View source</a>
</small>
@@ -2422,7 +2508,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1497" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1503" target="_blank">View source</a>
</small>

View File

@@ -37,7 +37,8 @@ fn config_override_python_version() -> anyhow::Result<()> {
5 | print(sys.last_exc)
| ^^^^^^^^^^^^
|
info: Python 3.11 was assumed when accessing `last_exc`
info: The member may be available on other Python versions or platforms
info: Python 3.11 was assumed when resolving the `last_exc` attribute
--> pyproject.toml:3:18
|
2 | [tool.ty.environment]
@@ -1179,6 +1180,8 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
import os
os.grantpt(1) # only available on unix, Python 3.13 or newer
from typing import LiteralString # added in Python 3.11
"#,
),
])?;
@@ -1194,8 +1197,11 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
3 |
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
| ^^^^^^^^^^
5 |
6 | from typing import LiteralString # added in Python 3.11
|
info: Python 3.10 was assumed when accessing `grantpt`
info: The member may be available on other Python versions or platforms
info: Python 3.10 was assumed when resolving the `grantpt` attribute
--> ty.toml:3:18
|
2 | [environment]
@@ -1205,7 +1211,26 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
info: rule `unresolved-attribute` is enabled by default
Found 1 diagnostic
error[unresolved-import]: Module `typing` has no member `LiteralString`
--> main.py:6:20
|
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
5 |
6 | from typing import LiteralString # added in Python 3.11
| ^^^^^^^^^^^^^
|
info: The member may be available on other Python versions or platforms
info: Python 3.10 was assumed when resolving imports
--> ty.toml:3:18
|
2 | [environment]
3 | python-version = "3.10"
| ^^^^^^ Python 3.10 assumed due to this configuration setting
4 | python-platform = "linux"
|
info: rule `unresolved-import` is enabled by default
Found 2 diagnostics
----- stderr -----
"#);
@@ -1225,6 +1250,8 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
import os
os.grantpt(1) # only available on unix, Python 3.13 or newer
from typing import LiteralString # added in Python 3.11
"#,
),
])?;

View File

@@ -18,11 +18,11 @@ numpy-array,main.py,1,1
object-attr-instance-methods,main.py,0,1
object-attr-instance-methods,main.py,1,1
pass-keyword-completion,main.py,0,1
raise-uses-base-exception,main.py,0,2
raise-uses-base-exception,main.py,0,1
scope-existing-over-new-import,main.py,0,1
scope-prioritize-closer,main.py,0,2
scope-simple-long-identifier,main.py,0,1
tstring-completions,main.py,0,1
ty-extensions-lower-stdlib,main.py,0,8
type-var-typing-over-ast,main.py,0,3
type-var-typing-over-ast,main.py,1,279
type-var-typing-over-ast,main.py,1,278
1 name file index rank
18 object-attr-instance-methods main.py 0 1
19 object-attr-instance-methods main.py 1 1
20 pass-keyword-completion main.py 0 1
21 raise-uses-base-exception main.py 0 2 1
22 scope-existing-over-new-import main.py 0 1
23 scope-prioritize-closer main.py 0 2
24 scope-simple-long-identifier main.py 0 1
25 tstring-completions main.py 0 1
26 ty-extensions-lower-stdlib main.py 0 8
27 type-var-typing-over-ast main.py 0 3
28 type-var-typing-over-ast main.py 1 279 278

View File

@@ -8,10 +8,11 @@ use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_python_codegen::Stylist;
use ruff_python_parser::{Token, TokenAt, TokenKind, Tokens};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ty_python_semantic::types::UnionType;
use ty_python_semantic::{
Completion as SemanticCompletion, ModuleName, NameKind, SemanticModel,
types::{CycleDetector, Type},
Completion as SemanticCompletion, KnownModule, ModuleName, NameKind, SemanticModel,
types::{CycleDetector, KnownClass, Type},
};
use crate::docstring::Docstring;
@@ -82,6 +83,31 @@ impl<'db> Completions<'db> {
fn force_add(&mut self, completion: Completion<'db>) {
self.items.push(completion);
}
/// Tags completions with whether they are known to be usable in
/// a `raise` context.
///
/// It's possible that some completions are usable in a `raise`
/// but aren't marked by this method. That is, false negatives are
/// possible but false positives are not.
fn tag_raisable(&mut self) {
let raisable_type = UnionType::from_elements(
self.db,
[
KnownClass::BaseException.to_subclass_of(self.db),
KnownClass::BaseException.to_instance(self.db),
],
);
for item in &mut self.items {
let Some(ty) = item.ty else { continue };
item.is_definitively_raisable = ty.is_assignable_to(self.db, raisable_type);
}
}
/// Removes any completion that doesn't satisfy the given predicate.
fn retain(&mut self, predicate: impl FnMut(&Completion<'_>) -> bool) {
self.items.retain(predicate);
}
}
impl<'db> Extend<SemanticCompletion<'db>> for Completions<'db> {
@@ -153,6 +179,13 @@ pub struct Completion<'db> {
/// Whether this item only exists for type checking purposes and
/// will be missing at runtime
pub is_type_check_only: bool,
/// Whether this item can definitively be used in a `raise` context.
///
/// Note that this may not always be computed. (i.e., Only computed
/// when we are in a `raise` context.) And also note that if this
/// is `true`, then it's definitively usable in `raise`, but if
/// it's `false`, it _may_ still be usable in `raise`.
pub is_definitively_raisable: bool,
/// The documentation associated with this item, if
/// available.
pub documentation: Option<Docstring>,
@@ -177,6 +210,7 @@ impl<'db> Completion<'db> {
import: None,
builtin: semantic.builtin,
is_type_check_only,
is_definitively_raisable: false,
documentation,
}
}
@@ -257,6 +291,7 @@ impl<'db> Completion<'db> {
import: None,
builtin: false,
is_type_check_only: false,
is_definitively_raisable: false,
documentation: None,
}
}
@@ -271,6 +306,7 @@ impl<'db> Completion<'db> {
import: None,
builtin: true,
is_type_check_only: false,
is_definitively_raisable: false,
documentation: None,
}
}
@@ -329,7 +365,7 @@ pub fn completion<'db>(
let tokens = tokens_start_before(parsed.tokens(), offset);
let typed = find_typed_text(db, file, &parsed, offset);
if is_in_no_completions_place(db, file, tokens, typed.as_deref()) {
if is_in_no_completions_place(db, file, &parsed, offset, tokens, typed.as_deref()) {
return vec![];
}
@@ -364,6 +400,20 @@ pub fn completion<'db>(
}
}
if is_raising_exception(tokens) {
completions.tag_raisable();
// As a special case, and because it's a common footgun, we
// specifically disallow `NotImplemented` in this context.
// `NotImplementedError` should be used instead. So if we can
// definitively detect `NotImplemented`, then we can safely
// omit it from suggestions.
completions.retain(|item| {
let Some(ty) = item.ty else { return true };
!ty.is_notimplemented(db)
});
}
completions.into_completions()
}
@@ -427,7 +477,8 @@ fn add_unimported_completions<'db>(
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
for symbol in all_symbols(db, &completions.query) {
if symbol.module.file(db) == Some(file) {
if symbol.module.file(db) == Some(file) || symbol.module.is_known(db, KnownModule::Builtins)
{
continue;
}
@@ -450,6 +501,7 @@ fn add_unimported_completions<'db>(
builtin: false,
// TODO: `is_type_check_only` requires inferring the type of the symbol
is_type_check_only: false,
is_definitively_raisable: false,
documentation: None,
});
}
@@ -1270,10 +1322,14 @@ fn find_typed_text(
fn is_in_no_completions_place(
db: &dyn Db,
file: File,
parsed: &ParsedModuleRef,
offset: TextSize,
tokens: &[Token],
typed: Option<&str>,
) -> bool {
is_in_comment(tokens) || is_in_string(tokens) || is_in_definition_place(db, file, tokens, typed)
is_in_comment(tokens)
|| is_in_string(tokens)
|| is_in_definition_place(db, file, parsed, offset, tokens, typed)
}
/// Whether the last token is within a comment or not.
@@ -1296,11 +1352,18 @@ fn is_in_string(tokens: &[Token]) -> bool {
/// Returns true when the tokens indicate that the definition of a new
/// name is being introduced at the end.
fn is_in_definition_place(db: &dyn Db, file: File, tokens: &[Token], typed: Option<&str>) -> bool {
fn is_in_definition_place(
db: &dyn Db,
file: File,
parsed: &ParsedModuleRef,
offset: TextSize,
tokens: &[Token],
typed: Option<&str>,
) -> bool {
fn is_definition_token(token: &Token) -> bool {
matches!(
token.kind(),
TokenKind::Def | TokenKind::Class | TokenKind::Type | TokenKind::As
TokenKind::Def | TokenKind::Class | TokenKind::Type | TokenKind::As | TokenKind::For
)
}
@@ -1314,11 +1377,61 @@ fn is_in_definition_place(db: &dyn Db, file: File, tokens: &[Token], typed: Opti
false
}
};
match tokens {
if match tokens {
[.., penultimate, _] if typed.is_some() => is_definition_keyword(penultimate),
[.., last] if typed.is_none() => is_definition_keyword(last),
_ => false,
} {
return true;
}
// Analyze the AST if token matching is insufficient
// to determine if we're inside a name definition.
is_in_variable_binding(parsed, offset, typed)
}
/// Returns true when the cursor sits on a binding statement.
/// E.g. naming a parameter, type parameter, or `for` <name>).
fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Option<&str>) -> bool {
let range = if let Some(typed) = typed {
let start = offset.saturating_sub(typed.text_len());
TextRange::new(start, offset)
} else {
TextRange::empty(offset)
};
let covering = covering_node(parsed.syntax().into(), range);
covering.ancestors().any(|node| match node {
ast::AnyNodeRef::Parameter(param) => param.name.range.contains_range(range),
ast::AnyNodeRef::TypeParamTypeVar(type_param) => {
type_param.name.range.contains_range(range)
}
ast::AnyNodeRef::StmtFor(stmt_for) => stmt_for.target.range().contains_range(range),
_ => false,
})
}
/// Returns true when the cursor is after a `raise` keyword.
fn is_raising_exception(tokens: &[Token]) -> bool {
/// The maximum number of tokens we're willing to
/// look-behind to find a `raise` keyword.
const LIMIT: usize = 10;
// This only looks for things like `raise foo.bar.baz.qu<CURSOR>`.
// Technically, any kind of expression is allowed after `raise`.
// But we may not always want to treat it specially. So we're
// rather conservative about what we consider "raising an
// exception" to be for the purposes of completions. The failure
// mode here is that we may wind up suggesting things that
// shouldn't be raised. The benefit is that when this heuristic
// does work, we won't suggest things that shouldn't be raised.
for token in tokens.iter().rev().take(LIMIT) {
match token.kind() {
TokenKind::Name | TokenKind::Dot => continue,
TokenKind::Raise => return true,
_ => return false,
}
}
false
}
/// Order completions according to the following rules:
@@ -1333,8 +1446,16 @@ fn is_in_definition_place(db: &dyn Db, file: File, tokens: &[Token], typed: Opti
/// This has the effect of putting all dunder attributes after "normal"
/// attributes, and all single-underscore attributes after dunder attributes.
fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering {
fn key<'a>(completion: &'a Completion) -> (bool, bool, bool, NameKind, bool, &'a Name) {
fn key<'a>(completion: &'a Completion) -> (bool, bool, bool, bool, NameKind, bool, &'a Name) {
(
// This is only true when we are both in a `raise` context
// *and* we know this suggestion is definitively usable
// in a `raise` context. So we should sort these before
// anything else.
!completion.is_definitively_raisable,
// When `None`, a completion is for something in the
// current module, which we should generally prefer over
// something from outside the module.
completion.module_name.is_some(),
// At time of writing (2025-11-11), keyword completions
// are classified as builtins, which makes them sort after
@@ -5174,6 +5295,96 @@ match status:
);
}
#[test]
fn no_completions_in_empty_for_variable_binding() {
let builder = completion_test_builder(
"\
for <CURSOR>
",
);
assert_snapshot!(
builder.build().snapshot(),
@"<No completions found>",
);
}
#[test]
fn no_completions_in_for_variable_binding() {
let builder = completion_test_builder(
"\
for foo<CURSOR>
",
);
assert_snapshot!(
builder.build().snapshot(),
@"<No completions found>",
);
}
#[test]
fn no_completions_in_for_tuple_variable_binding() {
let builder = completion_test_builder(
"\
for foo, bar<CURSOR>
",
);
assert_snapshot!(
builder.build().snapshot(),
@"<No completions found>",
);
}
#[test]
fn no_completions_in_function_param() {
let builder = completion_test_builder(
"\
def foo(p<CURSOR>
",
);
assert_snapshot!(
builder.build().snapshot(),
@"<No completions found>",
);
}
#[test]
fn no_completions_in_function_type_param() {
let builder = completion_test_builder(
"\
def foo[T<CURSOR>]
",
);
assert_snapshot!(
builder.build().snapshot(),
@"<No completions found>",
);
}
#[test]
fn completions_in_function_type_param_bound() {
completion_test_builder(
"\
def foo[T: s<CURSOR>]
",
)
.build()
.contains("str");
}
#[test]
fn completions_in_function_param_type_annotation() {
// Ensure that completions are no longer
// suppressed when have left the name
// definition block.
completion_test_builder(
"\
def foo(param: s<CURSOR>)
",
)
.build()
.contains("str");
}
#[test]
fn favour_symbols_currently_imported() {
let snapshot = CursorTest::builder()

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,7 @@ pub(crate) enum GotoTarget<'a> {
/// ```
ImportModuleComponent {
module_name: String,
level: u32,
component_index: usize,
component_range: TextRange,
},
@@ -302,12 +303,21 @@ impl GotoTarget<'_> {
// (i.e. the type of `MyClass` in `MyClass()` is `<class MyClass>` and not `() -> MyClass`)
GotoTarget::Call { callable, .. } => callable.inferred_type(model),
GotoTarget::TypeParamTypeVarName(typevar) => typevar.inferred_type(model),
GotoTarget::ImportModuleComponent {
module_name,
component_index,
level,
..
} => {
// We don't currently support hovering the bare `.` so there is always a name
let module = import_name(module_name, *component_index);
model.resolve_module_type(Some(module), *level)?
}
// TODO: Support identifier targets
GotoTarget::PatternMatchRest(_)
| GotoTarget::PatternKeywordArgument(_)
| GotoTarget::PatternMatchStarName(_)
| GotoTarget::PatternMatchAsName(_)
| GotoTarget::ImportModuleComponent { .. }
| GotoTarget::TypeParamParamSpecName(_)
| GotoTarget::TypeParamTypeVarTupleName(_)
| GotoTarget::NonLocal { .. }
@@ -353,37 +363,30 @@ impl GotoTarget<'_> {
/// as just returning a raw `NavigationTarget`.
pub(crate) fn get_definition_targets<'db>(
&self,
file: ruff_db::files::File,
db: &'db dyn crate::Db,
model: &SemanticModel<'db>,
alias_resolution: ImportAliasResolution,
) -> Option<DefinitionsOrTargets<'db>> {
use crate::NavigationTarget;
let db = model.db();
let file = model.file();
match self {
GotoTarget::Expression(expression) => definitions_for_expression(db, file, expression)
.map(DefinitionsOrTargets::Definitions),
GotoTarget::Expression(expression) => {
definitions_for_expression(model, expression).map(DefinitionsOrTargets::Definitions)
}
// For already-defined symbols, they are their own definitions
GotoTarget::FunctionDef(function) => {
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(function.definition(&model)),
]))
}
GotoTarget::FunctionDef(function) => Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(function.definition(model)),
])),
GotoTarget::ClassDef(class) => {
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(class.definition(&model)),
]))
}
GotoTarget::ClassDef(class) => Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(class.definition(model)),
])),
GotoTarget::Parameter(parameter) => {
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(parameter.definition(&model)),
]))
}
GotoTarget::Parameter(parameter) => Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(parameter.definition(model)),
])),
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
GotoTarget::ImportSymbolAlias {
@@ -404,24 +407,18 @@ impl GotoTarget<'_> {
GotoTarget::ImportModuleComponent {
module_name,
component_index,
level,
..
} => {
// Handle both `import foo.bar` and `from foo.bar import baz` where offset is within module component
let components: Vec<&str> = module_name.split('.').collect();
// Build the module name up to and including the component containing the offset
let target_module_name = components[..=*component_index].join(".");
// Try to resolve the module
definitions_for_module(db, &target_module_name)
// We don't currently support hovering the bare `.` so there is always a name
let module = import_name(module_name, *component_index);
definitions_for_module(model, Some(module), *level)
}
// Handle import aliases (offset within 'z' in "import x.y as z")
GotoTarget::ImportModuleAlias { alias } => {
if alias_resolution == ImportAliasResolution::ResolveAliases {
let full_module_name = alias.name.as_str();
// Try to resolve the module
definitions_for_module(db, full_module_name)
definitions_for_module(model, Some(alias.name.as_str()), 0)
} else {
let alias_range = alias.asname.as_ref().unwrap().range;
Some(DefinitionsOrTargets::Targets(
@@ -444,9 +441,8 @@ impl GotoTarget<'_> {
// For exception variables, they are their own definitions (like parameters)
GotoTarget::ExceptVariable(except_handler) => {
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(except_handler.definition(&model)),
ResolvedDefinition::Definition(except_handler.definition(model)),
]))
}
@@ -478,9 +474,9 @@ impl GotoTarget<'_> {
//
// Prefer the function impl over the callable so that its docstrings win if defined.
GotoTarget::Call { callable, call } => {
let mut definitions = definitions_for_callable(db, file, call);
let mut definitions = definitions_for_callable(model, call);
let expr_definitions =
definitions_for_expression(db, file, callable).unwrap_or_default();
definitions_for_expression(model, callable).unwrap_or_default();
definitions.extend(expr_definitions);
if definitions.is_empty() {
@@ -491,18 +487,15 @@ impl GotoTarget<'_> {
}
GotoTarget::BinOp { expression, .. } => {
let model = SemanticModel::new(db, file);
let (definitions, _) =
ty_python_semantic::definitions_for_bin_op(db, &model, expression)?;
ty_python_semantic::definitions_for_bin_op(db, model, expression)?;
Some(DefinitionsOrTargets::Definitions(definitions))
}
GotoTarget::UnaryOp { expression, .. } => {
let model = SemanticModel::new(db, file);
let (definitions, _) =
ty_python_semantic::definitions_for_unary_op(db, &model, expression)?;
ty_python_semantic::definitions_for_unary_op(db, model, expression)?;
Some(DefinitionsOrTargets::Definitions(definitions))
}
@@ -632,6 +625,7 @@ impl GotoTarget<'_> {
{
return Some(GotoTarget::ImportModuleComponent {
module_name: full_name.to_string(),
level: 0,
component_index,
component_range,
});
@@ -672,14 +666,12 @@ impl GotoTarget<'_> {
// Handle offset within module name in from import statements
if let Some(module_expr) = &from.module {
let full_module_name = module_expr.to_string();
if let Some((component_index, component_range)) = find_module_component(
&full_module_name,
module_expr.range.start(),
offset,
) {
if let Some((component_index, component_range)) =
find_module_component(&full_module_name, module_expr.start(), offset)
{
return Some(GotoTarget::ImportModuleComponent {
module_name: full_module_name,
level: from.level,
component_index,
component_range,
});
@@ -876,27 +868,26 @@ fn convert_resolved_definitions_to_targets(
/// Shared helper to get definitions for an expr (that is presumably a name/attr)
fn definitions_for_expression<'db>(
db: &'db dyn crate::Db,
file: ruff_db::files::File,
model: &SemanticModel<'db>,
expression: &ruff_python_ast::ExprRef<'_>,
) -> Option<Vec<ResolvedDefinition<'db>>> {
match expression {
ast::ExprRef::Name(name) => Some(definitions_for_name(db, file, name)),
ast::ExprRef::Name(name) => Some(definitions_for_name(model.db(), model.file(), name)),
ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute(
db, file, attribute,
model.db(),
model.file(),
attribute,
)),
_ => None,
}
}
fn definitions_for_callable<'db>(
db: &'db dyn crate::Db,
file: ruff_db::files::File,
model: &SemanticModel<'db>,
call: &ast::ExprCall,
) -> Vec<ResolvedDefinition<'db>> {
let model = SemanticModel::new(db, file);
// Attempt to refine to a specific call
let signature_info = call_signature_details(db, &model, call);
let signature_info = call_signature_details(model.db(), model, call);
signature_info
.into_iter()
.filter_map(|signature| signature.definition.map(ResolvedDefinition::Definition))
@@ -947,7 +938,9 @@ pub(crate) fn find_goto_target(
}
let covering_node = covering_node(parsed.syntax().into(), token.range())
.find_first(|node| node.is_identifier() || node.is_expression())
.find_first(|node| {
node.is_identifier() || node.is_expression() || node.is_stmt_import_from()
})
.ok()?;
GotoTarget::from_covering_node(&covering_node, offset, parsed.tokens())
@@ -955,21 +948,15 @@ pub(crate) fn find_goto_target(
/// Helper function to resolve a module name and create a navigation target.
fn definitions_for_module<'db>(
db: &'db dyn crate::Db,
module_name_str: &str,
model: &SemanticModel,
module: Option<&str>,
level: u32,
) -> Option<DefinitionsOrTargets<'db>> {
use ty_python_semantic::{ModuleName, resolve_module};
if let Some(module_name) = ModuleName::new(module_name_str) {
if let Some(resolved_module) = resolve_module(db, &module_name) {
if let Some(module_file) = resolved_module.file(db) {
return Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Module(module_file),
]));
}
}
}
None
let module = model.resolve_module(module, level)?;
let file = module.file(model.db())?;
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Module(file),
]))
}
/// Helper function to extract module component information from a dotted module name
@@ -983,9 +970,7 @@ fn find_module_component(
// Split the module name into components and find which one contains the offset
let mut current_pos = 0;
let components: Vec<&str> = full_module_name.split('.').collect();
for (i, component) in components.iter().enumerate() {
for (i, component) in full_module_name.split('.').enumerate() {
let component_start = current_pos;
let component_end = current_pos + component.len();
@@ -1004,3 +989,16 @@ fn find_module_component(
None
}
/// Helper to get the module name up to the given component index
fn import_name(module_name: &str, component_index: usize) -> &str {
// We want everything to the left of the nth `.`
// If there's no nth `.` then we want the whole thing.
let idx = module_name
.match_indices('.')
.nth(component_index)
.map(|(idx, _)| idx)
.unwrap_or(module_name.len());
&module_name[..idx]
}

View File

@@ -3,7 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize};
use ty_python_semantic::ImportAliasResolution;
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
/// Navigate to the declaration of a symbol.
///
@@ -18,8 +18,9 @@ pub fn goto_declaration(
let module = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&module, offset)?;
let model = SemanticModel::new(db, file);
let declaration_targets = goto_target
.get_definition_targets(file, db, ImportAliasResolution::ResolveAliases)?
.get_definition_targets(&model, ImportAliasResolution::ResolveAliases)?
.declaration_targets(db)?;
Some(RangedValue {

View File

@@ -3,7 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize};
use ty_python_semantic::ImportAliasResolution;
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
/// Navigate to the definition of a symbol.
///
@@ -18,9 +18,9 @@ pub fn goto_definition(
) -> Option<RangedValue<NavigationTargets>> {
let module = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&module, offset)?;
let model = SemanticModel::new(db, file);
let definition_targets = goto_target
.get_definition_targets(file, db, ImportAliasResolution::ResolveAliases)?
.get_definition_targets(&model, ImportAliasResolution::ResolveAliases)?
.definition_targets(db)?;
Some(RangedValue {

View File

@@ -285,6 +285,300 @@ mod tests {
");
}
#[test]
fn goto_type_of_import_module() {
let mut test = cursor_test(
r#"
import l<CURSOR>ib
"#,
);
test.write_file("lib.py", "a = 10").unwrap();
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> lib.py:1:1
|
1 | a = 10
| ^^^^^^
|
info: Source
--> main.py:2:8
|
2 | import lib
| ^^^
|
");
}
#[test]
fn goto_type_of_import_module_multi1() {
let mut test = cursor_test(
r#"
import li<CURSOR>b.submod
"#,
);
test.write_file("lib/__init__.py", "b = 7").unwrap();
test.write_file("lib/submod.py", "a = 10").unwrap();
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> lib/__init__.py:1:1
|
1 | b = 7
| ^^^^^
|
info: Source
--> main.py:2:8
|
2 | import lib.submod
| ^^^
|
");
}
#[test]
fn goto_type_of_import_module_multi2() {
let mut test = cursor_test(
r#"
import lib.subm<CURSOR>od
"#,
);
test.write_file("lib/__init__.py", "b = 7").unwrap();
test.write_file("lib/submod.py", "a = 10").unwrap();
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> lib/submod.py:1:1
|
1 | a = 10
| ^^^^^^
|
info: Source
--> main.py:2:12
|
2 | import lib.submod
| ^^^^^^
|
");
}
#[test]
fn goto_type_of_from_import_module() {
let mut test = cursor_test(
r#"
from l<CURSOR>ib import a
"#,
);
test.write_file("lib.py", "a = 10").unwrap();
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> lib.py:1:1
|
1 | a = 10
| ^^^^^^
|
info: Source
--> main.py:2:6
|
2 | from lib import a
| ^^^
|
");
}
#[test]
fn goto_type_of_from_import_module_multi1() {
let mut test = cursor_test(
r#"
from li<CURSOR>b.submod import a
"#,
);
test.write_file("lib/__init__.py", "b = 7").unwrap();
test.write_file("lib/submod.py", "a = 10").unwrap();
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> lib/__init__.py:1:1
|
1 | b = 7
| ^^^^^
|
info: Source
--> main.py:2:6
|
2 | from lib.submod import a
| ^^^
|
");
}
#[test]
fn goto_type_of_from_import_module_multi2() {
let mut test = cursor_test(
r#"
from lib.subm<CURSOR>od import a
"#,
);
test.write_file("lib/__init__.py", "b = 7").unwrap();
test.write_file("lib/submod.py", "a = 10").unwrap();
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> lib/submod.py:1:1
|
1 | a = 10
| ^^^^^^
|
info: Source
--> main.py:2:10
|
2 | from lib.submod import a
| ^^^^^^
|
");
}
#[test]
fn goto_type_of_from_import_rel1() {
let mut test = CursorTest::builder()
.source(
"lib/sub/__init__.py",
r#"
from .bot.bot<CURSOR>mod import *
sub = 2
"#,
)
.build();
test.write_file("lib/__init__.py", "lib = 1").unwrap();
// test.write_file("lib/sub/__init__.py", "sub = 2").unwrap();
test.write_file("lib/sub/bot/__init__.py", "bot = 3")
.unwrap();
test.write_file("lib/sub/submod.py", "submod = 21").unwrap();
test.write_file("lib/sub/bot/botmod.py", "botmod = 31")
.unwrap();
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> lib/sub/bot/botmod.py:1:1
|
1 | botmod = 31
| ^^^^^^^^^^^
|
info: Source
--> lib/sub/__init__.py:2:11
|
2 | from .bot.botmod import *
| ^^^^^^
3 | sub = 2
|
");
}
#[test]
fn goto_type_of_from_import_rel2() {
let mut test = CursorTest::builder()
.source(
"lib/sub/__init__.py",
r#"
from .bo<CURSOR>t.botmod import *
sub = 2
"#,
)
.build();
test.write_file("lib/__init__.py", "lib = 1").unwrap();
// test.write_file("lib/sub/__init__.py", "sub = 2").unwrap();
test.write_file("lib/sub/bot/__init__.py", "bot = 3")
.unwrap();
test.write_file("lib/sub/submod.py", "submod = 21").unwrap();
test.write_file("lib/sub/bot/botmod.py", "botmod = 31")
.unwrap();
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> lib/sub/bot/__init__.py:1:1
|
1 | bot = 3
| ^^^^^^^
|
info: Source
--> lib/sub/__init__.py:2:7
|
2 | from .bot.botmod import *
| ^^^
3 | sub = 2
|
");
}
#[test]
fn goto_type_of_from_import_rel3() {
let mut test = CursorTest::builder()
.source(
"lib/sub/__init__.py",
r#"
from .<CURSOR>bot.botmod import *
sub = 2
"#,
)
.build();
test.write_file("lib/__init__.py", "lib = 1").unwrap();
// test.write_file("lib/sub/__init__.py", "sub = 2").unwrap();
test.write_file("lib/sub/bot/__init__.py", "bot = 3")
.unwrap();
test.write_file("lib/sub/submod.py", "submod = 21").unwrap();
test.write_file("lib/sub/bot/botmod.py", "botmod = 31")
.unwrap();
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> lib/sub/bot/__init__.py:1:1
|
1 | bot = 3
| ^^^^^^^
|
info: Source
--> lib/sub/__init__.py:2:7
|
2 | from .bot.botmod import *
| ^^^
3 | sub = 2
|
");
}
#[test]
fn goto_type_of_from_import_rel4() {
let mut test = CursorTest::builder()
.source(
"lib/sub/__init__.py",
r#"
from .<CURSOR> import submod
sub = 2
"#,
)
.build();
test.write_file("lib/__init__.py", "lib = 1").unwrap();
// test.write_file("lib/sub/__init__.py", "sub = 2").unwrap();
test.write_file("lib/sub/bot/__init__.py", "bot = 3")
.unwrap();
test.write_file("lib/sub/submod.py", "submod = 21").unwrap();
test.write_file("lib/sub/bot/botmod.py", "botmod = 31")
.unwrap();
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
}
#[test]
fn goto_type_of_expression_with_module() {
let mut test = cursor_test(

View File

@@ -22,8 +22,7 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
let model = SemanticModel::new(db, file);
let docs = goto_target
.get_definition_targets(
file,
db,
&model,
ty_python_semantic::ImportAliasResolution::ResolveAliases,
)
.and_then(|definitions| definitions.docstring(db))
@@ -245,14 +244,11 @@ mod tests {
) -> Unknown
```
---
```text
This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
```
This is such a great func!!
Args:
&nbsp;&nbsp;&nbsp;&nbsp;a: first for a reason
&nbsp;&nbsp;&nbsp;&nbsp;b: coming for `a`'s title
---------------------------------------------
info[hover]: Hovered content is
--> main.py:11:1
@@ -303,14 +299,11 @@ mod tests {
) -> Unknown
```
---
```text
This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
```
This is such a great func!!
Args:
&nbsp;&nbsp;&nbsp;&nbsp;a: first for a reason
&nbsp;&nbsp;&nbsp;&nbsp;b: coming for `a`'s title
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:5
@@ -369,14 +362,11 @@ mod tests {
<class 'MyClass'>
```
---
```text
This is such a great class!!
Don't you know?
This is such a great class!!
&nbsp;&nbsp;&nbsp;&nbsp;Don't you know?
Everyone loves my class!!
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:24:1
@@ -434,14 +424,11 @@ mod tests {
<class 'MyClass'>
```
---
```text
This is such a great class!!
Don't you know?
This is such a great class!!
&nbsp;&nbsp;&nbsp;&nbsp;Don't you know?
Everyone loves my class!!
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:7
@@ -497,10 +484,7 @@ mod tests {
<class 'MyClass'>
```
---
```text
initializes MyClass (perfectly)
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:24:5
@@ -556,10 +540,7 @@ mod tests {
<class 'MyClass'>
```
---
```text
initializes MyClass (perfectly)
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:11
@@ -618,14 +599,11 @@ mod tests {
<class 'MyClass'>
```
---
```text
This is such a great class!!
Don't you know?
This is such a great class!!
&nbsp;&nbsp;&nbsp;&nbsp;Don't you know?
Everyone loves my class!!
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:23:5
@@ -692,14 +670,11 @@ mod tests {
) -> Unknown
```
---
```text
This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
```
This is such a great func!!
Args:
&nbsp;&nbsp;&nbsp;&nbsp;a: first for a reason
&nbsp;&nbsp;&nbsp;&nbsp;b: coming for `a`'s title
---------------------------------------------
info[hover]: Hovered content is
--> main.py:25:3
@@ -973,10 +948,7 @@ def ab(a: str): ...
(a: int) -> Unknown
```
---
```text
the int overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
@@ -1036,10 +1008,7 @@ def ab(a: str):
(a: str) -> Unknown
```
---
```text
the int overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
@@ -1105,10 +1074,7 @@ def ab(a: int):
) -> Unknown
```
---
```text
the two arg overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
@@ -1168,10 +1134,7 @@ def ab(a: int):
(a: int) -> Unknown
```
---
```text
the two arg overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
@@ -1243,10 +1206,7 @@ def ab(a: int, *, c: int):
) -> Unknown
```
---
```text
keywordless overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
@@ -1318,10 +1278,7 @@ def ab(a: int, *, c: int):
) -> Unknown
```
---
```text
keywordless overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
@@ -1386,10 +1343,7 @@ def ab(a: int, *, c: int):
) -> Unknown
```
---
```text
The first overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:19:1
@@ -1441,10 +1395,7 @@ def ab(a: int, *, c: int):
(a: str) -> Unknown
```
---
```text
The first overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:19:1
@@ -1494,12 +1445,9 @@ def ab(a: int, *, c: int):
<module 'lib'>
```
---
```text
The cool lib_py module!
The cool lib/_py module!
Wow this module rocks.
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:1
@@ -1539,17 +1487,20 @@ def ab(a: int, *, c: int):
.unwrap();
assert_snapshot!(test.hover(), @r"
<module 'lib'>
---------------------------------------------
The cool lib_py module!
Wow this module rocks.
---------------------------------------------
```text
The cool lib_py module!
Wow this module rocks.
```python
<module 'lib'>
```
---
The cool lib/_py module!
Wow this module rocks.
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:8
@@ -2499,10 +2450,7 @@ def ab(a: int, *, c: int):
bound method int.__add__(value: int, /) -> int
```
---
```text
Return self+value.
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:12
@@ -2618,10 +2566,7 @@ def ab(a: int, *, c: int):
int | float
```
---
```text
Convert a string or number to a floating-point number, if possible.
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:4

View File

@@ -6,8 +6,8 @@ use ruff_db::parsed::parsed_module;
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword, Expr, ExprUnaryOp, Stmt, UnaryOp};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::types::Type;
use ty_python_semantic::types::ide_support::inlay_hint_call_argument_details;
use ty_python_semantic::types::{Type, TypeDetail};
use ty_python_semantic::{HasType, SemanticModel};
#[derive(Debug, Clone)]
@@ -15,10 +15,12 @@ pub struct InlayHint {
pub position: TextSize,
pub kind: InlayHintKind,
pub label: InlayHintLabel,
pub text_edits: Vec<InlayHintTextEdit>,
}
impl InlayHint {
fn variable_type(position: TextSize, ty: Type, db: &dyn Db) -> Self {
fn variable_type(expr: &Expr, ty: Type, db: &dyn Db, allow_edits: bool) -> Self {
let position = expr.range().end();
// Render the type to a string, and get subspans for all the types that make it up
let details = ty.display(db).to_string_parts();
@@ -34,7 +36,7 @@ impl InlayHint {
let mut label_parts = vec![": ".into()];
for (target, detail) in details.targets.iter().zip(&details.details) {
match detail {
ty_python_semantic::types::TypeDetail::Type(ty) => {
TypeDetail::Type(ty) => {
let start = target.start().to_usize();
let end = target.end().to_usize();
// If we skipped over some bytes, push them with no target
@@ -50,9 +52,9 @@ impl InlayHint {
offset = end;
}
}
ty_python_semantic::types::TypeDetail::SignatureStart
| ty_python_semantic::types::TypeDetail::SignatureEnd
| ty_python_semantic::types::TypeDetail::Parameter(_) => {
TypeDetail::SignatureStart
| TypeDetail::SignatureEnd
| TypeDetail::Parameter(_) => {
// Don't care about these
}
}
@@ -62,10 +64,20 @@ impl InlayHint {
label_parts.push(details.label[offset..details.label.len()].into());
}
let text_edits = if details.is_valid_syntax && allow_edits {
vec![InlayHintTextEdit {
range: TextRange::new(position, position),
new_text: format!(": {}", details.label),
}]
} else {
vec![]
};
Self {
position,
kind: InlayHintKind::Type,
label: InlayHintLabel { parts: label_parts },
text_edits,
}
}
@@ -83,6 +95,7 @@ impl InlayHint {
position,
kind: InlayHintKind::CallArgumentName,
label: InlayHintLabel { parts: label_parts },
text_edits: vec![],
}
}
@@ -175,6 +188,12 @@ impl From<&str> for InlayHintLabelPart {
}
}
#[derive(Debug, Clone)]
pub struct InlayHintTextEdit {
pub range: TextRange,
pub new_text: String,
}
pub fn inlay_hints(
db: &dyn Db,
file: File,
@@ -234,6 +253,7 @@ struct InlayHintVisitor<'a, 'db> {
in_assignment: bool,
range: TextRange,
settings: &'a InlayHintSettings,
in_no_edits_allowed: bool,
}
impl<'a, 'db> InlayHintVisitor<'a, 'db> {
@@ -245,15 +265,16 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
in_assignment: false,
range,
settings,
in_no_edits_allowed: false,
}
}
fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) {
fn add_type_hint(&mut self, expr: &Expr, ty: Type<'db>, allow_edits: bool) {
if !self.settings.variable_types {
return;
}
let inlay_hint = InlayHint::variable_type(position, ty, self.db);
let inlay_hint = InlayHint::variable_type(expr, ty, self.db, allow_edits);
self.hints.push(inlay_hint);
}
@@ -297,9 +318,13 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
match stmt {
Stmt::Assign(assign) => {
self.in_assignment = !type_hint_is_excessive_for_expr(&assign.value);
if !annotations_are_valid_syntax(assign) {
self.in_no_edits_allowed = true;
}
for target in &assign.targets {
self.visit_expr(target);
}
self.in_no_edits_allowed = false;
self.in_assignment = false;
self.visit_expr(&assign.value);
@@ -325,7 +350,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
if self.in_assignment {
if name.ctx.is_store() {
let ty = expr.inferred_type(&self.model);
self.add_type_hint(expr.range().end(), ty);
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
}
}
source_order::walk_expr(self, expr);
@@ -334,7 +359,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
if self.in_assignment {
if attribute.ctx.is_store() {
let ty = expr.inferred_type(&self.model);
self.add_type_hint(expr.range().end(), ty);
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
}
}
source_order::walk_expr(self, expr);
@@ -420,13 +445,30 @@ fn type_hint_is_excessive_for_expr(expr: &Expr) -> bool {
}
}
fn annotations_are_valid_syntax(stmt_assign: &ruff_python_ast::StmtAssign) -> bool {
if stmt_assign.targets.len() > 1 {
return false;
}
if stmt_assign
.targets
.iter()
.any(|target| matches!(target, Expr::Tuple(_)))
{
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NavigationTarget;
use crate::tests::IntoDiagnostic;
use insta::assert_snapshot;
use insta::{assert_snapshot, internals::SettingsBindDropGuard};
use itertools::Itertools;
use ruff_db::{
diagnostic::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig,
@@ -473,13 +515,26 @@ mod tests {
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
InlayHintTest { db, file, range }
let mut insta_settings = insta::Settings::clone_current();
insta_settings.add_filter(r#"\\(\w\w|\.|")"#, "/$1");
// Filter out TODO types because they are different between debug and release builds.
insta_settings.add_filter(r"@Todo\(.+\)", "@Todo");
let insta_settings_guard = insta_settings.bind_to_scope();
InlayHintTest {
db,
file,
range,
_insta_settings_guard: insta_settings_guard,
}
}
pub(super) struct InlayHintTest {
pub(super) db: ty_project::TestDb,
pub(super) file: File,
pub(super) range: TextRange,
_insta_settings_guard: SettingsBindDropGuard,
}
impl InlayHintTest {
@@ -504,12 +559,15 @@ mod tests {
fn inlay_hints_with_settings(&mut self, settings: &InlayHintSettings) -> String {
let hints = inlay_hints(&self.db, self.file, self.range, settings);
let mut buf = source_text(&self.db, self.file).as_str().to_string();
let mut inlay_hint_buf = source_text(&self.db, self.file).as_str().to_string();
let mut text_edit_buf = inlay_hint_buf.clone();
let mut tbd_diagnostics = Vec::new();
let mut offset = 0;
let mut edit_offset = 0;
for hint in hints {
let end_position = hint.position.to_usize() + offset;
let mut hint_str = "[".to_string();
@@ -525,36 +583,65 @@ mod tests {
hint_str.push_str(part.text());
}
for edit in hint.text_edits {
let start = edit.range.start().to_usize() + edit_offset;
let end = edit.range.end().to_usize() + edit_offset;
text_edit_buf.replace_range(start..end, &edit.new_text);
if start == end {
edit_offset += edit.new_text.len();
} else {
edit_offset += edit.new_text.len() - edit.range.len().to_usize();
}
}
hint_str.push(']');
offset += hint_str.len();
buf.insert_str(end_position, &hint_str);
inlay_hint_buf.insert_str(end_position, &hint_str);
}
self.db.write_file("main2.py", &buf).unwrap();
self.db.write_file("main2.py", &inlay_hint_buf).unwrap();
let inlayed_file =
system_path_to_file(&self.db, "main2.py").expect("newly written file to existing");
let diagnostics = tbd_diagnostics.into_iter().map(|(label_range, target)| {
let location_diagnostics = tbd_diagnostics.into_iter().map(|(label_range, target)| {
InlayHintLocationDiagnostic::new(FileRange::new(inlayed_file, label_range), &target)
});
let mut rendered_diagnostics = self.render_diagnostics(diagnostics);
let mut rendered_diagnostics = location_diagnostics
.map(|diagnostic| self.render_diagnostic(diagnostic))
.join("");
if !rendered_diagnostics.is_empty() {
rendered_diagnostics = format!(
"{}{}",
crate::MarkupKind::PlainText.horizontal_line(),
rendered_diagnostics
.strip_suffix("\n")
.unwrap_or(&rendered_diagnostics)
);
}
format!("{buf}{rendered_diagnostics}",)
let rendered_edit_diagnostic = if edit_offset != 0 {
let edit_diagnostic = InlayHintEditDiagnostic::new(text_edit_buf);
let text_edit_buf = self.render_diagnostic(edit_diagnostic);
format!(
"{}{}",
crate::MarkupKind::PlainText.horizontal_line(),
text_edit_buf
)
} else {
String::new()
};
format!("{inlay_hint_buf}{rendered_diagnostics}{rendered_edit_diagnostic}",)
}
fn render_diagnostics<I, D>(&self, diagnostics: I) -> String
fn render_diagnostic<D>(&self, diagnostic: D) -> String
where
I: IntoIterator<Item = D>,
D: IntoDiagnostic,
{
use std::fmt::Write;
@@ -565,15 +652,10 @@ mod tests {
.color(false)
.format(DiagnosticFormat::Full);
for diagnostic in diagnostics {
let diag = diagnostic.into_diagnostic();
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
}
let diag = diagnostic.into_diagnostic();
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
// Windows path normalization for typeshed references
// "hey why is \x08 getting clobbered to /x08?"
// no it's not I don't know what you're talking about
buf.replace('\\', "/")
buf
}
}
@@ -718,6 +800,20 @@ mod tests {
10 | bb[: Literal[b"foo"]] = aa
| ^^^^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def i(x: int, /) -> int:
return x
x = 1
y: Literal[1] = x
z: int = i(1)
w: int = z
aa = b'foo'
bb: Literal[b"foo"] = aa
"#);
}
@@ -1311,6 +1407,20 @@ mod tests {
10 | w[: tuple[int, str]] = z
| ^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def i(x: int, /) -> int:
return x
def s(x: str, /) -> str:
return x
x = (1, 'abc')
y: tuple[Literal[1], Literal["abc"]] = x
z: tuple[int, str] = (i(1), s('abc'))
w: tuple[int, str] = z
"#);
}
@@ -1644,6 +1754,18 @@ mod tests {
8 | w[: int] = z
| ^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def i(x: int, /) -> int:
return x
x: int = 1
y: Literal[1] = x
z: int = i(1)
w: int = z
"#);
}
@@ -1681,6 +1803,15 @@ mod tests {
| ^^^
5 | z = x
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def i(x: int, /) -> int:
return x
x: int = i(1)
z = x
"#);
}
@@ -1800,6 +1931,18 @@ mod tests {
8 | a.y[: int] = int(3)
| ^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class A:
def __init__(self, y):
self.x: int = int(1)
self.y: Unknown = y
a: A = A(2)
a.y: int = int(3)
"#);
}
@@ -1830,7 +1973,7 @@ mod tests {
f = 'there'
g = f"{e} {f}"
h = t"wow %d"
i = b'\x00'
i = b'/x00'
j = +1
k = -1.0
"#);
@@ -1863,7 +2006,7 @@ mod tests {
f = ('the', 're')
g = (f"{ft}", f"{ft}")
h = (t"wow %d", t"wow %d")
i = (b'\x01', b'\x02')
i = (b'/x01', b'/x02')
j = (+1, +2.0)
k = (-1, -2.0)
"#);
@@ -1896,7 +2039,7 @@ mod tests {
f1, f2 = ('the', 're')
g1, g2 = (f"{ft}", f"{ft}")
h1, h2 = (t"wow %d", t"wow %d")
i1, i2 = (b'\x01', b'\x02')
i1, i2 = (b'/x01', b'/x02')
j1, j2 = (+1, +2.0)
k1, k2 = (-1, -2.0)
"#);
@@ -1929,7 +2072,7 @@ mod tests {
f1, f2 = 'the', 're'
g1, g2 = f"{ft}", f"{ft}"
h1, h2 = t"wow %d", t"wow %d"
i1, i2 = b'\x01', b'\x02'
i1, i2 = b'/x01', b'/x02'
j1, j2 = +1, +2.0
k1, k2 = -1, -2.0
"#);
@@ -1962,7 +2105,7 @@ mod tests {
f[: list[Unknown | str]] = ['the', 're']
g[: list[Unknown | str]] = [f"{ft}", f"{ft}"]
h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"]
i[: list[Unknown | bytes]] = [b'\x01', b'\x02']
i[: list[Unknown | bytes]] = [b'/x01', b'/x02']
j[: list[Unknown | int | float]] = [+1, +2.0]
k[: list[Unknown | int | float]] = [-1, -2.0]
@@ -2630,6 +2773,22 @@ mod tests {
12 | k[: list[Unknown | int | float]] = [-1, -2.0]
| ^^^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
a: list[Unknown | int] = [1, 2]
b: list[Unknown | float] = [1.0, 2.0]
c: list[Unknown | bool] = [True, False]
d: list[Unknown | None] = [None, None]
e: list[Unknown | str] = ["hel", "lo"]
f: list[Unknown | str] = ['the', 're']
g: list[Unknown | str] = [f"{ft}", f"{ft}"]
h: list[Unknown | Template] = [t"wow %d", t"wow %d"]
i: list[Unknown | bytes] = [b'/x01', b'/x02']
j: list[Unknown | int | float] = [+1, +2.0]
k: list[Unknown | int | float] = [-1, -2.0]
"#);
}
@@ -2801,6 +2960,19 @@ mod tests {
9 | c[: MyClass], d[: MyClass] = (MyClass(), MyClass())
| ^^^^^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class MyClass:
def __init__(self):
self.x: int = 1
x: MyClass = MyClass()
y: tuple[MyClass, MyClass] = (MyClass(), MyClass())
a, b = MyClass(), MyClass()
c, d = (MyClass(), MyClass())
"#);
}
@@ -3671,6 +3843,20 @@ mod tests {
10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "…
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class MyClass[T, U]:
def __init__(self, x: list[T], y: tuple[U, U]):
self.x = x
self.y = y
x: MyClass[Unknown | int, str] = MyClass([42], ("a", "b"))
y: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]] = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b")))
a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))
c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b")))
"#);
}
@@ -3826,6 +4012,20 @@ mod tests {
10 | foo([x=]val.y)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
self.x: int = 1
self.y: int = 2
val: MyClass = MyClass()
foo(val.x)
foo(val.y)
");
}
@@ -3891,6 +4091,20 @@ mod tests {
10 | foo([x=]x.y)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
self.x: int = 1
self.y: int = 2
x: MyClass = MyClass()
foo(x.x)
foo(x.y)
");
}
@@ -3959,6 +4173,22 @@ mod tests {
12 | foo([x=]val.y())
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
def x() -> int:
return 1
def y() -> int:
return 2
val: MyClass = MyClass()
foo(val.x())
foo(val.y())
");
}
@@ -4033,6 +4263,24 @@ mod tests {
14 | foo([x=]val.y()[1])
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
from typing import List
def foo(x: int): pass
class MyClass:
def __init__(self):
def x() -> List[int]:
return 1
def y() -> List[int]:
return 2
val: MyClass = MyClass()
foo(val.x()[0])
foo(val.y()[1])
");
}
@@ -4183,6 +4431,17 @@ mod tests {
7 | foo([x=]y[0])
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
x: list[Unknown | int] = [1]
y: list[Unknown | int] = [2]
foo(x[0])
foo(y[0])
"#);
}
@@ -4368,6 +4627,15 @@ mod tests {
5 | f[: Foo] = Foo([x=]1)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class Foo:
def __init__(self, x: int): pass
Foo(1)
f: Foo = Foo(1)
");
}
@@ -4440,6 +4708,15 @@ mod tests {
5 | f[: Foo] = Foo([x=]1)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class Foo:
def __new__(cls, x: int): pass
Foo(1)
f: Foo = Foo(1)
");
}
@@ -5187,6 +5464,15 @@ mod tests {
| ^^^^^^^^^^^^^
5 | my_func(x="hello")
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
from typing import LiteralString
def my_func(x: LiteralString):
y: LiteralString = x
my_func(x="hello")
"#);
}
@@ -5329,6 +5615,23 @@ mod tests {
13 | y[: Literal[1, 2, 3, "hello"] | None] = x
| ^^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def branch(cond: int):
if cond < 10:
x = 1
elif cond < 20:
x = 2
elif cond < 30:
x = 3
elif cond < 40:
x = "hello"
else:
x = None
y: Literal[1, 2, 3, "hello"] | None = x
"#);
}
@@ -5444,6 +5747,13 @@ mod tests {
3 | y[: type[list[str]]] = type(x)
| ^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def f(x: list[str]):
y: type[list[str]] = type(x)
"#);
}
@@ -5483,6 +5793,16 @@ mod tests {
6 | ab[: property] = F.whatever
| ^^^^^^^^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class F:
@property
def whatever(self): ...
ab: property = F.whatever
");
}
@@ -5800,6 +6120,180 @@ mod tests {
");
}
#[test]
fn test_function_signature_inlay_hint() {
let mut test = inlay_hint_test(
"
def foo(x: int, *y: bool, z: str | int | list[str]): ...
a = foo",
);
assert_snapshot!(test.inlay_hints(), @r#"
def foo(x: int, *y: bool, z: str | int | list[str]): ...
a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main2.py:4:16
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2591:7
|
2590 | @final
2591 | class bool(int):
| ^^^^
2592 | """Returns True when the argument is true, False otherwise.
2593 | The builtins True and False are the only two instances of the class bool.
|
info: Source
--> main2.py:4:25
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:915:7
|
914 | @disjoint_base
915 | class str(Sequence[str]):
| ^^^
916 | """str(object='') -> str
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main2.py:4:37
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main2.py:4:43
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2802:7
|
2801 | @disjoint_base
2802 | class list(MutableSequence[_T]):
| ^^^^
2803 | """Built-in mutable sequence.
|
info: Source
--> main2.py:4:49
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:915:7
|
914 | @disjoint_base
915 | class str(Sequence[str]):
| ^^^
916 | """str(object='') -> str
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main2.py:4:54
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main2.py:4:63
|
2 | def foo(x: int, *y: bool, z: str | int | list[str]): ...
3 |
4 | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^^^^^
|
"#);
}
#[test]
fn test_module_inlay_hint() {
let mut test = inlay_hint_test(
"
import foo
a = foo",
);
test.with_extra_file("foo.py", "'''Foo module'''");
assert_snapshot!(test.inlay_hints(), @r"
import foo
a[: <module 'foo'>] = foo
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> foo.py:1:1
|
1 | '''Foo module'''
| ^^^^^^^^^^^^^^^^
|
info: Source
--> main2.py:4:5
|
2 | import foo
3 |
4 | a[: <module 'foo'>] = foo
| ^^^^^^^^^^^^^^
|
");
}
struct InlayHintLocationDiagnostic {
source: FileRange,
target: FileRange,
@@ -5837,4 +6331,31 @@ mod tests {
main
}
}
struct InlayHintEditDiagnostic {
file_content: String,
}
impl InlayHintEditDiagnostic {
fn new(file_content: String) -> Self {
Self { file_content }
}
}
impl IntoDiagnostic for InlayHintEditDiagnostic {
fn into_diagnostic(self) -> Diagnostic {
let mut main = Diagnostic::new(
DiagnosticId::Lint(LintName::of("inlay-hint-edit")),
Severity::Info,
"File after edits".to_string(),
);
main.sub(SubDiagnostic::new(
SubDiagnosticSeverity::Info,
format!("{}\n{}", "Source", self.file_content),
));
main
}
}
}

View File

@@ -33,7 +33,9 @@ pub use document_symbols::document_symbols;
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
pub use goto_references::goto_references;
pub use hover::hover;
pub use inlay_hints::{InlayHintKind, InlayHintLabel, InlayHintSettings, inlay_hints};
pub use inlay_hints::{
InlayHintKind, InlayHintLabel, InlayHintSettings, InlayHintTextEdit, inlay_hints,
};
pub use markup::MarkupKind;
pub use references::ReferencesMode;
pub use rename::{can_rename, rename};

View File

@@ -20,7 +20,7 @@ use ruff_python_ast::{
};
use ruff_python_parser::Tokens;
use ruff_text_size::{Ranged, TextRange};
use ty_python_semantic::ImportAliasResolution;
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
/// Mode for references search behavior
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -48,8 +48,9 @@ pub(crate) fn references(
// Get the definitions for the symbol at the cursor position
// When finding references, do not resolve any local aliases.
let model = SemanticModel::new(db, file);
let target_definitions_nav = goto_target
.get_definition_targets(file, db, ImportAliasResolution::PreserveAliases)?
.get_definition_targets(&model, ImportAliasResolution::PreserveAliases)?
.definition_targets(db)?;
let target_definitions: Vec<NavigationTarget> = target_definitions_nav.into_iter().collect();
@@ -289,8 +290,9 @@ impl LocalReferencesFinder<'_> {
GotoTarget::from_covering_node(covering_node, offset, self.tokens)
{
// Get the definitions for this goto target
let model = SemanticModel::new(self.db, self.file);
if let Some(current_definitions_nav) = goto_target
.get_definition_targets(self.file, self.db, ImportAliasResolution::PreserveAliases)
.get_definition_targets(&model, ImportAliasResolution::PreserveAliases)
.and_then(|definitions| definitions.declaration_targets(self.db))
{
let current_definitions: Vec<NavigationTarget> =

View File

@@ -3,12 +3,13 @@ use crate::references::{ReferencesMode, references};
use crate::{Db, ReferenceTarget};
use ruff_db::files::File;
use ruff_text_size::{Ranged, TextSize};
use ty_python_semantic::ImportAliasResolution;
use ty_python_semantic::{ImportAliasResolution, SemanticModel};
/// Returns the range of the symbol if it can be renamed, None if not.
pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option<ruff_text_size::TextRange> {
let parsed = ruff_db::parsed::parsed_module(db, file);
let module = parsed.load(db);
let model = SemanticModel::new(db, file);
// Get the definitions for the symbol at the offset
let goto_target = find_goto_target(&module, offset)?;
@@ -24,7 +25,7 @@ pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option<ruff_text
let current_file_in_project = is_file_in_project(db, file);
if let Some(definition_targets) = goto_target
.get_definition_targets(file, db, ImportAliasResolution::PreserveAliases)
.get_definition_targets(&model, ImportAliasResolution::PreserveAliases)
.and_then(|definitions| definitions.declaration_targets(db))
{
for target in &definition_targets {

View File

@@ -715,3 +715,17 @@ def _(a: int, b: str, c: int | str):
x9: int | str | None = f(lst(c))
reveal_type(x9) # revealed: int | str | None
```
## Forward annotation with unclosed string literal
Regression test for [#1611](https://github.com/astral-sh/ty/issues/1611).
<!-- blacken-docs:off -->
```py
# error: [invalid-syntax]
# error: [invalid-syntax-in-forward-annotation]
a:'
```
<!-- blacken-docs:on -->

View File

@@ -79,9 +79,8 @@ async def main():
task("B"),
)
# TODO: these should be `int`
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
```
## Under the hood

View File

@@ -1904,6 +1904,7 @@ we only consider the attribute assignment to be valid if the assigned attribute
from typing import Literal
class Date:
# error: [invalid-method-override]
def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None:
pass
@@ -2683,6 +2684,39 @@ reveal_type(datetime.UTC) # revealed: Unknown
reveal_type(datetime.fakenotreal) # revealed: Unknown
```
## Unimported submodule incorrectly accessed as attribute
We give special diagnostics for this common case too:
<!-- snapshot-diagnostics -->
`foo/__init__.py`:
```py
```
`foo/bar.py`:
```py
```
`baz/bar.py`:
```py
```
`main.py`:
```py
import foo
import baz
# error: [possibly-missing-attribute]
reveal_type(foo.bar) # revealed: Unknown
# error: [possibly-missing-attribute]
reveal_type(baz.bar) # revealed: Unknown
```
## References
Some of the tests in the *Class and instance variables* section draw inspiration from

View File

@@ -237,4 +237,26 @@ class Matrix:
Matrix() < Matrix()
```
## `self`-binding behaviour of function-like `Callable`s
Binding the `self` parameter of a function-like `Callable` creates a new `Callable` that is also
function-like:
`main.py`:
```py
from typing import Callable
def my_lossy_decorator(fn: Callable[..., int]) -> Callable[..., int]:
return fn
class MyClass:
@my_lossy_decorator
def method(self) -> int:
return 42
reveal_type(MyClass().method) # revealed: (...) -> int
reveal_type(MyClass().method.__name__) # revealed: str
```
[`tensorbase`]: https://github.com/pytorch/pytorch/blob/f3913ea641d871f04fa2b6588a77f63efeeb9f10/torch/_tensor.py#L1084-L1092

View File

@@ -501,8 +501,8 @@ class A[T]:
return a
class B[T](A[T]):
def f(self, b: T) -> T:
return super().f(b)
def f(self, a: T) -> T:
return super().f(a)
```
## Invalid Usages

View File

@@ -24,10 +24,10 @@ class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: A) -> EqReturnType:
def __eq__(self, other: A) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, other: A) -> NeReturnType:
def __ne__(self, other: A) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, other: A) -> LtReturnType:
@@ -66,10 +66,10 @@ class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: B) -> EqReturnType:
def __eq__(self, other: B) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, other: B) -> NeReturnType:
def __ne__(self, other: B) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, other: B) -> LtReturnType:
@@ -111,10 +111,10 @@ class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: B) -> EqReturnType:
def __eq__(self, other: B) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, other: B) -> NeReturnType:
def __ne__(self, other: B) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, other: B) -> LtReturnType:
@@ -132,12 +132,10 @@ class A:
class Unrelated: ...
class B:
# To override builtins.object.__eq__ and builtins.object.__ne__
# TODO these should emit an invalid override diagnostic
def __eq__(self, other: Unrelated) -> B:
def __eq__(self, other: Unrelated) -> B: # error: [invalid-method-override]
return B()
def __ne__(self, other: Unrelated) -> B:
def __ne__(self, other: Unrelated) -> B: # error: [invalid-method-override]
return B()
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
@@ -180,10 +178,10 @@ class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: A) -> A:
def __eq__(self, other: A) -> A: # error: [invalid-method-override]
return A()
def __ne__(self, other: A) -> A:
def __ne__(self, other: A) -> A: # error: [invalid-method-override]
return A()
def __lt__(self, other: A) -> A:
@@ -199,22 +197,22 @@ class A:
return A()
class B(A):
def __eq__(self, other: A) -> EqReturnType:
def __eq__(self, other: A) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, other: A) -> NeReturnType:
def __ne__(self, other: A) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, other: A) -> LtReturnType:
def __lt__(self, other: A) -> LtReturnType: # error: [invalid-method-override]
return LtReturnType()
def __le__(self, other: A) -> LeReturnType:
def __le__(self, other: A) -> LeReturnType: # error: [invalid-method-override]
return LeReturnType()
def __gt__(self, other: A) -> GtReturnType:
def __gt__(self, other: A) -> GtReturnType: # error: [invalid-method-override]
return GtReturnType()
def __ge__(self, other: A) -> GeReturnType:
def __ge__(self, other: A) -> GeReturnType: # error: [invalid-method-override]
return GeReturnType()
reveal_type(A() == B()) # revealed: EqReturnType
@@ -243,10 +241,10 @@ class A:
return A()
class B(A):
def __lt__(self, other: int) -> B:
def __lt__(self, other: int) -> B: # error: [invalid-method-override]
return B()
def __gt__(self, other: int) -> B:
def __gt__(self, other: int) -> B: # error: [invalid-method-override]
return B()
reveal_type(A() < B()) # revealed: A
@@ -291,11 +289,10 @@ Please refer to the [docs](https://docs.python.org/3/reference/datamodel.html#ob
from __future__ import annotations
class A:
# TODO both these overrides should emit invalid-override diagnostic
def __eq__(self, other: int) -> A:
def __eq__(self, other: int) -> A: # error: [invalid-method-override]
return A()
def __ne__(self, other: int) -> A:
def __ne__(self, other: int) -> A: # error: [invalid-method-override]
return A()
reveal_type(A() == A()) # revealed: bool

View File

@@ -155,10 +155,10 @@ class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, o: object) -> EqReturnType:
def __eq__(self, o: object) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, o: object) -> NeReturnType:
def __ne__(self, o: object) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, o: A) -> LtReturnType:
@@ -386,6 +386,7 @@ class NotBoolable:
__bool__: None = None
class A:
# error: [invalid-method-override]
def __eq__(self, other) -> NotBoolable:
return NotBoolable()

View File

@@ -171,7 +171,7 @@ class Config:
import generic_a
import generic_b
# TODO should be error: [invalid-assignment] "Object of type `<class 'generic_b.Container[int]'>` is not assignable to `type[generic_a.Container[int]]`"
# error: [invalid-assignment] "Object of type `<class 'generic_b.Container[int]'>` is not assignable to `type[generic_a.Container[int]]`"
container: type[generic_a.Container[int]] = generic_b.Container[int]
```

View File

@@ -103,8 +103,7 @@ class UnknownLengthSubclassWithDunderLenOverridden(tuple[int, ...]):
reveal_type(len(UnknownLengthSubclassWithDunderLenOverridden())) # revealed: Literal[42]
class FixedLengthSubclassWithDunderLenOverridden(tuple[int]):
# TODO: we should complain about this as a Liskov violation (incompatible override)
def __len__(self) -> Literal[42]:
def __len__(self) -> Literal[42]: # error: [invalid-method-override]
return 42
reveal_type(len(FixedLengthSubclassWithDunderLenOverridden((1,)))) # revealed: Literal[42]

View File

@@ -137,84 +137,26 @@ class Sub(Base): ...
class Unrelated: ...
def unbounded_unconstrained[T, U](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, T))
static_assert(is_assignable_to(T, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, object))
static_assert(is_assignable_to(T, object))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Super))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Any))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(U, U))
static_assert(is_assignable_to(U, U))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(U, object))
static_assert(is_assignable_to(U, object))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(U, Super))
static_assert(not is_assignable_to(U, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, U))
static_assert(not is_assignable_to(T, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(U, T))
static_assert(not is_assignable_to(U, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, T))
static_assert(is_subtype_of(T, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, object))
static_assert(is_subtype_of(T, object))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(U, U))
static_assert(is_subtype_of(U, U))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(U, object))
static_assert(is_subtype_of(U, object))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U, Super))
static_assert(not is_subtype_of(U, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, U))
static_assert(not is_subtype_of(T, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U, T))
static_assert(not is_subtype_of(U, T))
```
@@ -229,137 +171,47 @@ from typing import Any
from typing_extensions import final
def bounded[T: Super](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Any))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super))
static_assert(is_assignable_to(T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super, T))
static_assert(not is_assignable_to(Super, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Sub, T))
static_assert(not is_assignable_to(Sub, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, Super))
static_assert(is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super, T))
static_assert(not is_subtype_of(Super, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Sub, T))
static_assert(not is_subtype_of(Sub, T))
def bounded_by_gradual[T: Any](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Any))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super))
static_assert(is_assignable_to(T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super, T))
static_assert(not is_assignable_to(Super, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Sub))
static_assert(is_assignable_to(T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Sub, T))
static_assert(not is_assignable_to(Sub, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super, T))
static_assert(not is_subtype_of(Super, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Sub, T))
static_assert(not is_subtype_of(Sub, T))
@final
class FinalClass: ...
def bounded_final[T: FinalClass](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Any))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, FinalClass))
static_assert(is_assignable_to(T, FinalClass))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(FinalClass, T))
static_assert(not is_assignable_to(FinalClass, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, FinalClass))
static_assert(is_subtype_of(T, FinalClass))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(FinalClass, T))
static_assert(not is_subtype_of(FinalClass, T))
```
@@ -370,37 +222,17 @@ typevars to `Never` in addition to that final class.
```py
def two_bounded[T: Super, U: Super](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, U))
static_assert(not is_assignable_to(T, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(U, T))
static_assert(not is_assignable_to(U, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, U))
static_assert(not is_subtype_of(T, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U, T))
static_assert(not is_subtype_of(U, T))
def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, U))
static_assert(not is_assignable_to(T, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(U, T))
static_assert(not is_assignable_to(U, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, U))
static_assert(not is_subtype_of(T, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U, T))
static_assert(not is_subtype_of(U, T))
```
@@ -412,237 +244,67 @@ intersection of all of its constraints is a subtype of the typevar.
from ty_extensions import Intersection
def constrained[T: (Base, Unrelated)](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Base))
static_assert(not is_assignable_to(T, Base))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Unrelated))
static_assert(not is_assignable_to(T, Unrelated))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Any))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super | Unrelated))
static_assert(is_assignable_to(T, Super | Unrelated))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Base | Unrelated))
static_assert(is_assignable_to(T, Base | Unrelated))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Sub | Unrelated))
static_assert(not is_assignable_to(T, Sub | Unrelated))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super, T))
static_assert(not is_assignable_to(Super, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Unrelated, T))
static_assert(not is_assignable_to(Unrelated, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super | Unrelated, T))
static_assert(not is_assignable_to(Super | Unrelated, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[Base, Unrelated], T))
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Base))
static_assert(not is_subtype_of(T, Base))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Unrelated))
static_assert(not is_subtype_of(T, Unrelated))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, Super | Unrelated))
static_assert(is_subtype_of(T, Super | Unrelated))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, Base | Unrelated))
static_assert(is_subtype_of(T, Base | Unrelated))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Sub | Unrelated))
static_assert(not is_subtype_of(T, Sub | Unrelated))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super, T))
static_assert(not is_subtype_of(Super, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Unrelated, T))
static_assert(not is_subtype_of(Unrelated, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super | Unrelated, T))
static_assert(not is_subtype_of(Super | Unrelated, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(Intersection[Base, Unrelated], T))
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super))
static_assert(is_assignable_to(T, Super))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Base))
static_assert(is_assignable_to(T, Base))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Unrelated))
static_assert(not is_assignable_to(T, Unrelated))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Any))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super | Any))
static_assert(is_assignable_to(T, Super | Any))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super | Unrelated))
static_assert(is_assignable_to(T, Super | Unrelated))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super, T))
static_assert(not is_assignable_to(Super, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Base, T))
static_assert(is_assignable_to(Base, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Unrelated, T))
static_assert(not is_assignable_to(Unrelated, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super | Any, T))
static_assert(not is_assignable_to(Super | Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Base | Any, T))
static_assert(is_assignable_to(Base | Any, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super | Unrelated, T))
static_assert(not is_assignable_to(Super | Unrelated, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[Base, Unrelated], T))
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[Base, Any], T))
static_assert(is_assignable_to(Intersection[Base, Any], T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Base))
static_assert(not is_subtype_of(T, Base))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Unrelated))
static_assert(not is_subtype_of(T, Unrelated))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super | Any))
static_assert(not is_subtype_of(T, Super | Any))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super | Unrelated))
static_assert(not is_subtype_of(T, Super | Unrelated))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super, T))
static_assert(not is_subtype_of(Super, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Base, T))
static_assert(not is_subtype_of(Base, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Unrelated, T))
static_assert(not is_subtype_of(Unrelated, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super | Any, T))
static_assert(not is_subtype_of(Super | Any, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Base | Any, T))
static_assert(not is_subtype_of(Base | Any, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super | Unrelated, T))
static_assert(not is_subtype_of(Super | Unrelated, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Intersection[Base, Unrelated], T))
static_assert(not is_subtype_of(Intersection[Base, Unrelated], T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Intersection[Base, Any], T))
static_assert(not is_subtype_of(Intersection[Base, Any], T))
```
@@ -653,40 +315,20 @@ the same type.
```py
def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, U))
static_assert(not is_assignable_to(T, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(U, T))
static_assert(not is_assignable_to(U, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, U))
static_assert(not is_subtype_of(T, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U, T))
static_assert(not is_subtype_of(U, T))
@final
class AnotherFinalClass: ...
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, U))
static_assert(not is_assignable_to(T, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(U, T))
static_assert(not is_assignable_to(U, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, U))
static_assert(not is_subtype_of(T, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U, T))
static_assert(not is_subtype_of(U, T))
```
@@ -694,20 +336,10 @@ A bound or constrained typevar is a subtype of itself in a union:
```py
def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, T | None))
static_assert(is_assignable_to(T, T | None))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(U, U | None))
static_assert(is_assignable_to(U, U | None))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, T | None))
static_assert(is_subtype_of(T, T | None))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(U, U | None))
static_assert(is_subtype_of(U, U | None))
```
@@ -715,20 +347,10 @@ A bound or constrained typevar in a union with a dynamic type is assignable to t
```py
def union_with_dynamic[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T | Any, T))
static_assert(is_assignable_to(T | Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(U | Any, U))
static_assert(is_assignable_to(U | Any, U))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T | Any, T))
static_assert(not is_subtype_of(T | Any, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U | Any, U))
static_assert(not is_subtype_of(U | Any, U))
```
@@ -740,20 +362,9 @@ from ty_extensions import Intersection, Not, is_disjoint_from
class A: ...
def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[T, Unrelated], T))
static_assert(is_assignable_to(Intersection[T, Unrelated], T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(Intersection[T, Unrelated], T))
static_assert(is_subtype_of(Intersection[T, Unrelated], T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[U, A], U))
static_assert(is_assignable_to(Intersection[U, A], U))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(Intersection[U, A], U))
static_assert(is_subtype_of(Intersection[U, A], U))
static_assert(is_disjoint_from(Not[T], T))
@@ -1054,20 +665,10 @@ of) itself.
from ty_extensions import is_assignable_to, is_subtype_of, Not, static_assert
def intersection_is_assignable[T](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[T, None], T))
static_assert(is_assignable_to(Intersection[T, None], T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[T, Not[None]], T))
static_assert(is_assignable_to(Intersection[T, Not[None]], T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(Intersection[T, None], T))
static_assert(is_subtype_of(Intersection[T, None], T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(Intersection[T, Not[None]], T))
static_assert(is_subtype_of(Intersection[T, Not[None]], T))
```

View File

@@ -22,8 +22,10 @@ from ty_extensions import ConstraintSet, generic_context
# fmt: off
def unbounded[T]():
# revealed: ty_extensions.Specialization[T@unbounded = object]
# revealed: ty_extensions.Specialization[T@unbounded = Unknown]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.always()))
# revealed: ty_extensions.Specialization[T@unbounded = object]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, object)))
# revealed: None
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.never()))
@@ -88,6 +90,7 @@ that makes the test succeed.
from typing import Any
def bounded_by_gradual[T: Any]():
# TODO: revealed: ty_extensions.Specialization[T@bounded_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@bounded_by_gradual = object]
reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.always()))
# revealed: None
@@ -168,12 +171,16 @@ from typing import Any
# fmt: off
def constrained_by_gradual[T: (Base, Any)]():
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Unknown]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.always()))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = object]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.always()))
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, object)))
# revealed: None
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.never()))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Base)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
@@ -181,14 +188,14 @@ def constrained_by_gradual[T: (Base, Any)]():
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Unrelated)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Super]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Super)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Super]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Super, T, Super)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = object]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Sub, T, object)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Sub]
@@ -288,7 +295,7 @@ class Unrelated: ...
# fmt: off
def mutually_bound[T: Base, U]():
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = object]
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Unknown]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.never()))
@@ -296,7 +303,7 @@ def mutually_bound[T: Base, U]():
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Base]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, U, T)))
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = object]
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = Unknown]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, T, Sub)))
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = Sub]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, T, Sub) & ConstraintSet.range(Never, U, T)))

View File

@@ -191,13 +191,13 @@ def _(
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
# TODO should be Unknown | int
reveal_type(type_var_or_int) # revealed: typing.TypeVar | int
reveal_type(type_var_or_int) # revealed: T@TypeVarOrInt | int
# TODO should be int | Unknown
reveal_type(int_or_type_var) # revealed: int | typing.TypeVar
reveal_type(int_or_type_var) # revealed: int | T@IntOrTypeVar
# TODO should be Unknown | None
reveal_type(type_var_or_none) # revealed: typing.TypeVar | None
reveal_type(type_var_or_none) # revealed: T@TypeVarOrNone | None
# TODO should be None | Unknown
reveal_type(none_or_type_var) # revealed: None | typing.TypeVar
reveal_type(none_or_type_var) # revealed: None | T@NoneOrTypeVar
```
If a type is unioned with itself in a value expression, the result is just that type. No
@@ -366,7 +366,9 @@ def g(obj: Y):
reveal_type(obj) # revealed: list[int | str]
```
## Generic types
## Generic implicit type aliases
### Functionality
Implicit type aliases can also be generic:
@@ -388,24 +390,25 @@ ListOrTuple = list[T] | tuple[T, ...]
ListOrTupleLegacy = Union[list[T], tuple[T, ...]]
MyCallable = Callable[P, T]
AnnotatedType = Annotated[T, "tag"]
TransparentAlias = T
MyOptional = T | None
# TODO: Consider displaying this as `<class 'list[T]'>`, … instead? (and similar for some others below)
reveal_type(MyList) # revealed: <class 'list[typing.TypeVar]'>
reveal_type(MyDict) # revealed: <class 'dict[typing.TypeVar, typing.TypeVar]'>
reveal_type(MyList) # revealed: <class 'list[T@MyList]'>
reveal_type(MyDict) # revealed: <class 'dict[T@MyDict, U@MyDict]'>
reveal_type(MyType) # revealed: GenericAlias
reveal_type(IntAndType) # revealed: <class 'tuple[int, typing.TypeVar]'>
reveal_type(Pair) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
reveal_type(Sum) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
reveal_type(IntAndType) # revealed: <class 'tuple[int, T@IntAndType]'>
reveal_type(Pair) # revealed: <class 'tuple[T@Pair, T@Pair]'>
reveal_type(Sum) # revealed: <class 'tuple[T@Sum, U@Sum]'>
reveal_type(ListOrTuple) # revealed: types.UnionType
reveal_type(ListOrTupleLegacy) # revealed: types.UnionType
reveal_type(MyCallable) # revealed: GenericAlias
reveal_type(AnnotatedType) # revealed: <typing.Annotated special form>
reveal_type(TransparentAlias) # revealed: typing.TypeVar
reveal_type(MyOptional) # revealed: types.UnionType
def _(
list_of_ints: MyList[int],
dict_str_to_int: MyDict[str, int],
# TODO: no error here
# error: [invalid-type-form] "`typing.TypeVar` is not a generic class"
subclass_of_int: MyType[int],
int_and_str: IntAndType[str],
pair_of_ints: Pair[int],
@@ -413,48 +416,40 @@ def _(
list_or_tuple: ListOrTuple[int],
list_or_tuple_legacy: ListOrTupleLegacy[int],
# TODO: no error here
# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2"
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[str, bytes]`?"
my_callable: MyCallable[[str, bytes], int],
annotated_int: AnnotatedType[int],
transparent_alias: TransparentAlias[int],
optional_int: MyOptional[int],
):
# TODO: This should be `list[int]`
reveal_type(list_of_ints) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `dict[str, int]`
reveal_type(dict_str_to_int) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `type[int]`
reveal_type(subclass_of_int) # revealed: Unknown
# TODO: This should be `tuple[int, str]`
reveal_type(int_and_str) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `tuple[int, int]`
reveal_type(pair_of_ints) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `tuple[int, bytes]`
reveal_type(int_and_bytes) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `list[int] | tuple[int, ...]`
reveal_type(list_or_tuple) # revealed: @Todo(Generic specialization of types.UnionType)
# TODO: This should be `list[int] | tuple[int, ...]`
reveal_type(list_or_tuple_legacy) # revealed: @Todo(Generic specialization of types.UnionType)
reveal_type(list_of_ints) # revealed: list[int]
reveal_type(dict_str_to_int) # revealed: dict[str, int]
reveal_type(subclass_of_int) # revealed: type[int]
reveal_type(int_and_str) # revealed: tuple[int, str]
reveal_type(pair_of_ints) # revealed: tuple[int, int]
reveal_type(int_and_bytes) # revealed: tuple[int, bytes]
reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...]
reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...]
reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...]
# TODO: This should be `(str, bytes) -> int`
reveal_type(my_callable) # revealed: @Todo(Generic specialization of typing.Callable)
# TODO: This should be `int`
reveal_type(annotated_int) # revealed: @Todo(Generic specialization of typing.Annotated)
reveal_type(my_callable) # revealed: Unknown
reveal_type(annotated_int) # revealed: int
reveal_type(transparent_alias) # revealed: int
reveal_type(optional_int) # revealed: int | None
```
Generic implicit type aliases can be partially specialized:
```py
U = TypeVar("U")
DictStrTo = MyDict[str, U]
reveal_type(DictStrTo) # revealed: GenericAlias
reveal_type(DictStrTo) # revealed: <class 'dict[str, U@DictStrTo]'>
def _(
# TODO: No error here
# error: [invalid-type-form] "Invalid subscript of object of type `GenericAlias` in type expression"
dict_str_to_int: DictStrTo[int],
):
# TODO: This should be `dict[str, int]`
reveal_type(dict_str_to_int) # revealed: Unknown
reveal_type(dict_str_to_int) # revealed: dict[str, int]
```
Using specializations of generic implicit type aliases in other implicit type aliases works as
@@ -464,26 +459,65 @@ expected:
IntsOrNone = MyList[int] | None
IntsOrStrs = Pair[int] | Pair[str]
ListOfPairs = MyList[Pair[str]]
ListOrTupleOfInts = ListOrTuple[int]
AnnotatedInt = AnnotatedType[int]
SubclassOfInt = MyType[int]
# TODO: No error here
# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2"
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `list[int]`?"
CallableIntToStr = MyCallable[[int], str]
reveal_type(IntsOrNone) # revealed: UnionType
reveal_type(IntsOrStrs) # revealed: UnionType
reveal_type(ListOfPairs) # revealed: GenericAlias
reveal_type(IntsOrNone) # revealed: types.UnionType
reveal_type(IntsOrStrs) # revealed: types.UnionType
reveal_type(ListOfPairs) # revealed: <class 'list[tuple[str, str]]'>
reveal_type(ListOrTupleOfInts) # revealed: types.UnionType
reveal_type(AnnotatedInt) # revealed: <typing.Annotated special form>
reveal_type(SubclassOfInt) # revealed: GenericAlias
reveal_type(CallableIntToStr) # revealed: Unknown
def _(
# TODO: This should not be an error
# error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression"
ints_or_none: IntsOrNone,
# TODO: This should not be an error
# error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression"
ints_or_strs: IntsOrStrs,
list_of_pairs: ListOfPairs,
list_or_tuple_of_ints: ListOrTupleOfInts,
annotated_int: AnnotatedInt,
subclass_of_int: SubclassOfInt,
callable_int_to_str: CallableIntToStr,
):
# TODO: This should be `list[int] | None`
reveal_type(ints_or_none) # revealed: Unknown
# TODO: This should be `tuple[int, int] | tuple[str, str]`
reveal_type(ints_or_strs) # revealed: Unknown
# TODO: This should be `list[tuple[str, str]]`
reveal_type(list_of_pairs) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
reveal_type(ints_or_none) # revealed: list[int] | None
reveal_type(ints_or_strs) # revealed: tuple[int, int] | tuple[str, str]
reveal_type(list_of_pairs) # revealed: list[tuple[str, str]]
reveal_type(list_or_tuple_of_ints) # revealed: list[int] | tuple[int, ...]
reveal_type(annotated_int) # revealed: int
reveal_type(subclass_of_int) # revealed: type[int]
# TODO: This should be `(int, /) -> str`
reveal_type(callable_int_to_str) # revealed: Unknown
```
A generic implicit type alias can also be used in another generic implicit type alias:
```py
from typing_extensions import Any
B = TypeVar("B", bound=int)
MyOtherList = MyList[T]
MyOtherType = MyType[T]
TypeOrList = MyType[B] | MyList[B]
reveal_type(MyOtherList) # revealed: <class 'list[T@MyOtherList]'>
reveal_type(MyOtherType) # revealed: GenericAlias
reveal_type(TypeOrList) # revealed: types.UnionType
def _(
list_of_ints: MyOtherList[int],
subclass_of_int: MyOtherType[int],
type_or_list: TypeOrList[Any],
):
reveal_type(list_of_ints) # revealed: list[int]
reveal_type(subclass_of_int) # revealed: type[int]
# TODO: Should be `type[Any] | list[Any]`
reveal_type(type_or_list) # revealed: @Todo(type[T] for typevar T) | list[Any]
```
If a generic implicit type alias is used unspecialized in a type expression, we treat it as an
@@ -496,11 +530,11 @@ def _(
my_callable: MyCallable,
):
# TODO: Should be `list[Unknown]`
reveal_type(my_list) # revealed: list[typing.TypeVar]
reveal_type(my_list) # revealed: list[T@MyList]
# TODO: Should be `dict[Unknown, Unknown]`
reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar]
reveal_type(my_dict) # revealed: dict[T@MyDict, U@MyDict]
# TODO: Should be `(...) -> Unknown`
reveal_type(my_callable) # revealed: (...) -> typing.TypeVar
reveal_type(my_callable) # revealed: (...) -> T@MyCallable
```
(Generic) implicit type aliases can be used as base classes:
@@ -522,37 +556,182 @@ reveal_mro(Derived1)
GenericBaseAlias = GenericBase[T]
# TODO: No error here
# error: [non-subscriptable] "Cannot subscript object of type `<class 'GenericBase[typing.TypeVar]'>` with no `__class_getitem__` method"
class Derived2(GenericBaseAlias[int]):
pass
```
### Imported aliases
Generic implicit type aliases can be imported from other modules and specialized:
`my_types.py`:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
MyList = list[T]
```
`main.py`:
```py
from my_types import MyList
import my_types as mt
def _(
list_of_ints1: MyList[int],
list_of_ints2: mt.MyList[int],
):
reveal_type(list_of_ints1) # revealed: list[int]
reveal_type(list_of_ints2) # revealed: list[int]
```
### In stringified annotations
Generic implicit type aliases can be specialized in stringified annotations:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
MyList = list[T]
def _(
list_of_ints: "MyList[int]",
):
reveal_type(list_of_ints) # revealed: list[int]
```
### Error cases
A generic alias that is already fully specialized cannot be specialized again:
```py
ListOfInts = list[int]
# TODO: this should be an error
# error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 1"
def _(doubly_specialized: ListOfInts[int]):
# TODO: this should be `Unknown`
reveal_type(doubly_specialized) # revealed: @Todo(specialized generic alias in type expression)
reveal_type(doubly_specialized) # revealed: Unknown
```
Specializing a generic implicit type alias with an incorrect number of type arguments also results
in an error:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
U = TypeVar("U")
MyList = list[T]
MyDict = dict[T, U]
def _(
# TODO: this should be an error
# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2"
list_too_many_args: MyList[int, str],
# TODO: this should be an error
# error: [missing-argument] "No argument provided for required parameter `U`"
dict_too_few_args: MyDict[int],
):
# TODO: this should be `Unknown`
reveal_type(list_too_many_args) # revealed: @Todo(specialized generic alias in type expression)
# TODO: this should be `Unknown`
reveal_type(dict_too_few_args) # revealed: @Todo(specialized generic alias in type expression)
reveal_type(list_too_many_args) # revealed: Unknown
reveal_type(dict_too_few_args) # revealed: Unknown
```
Trying to specialize a non-name node results in an error:
```py
from ty_extensions import TypeOf
IntOrStr = int | str
def this_does_not_work() -> TypeOf[IntOrStr]:
raise NotImplementedError()
def _(
# TODO: Better error message? `invalid-type-form`
# error: [too-many-positional-arguments] "Too many positional arguments: expected 0, got 1"
specialized: this_does_not_work()[int],
):
reveal_type(specialized) # revealed: Unknown
```
Similarly, if you try to specialize a union type without a binding context, we emit an error:
```py
# error: [invalid-type-form] "`types.UnionType` is not subscriptable"
x: (list[T] | set[T])[int]
def _():
reveal_type(x) # revealed: Unknown
```
### Multiple definitions
#### Shadowed definitions
When a generic type alias shadows a definition from an outer scope, the inner definition is used:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
MyAlias = list[T]
def outer():
MyAlias = set[T]
def _(x: MyAlias[int]):
reveal_type(x) # revealed: set[int]
```
#### Statically known conditions
```py
from typing_extensions import TypeVar
T = TypeVar("T")
if True:
MyAlias1 = list[T]
else:
MyAlias1 = set[T]
if False:
MyAlias2 = list[T]
else:
MyAlias2 = set[T]
def _(
x1: MyAlias1[int],
x2: MyAlias2[int],
):
reveal_type(x1) # revealed: list[int]
reveal_type(x2) # revealed: set[int]
```
#### Statically unknown conditions
If several definitions are visible, we emit an error:
```py
from typing_extensions import TypeVar
T = TypeVar("T")
def flag() -> bool:
return True
if flag():
MyAlias = list[T]
else:
MyAlias = set[T]
# error: [invalid-type-form] "Invalid subscript of object of type `<class 'list[T@MyAlias]'> | <class 'set[T@MyAlias]'>` in type expression"
def _(x: MyAlias[int]):
reveal_type(x) # revealed: Unknown
```
## `Literal`s
@@ -642,8 +821,7 @@ Deprecated = Annotated[T, "deprecated attribute"]
class C:
old: Deprecated[int]
# TODO: Should be `int`
reveal_type(C().old) # revealed: @Todo(Generic specialization of typing.Annotated)
reveal_type(C().old) # revealed: int
```
If the metadata argument is missing, we emit an error (because this code fails at runtime), but
@@ -1298,3 +1476,14 @@ def _(
reveal_type(recursive_dict3) # revealed: dict[Divergent, int]
reveal_type(recursive_dict4) # revealed: dict[Divergent, int]
```
### Self-referential generic implicit type aliases
<!-- expect-panic: execute: too many cycle iterations -->
```py
from typing import TypeVar
T = TypeVar("T")
NestedDict = dict[str, "NestedDict[T] | T"]
```

View File

@@ -60,7 +60,7 @@ Y: int = 47
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: "has no member `fails`"
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
@@ -90,7 +90,7 @@ Y: int = 47
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: "has no member `fails`"
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
@@ -125,7 +125,7 @@ Y: int = 47
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: "has no member `fails`"
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
@@ -155,7 +155,7 @@ Y: int = 47
import mypackage
reveal_type(mypackage.imported.X) # revealed: int
# error: "has no member `fails`"
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
@@ -184,7 +184,7 @@ X: int = 42
import mypackage
# TODO: this could work and would be nice to have?
# error: "has no member `imported`"
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
```
@@ -208,14 +208,14 @@ X: int = 42
import mypackage
# TODO: this could work and would be nice to have
# error: "has no member `imported`"
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
```
## Relative `from` Import of Nested Submodule in `__init__`
`from .submodule import nested` in an `__init__.pyi` does not re-export `mypackage.submodule`,
`mypackage.submodule.nested`, or `nested`.
`from .submodule import nested` in an `__init__.pyi` does re-export `mypackage.submodule`, but not
`mypackage.submodule.nested` or `nested`.
### In Stub
@@ -241,15 +241,14 @@ X: int = 42
```py
import mypackage
# error: "has no member `submodule`"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `submodule`"
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
# error: "has no member `nested`"
# error: [unresolved-attribute] "has no member `nested`"
reveal_type(mypackage.nested) # revealed: Unknown
# error: "has no member `nested`"
# error: [unresolved-attribute] "has no member `nested`"
reveal_type(mypackage.nested.X) # revealed: Unknown
```
@@ -281,9 +280,9 @@ import mypackage
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
# TODO: this would be nice to support
# error: "has no member `nested`"
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `nested`"
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
reveal_type(mypackage.nested.X) # revealed: int
@@ -318,16 +317,14 @@ X: int = 42
```py
import mypackage
# TODO: this could work and would be nice to have
# error: "has no member `submodule`"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: "has no member `submodule`"
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `submodule`"
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
# error: "has no member `nested`"
# error: [unresolved-attribute] "has no member `nested`"
reveal_type(mypackage.nested) # revealed: Unknown
# error: "has no member `nested`"
# error: [unresolved-attribute] "has no member `nested`"
reveal_type(mypackage.nested.X) # revealed: Unknown
```
@@ -359,9 +356,9 @@ import mypackage
reveal_type(mypackage.submodule) # revealed: <module 'mypackage.submodule'>
# TODO: this would be nice to support
# error: "has no member `nested`"
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `nested`"
# error: [possibly-missing-attribute] "Submodule `nested` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
reveal_type(mypackage.nested) # revealed: <module 'mypackage.submodule.nested'>
reveal_type(mypackage.nested.X) # revealed: int
@@ -396,11 +393,11 @@ X: int = 42
```py
import mypackage
# error: "has no member `submodule`"
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: "has no member `submodule`"
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `submodule`"
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
```
@@ -432,11 +429,11 @@ X: int = 42
import mypackage
# TODO: this would be nice to support
# error: "has no member `submodule`"
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule) # revealed: Unknown
# error: "has no member `submodule`"
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule.nested) # revealed: Unknown
# error: "has no member `submodule`"
# error: [possibly-missing-attribute] "Submodule `submodule` may not be available"
reveal_type(mypackage.submodule.nested.X) # revealed: Unknown
```
@@ -463,9 +460,9 @@ X: int = 42
```py
import mypackage
# error: "has no member `imported`"
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
# error: "has no member `imported_m`"
# error: [unresolved-attribute] "has no member `imported_m`"
reveal_type(mypackage.imported_m.X) # revealed: Unknown
```
@@ -489,7 +486,7 @@ X: int = 42
import mypackage
# TODO: this would be nice to support, as it works at runtime
# error: "has no member `imported`"
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
reveal_type(mypackage.imported_m.X) # revealed: int
```
@@ -569,7 +566,7 @@ X: int = 42
from mypackage import *
# TODO: this would be nice to support
# error: "`imported` used when not defined"
# error: [unresolved-reference] "`imported` used when not defined"
reveal_type(imported.X) # revealed: Unknown
reveal_type(Z) # revealed: int
```
@@ -623,8 +620,7 @@ X: int = 42
```py
import mypackage
# error: "no member `imported`"
reveal_type(mypackage.imported.X) # revealed: Unknown
reveal_type(mypackage.imported.X) # revealed: int
```
### In Non-Stub
@@ -673,10 +669,11 @@ X: int = 42
import mypackage
from mypackage import imported
reveal_type(imported.X) # revealed: int
# TODO: this would be nice to support, but it's dangerous with available_submodule_attributes
# for details, see: https://github.com/astral-sh/ty/issues/1488
reveal_type(imported.X) # revealed: int
# error: "has no member `imported`"
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
```
@@ -699,9 +696,10 @@ X: int = 42
import mypackage
from mypackage import imported
# TODO: this would be nice to support, as it works at runtime
reveal_type(imported.X) # revealed: int
# error: "has no member `imported`"
# TODO: this would be nice to support, as it works at runtime
# error: [possibly-missing-attribute] "Submodule `imported` may not be available"
reveal_type(mypackage.imported.X) # revealed: Unknown
```
@@ -737,9 +735,9 @@ import mypackage
from mypackage import imported
reveal_type(imported.X) # revealed: int
# error: "has no member `fails`"
# error: [unresolved-attribute] "has no member `fails`"
reveal_type(imported.fails.Y) # revealed: Unknown
# error: "has no member `fails`"
# error: [possibly-missing-attribute] "Submodule `fails` may not be available"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```
@@ -772,7 +770,7 @@ from mypackage import imported
reveal_type(imported.X) # revealed: int
reveal_type(imported.fails.Y) # revealed: int
# error: "has no member `fails`"
# error: [possibly-missing-attribute] "Submodule `fails`"
reveal_type(mypackage.fails.Y) # revealed: Unknown
```

View File

@@ -247,7 +247,7 @@ X: int = 42
from . import foo
import package
# error: [unresolved-attribute] "Module `package` has no member `foo`"
# error: [possibly-missing-attribute]
reveal_type(package.foo.X) # revealed: Unknown
```

View File

@@ -0,0 +1,585 @@
# The Liskov Substitution Principle
The Liskov Substitution Principle provides the basis for many of the assumptions a type checker
generally makes about types in Python:
> Subtype Requirement: Let `ϕ(x)` be a property provable about objects `x` of type `T`. Then
> `ϕ(y)` should be true for objects `y` of type `S` where `S` is a subtype of `T`.
In order for a type checker's assumptions to be sound, it is crucial for the type checker to enforce
the Liskov Substitution Principle on code that it checks. In practice, this usually manifests as
several checks for a type checker to perform when it checks a subclass `B` of a class `A`:
1. Read-only attributes should only ever be overridden covariantly: if a property `A.p` resolves to
`int` when accessed, accessing `B.p` should either resolve to `int` or a subtype of `int`.
1. Method return types should only ever be overridden covariantly: if a method `A.f` returns `int`
when called, calling `B.f` should also resolve to `int or a subtype of`int\`.
1. Method parameters should only ever be overridden contravariantly: if a method `A.f` can be called
with an argument of type `bool`, then the method `B.f` must also be callable with type `bool`
(though it is permitted for the override to also accept other types)
1. Mutable attributes should only ever be overridden invariantly: if a mutable attribute `A.attr`
resolves to type `str`, it can only be overridden on a subclass with exactly the same type.
## Method return types
<!-- snapshot-diagnostics -->
```pyi
class Super:
def method(self) -> int: ...
class Sub1(Super):
def method(self) -> int: ... # fine
class Sub2(Super):
def method(self) -> bool: ... # fine: `bool` is a subtype of `int`
class Sub3(Super):
def method(self) -> object: ... # error: [invalid-method-override]
class Sub4(Super):
def method(self) -> str: ... # error: [invalid-method-override]
```
## Method parameters
<!-- snapshot-diagnostics -->
```pyi
class Super:
def method(self, x: int, /): ...
class Sub1(Super):
def method(self, x: int, /): ... # fine
class Sub2(Super):
def method(self, x: object, /): ... # fine: `method` still accepts any argument of type `int`
class Sub4(Super):
def method(self, x: int | str, /): ... # fine
class Sub5(Super):
def method(self, x: int): ... # fine: `x` can still be passed positionally
class Sub6(Super):
# fine: `method()` can still be called with just a single argument
def method(self, x: int, *args): ...
class Sub7(Super):
def method(self, x: int, **kwargs): ... # fine
class Sub8(Super):
def method(self, x: int, *args, **kwargs): ... # fine
class Sub9(Super):
def method(self, x: int, extra_positional_arg=42, /): ... # fine
class Sub10(Super):
def method(self, x: int, extra_pos_or_kw_arg=42): ... # fine
class Sub11(Super):
def method(self, x: int, *, extra_kw_only_arg=42): ... # fine
class Sub12(Super):
# Some calls permitted by the superclass are now no longer allowed
# (the method can no longer be passed any arguments!)
def method(self, /): ... # error: [invalid-method-override]
class Sub13(Super):
# Some calls permitted by the superclass are now no longer allowed
# (the method can no longer be passed exactly one argument!)
def method(self, x, y, /): ... # error: [invalid-method-override]
class Sub14(Super):
# Some calls permitted by the superclass are now no longer allowed
# (x can no longer be passed positionally!)
def method(self, /, *, x): ... # error: [invalid-method-override]
class Sub15(Super):
# Some calls permitted by the superclass are now no longer allowed
# (x can no longer be passed any integer -- it now requires a bool!)
def method(self, x: bool, /): ... # error: [invalid-method-override]
class Super2:
def method2(self, x): ...
class Sub16(Super2):
def method2(self, x, /): ... # error: [invalid-method-override]
class Sub17(Super2):
def method2(self, *, x): ... # error: [invalid-method-override]
class Super3:
def method3(self, *, x): ...
class Sub18(Super3):
def method3(self, x): ... # fine: `x` can still be used as a keyword argument
class Sub19(Super3):
def method3(self, x, /): ... # error: [invalid-method-override]
class Super4:
def method(self, *args: int, **kwargs: str): ...
class Sub20(Super4):
def method(self, *args: object, **kwargs: object): ... # fine
class Sub21(Super4):
def method(self, *args): ... # error: [invalid-method-override]
class Sub22(Super4):
def method(self, **kwargs): ... # error: [invalid-method-override]
class Sub23(Super4):
def method(self, x, *args, y, **kwargs): ... # error: [invalid-method-override]
```
## The entire class hierarchy is checked
If a child class's method definition is Liskov-compatible with the method definition on its parent
class, Liskov compatibility must also nonetheless be checked with respect to the method definition
on its grandparent class. This is because type checkers will treat the child class as a subtype of
the grandparent class just as much as they treat it as a subtype of the parent class, so
substitutability with respect to the grandparent class is just as important:
<!-- snapshot-diagnostics -->
`stub.pyi`:
```pyi
from typing import Any
class Grandparent:
def method(self, x: int) -> None: ...
class Parent(Grandparent):
def method(self, x: str) -> None: ... # error: [invalid-method-override]
class Child(Parent):
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
def method(self, x: str) -> None: ... # error: [invalid-method-override]
class OtherChild(Parent):
# compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
def method(self, x: int) -> None: ... # error: [invalid-method-override]
class GradualParent(Grandparent):
def method(self, x: Any) -> None: ...
class ThirdChild(GradualParent):
# `GradualParent.method` is compatible with the signature of `Grandparent.method`,
# and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
# but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
def method(self, x: str) -> None: ... # error: [invalid-method-override]
```
`other_stub.pyi`:
```pyi
class A:
def get(self, default): ...
class B(A):
def get(self, default, /): ... # error: [invalid-method-override]
get = 56
class C(B):
# `get` appears in the symbol table of `C`,
# but that doesn't confuse our diagnostic...
foo = get
class D(C):
# compatible with `C.get` and `B.get`, but not with `A.get`
def get(self, my_default): ... # error: [invalid-method-override]
```
## Non-generic methods on generic classes work as expected
```toml
[environment]
python-version = "3.12"
```
```pyi
class A[T]:
def method(self, x: T) -> None: ...
class B[T](A[T]):
def method(self, x: T) -> None: ... # fine
class C(A[int]):
def method(self, x: int) -> None: ... # fine
class D[T](A[T]):
def method(self, x: object) -> None: ... # fine
class E(A[int]):
def method(self, x: object) -> None: ... # fine
class F[T](A[T]):
# TODO: we should emit `invalid-method-override` on this:
# `str` is not necessarily a supertype of `T`!
def method(self, x: str) -> None: ...
class G(A[int]):
def method(self, x: bool) -> None: ... # error: [invalid-method-override]
```
## Generic methods on non-generic classes work as expected
```toml
[environment]
python-version = "3.12"
```
```pyi
from typing import Never, Self
class A:
def method[T](self, x: T) -> T: ...
class B(A):
def method[T](self, x: T) -> T: ... # fine
class C(A):
def method(self, x: object) -> Never: ... # fine
class D(A):
# TODO: we should emit [invalid-method-override] here:
# `A.method` accepts an argument of any type,
# but `D.method` only accepts `int`s
def method(self, x: int) -> int: ...
class A2:
def method(self, x: int) -> int: ...
class B2(A2):
# fine: although `B2.method()` will not always return an `int`,
# an instance of `B2` can be substituted wherever an instance of `A2` is expected,
# and it *will* always return an `int` if it is passed an `int`
# (which is all that will be allowed if an instance of `A2` is expected)
def method[T](self, x: T) -> T: ...
class C2(A2):
def method[T: int](self, x: T) -> T: ...
class D2(A2):
# The type variable is bound to a type disjoint from `int`,
# so the method will not accept integers, and therefore this is an invalid override
def method[T: str](self, x: T) -> T: ... # error: [invalid-method-override]
class A3:
def method(self) -> Self: ...
class B3(A3):
def method(self) -> Self: ... # fine
class C3(A3):
# TODO: should this be allowed?
# Mypy/pyright/pyrefly all allow it,
# but conceptually it seems similar to `B4.method` below,
# which mypy/pyrefly agree is a Liskov violation
# (pyright disagrees as of 20/11/2025: https://github.com/microsoft/pyright/issues/11128)
# when called on a subclass, `C3.method()` will not return an
# instance of that subclass
def method(self) -> C3: ...
class D3(A3):
def method(self: Self) -> Self: ... # fine
class E3(A3):
def method(self: E3) -> Self: ... # fine
class F3(A3):
def method(self: A3) -> Self: ... # fine
class G3(A3):
def method(self: object) -> Self: ... # fine
class H3(A3):
# TODO: we should emit `invalid-method-override` here
# (`A3.method()` can be called on any instance of `A3`,
# but `H3.method()` can only be called on objects that are
# instances of `str`)
def method(self: str) -> Self: ...
class I3(A3):
# TODO: we should emit `invalid-method-override` here
# (`I3.method()` cannot be called with any inhabited type!)
def method(self: Never) -> Self: ...
class A4:
def method[T: int](self, x: T) -> T: ...
class B4(A4):
# TODO: we should emit `invalid-method-override` here.
# `A4.method` promises that if it is passed a `bool`, it will return a `bool`,
# but this is not necessarily true for `B4.method`: if passed a `bool`,
# it could return a non-`bool` `int`!
def method(self, x: int) -> int: ...
```
## Generic methods on generic classes work as expected
```toml
[environment]
python-version = "3.12"
```
```pyi
from typing import Never
class A[T]:
def method[S](self, x: T, y: S) -> S: ...
class B[T](A[T]):
def method[S](self, x: T, y: S) -> S: ... # fine
class C(A[int]):
def method[S](self, x: int, y: S) -> S: ... # fine
class D[T](A[T]):
def method[S](self, x: object, y: S) -> S: ... # fine
class E(A[int]):
def method[S](self, x: object, y: S) -> S: ... # fine
class F(A[int]):
def method(self, x: object, y: object) -> Never: ... # fine
class A2[T]:
def method(self, x: T, y: int) -> int: ...
class B2[T](A2[T]):
def method[S](self, x: T, y: S) -> S: ... # fine
```
## Fully qualified names are used in diagnostics where appropriate
<!-- snapshot-diagnostics -->
`a.pyi`:
```pyi
class A:
def foo(self, x): ...
```
`b.pyi`:
```pyi
import a
class A(a.A):
def foo(self, y): ... # error: [invalid-method-override]
```
## Excluded methods
Certain special constructor methods are excluded from Liskov checks. None of the following classes
cause us to emit any errors, therefore:
```toml
# This is so that the dataclasses machinery will generate `__replace__` methods for us
# (the synthesized `__replace__` methods should not be reported as invalid overrides!)
[environment]
python-version = "3.13"
```
```pyi
from dataclasses import dataclass
from typing_extensions import Self
class Grandparent: ...
class Parent(Grandparent):
def __new__(cls, x: int) -> Self: ...
def __init__(self, x: int) -> None: ...
class Child(Parent):
def __new__(cls, x: str, y: str) -> Self: ...
def __init__(self, x: str, y: str) -> Self: ...
@dataclass(init=False)
class DataSuper:
x: int
def __post_init__(self, x: int) -> None:
self.x = x
@dataclass(init=False)
class DataSub(DataSuper):
y: str
def __post_init__(self, x: int, y: str) -> None:
self.y = y
super().__post_init__(x)
```
## Edge case: function defined in another module and then assigned in a class body
<!-- snapshot-diagnostics -->
`foo.pyi`:
```pyi
def x(self, y: str): ...
```
`bar.pyi`:
```pyi
import foo
class A:
def x(self, y: int): ...
class B(A):
x = foo.x # error: [invalid-method-override]
class C:
x = foo.x
class D(C):
def x(self, y: int): ... # error: [invalid-method-override]
```
## Bad override of `__eq__`
<!-- snapshot-diagnostics -->
```py
class Bad:
x: int
def __eq__(self, other: "Bad") -> bool: # error: [invalid-method-override]
return self.x == other.x
```
## Synthesized methods
`NamedTuple` classes and dataclasses both have methods generated at runtime that do not have
source-code definitions. There are several scenarios to consider here:
1. A synthesized method on a superclass is overridden by a "normal" (not synthesized) method on a
subclass
1. A "normal" method on a superclass is overridden by a synthesized method on a subclass
1. A synthesized method on a superclass is overridden by a synthesized method on a subclass
<!-- snapshot-diagnostics -->
```pyi
from dataclasses import dataclass
from typing import NamedTuple
@dataclass(order=True)
class Foo:
x: int
class Bar(Foo):
def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
# TODO: specifying `order=True` on the subclass means that a `__lt__` method is
# generated that is incompatible with the generated `__lt__` method on the superclass.
# We could consider detecting this and emitting a diagnostic, though maybe it shouldn't
# be `invalid-method-override` since we'd emit it on the class definition rather than
# on any method definition. Note also that no other type checker complains about this
# as of 2025-11-21.
@dataclass(order=True)
class Bar2(Foo):
y: str
# TODO: Although this class does not override any methods of `Foo`, the design of the
# `order=True` stdlib dataclasses feature itself arguably violates the Liskov Substitution
# Principle! Instances of `Bar3` cannot be substituted wherever an instance of `Foo` is
# expected, because the generated `__lt__` method on `Foo` raises an error unless the r.h.s.
# and `l.h.s.` have exactly the same `__class__` (it does not permit instances of `Foo` to
# be compared with instances of subclasses of `Foo`).
#
# Many users would probably like their type checkers to alert them to cases where instances
# of subclasses cannot be substituted for instances of superclasses, as this violates many
# assumptions a type checker will make and makes it likely that a type checker will fail to
# catch type errors elsewhere in the user's code. We could therefore consider treating all
# `order=True` dataclasses as implicitly `@final` in order to enforce soundness. However,
# this probably shouldn't be reported with the same error code as Liskov violations, since
# the error does not stem from any method signatures written by the user. The example is
# only included here for completeness.
#
# Note that no other type checker catches this error as of 2025-11-21.
class Bar3(Foo): ...
class Eggs:
def __lt__(self, other: Eggs) -> bool: ...
# TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
# We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
# diagnostic here but pyright and pyrefly do not.
@dataclass(order=True)
class Ham(Eggs):
x: int
class Baz(NamedTuple):
x: int
class Spam(Baz):
def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
```
## Staticmethods and classmethods
Methods decorated with `@staticmethod` or `@classmethod` are checked in much the same way as other
methods.
<!-- snapshot-diagnostics -->
```pyi
class Parent:
def instance_method(self, x: int) -> int: ...
@classmethod
def class_method(cls, x: int) -> int: ...
@staticmethod
def static_method(x: int) -> int: ...
class BadChild1(Parent):
@staticmethod
def instance_method(self, x: int) -> int: ... # error: [invalid-method-override]
# TODO: we should emit `invalid-method-override` here.
# Although the method has the same signature as `Parent.class_method`
# when accessed on instances, it does not have the same signature as
# `Parent.class_method` when accessed on the class object itself
def class_method(cls, x: int) -> int: ...
def static_method(x: int) -> int: ... # error: [invalid-method-override]
class BadChild2(Parent):
# TODO: we should emit `invalid-method-override` here.
# Although the method has the same signature as `Parent.class_method`
# when accessed on instances, it does not have the same signature as
# `Parent.class_method` when accessed on the class object itself.
#
# Note that whereas `BadChild1.class_method` is reported as a Liskov violation by
# mypy, pyright and pyrefly, pyright is the only one of those three to report a
# Liskov violation on this method as of 2025-11-23.
@classmethod
def instance_method(self, x: int) -> int: ...
@staticmethod
def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
@classmethod
def static_method(x: int) -> int: ... # error: [invalid-method-override]
class BadChild3(Parent):
@classmethod
def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
@staticmethod
def static_method(x: bool) -> object: ... # error: [invalid-method-override]
class GoodChild1(Parent):
@classmethod
def class_method(cls, x: int) -> int: ...
@staticmethod
def static_method(x: int) -> int: ...
class GoodChild2(Parent):
@classmethod
def class_method(cls, x: object) -> bool: ...
@staticmethod
def static_method(x: object) -> bool: ...
```

View File

@@ -335,6 +335,9 @@ reveal_type(x19) # revealed: list[Literal[1]]
x20: list[Literal[1]] | None = [1]
reveal_type(x20) # revealed: list[Literal[1]]
x21: X[Literal[1]] | None = x(1)
x21: X[Literal[1]] | None = X(1)
reveal_type(x21) # revealed: X[Literal[1]]
x22: X[Literal[1]] | None = x(1)
reveal_type(x22) # revealed: X[Literal[1]]
```

View File

@@ -96,6 +96,24 @@ def _(x: MyAlias):
reveal_type(x) # revealed: int | ((str, /) -> int)
```
## Generic aliases
```py
from typing import TypeAlias, TypeVar
T = TypeVar("T")
MyList: TypeAlias = list[T]
ListOrSet: TypeAlias = list[T] | set[T]
reveal_type(MyList) # revealed: <class 'list[T]'>
reveal_type(ListOrSet) # revealed: types.UnionType
def _(list_of_int: MyList[int], list_or_set_of_str: ListOrSet[str]):
reveal_type(list_of_int) # revealed: list[int]
reveal_type(list_or_set_of_str) # revealed: list[str] | set[str]
```
## Subscripted generic alias in union
```py
@@ -107,8 +125,7 @@ Alias1: TypeAlias = list[T] | set[T]
MyAlias: TypeAlias = int | Alias1[str]
def _(x: MyAlias):
# TODO: int | list[str] | set[str]
reveal_type(x) # revealed: int | @Todo(Specialization of union type alias)
reveal_type(x) # revealed: int | list[str] | set[str]
```
## Imported

View File

@@ -2003,6 +2003,7 @@ python-version = "3.12"
```
```py
from typing import final
from typing_extensions import TypeVar, Self, Protocol
from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of
@@ -2094,6 +2095,13 @@ class NominalReturningSelfNotGeneric:
def g(self) -> "NominalReturningSelfNotGeneric":
return self
@final
class Other: ...
class NominalReturningOtherClass:
def g(self) -> Other:
raise NotImplementedError
# TODO: should pass
static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error]
@@ -2112,8 +2120,7 @@ static_assert(not is_assignable_to(NominalLegacy, UsesSelf))
static_assert(not is_assignable_to(NominalWithSelf, NewStyleFunctionScoped))
static_assert(not is_assignable_to(NominalWithSelf, LegacyFunctionScoped))
static_assert(is_assignable_to(NominalWithSelf, UsesSelf))
# TODO: should pass
static_assert(is_subtype_of(NominalWithSelf, UsesSelf)) # error: [static-assert-error]
static_assert(is_subtype_of(NominalWithSelf, UsesSelf))
# TODO: these should pass
static_assert(not is_assignable_to(NominalNotGeneric, NewStyleFunctionScoped)) # error: [static-assert-error]
@@ -2126,6 +2133,8 @@ static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, LegacyFunctio
# TODO: should pass
static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, UsesSelf)) # error: [static-assert-error]
static_assert(not is_assignable_to(NominalReturningOtherClass, UsesSelf))
# These test cases are taken from the typing conformance suite:
class ShapeProtocolImplicitSelf(Protocol):
def set_scale(self, scale: float) -> Self: ...
@@ -3069,18 +3078,15 @@ from typing import Protocol
from ty_extensions import static_assert, is_subtype_of, is_equivalent_to, is_disjoint_from
class HasRepr(Protocol):
# TODO: we should emit a diagnostic here complaining about a Liskov violation
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasRepr`)
# error: [invalid-method-override]
def __repr__(self) -> object: ...
class HasReprRecursive(Protocol):
# TODO: we should emit a diagnostic here complaining about a Liskov violation
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasReprRecursive`)
# error: [invalid-method-override]
def __repr__(self) -> "HasReprRecursive": ...
class HasReprRecursiveAndFoo(Protocol):
# TODO: we should emit a diagnostic here complaining about a Liskov violation
# (it incompatibly overrides `__repr__` from `object`, a supertype of `HasReprRecursiveAndFoo`)
# error: [invalid-method-override]
def __repr__(self) -> "HasReprRecursiveAndFoo": ...
foo: int
@@ -3180,6 +3186,33 @@ from ty_extensions import reveal_protocol_interface
reveal_protocol_interface(Foo)
```
## Known panics
### Protocols generic over TypeVars bound to forward references
This test currently panics because the `ClassLiteral::explicit_bases` query fails to converge. See
issue <https://github.com/astral-sh/ty/issues/1587>.
<!-- expect-panic: execute: too many cycle iterations -->
```py
from typing import Any, Protocol, TypeVar
T1 = TypeVar("T1", bound="A2[Any]")
T2 = TypeVar("T2", bound="A1[Any]")
T3 = TypeVar("T3", bound="B2[Any]")
T4 = TypeVar("T4", bound="B1[Any]")
class A1(Protocol[T1]):
def get_x(self): ...
class A2(Protocol[T2]):
def get_y(self): ...
class B1(A1[T3], Protocol[T3]): ...
class B2(A2[T4], Protocol[T4]): ...
```
## TODO
Add tests for:

View File

@@ -32,7 +32,8 @@ error[unresolved-attribute]: Module `datetime` has no member `UTC`
5 | # error: [unresolved-attribute]
6 | reveal_type(datetime.fakenotreal) # revealed: Unknown
|
info: Python 3.10 was assumed when accessing `UTC` because it was specified on the command line
info: The member may be available on other Python versions or platforms
info: Python 3.10 was assumed when resolving the `UTC` attribute because it was specified on the command line
info: rule `unresolved-attribute` is enabled by default
```

View File

@@ -0,0 +1,68 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attributes.md - Attributes - Unimported submodule incorrectly accessed as attribute
mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
---
# Python source files
## foo/__init__.py
```
```
## foo/bar.py
```
```
## baz/bar.py
```
```
## main.py
```
1 | import foo
2 | import baz
3 |
4 | # error: [possibly-missing-attribute]
5 | reveal_type(foo.bar) # revealed: Unknown
6 | # error: [possibly-missing-attribute]
7 | reveal_type(baz.bar) # revealed: Unknown
```
# Diagnostics
```
warning[possibly-missing-attribute]: Submodule `bar` may not be available as an attribute on module `foo`
--> src/main.py:5:13
|
4 | # error: [possibly-missing-attribute]
5 | reveal_type(foo.bar) # revealed: Unknown
| ^^^^^^^
6 | # error: [possibly-missing-attribute]
7 | reveal_type(baz.bar) # revealed: Unknown
|
help: Consider explicitly importing `foo.bar`
info: rule `possibly-missing-attribute` is enabled by default
```
```
warning[possibly-missing-attribute]: Submodule `bar` may not be available as an attribute on module `baz`
--> src/main.py:7:13
|
5 | reveal_type(foo.bar) # revealed: Unknown
6 | # error: [possibly-missing-attribute]
7 | reveal_type(baz.bar) # revealed: Unknown
| ^^^^^^^
|
help: Consider explicitly importing `baz.bar`
info: rule `possibly-missing-attribute` is enabled by default
```

View File

@@ -0,0 +1,52 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Bad override of `__eq__`
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## mdtest_snippet.py
```
1 | class Bad:
2 | x: int
3 | def __eq__(self, other: "Bad") -> bool: # error: [invalid-method-override]
4 | return self.x == other.x
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `__eq__`
--> src/mdtest_snippet.py:3:9
|
1 | class Bad:
2 | x: int
3 | def __eq__(self, other: "Bad") -> bool: # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `object.__eq__`
4 | return self.x == other.x
|
::: stdlib/builtins.pyi:142:9
|
140 | def __setattr__(self, name: str, value: Any, /) -> None: ...
141 | def __delattr__(self, name: str, /) -> None: ...
142 | def __eq__(self, value: object, /) -> bool: ...
| -------------------------------------- `object.__eq__` defined here
143 | def __ne__(self, value: object, /) -> bool: ...
144 | def __str__(self) -> str: ... # noqa: Y029
|
info: This violates the Liskov Substitution Principle
help: It is recommended for `__eq__` to work with arbitrary objects, for example:
help
help: def __eq__(self, other: object) -> bool:
help: if not isinstance(other, Bad):
help: return False
help: return <logic to compare two `Bad` instances>
help
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,82 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Edge case: function defined in another module and then assigned in a class body
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## foo.pyi
```
1 | def x(self, y: str): ...
```
## bar.pyi
```
1 | import foo
2 |
3 | class A:
4 | def x(self, y: int): ...
5 |
6 | class B(A):
7 | x = foo.x # error: [invalid-method-override]
8 |
9 | class C:
10 | x = foo.x
11 |
12 | class D(C):
13 | def x(self, y: int): ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `x`
--> src/bar.pyi:4:9
|
3 | class A:
4 | def x(self, y: int): ...
| --------------- `A.x` defined here
5 |
6 | class B(A):
7 | x = foo.x # error: [invalid-method-override]
| ^^^^^^^^^ Definition is incompatible with `A.x`
8 |
9 | class C:
|
::: src/foo.pyi:1:5
|
1 | def x(self, y: str): ...
| --------------- Signature of `B.x`
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `x`
--> src/bar.pyi:10:5
|
9 | class C:
10 | x = foo.x
| --------- `C.x` defined here
11 |
12 | class D(C):
13 | def x(self, y: int): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^ Definition is incompatible with `C.x`
|
::: src/foo.pyi:1:5
|
1 | def x(self, y: str): ...
| --------------- Signature of `C.x`
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,47 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Fully qualified names are used in diagnostics where appropriate
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## a.pyi
```
1 | class A:
2 | def foo(self, x): ...
```
## b.pyi
```
1 | import a
2 |
3 | class A(a.A):
4 | def foo(self, y): ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `foo`
--> src/b.pyi:4:9
|
3 | class A(a.A):
4 | def foo(self, y): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^ Definition is incompatible with `a.A.foo`
|
::: src/a.pyi:2:9
|
1 | class A:
2 | def foo(self, x): ...
| ------------ `a.A.foo` defined here
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,331 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Method parameters
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## mdtest_snippet.pyi
```
1 | class Super:
2 | def method(self, x: int, /): ...
3 |
4 | class Sub1(Super):
5 | def method(self, x: int, /): ... # fine
6 |
7 | class Sub2(Super):
8 | def method(self, x: object, /): ... # fine: `method` still accepts any argument of type `int`
9 |
10 | class Sub4(Super):
11 | def method(self, x: int | str, /): ... # fine
12 |
13 | class Sub5(Super):
14 | def method(self, x: int): ... # fine: `x` can still be passed positionally
15 |
16 | class Sub6(Super):
17 | # fine: `method()` can still be called with just a single argument
18 | def method(self, x: int, *args): ...
19 |
20 | class Sub7(Super):
21 | def method(self, x: int, **kwargs): ... # fine
22 |
23 | class Sub8(Super):
24 | def method(self, x: int, *args, **kwargs): ... # fine
25 |
26 | class Sub9(Super):
27 | def method(self, x: int, extra_positional_arg=42, /): ... # fine
28 |
29 | class Sub10(Super):
30 | def method(self, x: int, extra_pos_or_kw_arg=42): ... # fine
31 |
32 | class Sub11(Super):
33 | def method(self, x: int, *, extra_kw_only_arg=42): ... # fine
34 |
35 | class Sub12(Super):
36 | # Some calls permitted by the superclass are now no longer allowed
37 | # (the method can no longer be passed any arguments!)
38 | def method(self, /): ... # error: [invalid-method-override]
39 |
40 | class Sub13(Super):
41 | # Some calls permitted by the superclass are now no longer allowed
42 | # (the method can no longer be passed exactly one argument!)
43 | def method(self, x, y, /): ... # error: [invalid-method-override]
44 |
45 | class Sub14(Super):
46 | # Some calls permitted by the superclass are now no longer allowed
47 | # (x can no longer be passed positionally!)
48 | def method(self, /, *, x): ... # error: [invalid-method-override]
49 |
50 | class Sub15(Super):
51 | # Some calls permitted by the superclass are now no longer allowed
52 | # (x can no longer be passed any integer -- it now requires a bool!)
53 | def method(self, x: bool, /): ... # error: [invalid-method-override]
54 |
55 | class Super2:
56 | def method2(self, x): ...
57 |
58 | class Sub16(Super2):
59 | def method2(self, x, /): ... # error: [invalid-method-override]
60 |
61 | class Sub17(Super2):
62 | def method2(self, *, x): ... # error: [invalid-method-override]
63 |
64 | class Super3:
65 | def method3(self, *, x): ...
66 |
67 | class Sub18(Super3):
68 | def method3(self, x): ... # fine: `x` can still be used as a keyword argument
69 |
70 | class Sub19(Super3):
71 | def method3(self, x, /): ... # error: [invalid-method-override]
72 |
73 | class Super4:
74 | def method(self, *args: int, **kwargs: str): ...
75 |
76 | class Sub20(Super4):
77 | def method(self, *args: object, **kwargs: object): ... # fine
78 |
79 | class Sub21(Super4):
80 | def method(self, *args): ... # error: [invalid-method-override]
81 |
82 | class Sub22(Super4):
83 | def method(self, **kwargs): ... # error: [invalid-method-override]
84 |
85 | class Sub23(Super4):
86 | def method(self, x, *args, y, **kwargs): ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:38:9
|
36 | # Some calls permitted by the superclass are now no longer allowed
37 | # (the method can no longer be passed any arguments!)
38 | def method(self, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
39 |
40 | class Sub13(Super):
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self, x: int, /): ...
| ----------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:43:9
|
41 | # Some calls permitted by the superclass are now no longer allowed
42 | # (the method can no longer be passed exactly one argument!)
43 | def method(self, x, y, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
44 |
45 | class Sub14(Super):
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self, x: int, /): ...
| ----------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:48:9
|
46 | # Some calls permitted by the superclass are now no longer allowed
47 | # (x can no longer be passed positionally!)
48 | def method(self, /, *, x): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
49 |
50 | class Sub15(Super):
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self, x: int, /): ...
| ----------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:53:9
|
51 | # Some calls permitted by the superclass are now no longer allowed
52 | # (x can no longer be passed any integer -- it now requires a bool!)
53 | def method(self, x: bool, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
54 |
55 | class Super2:
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self, x: int, /): ...
| ----------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method2`
--> src/mdtest_snippet.pyi:56:9
|
55 | class Super2:
56 | def method2(self, x): ...
| ---------------- `Super2.method2` defined here
57 |
58 | class Sub16(Super2):
59 | def method2(self, x, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super2.method2`
60 |
61 | class Sub17(Super2):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method2`
--> src/mdtest_snippet.pyi:62:9
|
61 | class Sub17(Super2):
62 | def method2(self, *, x): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super2.method2`
63 |
64 | class Super3:
|
::: src/mdtest_snippet.pyi:56:9
|
55 | class Super2:
56 | def method2(self, x): ...
| ---------------- `Super2.method2` defined here
57 |
58 | class Sub16(Super2):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method3`
--> src/mdtest_snippet.pyi:71:9
|
70 | class Sub19(Super3):
71 | def method3(self, x, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super3.method3`
72 |
73 | class Super4:
|
::: src/mdtest_snippet.pyi:65:9
|
64 | class Super3:
65 | def method3(self, *, x): ...
| ------------------- `Super3.method3` defined here
66 |
67 | class Sub18(Super3):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:80:9
|
79 | class Sub21(Super4):
80 | def method(self, *args): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method`
81 |
82 | class Sub22(Super4):
|
::: src/mdtest_snippet.pyi:74:9
|
73 | class Super4:
74 | def method(self, *args: int, **kwargs: str): ...
| --------------------------------------- `Super4.method` defined here
75 |
76 | class Sub20(Super4):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:83:9
|
82 | class Sub22(Super4):
83 | def method(self, **kwargs): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method`
84 |
85 | class Sub23(Super4):
|
::: src/mdtest_snippet.pyi:74:9
|
73 | class Super4:
74 | def method(self, *args: int, **kwargs: str): ...
| --------------------------------------- `Super4.method` defined here
75 |
76 | class Sub20(Super4):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:86:9
|
85 | class Sub23(Super4):
86 | def method(self, x, *args, y, **kwargs): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method`
|
::: src/mdtest_snippet.pyi:74:9
|
73 | class Super4:
74 | def method(self, *args: int, **kwargs: str): ...
| --------------------------------------- `Super4.method` defined here
75 |
76 | class Sub20(Super4):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,75 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Method return types
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## mdtest_snippet.pyi
```
1 | class Super:
2 | def method(self) -> int: ...
3 |
4 | class Sub1(Super):
5 | def method(self) -> int: ... # fine
6 |
7 | class Sub2(Super):
8 | def method(self) -> bool: ... # fine: `bool` is a subtype of `int`
9 |
10 | class Sub3(Super):
11 | def method(self) -> object: ... # error: [invalid-method-override]
12 |
13 | class Sub4(Super):
14 | def method(self) -> str: ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:11:9
|
10 | class Sub3(Super):
11 | def method(self) -> object: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
12 |
13 | class Sub4(Super):
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self) -> int: ...
| ------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/mdtest_snippet.pyi:14:9
|
13 | class Sub4(Super):
14 | def method(self) -> str: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Super:
2 | def method(self) -> int: ...
| ------------------- `Super.method` defined here
3 |
4 | class Sub1(Super):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,220 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Staticmethods and classmethods
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## mdtest_snippet.pyi
```
1 | class Parent:
2 | def instance_method(self, x: int) -> int: ...
3 | @classmethod
4 | def class_method(cls, x: int) -> int: ...
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
7 |
8 | class BadChild1(Parent):
9 | @staticmethod
10 | def instance_method(self, x: int) -> int: ... # error: [invalid-method-override]
11 | # TODO: we should emit `invalid-method-override` here.
12 | # Although the method has the same signature as `Parent.class_method`
13 | # when accessed on instances, it does not have the same signature as
14 | # `Parent.class_method` when accessed on the class object itself
15 | def class_method(cls, x: int) -> int: ...
16 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
17 |
18 | class BadChild2(Parent):
19 | # TODO: we should emit `invalid-method-override` here.
20 | # Although the method has the same signature as `Parent.class_method`
21 | # when accessed on instances, it does not have the same signature as
22 | # `Parent.class_method` when accessed on the class object itself.
23 | #
24 | # Note that whereas `BadChild1.class_method` is reported as a Liskov violation by
25 | # mypy, pyright and pyrefly, pyright is the only one of those three to report a
26 | # Liskov violation on this method as of 2025-11-23.
27 | @classmethod
28 | def instance_method(self, x: int) -> int: ...
29 | @staticmethod
30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
31 | @classmethod
32 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
33 |
34 | class BadChild3(Parent):
35 | @classmethod
36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
37 | @staticmethod
38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override]
39 |
40 | class GoodChild1(Parent):
41 | @classmethod
42 | def class_method(cls, x: int) -> int: ...
43 | @staticmethod
44 | def static_method(x: int) -> int: ...
45 |
46 | class GoodChild2(Parent):
47 | @classmethod
48 | def class_method(cls, x: object) -> bool: ...
49 | @staticmethod
50 | def static_method(x: object) -> bool: ...
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `instance_method`
--> src/mdtest_snippet.pyi:10:9
|
8 | class BadChild1(Parent):
9 | @staticmethod
10 | def instance_method(self, x: int) -> int: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.instance_method`
11 | # TODO: we should emit `invalid-method-override` here.
12 | # Although the method has the same signature as `Parent.class_method`
|
::: src/mdtest_snippet.pyi:2:9
|
1 | class Parent:
2 | def instance_method(self, x: int) -> int: ...
| ------------------------------------ `Parent.instance_method` defined here
3 | @classmethod
4 | def class_method(cls, x: int) -> int: ...
|
info: `BadChild1.instance_method` is a staticmethod but `Parent.instance_method` is an instance method
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `static_method`
--> src/mdtest_snippet.pyi:16:9
|
14 | # `Parent.class_method` when accessed on the class object itself
15 | def class_method(cls, x: int) -> int: ...
16 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method`
17 |
18 | class BadChild2(Parent):
|
::: src/mdtest_snippet.pyi:6:9
|
4 | def class_method(cls, x: int) -> int: ...
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
| ---------------------------- `Parent.static_method` defined here
7 |
8 | class BadChild1(Parent):
|
info: `BadChild1.static_method` is an instance method but `Parent.static_method` is a staticmethod
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `class_method`
--> src/mdtest_snippet.pyi:30:9
|
28 | def instance_method(self, x: int) -> int: ...
29 | @staticmethod
30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.class_method`
31 | @classmethod
32 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
|
::: src/mdtest_snippet.pyi:4:9
|
2 | def instance_method(self, x: int) -> int: ...
3 | @classmethod
4 | def class_method(cls, x: int) -> int: ...
| -------------------------------- `Parent.class_method` defined here
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
|
info: `BadChild2.class_method` is a staticmethod but `Parent.class_method` is a classmethod
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `static_method`
--> src/mdtest_snippet.pyi:32:9
|
30 | def class_method(cls, x: int) -> int: ... # error: [invalid-method-override]
31 | @classmethod
32 | def static_method(x: int) -> int: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method`
33 |
34 | class BadChild3(Parent):
|
::: src/mdtest_snippet.pyi:6:9
|
4 | def class_method(cls, x: int) -> int: ...
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
| ---------------------------- `Parent.static_method` defined here
7 |
8 | class BadChild1(Parent):
|
info: `BadChild2.static_method` is a classmethod but `Parent.static_method` is a staticmethod
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `class_method`
--> src/mdtest_snippet.pyi:36:9
|
34 | class BadChild3(Parent):
35 | @classmethod
36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.class_method`
37 | @staticmethod
38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override]
|
::: src/mdtest_snippet.pyi:4:9
|
2 | def instance_method(self, x: int) -> int: ...
3 | @classmethod
4 | def class_method(cls, x: int) -> int: ...
| -------------------------------- `Parent.class_method` defined here
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `static_method`
--> src/mdtest_snippet.pyi:38:9
|
36 | def class_method(cls, x: bool) -> object: ... # error: [invalid-method-override]
37 | @staticmethod
38 | def static_method(x: bool) -> object: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.static_method`
39 |
40 | class GoodChild1(Parent):
|
::: src/mdtest_snippet.pyi:6:9
|
4 | def class_method(cls, x: int) -> int: ...
5 | @staticmethod
6 | def static_method(x: int) -> int: ...
| ---------------------------- `Parent.static_method` defined here
7 |
8 | class BadChild1(Parent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,116 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - Synthesized methods
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## mdtest_snippet.pyi
```
1 | from dataclasses import dataclass
2 | from typing import NamedTuple
3 |
4 | @dataclass(order=True)
5 | class Foo:
6 | x: int
7 |
8 | class Bar(Foo):
9 | def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
10 |
11 | # TODO: specifying `order=True` on the subclass means that a `__lt__` method is
12 | # generated that is incompatible with the generated `__lt__` method on the superclass.
13 | # We could consider detecting this and emitting a diagnostic, though maybe it shouldn't
14 | # be `invalid-method-override` since we'd emit it on the class definition rather than
15 | # on any method definition. Note also that no other type checker complains about this
16 | # as of 2025-11-21.
17 | @dataclass(order=True)
18 | class Bar2(Foo):
19 | y: str
20 |
21 | # TODO: Although this class does not override any methods of `Foo`, the design of the
22 | # `order=True` stdlib dataclasses feature itself arguably violates the Liskov Substitution
23 | # Principle! Instances of `Bar3` cannot be substituted wherever an instance of `Foo` is
24 | # expected, because the generated `__lt__` method on `Foo` raises an error unless the r.h.s.
25 | # and `l.h.s.` have exactly the same `__class__` (it does not permit instances of `Foo` to
26 | # be compared with instances of subclasses of `Foo`).
27 | #
28 | # Many users would probably like their type checkers to alert them to cases where instances
29 | # of subclasses cannot be substituted for instances of superclasses, as this violates many
30 | # assumptions a type checker will make and makes it likely that a type checker will fail to
31 | # catch type errors elsewhere in the user's code. We could therefore consider treating all
32 | # `order=True` dataclasses as implicitly `@final` in order to enforce soundness. However,
33 | # this probably shouldn't be reported with the same error code as Liskov violations, since
34 | # the error does not stem from any method signatures written by the user. The example is
35 | # only included here for completeness.
36 | #
37 | # Note that no other type checker catches this error as of 2025-11-21.
38 | class Bar3(Foo): ...
39 |
40 | class Eggs:
41 | def __lt__(self, other: Eggs) -> bool: ...
42 |
43 | # TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
44 | # We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
45 | # diagnostic here but pyright and pyrefly do not.
46 | @dataclass(order=True)
47 | class Ham(Eggs):
48 | x: int
49 |
50 | class Baz(NamedTuple):
51 | x: int
52 |
53 | class Spam(Baz):
54 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `__lt__`
--> src/mdtest_snippet.pyi:9:9
|
8 | class Bar(Foo):
9 | def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Foo.__lt__`
10 |
11 | # TODO: specifying `order=True` on the subclass means that a `__lt__` method is
|
info: This violates the Liskov Substitution Principle
info: `Foo.__lt__` is a generated method created because `Foo` is a dataclass
--> src/mdtest_snippet.pyi:5:7
|
4 | @dataclass(order=True)
5 | class Foo:
| ^^^ Definition of `Foo`
6 | x: int
|
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `_asdict`
--> src/mdtest_snippet.pyi:54:9
|
53 | class Spam(Baz):
54 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Baz._asdict`
|
info: This violates the Liskov Substitution Principle
info: `Baz._asdict` is a generated method created because `Baz` inherits from `typing.NamedTuple`
--> src/mdtest_snippet.pyi:50:7
|
48 | x: int
49 |
50 | class Baz(NamedTuple):
| ^^^^^^^^^^^^^^^ Definition of `Baz`
51 | x: int
|
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -0,0 +1,192 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: liskov.md - The Liskov Substitution Principle - The entire class hierarchy is checked
mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
---
# Python source files
## stub.pyi
```
1 | from typing import Any
2 |
3 | class Grandparent:
4 | def method(self, x: int) -> None: ...
5 |
6 | class Parent(Grandparent):
7 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
8 |
9 | class Child(Parent):
10 | # compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
11 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
12 |
13 | class OtherChild(Parent):
14 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
15 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
16 |
17 | class GradualParent(Grandparent):
18 | def method(self, x: Any) -> None: ...
19 |
20 | class ThirdChild(GradualParent):
21 | # `GradualParent.method` is compatible with the signature of `Grandparent.method`,
22 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
23 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
24 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
```
## other_stub.pyi
```
1 | class A:
2 | def get(self, default): ...
3 |
4 | class B(A):
5 | def get(self, default, /): ... # error: [invalid-method-override]
6 |
7 | get = 56
8 |
9 | class C(B):
10 | # `get` appears in the symbol table of `C`,
11 | # but that doesn't confuse our diagnostic...
12 | foo = get
13 |
14 | class D(C):
15 | # compatible with `C.get` and `B.get`, but not with `A.get`
16 | def get(self, my_default): ... # error: [invalid-method-override]
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:4:9
|
3 | class Grandparent:
4 | def method(self, x: int) -> None: ...
| ---------------------------- `Grandparent.method` defined here
5 |
6 | class Parent(Grandparent):
7 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method`
8 |
9 | class Child(Parent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:11:9
|
9 | class Child(Parent):
10 | # compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
11 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method`
12 |
13 | class OtherChild(Parent):
|
::: src/stub.pyi:4:9
|
3 | class Grandparent:
4 | def method(self, x: int) -> None: ...
| ---------------------------- `Grandparent.method` defined here
5 |
6 | class Parent(Grandparent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:15:9
|
13 | class OtherChild(Parent):
14 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
15 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method`
16 |
17 | class GradualParent(Grandparent):
|
::: src/stub.pyi:7:9
|
6 | class Parent(Grandparent):
7 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ---------------------------- `Parent.method` defined here
8 |
9 | class Child(Parent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `method`
--> src/stub.pyi:24:9
|
22 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
23 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
24 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method`
|
::: src/stub.pyi:4:9
|
3 | class Grandparent:
4 | def method(self, x: int) -> None: ...
| ---------------------------- `Grandparent.method` defined here
5 |
6 | class Parent(Grandparent):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `get`
--> src/other_stub.pyi:2:9
|
1 | class A:
2 | def get(self, default): ...
| ------------------ `A.get` defined here
3 |
4 | class B(A):
5 | def get(self, default, /): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `A.get`
6 |
7 | get = 56
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `get`
--> src/other_stub.pyi:16:9
|
14 | class D(C):
15 | # compatible with `C.get` and `B.get`, but not with `A.get`
16 | def get(self, my_default): ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `A.get`
|
::: src/other_stub.pyi:2:9
|
1 | class A:
2 | def get(self, default): ...
| ------------------ `A.get` defined here
3 |
4 | class B(A):
|
info: This violates the Liskov Substitution Principle
info: rule `invalid-method-override` is enabled by default
```

View File

@@ -303,13 +303,13 @@ info: rule `duplicate-base` is enabled by default
```
```
info[unused-ignore-comment]
info[unused-ignore-comment]: Unused blanket `type: ignore` directive
--> src/mdtest_snippet.py:72:9
|
70 | A,
71 | # error: [unused-ignore-comment]
72 | A, # type: ignore[duplicate-base]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unused blanket `type: ignore` directive
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
73 | ): ...
|
@@ -346,13 +346,13 @@ info: rule `duplicate-base` is enabled by default
```
```
info[unused-ignore-comment]
info[unused-ignore-comment]: Unused blanket `type: ignore` directive
--> src/mdtest_snippet.py:81:13
|
79 | ):
80 | # error: [unused-ignore-comment]
81 | x: int # type: ignore[duplicate-base]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unused blanket `type: ignore` directive
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 |
83 | # fmt: on
|

View File

@@ -12,27 +12,59 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md
## mdtest_snippet.py
```
1 | class NotBoolable:
2 | __bool__: None = None
3 |
4 | class A:
5 | def __eq__(self, other) -> NotBoolable:
6 | return NotBoolable()
7 |
8 | # error: [unsupported-bool-conversion]
9 | (A(),) == (A(),)
1 | class NotBoolable:
2 | __bool__: None = None
3 |
4 | class A:
5 | # error: [invalid-method-override]
6 | def __eq__(self, other) -> NotBoolable:
7 | return NotBoolable()
8 |
9 | # error: [unsupported-bool-conversion]
10 | (A(),) == (A(),)
```
# Diagnostics
```
error[invalid-method-override]: Invalid override of method `__eq__`
--> src/mdtest_snippet.py:6:9
|
4 | class A:
5 | # error: [invalid-method-override]
6 | def __eq__(self, other) -> NotBoolable:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `object.__eq__`
7 | return NotBoolable()
|
::: stdlib/builtins.pyi:142:9
|
140 | def __setattr__(self, name: str, value: Any, /) -> None: ...
141 | def __delattr__(self, name: str, /) -> None: ...
142 | def __eq__(self, value: object, /) -> bool: ...
| -------------------------------------- `object.__eq__` defined here
143 | def __ne__(self, value: object, /) -> bool: ...
144 | def __str__(self) -> str: ... # noqa: Y029
|
info: This violates the Liskov Substitution Principle
help: It is recommended for `__eq__` to work with arbitrary objects, for example:
help
help: def __eq__(self, other: object) -> bool:
help: if not isinstance(other, A):
help: return False
help: return <logic to compare two `A` instances>
help
info: rule `invalid-method-override` is enabled by default
```
```
error[unsupported-bool-conversion]: Boolean conversion is unsupported for type `NotBoolable`
--> src/mdtest_snippet.py:9:1
|
8 | # error: [unsupported-bool-conversion]
9 | (A(),) == (A(),)
| ^^^^^^^^^^^^^^^^
|
--> src/mdtest_snippet.py:10:1
|
9 | # error: [unsupported-bool-conversion]
10 | (A(),) == (A(),)
| ^^^^^^^^^^^^^^^^
|
info: `__bool__` on `NotBoolable` must be callable
info: rule `unsupported-bool-conversion` is enabled by default

View File

@@ -40,6 +40,8 @@ error[unresolved-import]: Cannot resolve imported module `....foo`
2 |
3 | stat = add(10, 15)
|
help: The module can be resolved if the number of leading dots is reduced
help: Did you mean `...foo`?
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)

View File

@@ -174,6 +174,39 @@ def _(x: Foo[int], y: Bar[str], z: list[bytes]):
reveal_type(type(z)) # revealed: type[list[bytes]]
```
## Checking generic `type[]` types
```toml
[environment]
python-version = "3.12"
```
```py
class C[T]:
pass
class D[T]:
pass
var: type[C[int]] = C[int]
var: type[C[int]] = D[int] # error: [invalid-assignment] "Object of type `<class 'D[int]'>` is not assignable to `type[C[int]]`"
```
However, generic `Protocol` classes are still TODO:
```py
from typing import Protocol
class Proto[U](Protocol):
def some_method(self): ...
# TODO: should be error: [invalid-assignment]
var: type[Proto[int]] = C[int]
def _(p: type[Proto[int]]):
reveal_type(p) # revealed: type[@Todo(type[T] for protocols)]
```
## `@final` classes
`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is

View File

@@ -66,12 +66,15 @@ def _[T]() -> None:
reveal_type(ConstraintSet.range(Base, T, object))
```
And a range constraint with _both_ a lower bound of `Never` and an upper bound of `object` does not
constrain the typevar at all.
And a range constraint with a lower bound of `Never` and an upper bound of `object` allows the
typevar to take on any type. We treat this differently than the `always` constraint set. During
specialization inference, that allows us to distinguish between not constraining a typevar (and
therefore falling back on its default specialization) and explicitly constraining it to any subtype
of `object`.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always]
# revealed: ty_extensions.ConstraintSet[(T@_ = *)]
reveal_type(ConstraintSet.range(Never, T, object))
```
@@ -156,7 +159,7 @@ cannot be satisfied at all.
```py
def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[never]
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ *)]
reveal_type(~ConstraintSet.range(Never, T, object))
```
@@ -654,7 +657,7 @@ def _[T]() -> None:
reveal_type(~ConstraintSet.range(Never, T, Base))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_)]
reveal_type(~ConstraintSet.range(Sub, T, object))
# revealed: ty_extensions.ConstraintSet[never]
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ *)]
reveal_type(~ConstraintSet.range(Never, T, object))
```
@@ -811,7 +814,7 @@ def f[T]():
# "domain", which maps valid inputs to `true` and invalid inputs to `false`. This means that two
# constraint sets that are both always satisfied will not be identical if they have different
# domains!
always = ConstraintSet.range(Never, T, object)
always = ConstraintSet.always()
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(always)
static_assert(always)
@@ -846,11 +849,11 @@ from typing import Never
from ty_extensions import ConstraintSet
def same_typevar[T]():
# revealed: ty_extensions.ConstraintSet[always]
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(Never, T, T))
# revealed: ty_extensions.ConstraintSet[always]
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(T, T, object))
# revealed: ty_extensions.ConstraintSet[always]
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(T, T, T))
```
@@ -862,11 +865,11 @@ as shown above.)
from ty_extensions import Intersection
def same_typevar[T]():
# revealed: ty_extensions.ConstraintSet[always]
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(Never, T, T | None))
# revealed: ty_extensions.ConstraintSet[always]
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(Intersection[T, None], T, object))
# revealed: ty_extensions.ConstraintSet[always]
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
reveal_type(ConstraintSet.range(Intersection[T, None], T, T | None))
```
@@ -877,8 +880,8 @@ constraint set can never be satisfied, since every type is disjoint with its neg
from ty_extensions import Not
def same_typevar[T]():
# revealed: ty_extensions.ConstraintSet[never]
# revealed: ty_extensions.ConstraintSet[(T@same_typevar ≠ *)]
reveal_type(ConstraintSet.range(Intersection[Not[T], None], T, object))
# revealed: ty_extensions.ConstraintSet[never]
# revealed: ty_extensions.ConstraintSet[(T@same_typevar ≠ *)]
reveal_type(ConstraintSet.range(Not[T], T, object))
```

View File

@@ -243,13 +243,13 @@ static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar[Unknown]], type[Foo[int]]))
static_assert(is_assignable_to(TypeOf[Bar], type[Foo]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[Any]]))
static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]]))
# TODO: these should pass (all subscripts inside `type[]` type expressions are currently TODO types)
static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) # error: [static-assert-error]
static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # error: [static-assert-error]
static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]]))
static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]]))
```
## `type[]` is not assignable to types disjoint from `builtins.type`

View File

@@ -628,7 +628,7 @@ import imported
from module2 import imported as other_imported
from ty_extensions import TypeOf, static_assert, is_equivalent_to
# error: [unresolved-attribute] "Module `imported` has no member `abc`"
# error: [possibly-missing-attribute]
reveal_type(imported.abc) # revealed: Unknown
reveal_type(other_imported.abc) # revealed: <module 'imported.abc'>

View File

@@ -12,8 +12,8 @@ pub use db::Db;
pub use diagnostic::add_inferred_python_version_hint_to_diagnostic;
pub use module_name::{ModuleName, ModuleNameResolutionError};
pub use module_resolver::{
Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules, list_modules,
resolve_module, resolve_real_module, system_module_search_paths,
KnownModule, Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules,
list_modules, resolve_module, resolve_real_module, system_module_search_paths,
};
pub use program::{
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,

View File

@@ -1,7 +1,7 @@
use std::iter::FusedIterator;
pub use list::{all_modules, list_modules};
pub(crate) use module::KnownModule;
pub use module::KnownModule;
pub use module::Module;
pub use path::{SearchPath, SearchPathValidationError};
pub use resolver::SearchPaths;

View File

@@ -67,7 +67,7 @@ impl<'db> Module<'db> {
}
/// Does this module represent the given known module?
pub(crate) fn is_known(self, db: &'db dyn Database, known_module: KnownModule) -> bool {
pub fn is_known(self, db: &'db dyn Database, known_module: KnownModule) -> bool {
self.known(db) == Some(known_module)
}

View File

@@ -1376,9 +1376,7 @@ mod implicit_globals {
use crate::place::{Definedness, PlaceAndQualifiers, TypeOrigin};
use crate::semantic_index::symbol::Symbol;
use crate::semantic_index::{place_table, use_def_map};
use crate::types::{
CallableType, KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type,
};
use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type};
use ruff_python_ast::PythonVersion;
use super::{Place, place_from_declarations};
@@ -1461,7 +1459,7 @@ mod implicit_globals {
)),
);
Place::Defined(
CallableType::function_like(db, signature),
Type::function_like_callable(db, signature),
TypeOrigin::Inferred,
Definedness::PossiblyUndefined,
)

View File

@@ -90,6 +90,12 @@ impl<'db> Definition<'db> {
.to_string(),
)
}
DefinitionKind::Assignment(assignment) => {
let target_node = assignment.target.node(&module);
target_node
.as_name_expr()
.map(|name_expr| name_expr.id.as_str().to_string())
}
_ => None,
}
}
@@ -364,10 +370,12 @@ pub(crate) struct ImportFromDefinitionNodeRef<'ast> {
pub(crate) alias_index: usize,
pub(crate) is_reexported: bool,
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct ImportFromSubmoduleDefinitionNodeRef<'ast> {
pub(crate) node: &'ast ast::StmtImportFrom,
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct AssignmentDefinitionNodeRef<'ast, 'db> {
pub(crate) unpack: Option<(UnpackPosition, Unpack<'db>)>,
@@ -702,7 +710,7 @@ impl DefinitionKind<'_> {
match self {
DefinitionKind::Import(import) => import.is_reexported(),
DefinitionKind::ImportFrom(import) => import.is_reexported(),
DefinitionKind::ImportFromSubmodule(_) => false,
DefinitionKind::ImportFromSubmodule(_) => true,
_ => true,
}
}
@@ -735,6 +743,10 @@ impl DefinitionKind<'_> {
matches!(self, DefinitionKind::Assignment(_))
}
pub(crate) const fn is_function_def(&self) -> bool {
matches!(self, DefinitionKind::Function(_))
}
/// Returns the [`TextRange`] of the definition target.
///
/// A definition target would mainly be the node representing the place being defined i.e.,

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