Compare commits

...

79 Commits

Author SHA1 Message Date
Alex Waygood
4c86b40ee1 [ty] Always infer the submodule literal for submodule attribute-access, even when we emit a diagnostic 2025-11-24 19:34:31 +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
Andrew Gallant
1af318534a [ty] Add support for relative import completions
We already supported `from .. import <CURSOR>`, but we didn't support
`from ..<CURSOR>`. This adds support for that.
2025-11-21 08:01:02 -05:00
Andrew Gallant
553e568624 [ty] Refactor detection of import statements for completions
This commit essentially does away of all our old heuristic and piecemeal
code for detecting different kinds of import statements. Instead, we
offer one single state machine that does everything. This on its own
fixes a few bugs. For example, `import collections.abc, unico<CURSOR>`
would previously offer global scope completions instead of module
completions.

For the most part though, this commit is a refactoring that preserves
parity. In the next commit, we'll add support for completions on
relative imports.
2025-11-21 08:01:02 -05:00
Andrew Gallant
cdef3f5ab8 [ty] Use dedicated collector for completions
This is a small refactor that helps centralize the
logic for how we gather, convert and possibly filter
completions.

Some of this logic was spread out before, which
motivated this refactor. Moreover, as part of other
refactoring, I found myself chaffing against the
lack of this abstraction.
2025-11-21 08:01:02 -05:00
Alex Waygood
6178822427 [ty] Attach subdiagnostics to unresolved-import errors for relative imports as well as absolute imports (#21554) 2025-11-21 12:40:53 +00:00
Carl Meyer
6b7adb0537 [ty] support PEP 613 type aliases (#21394)
Refs https://github.com/astral-sh/ty/issues/544

## Summary

Takes a more incremental approach to PEP 613 type alias support (vs
https://github.com/astral-sh/ruff/pull/20107). Instead of eagerly
inferring the RHS of a PEP 613 type alias as a type expression, infer it
as a value expression, just like we do for implicit type aliases, taking
advantage of the same support for e.g. unions and other type special
forms.

The main reason I'm following this path instead of the one in
https://github.com/astral-sh/ruff/pull/20107 is that we've realized that
people do sometimes use PEP 613 type aliases as values, not just as
types (because they are just a normal runtime assignment, unlike PEP 695
type aliases which create an opaque `TypeAliasType`).

This PR doesn't yet provide full support for recursive type aliases
(they don't panic, but they just fall back to `Unknown` at the recursion
point). This is future work.

## Test Plan

Added mdtests.

Many new ecosystem diagnostics, mostly because we
understand new types in lots of places.

Conformance suite changes are correct.

Performance regression is due to understanding lots of new
types; nothing we do in this PR is inherently expensive.
2025-11-20 17:59:35 -08:00
Alex Waygood
06941c1987 [ty] More low-hanging fruit for inlay hint goto-definition (#21548) 2025-11-20 23:15:59 +00:00
Jack O'Connor
eb7c098d6b [ty] implement TypedDict structural assignment (#21467)
Closes https://github.com/astral-sh/ty/issues/1387.
2025-11-20 13:15:28 -08:00
Aria Desires
1b28fc1f14 [ty] Add more random TypeDetails and tests (#21546) 2025-11-20 19:46:17 +00:00
Alex Waygood
290a5720cb [ty] Add goto for Unknown when it appears in an inlay hint (#21545) 2025-11-20 18:55:14 +00:00
Alex Waygood
c4767f5aa8 [ty] Add type definitions for Type::SpecialForms (#21544) 2025-11-20 18:14:30 +00:00
Aria Desires
6e84f4fd7a [ty] Resolve overloads for hovers (#21417)
This is a very conservative minimal implementation of applying overloads
to resolve a callable-type-being-called down to a single function
signature on hover. If we ever encounter a situation where the answer
doesn't simplify down to a single function call, we bail out to preserve
prettier printing of non-raw-Signatures.

The resulting Signatures are still a bit bare, I'm going to try to
improve that in a followup to improve our Signature printing in general.

Fixes https://github.com/astral-sh/ty/issues/73
2025-11-20 12:45:02 -05:00
Aria Desires
78ce17ce8f [ty] Add more TypeDetails to the display code (#21541)
As far as I know this change is largely non-functional, largely because
of https://github.com/astral-sh/ty/issues/1601

It's possible some of these like `Type::KnownInstance` produce something
useful sometimes. `LiteralString` is a new introduction, although its
goto-type jumps to `str` which is a bit sad (considering that part of
the SpecialForm discourse for now).

Also wrt the generics testing followup: turns out the snapshot tests
were full of those already.
2025-11-20 12:08:59 -05:00
David Peter
0761ea42d9 [ty] Eagerly evaluate types.UnionType elements as type expressions (#21531)
## Summary

Eagerly evaluate the elements of a PEP 604 union in value position (e.g.
`IntOrStr = int | str`) as type expressions and store the result (the
corresponding `Type::Union` if all elements are valid type expressions,
or the first encountered `InvalidTypeExpressionError`) on the
`UnionTypeInstance`, such that the `Type::Union(…)` does not need to be
recomputed every time the implicit type alias is used in a type
annotation.

This might lead to performance improvements for large unions, but is
also necessary for correctness, because the elements of the union might
refer to type variables that need to be looked up in the scope of the
type alias, not at the usage site.

## Test Plan

New Markdown tests
2025-11-20 17:28:48 +01:00
Aria Desires
416e2267da [ty] Implement goto-type for inlay type hints (#21533)
This PR generalizes the signature_help system's SignatureWriter which
could get the subspans of function parameters.
We now have TypeDetailsWriter which is threaded between type's display
implementations via a new `fmt_detailed` method that many of the Display
types now have.

With this information we can properly add goto-type targets to our inlay
hints. This also lays groundwork for any future "I want to render a type
but get spans" work.

Also a ton of lifetimes are introduced to avoid things getting conflated
with `'db`.

This PR is broken up into a series of commits:

* Generalizing `SignatureWriter` to `TypeDetailsWriter`, but not using
it anywhere else. This commit was confirmed to be a non-functional
change (no test results changed)
* Introducing `fmt_detailed` everywhere to thread through
`TypeDetailsWriter` and annotate various spans as "being" a given Type
-- this is also where I had to reckon with a ton of erroneous `&'db
self`. This commit was also confirmed to be a non-functional change.
* Finally, actually using the results for goto-type on inlay hints!
* Regenerating snapshots, fixups, etc.
2025-11-20 09:40:40 -05:00
David Peter
02c102da88 [ty] Add tests: types.UnionType in isinstance/issubclass (#21537)
## Summary

Add some tests documenting the fact that we don't support
`types.UnionType` in `isinstance`/`issubclass` at the moment.
2025-11-20 11:59:36 +00:00
David Peter
29c24bc8a6 [ty] Ecosystem analyzer: support custom ty_cmds (#21534)
## Summary

Pulls in
e26ebfb78d
in order to support some projects that now require a custom `ty_cmd`.

## Test Plan

CI on this PR
2025-11-20 09:10:56 +01:00
Douglas Creager
83134fb380 [ty] Handle nested types when creating specializations from constraint sets (#21530)
#21414 added the ability to create a specialization from a constraint
set. It handled mutually constrained typevars just fine, e.g. given `T ≤
int ∧ U = T` we can infer `T = int, U = int`.

But it didn't handle _nested_ constraints correctly, e.g. `T ≤ int ∧ U =
list[T]`. Now we do! This requires doing a fixed-point "apply the
specialization to itself" step to propagate the assignments of any
nested typevars, and then a cycle detection check to make sure we don't
have an infinite expansion in the specialization.

This gets at an interesting nuance in our constraint set structure that
@sharkdp has asked about before. Constraint sets are BDDs, and each
internal node represents an _individual constraint_, of the form `lower
≤ T ≤ upper`. `lower` and `upper` are allowed to be other typevars, but
only if they appear "later" in the arbitary ordering that we establish
over typevars. The main purpose of this is to avoid infinite expansion
for mutually constrained typevars.

However, that restriction doesn't help us here, because only applies
when `lower` and `upper` _are_ typevars, not when they _contain_
typevars. That distinction is important, since it means the restriction
does not affect our expressiveness: we can always rewrite `Never ≤ T ≤
U` (a constraint on `T`) into `T ≤ U ≤ object` (a constraint on `U`).
The same is not true of `Never ≤ T ≤ list[U]` — there is no "inverse" of
`list` that we could apply to both sides to transform this into a
constraint on a bare `U`.
2025-11-19 17:37:16 -05:00
Dan Parizher
0d47334f3b [flake8-bandit] Support new PySNMP API paths (S508, S509) (#21374)
## Summary

Updated `S508` (snmp-insecure-version) and `S509`
(snmp-weak-cryptography) rules to support both old and new PySNMP API
module paths. Previously, these rules only detected the old API path
`pysnmp.hlapi.*`, but now they correctly detect all PySNMP API variants
including `pysnmp.hlapi.asyncio.*`, `pysnmp.hlapi.v1arch.*`,
`pysnmp.hlapi.v3arch.*`, and `pysnmp.hlapi.auth.*`.

Fixes #21364

## Problem Analysis

The `S508` and `S509` rules used exact pattern matching on qualified
names:
- `S509` only matched `["pysnmp", "hlapi", "UsmUserData"]`
- `S508` only matched `["pysnmp", "hlapi", "CommunityData"]`

This meant that newer PySNMP API paths were not detected, such as:
- `pysnmp.hlapi.asyncio.UsmUserData`
- `pysnmp.hlapi.v3arch.asyncio.UsmUserData`
- `pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData`
- `pysnmp.hlapi.auth.UsmUserData`
- Similar variants for `CommunityData` in `S508`

Additionally, the old API path `pysnmp.hlapi.auth.*` was also missing
from both rules.

## Approach

Instead of exact pattern matching, both rules now check if:
1. The qualified name starts with `["pysnmp", "hlapi"]`
2. The qualified name ends with the target class name (`"UsmUserData"`
for `S509`, `"CommunityData"` for `S508`)

This flexible approach matches all PySNMP API paths without hardcoding
each variant, making the rules more maintainable and future-proof.

## Test Plan

Added comprehensive test cases to both `S508.py` and `S509.py` test
files covering:
- New API paths: `pysnmp.hlapi.asyncio.*`, `pysnmp.hlapi.v1arch.*`,
`pysnmp.hlapi.v3arch.*`
- Old API path: `pysnmp.hlapi.auth.*`
- Both insecure and secure usage patterns

All existing tests pass, and new snapshot tests were added and accepted.
Manual verification confirms both rules correctly detect all PySNMP API
variants.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-11-19 15:03:23 -05:00
Alex Waygood
a8f7ccf2ca [ty] Improve diagnostics when NotImplemented is called (#21523)
## Summary

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

I realised I was overcomplicating things when I described what we should
do in that issue description. The simplest thing to do here is just to
special-case call expressions and short-circuit the call-binding
machinery entirely if we see it's `NotImplemented` being called. It
doesn't really matter if the subdiagnostic doesn't fire when a union is
called and one element of the union is `NotImplemented` -- the
subdiagnostic doesn't need to be exhaustive; it's just to help people in
some common cases.

## Test Plan

Added snapshots
2025-11-19 19:27:12 +00:00
Alex Waygood
ce06094ada [ty] Remove unnecessary .expect() call from types/instance.rs (#21527)
## Summary

The `.expect()` call here:


5dd56264fb/crates/ty_python_semantic/src/types/instance.rs (L816-L827)

is the direct cause of the panic in
https://github.com/astral-sh/ty/issues/1587. This patch gets rid of the
panic by refactoring our `Protocol` enum so that the
`Protocol::FromClass` variant holds a `ProtocolClass` instance rather
than a `ClassType` instance (all the `.expect()` call was doing was
attempting to convert form a `ClassType` to a `ProtocolClass`).

I hoped that this would provide a fix for
https://github.com/astral-sh/ty/issues/1587, but we still panic on the
provided reproducible examples in that issue even with this PR.
Nonetheless, I think this PR is a worthwhile change to make because:
- It's probably slightly more efficient this way (we no longer have to
re-verify that the wrapped class in a `Protocol::FromClass()` variant is
a protocol class every time we want to access its interface)
- It's nice to get rid of `.expect()` calls where possible, and this one
seems definitely unnecessary
- The _new_ panic message on this PR branch makes it much clearer what
the underlying cause of the bug in
https://github.com/astral-sh/ty/issues/1587 is:

    <details>
    <summary>New panic message</summary>

    ```
error[panic]: Panicked at
/Users/alexw/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a885bb4/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+60 (18a14bfaf 2025-11-19)
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:790
	   1: is_equivalent_to_object_inner(Id(8003))
	             at crates/ty_python_semantic/src/types/instance.rs:667
	   2: infer_deferred_types(Id(1409))
	             at crates/ty_python_semantic/src/types/infer.rs:141
cycle heads: infer_definition_types(Id(140b)) -> iteration = 200,
TypeVarInstance < 'db >::lazy_bound_(Id(5803)) -> iteration = 200
	   3: TypeVarInstance < 'db >::lazy_bound_(Id(5802))
	             at crates/ty_python_semantic/src/types.rs:8734
	   4: infer_definition_types(Id(140c))
	             at crates/ty_python_semantic/src/types/infer.rs:94
	   5: infer_deferred_types(Id(140a))
	             at crates/ty_python_semantic/src/types/infer.rs:141
	   6: TypeVarInstance < 'db >::lazy_bound_(Id(5803))
	             at crates/ty_python_semantic/src/types.rs:8734
	   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
	
	
	Found 1 diagnostic
WARN A fatal error occurred while checking some files. Not all project
files were analyzed. See the diagnostics list above for details.
    ```

    </details>

## Test Plan

All existing tests pass.
2025-11-19 19:26:36 +00:00
Douglas Creager
97935518e9 [ty] Create a specialization from a constraint set (#21414)
This patch lets us create specializations from a constraint set. The
constraint encodes the restrictions on which types each typevar can
specialize to. Given a generic context and a constraint set, we iterate
through all of the generic context's typevars. For each typevar, we
abstract the constraint set so that it only mentions the typevar in
question (propagating derived facts if needed). We then find the "best
representative type" for the typevar given the abstracted constraint
set.

When considering the BDD structure of the abstracted constraint set,
each path from the BDD root to the `true` terminal represents one way
that the constraint set can be satisfied. (This is also one of the
clauses in the DNF representation of the constraint set's boolean
formula.) Each of those paths is the conjunction of the individual
constraints of each internal node that we traverse as we walk that path,
giving a single lower/upper bound for the path. We use the upper bound
as the "best" (i.e. "closest to `object`") type for that path.

If there are multiple paths in the BDD, they technically represent
independent possible specializations. If there's a single specialization
that satisfies all of them, we will return that as the specialization.
If not, then the constraint set is ambiguous. (This happens most often
with constrained typevars.) We could in the future turn _each_ of the
paths into separate specializations, but it's not clear what we would do
with that, so instead we just report the ambiguity as a specialization
failure.
2025-11-19 14:20:33 -05:00
Douglas Creager
68ebd5132c [ty] Only normalize constraint bounds for display (#21516)
We were previously normalizing the upper and lower bounds of each
constraint when constructing constraint sets. Like in #21463, this was
for conflated reasons: It made constraint set displays nicer, since we
wouldn't render multiple constraints with obviously equivalent bounds.
(Think `T ≤ A & B` and `T ≤ B & A`) But it was also useful for
correctness, since prior to #21463 we were (trying to) add the full
transitive closure to a constraint set's BDD, and normalization gave a
useful reduction in the number of nodes in a typical BDD.

Now that we don't store the transitive closure explicitly, that second
reason is no longer relevant. Our sequent map can store that full
transitive closure much more efficiently than the expanded BDD would
have. This helps fix some false positives on #20933, where we're seeing
some (incorrect, need to be fixed, but ideally not blocking this effort)
assignability failures between a type and its normalization.

Normalization is still useful for display purposes, and so we do
normalize the upper/lower bounds before building up our display
representation of a constraint set BDD.

---------

Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
2025-11-19 11:49:47 -05:00
Douglas Creager
ac9c83e581 [ty] Fix flaky tests on macos (#21524)
We're seeing flaky test failures on macos, which seems to be caused by
different Salsa ID orderings on the different platforms. Constraint set
BDDs order their internal nodes based on the Salsa IDs of the interned
typevar structs, and we had some code that depended on variable ordering
in an unexpected way.

This patch definitely fixes the macos test failure on #21414, and
hopefully fixes it on #21436, too.
2025-11-19 09:44:32 -05:00
David Peter
5dd56264fb [ty] Add tests for generic implicit type aliases (#21522)
## Summary

Add a set of comprehensive tests for generic implicit type aliases to
illustrate the current behavior with many flavors of `@Todo` types and
false positive diagnostics.

The tests are partially based on the typing conformance suite, and the
expected behavior has been checked against other type checkers.
2025-11-19 14:06:18 +00:00
Luca Chiodini
18a14bfaf1 [ty] Semantic tokens: consistently add the DEFINITION modifier (#21521) 2025-11-19 12:45:39 +01:00
Micha Reiser
ffce0de3c4 Only render hyperlinks for terminals known to support them (#21519) 2025-11-19 10:02:58 +01:00
David Peter
e29f3ed0fe [ty] Keep colorizing mypy_primer output (#21515)
## Summary

After an update to `mypy_primer`, we now need to set the environment
variable ourselves.
2025-11-19 08:52:51 +00:00
Micha Reiser
663f78e644 [ty] Exit with 2 if there's any IO error (#21508)
Co-authored-by: David Peter <mail@david-peter.de>
2025-11-19 09:39:19 +01:00
Dan Parizher
c796a70ec9 [ruff] Fix false positive for complex conversion specifiers in logging-eager-conversion (RUF065) (#21464)
Co-authored-by: Amethyst Reese <amethyst@n7.gg>
2025-11-19 09:38:33 +01:00
Carl Meyer
192c37d540 [ty] tighten up handling of subscripts in type expressions (#21503)
## Summary

Get rid of the catch-all todo type from subscripting a base type we
haven't implemented handling for yet in a type expression, and turn it
into a diagnostic instead.

Handle a few more cases explicitly, to avoid false positives from the
above change:
1. Subscripting any dynamic type (not just a todo type) in a type
expression should just result in that same dynamic type. This is
important for gradual guarantee, and matches other type checkers.
2. Subscripting a generic alias may be an error or not, depending
whether the specialization itself contains typevars. Don't try to handle
this yet (it should be handled in a later PR for specializing generic
non-PEP695 type aliases), just use a dedicated todo type for it.
3. Add a temporary todo branch to avoid false positives from string PEP
613 type aliases. This can be removed in the next PR, with PEP 613 type
alias support.

## Test Plan

Adjusted mdtests, ecosystem.

All new diagnostics in conformance suite are supposed to be diagnostics,
so this PR is a strict improvement there.

New diagnostics in the ecosystem are surfacing cases where we already
don't understand an annotation, but now we emit a diagnostic about it.
They are mostly intentional choices. Analysis of particular cases:

* `attrs`, `bokeh`, `django-stubs`, `dulwich`, `ibis`, `kornia`,
`mitmproxy`, `mongo-python-driver`, `mypy`, `pandas`, `poetry`,
`prefect`, `pydantic`, `pytest`, `scrapy`, `trio`, `werkzeug`, and
`xarray` are all cases where under `from __future__ import annotations`
or Python 3.14 deferred-annotations semantics, we follow normal
name-scoping rules, whereas some other type checkers prefer global names
over local names. This means we don't like it if e.g. you have a class
with a method or attribute named `type` or `tuple`, and you also try to
use `type` or `tuple` in method/attribute annotations of that class.
This PR isn't changing those semantics, just revealing them in more
cases where previously we just silently fell back to `Unknown`. I think
failing with a diagnostic (so authors can alias names as needed to avoid
relying on scoping rules that differ between type checkers) is better
than failing silently here.
* `beartype` assumes we support `TypeForm` (because it only supports
mypy and pyright, it uses `if MYPY:` to hide the `TypeForm` from mypy,
and pyright supports `TypeForm`), and we don't yet.
* `graphql-core` likes to use a `try: ... except ImportError: ...`
pattern for importing special forms from `typing` with fallback to
`typing_extensions`, instead of using `sys.version_info` checks. We
don't handle this well when type checking under an older Python version
(where the import from `typing` is not found); we see the imported name
as of type e.g. `Unknown | SpecialFormType(...)`, and because of the
union with `Unknown` we fail to handle it as the special form type. Mypy
and pyright also don't seem to support this pattern. They don't complain
about subscripting such special forms, but they do silently fail to
treat them as the desired special form. Again here, if we are going to
fail I'd rather fail with a diagnostic rather than silently.
* `ibis` is [trying to
use](https://github.com/ibis-project/ibis/blob/main/ibis/common/collections.py#L372)
`frozendict: type[FrozenDict]` as a way to create a "type alias" to
`FrozenDict`, but this is wrong: that means `frozendict:
type[FrozenDict[Any, Any]]`.
* `mypy` has some errors due to the fact that type-checking `typing.pyi`
itself (without knowing that it's the real `typing.pyi`) doesn't work
very well.
* `mypy-protobuf` imports some types from the protobufs library that end
up unioned with `Unknown` for some reason, and so we don't allow
explicit-specialization of them. Depending on the reason they end up
unioned with `Unknown`, we might want to better support this? But it's
orthogonal to this PR -- we aren't failing any worse here, just alerting
the author that we didn't understand their annotation.
* `pwndbg` has unresolved references due to star-importing from a
dependency that isn't installed, and uses un-imported names like `Dict`
in annotation expressions. Some of the unresolved references were hidden
by
https://github.com/astral-sh/ruff/blob/main/crates/ty_python_semantic/src/types/infer/builder.rs#L7223-L7228
when some annotations previously resolved to a Todo type that no longer
do.
2025-11-18 10:43:07 -08:00
Brent Westbrook
0645418f00 Set the diagnostic URL for lint errors (#21514)
Summary
--

This PR wires up the `Diagnostic::set_documentation_url` method from
#21502 to Ruff's lint diagnostics. This enables the links for the full
and concise output formats without any other changes.

I considered also including the URLs for the grouped and pylint output
formats, but the grouped format is still in `ruff_linter` instead of
`ruff_db`, so we'd have to export some additional functionality to wire
it up with `fmt_with_hyperlink`; and the pylint format doesn't currently
render with color, so I think it might actually be machine readable
rather than human readable?

The other ouput formats (json, json-lines, junit, github, gitlab,
rdjson, azure, sarif) seem more clearly not to need the links.

Test Plan
--

I guess you can't see my cursor or the browser opening, but it works for
lint rules, which have links, and doesn't include a link for syntax
errors, which don't have valid links.


![out](https://github.com/user-attachments/assets/a520c7f9-6d7b-4e5f-a1a9-3c5e21a51d3c)
2025-11-18 13:34:50 -05:00
Dylan
62343a101a Respect fmt: skip for compound statements on single line (#20633)
Closes #11216

Essentially the approach is to implement `Format` for a new struct
`FormatClause` which is just a clause header _and_ its body. We then
have the information we need to see whether there is a skip suppression
comment on the last child in the body and it all fits on one line.
2025-11-18 12:02:09 -06:00
Alex Waygood
8dad289062 [ty] Add Salsa caching to ClassLiteral::fields (#21512) 2025-11-18 17:48:36 +00:00
Douglas Creager
f67236b932 [ty] Better handling of "derived information" in constraint sets (#21463)
This saga began with a regression in how we handle constraint sets where
a typevar is constrained by another typevar, which #21068 first added
support for:

```py
def mutually_constrained[T, U]():
    # If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well.
    given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int)
    static_assert(given_int.implies_subtype_of(T, int))
```

While working on #21414, I saw a regression in this test, which was
strange, since that PR has nothing to do with this logic! The issue is
that something in that PR made us instantiate the typevars `T` and `U`
in a different order, giving them differently ordered salsa IDs. And
importantly, we use these salsa IDs to define the variable ordering that
is used in our constraint set BDDs. This showed that our "mutually
constrained" logic only worked for one of the two possible orderings.
(We can — and now do — test this in a brute-force way by copy/pasting
the test with both typevar orderings.)

The underlying bug was in our `ConstraintSet::simplify_and_domain`
method. It would correctly detect `(U ≤ T ≤ U) ∧ (U ≤ int)`, because
those two constraints affect different typevars, and from that, infer `T
≤ int`. But it wouldn't detect the equivalent pattern in `(T ≤ U ≤ T) ∧
(U ≤ int)`, since those constraints affect the same typevar. At first I
tried adding that as yet more pattern-match logic in the ever-growing
`simplify_and_domain` method. But doing so caused other tests to start
failing.

At that point, I realized that `simplify_and_domain` had gotten to the
point where it was trying to do too much, and for conflicting consumers.
It was first written as part of our display logic, where the goal is to
remove redundant information from a BDD to make its string rendering
simpler. But we also started using it to add "derived facts" to a BDD. A
derived fact is a constraint that doesn't appear in the BDD directly,
but which we can still infer to be true. Our failing test relies on
derived facts — being able to infer that `T ≤ int` even though that
particular constraint doesn't appear in the original BDD. Before,
`simplify_and_domain` would trace through all of the constraints in a
BDD, figure out the full set of derived facts, and _add those derived
facts_ to the BDD structure. This is brittle, because those derived
facts are not universally true! In our example, `T ≤ int` only holds
along the BDD paths where both `T = U` and `U ≤ int`. Other paths will
test the negations of those constraints, and on those, we _shouldn't_
infer `T ≤ int`. In theory it's possible (and we were trying) to use BDD
operators to express that dependency...but that runs afoul of how we
were simultaneously trying to _remove_ information to make our displays
simpler.

So, I ripped off the band-aid. `simplify_and_domain` is now _only_ used
for display purposes. I have not touched it at all, except to remove
some logic that is definitely not used by our `Display` impl. Otherwise,
I did not want to touch that house of cards for now, since the display
logic is not load-bearing for any type inference logic.

For all non-display callers, we have a new **_sequent map_** data type,
which tracks exactly the same derived information. But it does so (a)
without trying to remove anything from the BDD, and (b) lazily, without
updating the BDD structure.

So the end result is that all of the tests (including the new
regressions) pass, via a more efficient (and hopefully better
structured/documented) implementation, at the cost of hanging onto a
pile of display-related tech debt that we'll want to clean up at some
point.
2025-11-18 12:02:25 -05:00
Brent Westbrook
cbc6863b8c Fix panic when formatting comments in unary expressions (#21501)
## Summary

This is another attempt at https://github.com/astral-sh/ruff/pull/21410
that fixes https://github.com/astral-sh/ruff/issues/19226.

@MichaReiser helped me get something working in a very helpful pairing
session. I pushed one additional commit moving the comments back from
leading comments to trailing comments, which I think retains more of the
input formatting.

I was inspired by Dylan's PR (#21185) to make one of these tables:

<table>
                <thead>
                    <tr>
                    <th scope="col">Input</th>
                    <th scope="col">Main</th>
                    <th scope="col">PR</th>
                    </tr>
                </thead>
                <tbody>
<tr>
<td><pre lang="python">
if (
    not
    # comment
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
    pass
</pre></td>
<td><pre lang="python">
if (
    # comment
    not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
    pass

</pre></td>
<td><pre lang="python">
if (
    not
    # comment
    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
    pass

</pre></td>
</tr>
<tr>
<td><pre lang="python">
if (
    # unary comment
    not
    # operand comment
    (
        # comment
        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
        + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
    )
):
    pass
</pre></td>
<td><pre lang="python">
if (
    # unary comment
    # operand comment
    not (
        # comment
        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
        + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
    )
):
    pass

</pre></td>
<td><pre lang="python">
if (
    # unary comment
    not
    # operand comment
    (
        # comment
        aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
        + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
    )
):
    pass

</pre></td>
</tr>
<tr>
<td><pre lang="python">
if (
    not # comment
    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
    pass
</pre></td>
<td><pre lang="python">
if (  # comment
    not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
    pass

</pre></td>
<td><pre lang="python">
if (
    not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa  # comment
    + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
    pass

</pre></td>
</tr>
</tbody>
            </table>

hopefully it helps even though the snippets are much wider here.

The two main differences are (1) that we now retain own-line comments
between the unary operator and its operand instead of moving these to
leading comments on the operator itself, and (2) that we move
end-of-line comments between the operator and operand to dangling
end-of-line comments on the operand (the last example in the table).

## Test Plan

Existing tests, plus new ones based on the issue. As I noted below, I
also ran the output from main on the unary.py file back through this
branch to check that we don't reformat code from main. This made me feel
a bit better about not preview-gating the changes in this PR.

```shell
> git show main:crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py | ruff format - | ./target/debug/ruff format --diff -
> echo $?
0
```

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Takayuki Maeda <takoyaki0316@gmail.com>
2025-11-18 10:48:14 -05:00
Micha Reiser
7043d51df0 [ty] Add hyperlinks to rule codes in CLI (#21502) 2025-11-18 16:36:59 +01:00
David Peter
5ca9c15fc8 [ty] Better invalid-assignment diagnostics (#21476)
## Summary

Improve the diagnostic range for `invalid-assignment` diagnostics, and
add source annotations for the value and target type.

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

### Before

<img width="836" height="601" alt="image"
src="https://github.com/user-attachments/assets/a48219bb-58a8-4a83-b290-d09ef50ce5f0"
/>

### After

<img width="857" height="742" alt="image"
src="https://github.com/user-attachments/assets/cfcaa4f4-94fb-459e-8d64-97050dfecb50"
/>

## Ecosystem impact

Very good! Due to the wider diagnostic range, we now pick up more `#
type: ignore` directives that were supposed to suppress an invalid
assignment diagnostic.

## Test Plan

New snapshot tests
2025-11-18 14:31:04 +01:00
David Peter
7a739d6b76 [ty] Custom concise diagnostic messages (#21498)
## Summary

This PR proposes that we add a new `set_concise_message` functionality
to our `Diagnostic` construction API. When used, the concise message
that is otherwise auto-generated from the main diagnostic message and
the primary annotation will be overwritten with the custom message.

To understand why this is desirable, let's look at the `invalid-key`
diagnostic. This is how I *want* the full diagnostic to look like:

<img width="620" height="282" alt="image"
src="https://github.com/user-attachments/assets/3bf70f52-9d9f-4817-bc16-fb0ebf7c2113"
/>

However, without the change in this PR, the concise message would have
the following form:

```
error[invalid-key]: Unknown key "Age" for TypedDict `Person`: Unknown key "Age" - did you mean "age"?
```

This duplication is why the full `invalid-key` diagnostic used a main
diagnostic message that is only "Invalid key for TypedDict `Person`", to
make that bearable:

```
error[invalid-key] Invalid key for TypedDict `Person`: Unknown key "Age" - did you mean "age"?
```

This is still less than ideal, *and* we had to make the "full"
diagnostic worse. With the new API here, we have to make no such
compromises. We need to do slightly more work (provide one additional
custom-designed message), but we get to keep the "full" diagnostic that
we actually want, and we can make the concise message more terse and
readable:

```
error[invalid-key] Unknown key "Age" for TypedDict `Person` - did you mean "age"?
```

Similar problems exist for other diagnostics as well (I really want this
for https://github.com/astral-sh/ruff/pull/21476). In this PR, I only
changed `invalid-key` and `type-assertion-failure`.

The PR here is somewhat related to the discussion in
https://github.com/astral-sh/ty/issues/1418, but note that we are
solving a problem that is unrelated to sub-diagnostics.

## Test Plan

Updated tests
2025-11-18 09:35:40 +01:00
David Peter
d5a95ec824 [ty] Implicit type aliases: Add support for Callable (#21496)
## Summary

Add support for `Callable` special forms in implicit type aliases.

## Typing conformance

Four new tests are passing

## Ecosystem impact

* All of the `invalid-type-form` errors are from libraries that use
`mypy_extensions` and do something like `Callable[[NamedArg("x", str)],
int]`.
* A handful of new false positives because we do not support generic
specializations of implicit type aliases, yet. But other
* Everything else looks like true positives or known limitations

## Test Plan

New Markdown tests.
2025-11-18 09:06:05 +01:00
237 changed files with 17784 additions and 4225 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)
@@ -319,11 +319,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 +352,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 +462,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 +497,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 +532,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 +638,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 +697,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 +748,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 +792,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 +947,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 +987,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 +1027,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:
@@ -55,6 +55,7 @@ jobs:
- name: Run mypy_primer
env:
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
CLICOLOR_FORCE: "1"
DIFF_FILE: mypy_primer.diff
run: |
cd ruff
@@ -80,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
@@ -67,7 +67,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e26ebfb78d372b8b091e1cb1d6fc522e135474c1"
ecosystem-analyzer \
--repository ruff \

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
@@ -52,7 +52,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e26ebfb78d372b8b091e1cb1d6fc522e135474c1"
ecosystem-analyzer \
--verbose \

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.

58
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",
@@ -642,7 +642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -651,7 +651,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1016,7 +1016,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -1255,7 +1255,8 @@ checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af"
dependencies = [
"compact_str",
"get-size-derive2",
"hashbrown 0.16.0",
"hashbrown 0.16.1",
"indexmap",
"smallvec",
]
@@ -1352,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",
]
@@ -1563,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",
]
@@ -1698,7 +1699,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2858,7 +2859,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.5"
version = "0.14.6"
dependencies = [
"anyhow",
"argfile",
@@ -3004,6 +3005,7 @@ dependencies = [
"serde",
"serde_json",
"similar",
"supports-hyperlinks",
"tempfile",
"thiserror 2.0.17",
"tracing",
@@ -3115,7 +3117,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.5"
version = "0.14.6"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3125,7 +3127,7 @@ dependencies = [
"fern",
"glob",
"globset",
"hashbrown 0.16.0",
"hashbrown 0.16.1",
"imperative",
"insta",
"is-macro",
@@ -3470,7 +3472,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.5"
version = "0.14.6"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3586,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",
@@ -3610,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",
@@ -3926,10 +3928,16 @@ dependencies = [
]
[[package]]
name = "syn"
version = "2.0.110"
name = "supports-hyperlinks"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
@@ -4454,7 +4462,7 @@ dependencies = [
"drop_bomb",
"get-size2",
"glob",
"hashbrown 0.16.0",
"hashbrown 0.16.1",
"indexmap",
"indoc",
"insta",

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",
@@ -173,6 +173,7 @@ snapbox = { version = "0.6.0", features = [
static_assertions = "1.1.0"
strum = { version = "0.27.0", features = ["strum_macros"] }
strum_macros = { version = "0.27.0" }
supports-hyperlinks = { version = "3.1.0" }
syn = { version = "2.0.55" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }

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

@@ -31,7 +31,7 @@
//! styling.
//!
//! The above snippet has been built out of the following structure:
use crate::snippet;
use crate::{Id, snippet};
use std::cmp::{Reverse, max, min};
use std::collections::HashMap;
use std::fmt::Display;
@@ -189,6 +189,7 @@ impl DisplaySet<'_> {
}
Ok(())
}
fn format_annotation(
&self,
line_offset: usize,
@@ -199,11 +200,13 @@ impl DisplaySet<'_> {
) -> fmt::Result {
let hide_severity = annotation.annotation_type.is_none();
let color = get_annotation_style(&annotation.annotation_type, stylesheet);
let formatted_len = if let Some(id) = &annotation.id {
let id_len = id.id.len();
if hide_severity {
id.len()
id_len
} else {
2 + id.len() + annotation_type_len(&annotation.annotation_type)
2 + id_len + annotation_type_len(&annotation.annotation_type)
}
} else {
annotation_type_len(&annotation.annotation_type)
@@ -256,9 +259,20 @@ impl DisplaySet<'_> {
let annotation_type = annotation_type_str(&annotation.annotation_type);
if let Some(id) = annotation.id {
if hide_severity {
buffer.append(line_offset, &format!("{id} "), *stylesheet.error());
buffer.append(
line_offset,
&format!("{id} ", id = fmt_with_hyperlink(id.id, id.url, stylesheet)),
*stylesheet.error(),
);
} else {
buffer.append(line_offset, &format!("{annotation_type}[{id}]"), *color);
buffer.append(
line_offset,
&format!(
"{annotation_type}[{id}]",
id = fmt_with_hyperlink(id.id, id.url, stylesheet)
),
*color,
);
}
} else {
buffer.append(line_offset, annotation_type, *color);
@@ -707,7 +721,7 @@ impl DisplaySet<'_> {
let style =
get_annotation_style(&annotation.annotation_type, stylesheet);
let mut formatted_len = if let Some(id) = &annotation.annotation.id {
2 + id.len()
2 + id.id.len()
+ annotation_type_len(&annotation.annotation.annotation_type)
} else {
annotation_type_len(&annotation.annotation.annotation_type)
@@ -724,7 +738,10 @@ impl DisplaySet<'_> {
} else if formatted_len != 0 {
formatted_len += 2;
let id = match &annotation.annotation.id {
Some(id) => format!("[{id}]"),
Some(id) => format!(
"[{id}]",
id = fmt_with_hyperlink(&id.id, id.url, stylesheet)
),
None => String::new(),
};
buffer.puts(
@@ -827,7 +844,7 @@ impl DisplaySet<'_> {
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Annotation<'a> {
pub(crate) annotation_type: DisplayAnnotationType,
pub(crate) id: Option<&'a str>,
pub(crate) id: Option<Id<'a>>,
pub(crate) label: Vec<DisplayTextFragment<'a>>,
pub(crate) is_fixable: bool,
}
@@ -1140,7 +1157,7 @@ fn format_message<'m>(
fn format_title<'a>(
level: crate::Level,
id: Option<&'a str>,
id: Option<Id<'a>>,
label: &'a str,
is_fixable: bool,
) -> DisplayLine<'a> {
@@ -1158,7 +1175,7 @@ fn format_title<'a>(
fn format_footer<'a>(
level: crate::Level,
id: Option<&'a str>,
id: Option<Id<'a>>,
label: &'a str,
) -> Vec<DisplayLine<'a>> {
let mut result = vec![];
@@ -1706,6 +1723,7 @@ fn format_body<'m>(
annotation: Annotation {
annotation_type,
id: None,
label: format_label(annotation.label, None),
is_fixable: false,
},
@@ -1887,3 +1905,40 @@ fn char_width(c: char) -> Option<usize> {
unicode_width::UnicodeWidthChar::width(c)
}
}
pub(super) fn fmt_with_hyperlink<'a, T>(
content: T,
url: Option<&'a str>,
stylesheet: &Stylesheet,
) -> impl std::fmt::Display + 'a
where
T: std::fmt::Display + 'a,
{
struct FmtHyperlink<'a, T> {
content: T,
url: Option<&'a str>,
}
impl<T> std::fmt::Display for FmtHyperlink<'_, T>
where
T: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(url) = self.url {
write!(f, "\x1B]8;;{url}\x1B\\")?;
}
self.content.fmt(f)?;
if self.url.is_some() {
f.write_str("\x1B]8;;\x1B\\")?;
}
Ok(())
}
}
let url = if stylesheet.hyperlink { url } else { None };
FmtHyperlink { content, url }
}

View File

@@ -76,6 +76,7 @@ impl Renderer {
}
.effects(Effects::BOLD),
none: Style::new(),
hyperlink: true,
},
..Self::plain()
}
@@ -154,6 +155,11 @@ impl Renderer {
self
}
pub const fn hyperlink(mut self, hyperlink: bool) -> Self {
self.stylesheet.hyperlink = hyperlink;
self
}
/// Set the string used for when a long line is cut.
///
/// The default is `...` (three `U+002E` characters).

View File

@@ -10,6 +10,7 @@ pub(crate) struct Stylesheet {
pub(crate) line_no: Style,
pub(crate) emphasis: Style,
pub(crate) none: Style,
pub(crate) hyperlink: bool,
}
impl Default for Stylesheet {
@@ -29,6 +30,7 @@ impl Stylesheet {
line_no: Style::new(),
emphasis: Style::new(),
none: Style::new(),
hyperlink: false,
}
}
}

View File

@@ -12,13 +12,19 @@
use std::ops::Range;
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub(crate) struct Id<'a> {
pub(crate) id: &'a str,
pub(crate) url: Option<&'a str>,
}
/// Primary structure provided for formatting
///
/// See [`Level::title`] to create a [`Message`]
#[derive(Debug)]
pub struct Message<'a> {
pub(crate) level: Level,
pub(crate) id: Option<&'a str>,
pub(crate) id: Option<Id<'a>>,
pub(crate) title: &'a str,
pub(crate) snippets: Vec<Snippet<'a>>,
pub(crate) footer: Vec<Message<'a>>,
@@ -28,7 +34,12 @@ pub struct Message<'a> {
impl<'a> Message<'a> {
pub fn id(mut self, id: &'a str) -> Self {
self.id = Some(id);
self.id = Some(Id { id, url: None });
self
}
pub fn id_with_url(mut self, id: &'a str, url: Option<&'a str>) -> Self {
self.id = Some(Id { id, url });
self
}

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

@@ -667,7 +667,7 @@ fn attrs(criterion: &mut Criterion) {
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
110,
120,
);
bench_project(&benchmark, criterion);

View File

@@ -143,7 +143,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
525,
600,
);
static PANDAS: Benchmark = Benchmark::new(
@@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
3000,
4000,
);
static PYDANTIC: Benchmark = Benchmark::new(
@@ -181,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY39,
},
5000,
7000,
);
static SYMPY: Benchmark = Benchmark::new(

View File

@@ -42,6 +42,7 @@ schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
similar = { workspace = true }
supports-hyperlinks = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, optional = true }

View File

@@ -64,6 +64,8 @@ impl Diagnostic {
id,
severity,
message: message.into_diagnostic_message(),
custom_concise_message: None,
documentation_url: None,
annotations: vec![],
subs: vec![],
fix: None,
@@ -213,6 +215,10 @@ impl Diagnostic {
/// cases, just converting it to a string (or printing it) will do what
/// you want.
pub fn concise_message(&self) -> ConciseMessage<'_> {
if let Some(custom_message) = &self.inner.custom_concise_message {
return ConciseMessage::Custom(custom_message.as_str());
}
let main = self.inner.message.as_str();
let annotation = self
.primary_annotation()
@@ -226,6 +232,15 @@ impl Diagnostic {
}
}
/// Set a custom message for the concise formatting of this diagnostic.
///
/// This overrides the default behavior of generating a concise message
/// from the main diagnostic message and the primary annotation.
pub fn set_concise_message(&mut self, message: impl IntoDiagnosticMessage) {
Arc::make_mut(&mut self.inner).custom_concise_message =
Some(message.into_diagnostic_message());
}
/// Returns the severity of this diagnostic.
///
/// Note that this may be different than the severity of sub-diagnostics.
@@ -356,6 +371,14 @@ impl Diagnostic {
.is_some_and(|fix| fix.applies(config.fix_applicability))
}
pub fn documentation_url(&self) -> Option<&str> {
self.inner.documentation_url.as_deref()
}
pub fn set_documentation_url(&mut self, url: Option<String>) {
Arc::make_mut(&mut self.inner).documentation_url = url;
}
/// Returns the offset of the parent statement for this diagnostic if it exists.
///
/// This is primarily used for checking noqa/secondary code suppressions.
@@ -429,28 +452,6 @@ impl Diagnostic {
.map(|sub| sub.inner.message.as_str())
}
/// Returns the URL for the rule documentation, if it exists.
pub fn to_ruff_url(&self) -> Option<String> {
match self.id() {
DiagnosticId::Panic
| DiagnosticId::Io
| DiagnosticId::InvalidSyntax
| DiagnosticId::RevealedType
| DiagnosticId::UnknownRule
| DiagnosticId::InvalidGlob
| DiagnosticId::EmptyInclude
| DiagnosticId::UnnecessaryOverridesSection
| DiagnosticId::UselessOverridesSection
| DiagnosticId::DeprecatedSetting
| DiagnosticId::Unformatted
| DiagnosticId::InvalidCliOption
| DiagnosticId::InternalError => None,
DiagnosticId::Lint(lint_name) => {
Some(format!("{}/rules/{lint_name}", env!("CARGO_PKG_HOMEPAGE")))
}
}
}
/// Returns the filename for the message.
///
/// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`.
@@ -530,8 +531,10 @@ impl Diagnostic {
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
struct DiagnosticInner {
id: DiagnosticId,
documentation_url: Option<String>,
severity: Severity,
message: DiagnosticMessage,
custom_concise_message: Option<DiagnosticMessage>,
annotations: Vec<Annotation>,
subs: Vec<SubDiagnostic>,
fix: Option<Fix>,
@@ -1520,6 +1523,8 @@ pub enum ConciseMessage<'a> {
/// This indicates that the diagnostic is probably using the old
/// model.
Empty,
/// A custom concise message has been provided.
Custom(&'a str),
}
impl std::fmt::Display for ConciseMessage<'_> {
@@ -1535,6 +1540,9 @@ impl std::fmt::Display for ConciseMessage<'_> {
write!(f, "{main}: {annotation}")
}
ConciseMessage::Empty => Ok(()),
ConciseMessage::Custom(message) => {
write!(f, "{message}")
}
}
}
}

View File

@@ -205,6 +205,7 @@ impl<'a> Resolved<'a> {
struct ResolvedDiagnostic<'a> {
level: AnnotateLevel,
id: Option<String>,
documentation_url: Option<String>,
message: String,
annotations: Vec<ResolvedAnnotation<'a>>,
is_fixable: bool,
@@ -240,12 +241,12 @@ impl<'a> ResolvedDiagnostic<'a> {
// `DisplaySet::format_annotation` for both cases, but this is a small hack to improve
// the formatting of syntax errors for now. This should also be kept consistent with the
// concise formatting.
Some(diag.secondary_code().map_or_else(
diag.secondary_code().map_or_else(
|| format!("{id}:", id = diag.inner.id),
|code| code.to_string(),
))
)
} else {
Some(diag.inner.id.to_string())
diag.inner.id.to_string()
};
let level = if config.hide_severity {
@@ -256,7 +257,8 @@ impl<'a> ResolvedDiagnostic<'a> {
ResolvedDiagnostic {
level,
id,
id: Some(id),
documentation_url: diag.documentation_url().map(ToString::to_string),
message: diag.inner.message.as_str().to_string(),
annotations,
is_fixable: config.show_fix_status && diag.has_applicable_fix(config),
@@ -287,6 +289,7 @@ impl<'a> ResolvedDiagnostic<'a> {
ResolvedDiagnostic {
level: diag.inner.severity.to_annotate(),
id: None,
documentation_url: None,
message: diag.inner.message.as_str().to_string(),
annotations,
is_fixable: false,
@@ -385,6 +388,7 @@ impl<'a> ResolvedDiagnostic<'a> {
RenderableDiagnostic {
level: self.level,
id: self.id.as_deref(),
documentation_url: self.documentation_url.as_deref(),
message: &self.message,
snippets_by_input,
is_fixable: self.is_fixable,
@@ -485,6 +489,7 @@ struct RenderableDiagnostic<'r> {
/// An ID is always present for top-level diagnostics and always absent for
/// sub-diagnostics.
id: Option<&'r str>,
documentation_url: Option<&'r str>,
/// The message emitted with the diagnostic, before any snippets are
/// rendered.
message: &'r str,
@@ -519,7 +524,7 @@ impl RenderableDiagnostic<'_> {
.is_fixable(self.is_fixable)
.lineno_offset(self.header_offset);
if let Some(id) = self.id {
message = message.id(id);
message = message.id_with_url(id, self.documentation_url);
}
message.snippets(snippets)
}
@@ -2876,6 +2881,12 @@ watermelon
self.diag.help(message);
self
}
/// Set the documentation URL for the diagnostic.
pub(super) fn documentation_url(mut self, url: impl Into<String>) -> DiagnosticBuilder<'e> {
self.diag.set_documentation_url(Some(url.into()));
self
}
}
/// A helper builder for tersely populating a `SubDiagnostic`.
@@ -2990,6 +3001,7 @@ def fibonacci(n):
TextSize::from(10),
))))
.noqa_offset(TextSize::from(7))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-variable",
@@ -3004,11 +3016,13 @@ def fibonacci(n):
TextSize::from(99),
)))
.noqa_offset(TextSize::from(94))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
.build(),
env.builder("undefined-name", Severity::Error, "Undefined name `a`")
.primary("undef.py", "1:3", "1:4", "")
.secondary_code("F821")
.noqa_offset(TextSize::from(3))
.documentation_url("https://docs.astral.sh/ruff/rules/undefined-name")
.build(),
];
@@ -3123,6 +3137,7 @@ if call(foo
TextSize::from(19),
))))
.noqa_offset(TextSize::from(16))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-import",
@@ -3137,6 +3152,7 @@ if call(foo
TextSize::from(40),
))))
.noqa_offset(TextSize::from(35))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-variable",
@@ -3151,6 +3167,7 @@ if call(foo
TextSize::from(104),
))))
.noqa_offset(TextSize::from(98))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
.build(),
];

View File

@@ -1,6 +1,6 @@
use crate::diagnostic::{
Diagnostic, DisplayDiagnosticConfig, Severity,
stylesheet::{DiagnosticStylesheet, fmt_styled},
stylesheet::{DiagnosticStylesheet, fmt_styled, fmt_with_hyperlink},
};
use super::FileResolver;
@@ -62,18 +62,29 @@ impl<'a> ConciseRenderer<'a> {
}
write!(f, "{sep} ")?;
}
if self.config.hide_severity {
if let Some(code) = diag.secondary_code() {
write!(
f,
"{code} ",
code = fmt_styled(code, stylesheet.secondary_code)
code = fmt_styled(
fmt_with_hyperlink(&code, diag.documentation_url(), &stylesheet),
stylesheet.secondary_code
)
)?;
} else {
write!(
f,
"{id}: ",
id = fmt_styled(diag.inner.id.as_str(), stylesheet.secondary_code)
id = fmt_styled(
fmt_with_hyperlink(
&diag.inner.id,
diag.documentation_url(),
&stylesheet
),
stylesheet.secondary_code
)
)?;
}
if self.config.show_fix_status {
@@ -93,7 +104,10 @@ impl<'a> ConciseRenderer<'a> {
f,
"{severity}[{id}] ",
severity = fmt_styled(severity, severity_style),
id = fmt_styled(diag.id(), stylesheet.emphasis)
id = fmt_styled(
fmt_with_hyperlink(&diag.id(), diag.documentation_url(), &stylesheet),
stylesheet.emphasis
)
)?;
}

View File

@@ -49,7 +49,8 @@ impl<'a> FullRenderer<'a> {
.help(stylesheet.help)
.line_no(stylesheet.line_no)
.emphasis(stylesheet.emphasis)
.none(stylesheet.none);
.none(stylesheet.none)
.hyperlink(stylesheet.hyperlink);
for diag in diagnostics {
let resolved = Resolved::new(self.resolver, diag, self.config);
@@ -703,52 +704,7 @@ print()
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
error[unused-import][*]: `math` imported but unused
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^
3 |
4 | print('hello world')
|
help: Remove unused import: `math`
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
error[unused-variable][*]: Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| ^
|
help: Remove assignment to unused variable `x`
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior
");
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
}
#[test]
@@ -768,31 +724,7 @@ print()
}
*fix = Fix::unsafe_edits(edits.remove(0), edits);
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior
");
insta::assert_snapshot!(env.render(&diagnostic));
}
/// Carriage return (`\r`) is a valid line-ending in Python, so we should normalize this to a

View File

@@ -100,7 +100,7 @@ pub(super) fn diagnostic_to_json<'a>(
if config.preview {
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
url: diagnostic.to_ruff_url(),
url: diagnostic.documentation_url(),
message: diagnostic.body(),
fix,
cell: notebook_cell_index,
@@ -112,7 +112,7 @@ pub(super) fn diagnostic_to_json<'a>(
} else {
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
url: diagnostic.to_ruff_url(),
url: diagnostic.documentation_url(),
message: diagnostic.body(),
fix,
cell: notebook_cell_index,
@@ -228,7 +228,7 @@ pub(crate) struct JsonDiagnostic<'a> {
location: Option<JsonLocation>,
message: &'a str,
noqa_row: Option<OneIndexed>,
url: Option<String>,
url: Option<&'a str>,
}
#[derive(Serialize)]
@@ -294,7 +294,10 @@ mod tests {
env.format(DiagnosticFormat::Json);
env.preview(false);
let diag = env.err().build();
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
insta::assert_snapshot!(
env.render(&diag),
@@ -328,7 +331,10 @@ mod tests {
env.format(DiagnosticFormat::Json);
env.preview(true);
let diag = env.err().build();
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
insta::assert_snapshot!(
env.render(&diag),

View File

@@ -82,7 +82,7 @@ fn diagnostic_to_rdjson<'a>(
value: diagnostic
.secondary_code()
.map_or_else(|| diagnostic.name(), |code| code.as_str()),
url: diagnostic.to_ruff_url(),
url: diagnostic.documentation_url(),
},
suggestions: rdjson_suggestions(
edits,
@@ -182,7 +182,7 @@ impl RdjsonRange {
#[derive(Serialize)]
struct RdjsonCode<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
url: Option<&'a str>,
value: &'a str,
}
@@ -217,7 +217,10 @@ mod tests {
env.format(DiagnosticFormat::Rdjson);
env.preview(false);
let diag = env.err().build();
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
insta::assert_snapshot!(env.render(&diag));
}
@@ -228,7 +231,10 @@ mod tests {
env.format(DiagnosticFormat::Rdjson);
env.preview(true);
let diag = env.err().build();
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
insta::assert_snapshot!(env.render(&diag));
}

View File

@@ -0,0 +1,48 @@
---
source: crates/ruff_db/src/diagnostic/render/full.rs
expression: env.render_diagnostics(&diagnostics)
---
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
error[unused-import][*]: `math` imported but unused
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^
3 |
4 | print('hello world')
|
help: Remove unused import: `math`
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
error[unused-variable][*]: Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| ^
|
help: Remove assignment to unused variable `x`
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -0,0 +1,27 @@
---
source: crates/ruff_db/src/diagnostic/render/full.rs
expression: env.render(&diagnostic)
---
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -31,6 +31,43 @@ where
FmtStyled { content, style }
}
pub(super) fn fmt_with_hyperlink<'a, T>(
content: T,
url: Option<&'a str>,
stylesheet: &DiagnosticStylesheet,
) -> impl std::fmt::Display + 'a
where
T: std::fmt::Display + 'a,
{
struct FmtHyperlink<'a, T> {
content: T,
url: Option<&'a str>,
}
impl<T> std::fmt::Display for FmtHyperlink<'_, T>
where
T: std::fmt::Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(url) = self.url {
write!(f, "\x1B]8;;{url}\x1B\\")?;
}
self.content.fmt(f)?;
if self.url.is_some() {
f.write_str("\x1B]8;;\x1B\\")?;
}
Ok(())
}
}
let url = if stylesheet.hyperlink { url } else { None };
FmtHyperlink { content, url }
}
#[derive(Clone, Debug)]
pub struct DiagnosticStylesheet {
pub(crate) error: Style,
@@ -47,6 +84,7 @@ pub struct DiagnosticStylesheet {
pub(crate) deletion: Style,
pub(crate) insertion_line_no: Style,
pub(crate) deletion_line_no: Style,
pub(crate) hyperlink: bool,
}
impl Default for DiagnosticStylesheet {
@@ -59,6 +97,8 @@ impl DiagnosticStylesheet {
/// Default terminal styling
pub fn styled() -> Self {
let bright_blue = AnsiColor::BrightBlue.on_default();
let hyperlink = supports_hyperlinks::supports_hyperlinks();
Self {
error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD),
warning: AnsiColor::Yellow.on_default().effects(Effects::BOLD),
@@ -74,6 +114,7 @@ impl DiagnosticStylesheet {
deletion: AnsiColor::Red.on_default(),
insertion_line_no: AnsiColor::Green.on_default().effects(Effects::BOLD),
deletion_line_no: AnsiColor::Red.on_default().effects(Effects::BOLD),
hyperlink,
}
}
@@ -93,6 +134,7 @@ impl DiagnosticStylesheet {
deletion: Style::new(),
insertion_line_no: Style::new(),
deletion_line_no: Style::new(),
hyperlink: false,
}
}
}

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

@@ -4,3 +4,31 @@ CommunityData("public", mpModel=0) # S508
CommunityData("public", mpModel=1) # S508
CommunityData("public", mpModel=2) # OK
# New API paths
import pysnmp.hlapi.asyncio
import pysnmp.hlapi.v1arch
import pysnmp.hlapi.v1arch.asyncio
import pysnmp.hlapi.v1arch.asyncio.auth
import pysnmp.hlapi.v3arch
import pysnmp.hlapi.v3arch.asyncio
import pysnmp.hlapi.v3arch.asyncio.auth
import pysnmp.hlapi.auth
pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v1arch.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v3arch.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.auth.CommunityData("public", mpModel=2) # OK

View File

@@ -5,3 +5,19 @@ insecure = UsmUserData("securityName") # S509
auth_no_priv = UsmUserData("securityName", "authName") # S509
less_insecure = UsmUserData("securityName", "authName", "privName") # OK
# New API paths
import pysnmp.hlapi.asyncio
import pysnmp.hlapi.v3arch.asyncio
import pysnmp.hlapi.v3arch.asyncio.auth
import pysnmp.hlapi.auth
pysnmp.hlapi.asyncio.UsmUserData("user") # S509
pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
pysnmp.hlapi.auth.UsmUserData("user") # S509
pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK
pysnmp.hlapi.v3arch.asyncio.UsmUserData("user", "authkey", "privkey") # OK
pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user", "authkey", "privkey") # OK
pysnmp.hlapi.auth.UsmUserData("user", "authkey", "privkey") # OK

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

@@ -16,3 +16,19 @@ logging.warning("%s", str(**{"object": b"\xf0\x9f\x9a\xa8", "encoding": "utf-8"}
# str() with single keyword argument - should be flagged (equivalent to str("!"))
logging.warning("%s", str(object="!"))
# Complex conversion specifiers that make oct() and hex() necessary
# These should NOT be flagged because the behavior differs between %s and %#o/%#x
# https://github.com/astral-sh/ruff/issues/21458
# %06s with oct() - zero-pad flag with width (should NOT be flagged)
logging.warning("%06s", oct(123))
# % s with oct() - blank sign flag (should NOT be flagged)
logging.warning("% s", oct(123))
# %+s with oct() - sign char flag (should NOT be flagged)
logging.warning("%+s", oct(123))
# %.3s with hex() - precision (should NOT be flagged)
logging.warning("%.3s", hex(123))

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

@@ -125,6 +125,7 @@ where
}
diagnostic.set_secondary_code(SecondaryCode::new(rule.noqa_code().to_string()));
diagnostic.set_documentation_url(rule.url());
diagnostic
}

View File

@@ -270,6 +270,11 @@ pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &Linter
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21374
pub(crate) const fn is_extended_snmp_api_path_detection_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21395
pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()

View File

@@ -104,6 +104,8 @@ mod tests {
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
#[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))]
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -4,6 +4,7 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_extended_snmp_api_path_detection_enabled;
/// ## What it does
/// Checks for uses of SNMPv1 or SNMPv2.
@@ -47,10 +48,17 @@ pub(crate) fn snmp_insecure_version(checker: &Checker, call: &ast::ExprCall) {
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "CommunityData"]
)
if is_extended_snmp_api_path_detection_enabled(checker.settings()) {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", .., "CommunityData"]
)
} else {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "CommunityData"]
)
}
})
{
if let Some(keyword) = call.arguments.find_keyword("mpModel") {

View File

@@ -4,6 +4,7 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_extended_snmp_api_path_detection_enabled;
/// ## What it does
/// Checks for uses of the SNMPv3 protocol without encryption.
@@ -47,10 +48,17 @@ pub(crate) fn snmp_weak_cryptography(checker: &Checker, call: &ast::ExprCall) {
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "UsmUserData"]
)
if is_extended_snmp_api_path_detection_enabled(checker.settings()) {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", .., "UsmUserData"]
)
} else {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "UsmUserData"]
)
}
})
{
checker.report_diagnostic(SnmpWeakCryptography, call.func.range());

View File

@@ -0,0 +1,108 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:3:25
|
1 | from pysnmp.hlapi import CommunityData
2 |
3 | CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
4 | CommunityData("public", mpModel=1) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:4:25
|
3 | CommunityData("public", mpModel=0) # S508
4 | CommunityData("public", mpModel=1) # S508
| ^^^^^^^^^
5 |
6 | CommunityData("public", mpModel=2) # OK
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:18:46
|
16 | import pysnmp.hlapi.auth
17 |
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:19:58
|
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:20:53
|
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:21:45
|
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:22:58
|
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:23:53
|
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:24:45
|
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:25:43
|
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
26 |
27 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK
|

View File

@@ -0,0 +1,62 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:4:12
|
4 | insecure = UsmUserData("securityName") # S509
| ^^^^^^^^^^^
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:5:16
|
4 | insecure = UsmUserData("securityName") # S509
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
| ^^^^^^^^^^^
6 |
7 | less_insecure = UsmUserData("securityName", "authName", "privName") # OK
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:15:1
|
13 | import pysnmp.hlapi.auth
14 |
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:16:1
|
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:17:1
|
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:18:1
|
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
19 |
20 | pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK
|

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

@@ -2,7 +2,9 @@ use std::str::FromStr;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_literal::cformat::{CFormatPart, CFormatString, CFormatType};
use ruff_python_literal::cformat::{
CConversionFlags, CFormatPart, CFormatSpec, CFormatString, CFormatType,
};
use ruff_python_literal::format::FormatConversion;
use ruff_text_size::Ranged;
@@ -195,7 +197,8 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
}
// %s with oct() - suggest using %#o instead
FormatConversion::Str
if checker.semantic().match_builtin_expr(func.as_ref(), "oct") =>
if checker.semantic().match_builtin_expr(func.as_ref(), "oct")
&& !has_complex_conversion_specifier(spec) =>
{
checker.report_diagnostic(
LoggingEagerConversion {
@@ -207,7 +210,8 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
}
// %s with hex() - suggest using %#x instead
FormatConversion::Str
if checker.semantic().match_builtin_expr(func.as_ref(), "hex") =>
if checker.semantic().match_builtin_expr(func.as_ref(), "hex")
&& !has_complex_conversion_specifier(spec) =>
{
checker.report_diagnostic(
LoggingEagerConversion {
@@ -222,3 +226,23 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
}
}
}
/// Check if a conversion specifier has complex flags or precision that make `oct()` or `hex()` necessary.
///
/// Returns `true` if any of these conditions are met:
/// - Flag `0` (zero-pad) is used, flag `-` (left-adjust) is not used, and minimum width is specified
/// - Flag ` ` (blank sign) is used
/// - Flag `+` (sign char) is used
/// - Precision is specified
fn has_complex_conversion_specifier(spec: &CFormatSpec) -> bool {
if spec.flags.intersects(CConversionFlags::ZERO_PAD)
&& !spec.flags.intersects(CConversionFlags::LEFT_ADJUST)
&& spec.min_field_width.is_some()
{
return true;
}
spec.flags
.intersects(CConversionFlags::BLANK_SIGN | CConversionFlags::SIGN_CHAR)
|| spec.precision.is_some()
}

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

@@ -193,3 +193,58 @@ def foo():
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
):
pass
# Regression tests for https://github.com/astral-sh/ruff/issues/19226
if '' and (not #
0):
pass
if '' and (not #
(0)
):
pass
if '' and (not
( #
0
)):
pass
if (
not
# comment
(a)):
pass
if not ( # comment
a):
pass
if not (
# comment
(a)):
pass
if not (
# comment
a):
pass
not (# comment
(a))
(-#comment
(a))
if ( # a
# b
not # c
# d
( # e
# f
a # g
# h
) # i
# j
):
pass

View File

@@ -0,0 +1,149 @@
# Test cases for fmt: skip on compound statements that fit on one line
# Basic single-line compound statements
def simple_func(): return "hello" # fmt: skip
if True: print("condition met") # fmt: skip
for i in range(5): print(i) # fmt: skip
while x < 10: x += 1 # fmt: skip
# With expressions that would normally trigger formatting
def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip
if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip
# Nested compound statements (outer should be preserved)
if True:
for i in range(10): print(i) # fmt: skip
# Multiple statements in body (should not apply - multiline)
if True:
x = 1
y = 2 # fmt: skip
# With decorators - decorated function on one line
@overload
def decorated_func(x: int) -> str: return str(x) # fmt: skip
@property
def prop_method(self): return self._value # fmt: skip
# Class definitions on one line
class SimpleClass: pass # fmt: skip
class GenericClass(Generic[T]): pass # fmt: skip
# Try/except blocks
try: risky_operation() # fmt: skip
except ValueError: handle_error() # fmt: skip
except: handle_any_error() # fmt: skip
else: success_case() # fmt: skip
finally: cleanup() # fmt: skip
# Match statements (Python 3.10+)
match value:
case 1: print("one") # fmt: skip
case _: print("other") # fmt: skip
# With statements
with open("file.txt") as f: content = f.read() # fmt: skip
with context_manager() as cm: result = cm.process() # fmt: skip
# Async variants
async def async_func(): return await some_call() # fmt: skip
async for item in async_iterator(): await process(item) # fmt: skip
async with async_context() as ctx: await ctx.work() # fmt: skip
# Complex expressions that would normally format
def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip
if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip
# Edge case: comment positioning
def func_with_comment(): # some comment
return "value" # fmt: skip
# Edge case: multiple fmt: skip (only last one should matter)
def multiple_skip(): return "test" # fmt: skip # fmt: skip
# Should NOT be affected (already multiline)
def multiline_func():
return "this should format normally"
if long_condition_that_spans \
and continues_on_next_line:
print("multiline condition")
# Mix of skipped and non-skipped
for i in range(10): print(f"item {i}") # fmt: skip
for j in range(5):
print(f"formatted item {j}")
# With trailing comma that would normally be removed
def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip
# Dictionary/list comprehensions
def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip
def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip
# Lambda in one-liner
def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip
# String formatting that would normally be reformatted
def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip
# loop else clauses
for i in range(2): print(i) # fmt: skip
else: print("this") # fmt: skip
while foo(): print(i) # fmt: skip
else: print("this") # fmt: skip
# again but only the first skip
for i in range(2): print(i) # fmt: skip
else: print("this")
while foo(): print(i) # fmt: skip
else: print("this")
# again but only the second skip
for i in range(2): print(i)
else: print("this") # fmt: skip
while foo(): print(i)
else: print("this") # fmt: skip
# multiple statements in body
if True: print("this"); print("that") # fmt: skip
# Examples with more comments
try: risky_operation() # fmt: skip
# leading 1
except ValueError: handle_error() # fmt: skip
# leading 2
except: handle_any_error() # fmt: skip
# leading 3
else: success_case() # fmt: skip
# leading 4
finally: cleanup() # fmt: skip
# trailing
# multi-line before colon (should remain as is)
if (
long_condition
): a + b # fmt: skip
# over-indented comment example
# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910
# and https://github.com/astral-sh/ruff/pull/21185
for x in it: foo()
# comment
else: bar() # fmt: skip
if this(
'is a long',
# commented
'condition'
): with_a_skip # fmt: skip

View File

@@ -1890,9 +1890,11 @@ fn handle_lambda_comment<'a>(
CommentPlacement::Default(comment)
}
/// Move comment between a unary op and its operand before the unary op by marking them as trailing.
/// Move an end-of-line comment between a unary op and its operand after the operand by marking
/// it as dangling.
///
/// For example, given:
///
/// ```python
/// (
/// not # comment
@@ -1900,8 +1902,13 @@ fn handle_lambda_comment<'a>(
/// )
/// ```
///
/// The `# comment` will be attached as a dangling comment on the enclosing node, to ensure that
/// it remains on the same line as the operator.
/// the `# comment` will be attached as a dangling comment on the unary op and formatted as:
///
/// ```python
/// (
/// not True # comment
/// )
/// ```
fn handle_unary_op_comment<'a>(
comment: DecoratedComment<'a>,
unary_op: &'a ast::ExprUnaryOp,
@@ -1923,8 +1930,8 @@ fn handle_unary_op_comment<'a>(
let up_to = tokenizer
.find(|token| token.kind == SimpleTokenKind::LParen)
.map_or(unary_op.operand.start(), |lparen| lparen.start());
if comment.end() < up_to {
CommentPlacement::leading(unary_op, comment)
if comment.end() < up_to && comment.line_position().is_end_of_line() {
CommentPlacement::dangling(unary_op, comment)
} else {
CommentPlacement::Default(comment)
}

View File

@@ -1,6 +1,8 @@
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprUnaryOp;
use ruff_python_ast::UnaryOp;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_text_size::Ranged;
use crate::comments::trailing_comments;
use crate::expression::parentheses::{
@@ -39,20 +41,25 @@ impl FormatNodeRule<ExprUnaryOp> for FormatExprUnaryOp {
// ```
trailing_comments(dangling).fmt(f)?;
// Insert a line break if the operand has comments but itself is not parenthesized.
// Insert a line break if the operand has comments but itself is not parenthesized or if the
// operand is parenthesized but has a leading comment before the parentheses.
// ```python
// if (
// not
// # comment
// a)
// a):
// pass
//
// if 1 and (
// not
// # comment
// (
// a
// )
// ):
// pass
// ```
if comments.has_leading(operand.as_ref())
&& !is_expression_parenthesized(
operand.as_ref().into(),
f.context().comments().ranges(),
f.context().source(),
)
{
if needs_line_break(item, f.context()) {
hard_line_break().fmt(f)?;
} else if op.is_not() {
space().fmt(f)?;
@@ -76,17 +83,51 @@ impl NeedsParentheses for ExprUnaryOp {
context: &PyFormatContext,
) -> OptionalParentheses {
if parent.is_expr_await() {
OptionalParentheses::Always
} else if is_expression_parenthesized(
return OptionalParentheses::Always;
}
if needs_line_break(self, context) {
return OptionalParentheses::Always;
}
if is_expression_parenthesized(
self.operand.as_ref().into(),
context.comments().ranges(),
context.source(),
) {
OptionalParentheses::Never
} else if context.comments().has(self.operand.as_ref()) {
OptionalParentheses::Always
} else {
self.operand.needs_parentheses(self.into(), context)
return OptionalParentheses::Never;
}
if context.comments().has(self.operand.as_ref()) {
return OptionalParentheses::Always;
}
self.operand.needs_parentheses(self.into(), context)
}
}
/// Returns `true` if the unary operator will have a hard line break between the operator and its
/// operand and thus requires parentheses.
fn needs_line_break(item: &ExprUnaryOp, context: &PyFormatContext) -> bool {
let comments = context.comments();
let parenthesized_operand_range = parenthesized_range(
item.operand.as_ref().into(),
item.into(),
comments.ranges(),
context.source(),
);
let leading_operand_comments = comments.leading(item.operand.as_ref());
let has_leading_comments_before_parens = parenthesized_operand_range.is_some_and(|range| {
leading_operand_comments
.iter()
.any(|comment| comment.start() < range.start())
});
!leading_operand_comments.is_empty()
&& !is_expression_parenthesized(
item.operand.as_ref().into(),
context.comments().ranges(),
context.source(),
)
|| has_leading_comments_before_parens
}

View File

@@ -7,7 +7,7 @@ use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::preview::is_remove_parens_around_except_types_enabled;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::suite::SuiteKind;
#[derive(Copy, Clone, Default)]
@@ -55,77 +55,68 @@ impl FormatNodeRule<ExceptHandlerExceptHandler> for FormatExceptHandlerExceptHan
write!(
f,
[
clause_header(
ClauseHeader::ExceptHandler(item),
dangling_comments,
&format_with(|f: &mut PyFormatter| {
write!(
f,
[
token("except"),
match except_handler_kind {
ExceptHandlerKind::Regular => None,
ExceptHandlerKind::Starred => Some(token("*")),
}
]
)?;
[clause(
ClauseHeader::ExceptHandler(item),
&format_with(|f: &mut PyFormatter| {
write!(
f,
[
token("except"),
match except_handler_kind {
ExceptHandlerKind::Regular => None,
ExceptHandlerKind::Starred => Some(token("*")),
}
]
)?;
match type_.as_deref() {
// For tuples of exception types without an `as` name and on 3.14+, the
// parentheses are optional.
//
// ```py
// try:
// ...
// except BaseException, Exception: # Ok
// ...
// ```
Some(Expr::Tuple(tuple))
if f.options().target_version() >= PythonVersion::PY314
&& is_remove_parens_around_except_types_enabled(
f.context(),
)
&& name.is_none() =>
{
write!(
f,
[
space(),
tuple
.format()
.with_options(TupleParentheses::NeverPreserve)
]
)?;
}
Some(type_) => {
write!(
f,
[
space(),
maybe_parenthesize_expression(
type_,
item,
Parenthesize::IfBreaks
)
]
)?;
if let Some(name) = name {
write!(f, [space(), token("as"), space(), name.format()])?;
}
}
_ => {}
match type_.as_deref() {
// For tuples of exception types without an `as` name and on 3.14+, the
// parentheses are optional.
//
// ```py
// try:
// ...
// except BaseException, Exception: # Ok
// ...
// ```
Some(Expr::Tuple(tuple))
if f.options().target_version() >= PythonVersion::PY314
&& is_remove_parens_around_except_types_enabled(f.context())
&& name.is_none() =>
{
write!(
f,
[
space(),
tuple.format().with_options(TupleParentheses::NeverPreserve)
]
)?;
}
Some(type_) => {
write!(
f,
[
space(),
maybe_parenthesize_expression(
type_,
item,
Parenthesize::IfBreaks
)
]
)?;
if let Some(name) = name {
write!(f, [space(), token("as"), space(), name.format()])?;
}
}
_ => {}
}
Ok(())
}),
),
clause_body(
body,
SuiteKind::other(self.last_suite_in_statement),
dangling_comments
),
]
Ok(())
}),
dangling_comments,
body,
SuiteKind::other(self.last_suite_in_statement),
)]
)
}
}

View File

@@ -5,7 +5,7 @@ use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::pattern::maybe_parenthesize_pattern;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -46,23 +46,18 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
write!(
f,
[
clause_header(
ClauseHeader::MatchCase(item),
dangling_item_comments,
&format_args![
token("case"),
space(),
maybe_parenthesize_pattern(pattern, item),
format_guard
],
),
clause_body(
body,
SuiteKind::other(self.last_suite_in_statement),
dangling_item_comments
),
]
[clause(
ClauseHeader::MatchCase(item),
&format_args![
token("case"),
space(),
maybe_parenthesize_pattern(pattern, item),
format_guard
],
dangling_item_comments,
body,
SuiteKind::other(self.last_suite_in_statement),
)]
)
}
}

View File

@@ -5,11 +5,12 @@ use ruff_python_ast::{
StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith, Suite,
};
use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::comments::{SourceComment, leading_alternate_branch_comments, trailing_comments};
use crate::statement::suite::{SuiteKind, as_only_an_ellipsis};
use crate::verbatim::write_suppressed_clause_header;
use crate::verbatim::{verbatim_text, write_suppressed_clause_header};
use crate::{has_skip_comment, prelude::*};
/// The header of a compound statement clause.
@@ -36,7 +37,41 @@ pub(crate) enum ClauseHeader<'a> {
OrElse(ElseClause<'a>),
}
impl ClauseHeader<'_> {
impl<'a> ClauseHeader<'a> {
/// Returns the last child in the clause body immediately following this clause header.
///
/// For most clauses, this is the last statement in
/// the primary body. For clauses like `try`, it specifically returns the last child
/// in the `try` body, not the `except`/`else`/`finally` clauses.
///
/// This is similar to [`ruff_python_ast::AnyNodeRef::last_child_in_body`]
/// but restricted to the clause.
pub(crate) fn last_child_in_clause(self) -> Option<AnyNodeRef<'a>> {
match self {
ClauseHeader::Class(StmtClassDef { body, .. })
| ClauseHeader::Function(StmtFunctionDef { body, .. })
| ClauseHeader::If(StmtIf { body, .. })
| ClauseHeader::ElifElse(ElifElseClause { body, .. })
| ClauseHeader::Try(StmtTry { body, .. })
| ClauseHeader::MatchCase(MatchCase { body, .. })
| ClauseHeader::For(StmtFor { body, .. })
| ClauseHeader::While(StmtWhile { body, .. })
| ClauseHeader::With(StmtWith { body, .. })
| ClauseHeader::ExceptHandler(ExceptHandlerExceptHandler { body, .. })
| ClauseHeader::OrElse(
ElseClause::Try(StmtTry { orelse: body, .. })
| ElseClause::For(StmtFor { orelse: body, .. })
| ElseClause::While(StmtWhile { orelse: body, .. }),
)
| ClauseHeader::TryFinally(StmtTry {
finalbody: body, ..
}) => body.last().map(AnyNodeRef::from),
ClauseHeader::Match(StmtMatch { cases, .. }) => cases
.last()
.and_then(|case| case.body.last().map(AnyNodeRef::from)),
}
}
/// The range from the clause keyword up to and including the final colon.
pub(crate) fn range(self, source: &str) -> FormatResult<TextRange> {
let keyword_range = self.first_keyword_range(source)?;
@@ -338,6 +373,28 @@ impl ClauseHeader<'_> {
}
}
impl<'a> From<ClauseHeader<'a>> for AnyNodeRef<'a> {
fn from(value: ClauseHeader<'a>) -> Self {
match value {
ClauseHeader::Class(stmt_class_def) => stmt_class_def.into(),
ClauseHeader::Function(stmt_function_def) => stmt_function_def.into(),
ClauseHeader::If(stmt_if) => stmt_if.into(),
ClauseHeader::ElifElse(elif_else_clause) => elif_else_clause.into(),
ClauseHeader::Try(stmt_try) => stmt_try.into(),
ClauseHeader::ExceptHandler(except_handler_except_handler) => {
except_handler_except_handler.into()
}
ClauseHeader::TryFinally(stmt_try) => stmt_try.into(),
ClauseHeader::Match(stmt_match) => stmt_match.into(),
ClauseHeader::MatchCase(match_case) => match_case.into(),
ClauseHeader::For(stmt_for) => stmt_for.into(),
ClauseHeader::While(stmt_while) => stmt_while.into(),
ClauseHeader::With(stmt_with) => stmt_with.into(),
ClauseHeader::OrElse(else_clause) => else_clause.into(),
}
}
}
#[derive(Copy, Clone)]
pub(crate) enum ElseClause<'a> {
Try(&'a StmtTry),
@@ -345,6 +402,16 @@ pub(crate) enum ElseClause<'a> {
While(&'a StmtWhile),
}
impl<'a> From<ElseClause<'a>> for AnyNodeRef<'a> {
fn from(value: ElseClause<'a>) -> Self {
match value {
ElseClause::Try(stmt_try) => stmt_try.into(),
ElseClause::For(stmt_for) => stmt_for.into(),
ElseClause::While(stmt_while) => stmt_while.into(),
}
}
}
pub(crate) struct FormatClauseHeader<'a, 'ast> {
header: ClauseHeader<'a>,
/// How to format the clause header
@@ -378,22 +445,6 @@ where
}
}
impl<'a> FormatClauseHeader<'a, '_> {
/// Sets the leading comments that precede an alternate branch.
#[must_use]
pub(crate) fn with_leading_comments<N>(
mut self,
comments: &'a [SourceComment],
last_node: Option<N>,
) -> Self
where
N: Into<AnyNodeRef<'a>>,
{
self.leading_comments = Some((comments, last_node.map(Into::into)));
self
}
}
impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
if let Some((leading_comments, last_node)) = self.leading_comments {
@@ -423,13 +474,13 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> {
}
}
pub(crate) struct FormatClauseBody<'a> {
struct FormatClauseBody<'a> {
body: &'a Suite,
kind: SuiteKind,
trailing_comments: &'a [SourceComment],
}
pub(crate) fn clause_body<'a>(
fn clause_body<'a>(
body: &'a Suite,
kind: SuiteKind,
trailing_comments: &'a [SourceComment],
@@ -465,6 +516,84 @@ impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
}
}
pub(crate) struct FormatClause<'a, 'ast> {
header: ClauseHeader<'a>,
/// How to format the clause header
header_formatter: Argument<'a, PyFormatContext<'ast>>,
/// Leading comments coming before the branch, together with the previous node, if any. Only relevant
/// for alternate branches.
leading_comments: Option<(&'a [SourceComment], Option<AnyNodeRef<'a>>)>,
/// The trailing comments coming after the colon.
trailing_colon_comment: &'a [SourceComment],
body: &'a Suite,
kind: SuiteKind,
}
impl<'a, 'ast> FormatClause<'a, 'ast> {
/// Sets the leading comments that precede an alternate branch.
#[must_use]
pub(crate) fn with_leading_comments<N>(
mut self,
comments: &'a [SourceComment],
last_node: Option<N>,
) -> Self
where
N: Into<AnyNodeRef<'a>>,
{
self.leading_comments = Some((comments, last_node.map(Into::into)));
self
}
fn clause_header(&self) -> FormatClauseHeader<'a, 'ast> {
FormatClauseHeader {
header: self.header,
formatter: self.header_formatter,
leading_comments: self.leading_comments,
trailing_colon_comment: self.trailing_colon_comment,
}
}
fn clause_body(&self) -> FormatClauseBody<'a> {
clause_body(self.body, self.kind, self.trailing_colon_comment)
}
}
/// Formats a clause, handling the case where the compound
/// statement lies on a single line with `# fmt: skip` and
/// should be suppressed.
pub(crate) fn clause<'a, 'ast, Content>(
header: ClauseHeader<'a>,
header_formatter: &'a Content,
trailing_colon_comment: &'a [SourceComment],
body: &'a Suite,
kind: SuiteKind,
) -> FormatClause<'a, 'ast>
where
Content: Format<PyFormatContext<'ast>>,
{
FormatClause {
header,
header_formatter: Argument::new(header_formatter),
leading_comments: None,
trailing_colon_comment,
body,
kind,
}
}
impl<'ast> Format<PyFormatContext<'ast>> for FormatClause<'_, 'ast> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
match should_suppress_clause(self, f)? {
SuppressClauseHeader::Yes {
last_child_in_clause,
} => write_suppressed_clause(self, f, last_child_in_clause),
SuppressClauseHeader::No => {
write!(f, [self.clause_header(), self.clause_body()])
}
}
}
}
/// Finds the range of `keyword` starting the search at `start_position`.
///
/// If the start position is at the end of the previous statement, the
@@ -587,3 +716,96 @@ fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResu
}
}
}
fn should_suppress_clause<'a>(
clause: &FormatClause<'a, '_>,
f: &mut Formatter<PyFormatContext<'_>>,
) -> FormatResult<SuppressClauseHeader<'a>> {
let source = f.context().source();
let Some(last_child_in_clause) = clause.header.last_child_in_clause() else {
return Ok(SuppressClauseHeader::No);
};
// Early return if we don't have a skip comment
// to avoid computing header range in the common case
if !has_skip_comment(
f.context().comments().trailing(last_child_in_clause),
source,
) {
return Ok(SuppressClauseHeader::No);
}
let clause_start = clause.header.range(source)?.end();
let clause_range = TextRange::new(clause_start, last_child_in_clause.end());
// Only applies to clauses on a single line
if source.contains_line_break(clause_range) {
return Ok(SuppressClauseHeader::No);
}
Ok(SuppressClauseHeader::Yes {
last_child_in_clause,
})
}
#[cold]
fn write_suppressed_clause(
clause: &FormatClause,
f: &mut Formatter<PyFormatContext<'_>>,
last_child_in_clause: AnyNodeRef,
) -> FormatResult<()> {
if let Some((leading_comments, last_node)) = clause.leading_comments {
leading_alternate_branch_comments(leading_comments, last_node).fmt(f)?;
}
let header = clause.header;
let clause_start = header.first_keyword_range(f.context().source())?.start();
let comments = f.context().comments().clone();
let clause_end = last_child_in_clause.end();
// Write the outer comments and format the node as verbatim
write!(
f,
[
source_position(clause_start),
verbatim_text(TextRange::new(clause_start, clause_end)),
source_position(clause_end),
trailing_comments(comments.trailing(last_child_in_clause)),
hard_line_break()
]
)?;
// We mark comments in the header as formatted as in
// the implementation of [`write_suppressed_clause_header`].
//
// Note that the header may be multi-line and contain
// various comments since we only require that the range
// starting at the _colon_ and ending at the `# fmt: skip`
// fits on one line.
header.visit(&mut |child| {
for comment in comments.leading_trailing(child) {
comment.mark_formatted();
}
comments.mark_verbatim_node_comments_formatted(child);
});
// Similarly we mark the comments in the body as formatted.
// Note that the trailing comments for the last child in the
// body have already been handled above.
for stmt in clause.body {
comments.mark_verbatim_node_comments_formatted(stmt.into());
}
Ok(())
}
enum SuppressClauseHeader<'a> {
No,
Yes {
last_child_in_clause: AnyNodeRef<'a>,
},
}

View File

@@ -8,7 +8,7 @@ use crate::comments::format::{
};
use crate::comments::{SourceComment, leading_comments, trailing_comments};
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -65,9 +65,8 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
decorators: decorator_list,
leading_definition_comments,
},
clause_header(
clause(
ClauseHeader::Class(item),
trailing_definition_comments,
&format_with(|f| {
write!(f, [token("class"), space(), name.format()])?;
@@ -132,8 +131,10 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
Ok(())
}),
trailing_definition_comments,
body,
SuiteKind::Class,
),
clause_body(body, SuiteKind::Class, trailing_definition_comments),
]
)?;

View File

@@ -6,7 +6,7 @@ use crate::expression::expr_tuple::TupleParentheses;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header};
use crate::statement::clause::{ClauseHeader, ElseClause, clause};
use crate::statement::suite::SuiteKind;
#[derive(Debug)]
@@ -50,27 +50,22 @@ impl FormatNodeRule<StmtFor> for FormatStmtFor {
write!(
f,
[
clause_header(
ClauseHeader::For(item),
trailing_condition_comments,
&format_args![
is_async.then_some(format_args![token("async"), space()]),
token("for"),
space(),
ExprTupleWithoutParentheses(target),
space(),
token("in"),
space(),
maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks),
],
),
clause_body(
body,
SuiteKind::other(orelse.is_empty()),
trailing_condition_comments
),
]
[clause(
ClauseHeader::For(item),
&format_args![
is_async.then_some(format_args![token("async"), space()]),
token("for"),
space(),
ExprTupleWithoutParentheses(target),
space(),
token("in"),
space(),
maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks),
],
trailing_condition_comments,
body,
SuiteKind::other(orelse.is_empty()),
),]
)?;
if orelse.is_empty() {
@@ -84,15 +79,14 @@ impl FormatNodeRule<StmtFor> for FormatStmtFor {
write!(
f,
[
clause_header(
ClauseHeader::OrElse(ElseClause::For(item)),
trailing,
&token("else"),
)
.with_leading_comments(leading, body.last()),
clause_body(orelse, SuiteKind::other(true), trailing),
]
[clause(
ClauseHeader::OrElse(ElseClause::For(item)),
&token("else"),
trailing,
orelse,
SuiteKind::other(true),
)
.with_leading_comments(leading, body.last())]
)?;
}

View File

@@ -4,7 +4,7 @@ use crate::comments::format::{
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::{Parentheses, Parenthesize};
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::stmt_class_def::FormatDecorators;
use crate::statement::suite::SuiteKind;
use ruff_formatter::write;
@@ -60,12 +60,13 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
decorators: decorator_list,
leading_definition_comments,
},
clause_header(
clause(
ClauseHeader::Function(item),
trailing_definition_comments,
&format_with(|f| format_function_header(f, item)),
trailing_definition_comments,
body,
SuiteKind::Function,
),
clause_body(body, SuiteKind::Function, trailing_definition_comments),
]
)?;

View File

@@ -5,7 +5,7 @@ use ruff_text_size::Ranged;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -26,22 +26,17 @@ impl FormatNodeRule<StmtIf> for FormatStmtIf {
write!(
f,
[
clause_header(
ClauseHeader::If(item),
trailing_colon_comment,
&format_args![
token("if"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
],
),
clause_body(
body,
SuiteKind::other(elif_else_clauses.is_empty()),
trailing_colon_comment
),
]
[clause(
ClauseHeader::If(item),
&format_args![
token("if"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
],
trailing_colon_comment,
body,
SuiteKind::other(elif_else_clauses.is_empty()),
)]
)?;
let mut last_node = body.last().unwrap().into();
@@ -81,9 +76,8 @@ pub(crate) fn format_elif_else_clause(
write!(
f,
[
clause_header(
clause(
ClauseHeader::ElifElse(item),
trailing_colon_comment,
&format_with(|f: &mut PyFormatter| {
f.options()
.source_map_generation()
@@ -103,9 +97,11 @@ pub(crate) fn format_elif_else_clause(
token("else").fmt(f)
}
}),
trailing_colon_comment,
body,
suite_kind,
)
.with_leading_comments(leading_comments, last_node),
clause_body(body, suite_kind, trailing_colon_comment),
f.options()
.source_map_generation()
.is_enabled()

View File

@@ -9,7 +9,7 @@ use crate::other::except_handler_except_handler::{
ExceptHandlerKind, FormatExceptHandlerExceptHandler,
};
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header};
use crate::statement::clause::{ClauseHeader, ElseClause, clause};
use crate::statement::suite::SuiteKind;
use crate::statement::{FormatRefWithRule, Stmt};
@@ -154,15 +154,14 @@ fn format_case<'a>(
write!(
f,
[
clause_header(header, trailing_case_comments, &token(kind.keyword()))
.with_leading_comments(leading_case_comments, previous_node),
clause_body(
body,
SuiteKind::other(last_suite_in_statement),
trailing_case_comments
),
]
[clause(
header,
&token(kind.keyword()),
trailing_case_comments,
body,
SuiteKind::other(last_suite_in_statement),
)
.with_leading_comments(leading_case_comments, previous_node),]
)?;
(Some(last), rest)
} else {

View File

@@ -5,7 +5,7 @@ use ruff_text_size::Ranged;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header};
use crate::statement::clause::{ClauseHeader, ElseClause, clause};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -33,22 +33,17 @@ impl FormatNodeRule<StmtWhile> for FormatStmtWhile {
write!(
f,
[
clause_header(
ClauseHeader::While(item),
trailing_condition_comments,
&format_args![
token("while"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
]
),
clause_body(
body,
SuiteKind::other(orelse.is_empty()),
trailing_condition_comments
),
]
[clause(
ClauseHeader::While(item),
&format_args![
token("while"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
],
trailing_condition_comments,
body,
SuiteKind::other(orelse.is_empty()),
)]
)?;
if !orelse.is_empty() {
@@ -60,15 +55,14 @@ impl FormatNodeRule<StmtWhile> for FormatStmtWhile {
write!(
f,
[
clause_header(
ClauseHeader::OrElse(ElseClause::While(item)),
trailing,
&token("else")
)
.with_leading_comments(leading, body.last()),
clause_body(orelse, SuiteKind::other(true), trailing),
]
[clause(
ClauseHeader::OrElse(ElseClause::While(item)),
&token("else"),
trailing,
orelse,
SuiteKind::other(true),
)
.with_leading_comments(leading, body.last()),]
)?;
}

View File

@@ -13,7 +13,7 @@ use crate::expression::parentheses::{
use crate::other::commas;
use crate::other::with_item::WithItemLayout;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -46,106 +46,103 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
write!(
f,
[
clause_header(
ClauseHeader::With(with_stmt),
colon_comments,
&format_with(|f| {
write!(
f,
[
with_stmt
.is_async
.then_some(format_args![token("async"), space()]),
token("with"),
space()
]
)?;
[clause(
ClauseHeader::With(with_stmt),
&format_with(|f| {
write!(
f,
[
with_stmt
.is_async
.then_some(format_args![token("async"), space()]),
token("with"),
space()
]
)?;
let layout = WithItemsLayout::from_statement(
with_stmt,
f.context(),
parenthesized_comments,
)?;
let layout = WithItemsLayout::from_statement(
with_stmt,
f.context(),
parenthesized_comments,
)?;
match layout {
WithItemsLayout::SingleWithTarget(single) => {
optional_parentheses(&single.format().with_options(
WithItemLayout::ParenthesizedContextManagers { single: true },
))
.fmt(f)
}
WithItemsLayout::SingleWithoutTarget(single) => single
.format()
.with_options(WithItemLayout::SingleWithoutTarget)
.fmt(f),
WithItemsLayout::SingleParenthesizedContextManager(single) => single
.format()
.with_options(WithItemLayout::SingleParenthesizedContextManager)
.fmt(f),
WithItemsLayout::ParenthesizeIfExpands => {
parenthesize_if_expands(&format_with(|f| {
let mut joiner = f.join_comma_separated(
with_stmt.body.first().unwrap().start(),
);
for item in &with_stmt.items {
joiner.entry_with_line_separator(
item,
&item.format().with_options(
WithItemLayout::ParenthesizedContextManagers {
single: with_stmt.items.len() == 1,
},
),
soft_line_break_or_space(),
);
}
joiner.finish()
}))
.fmt(f)
}
WithItemsLayout::Python38OrOlder => f
.join_with(format_args![token(","), space()])
.entries(with_stmt.items.iter().map(|item| {
item.format().with_options(WithItemLayout::Python38OrOlder {
single: with_stmt.items.len() == 1,
})
}))
.finish(),
WithItemsLayout::Parenthesized => parenthesized(
"(",
&format_with(|f: &mut PyFormatter| {
let mut joiner = f.join_comma_separated(
with_stmt.body.first().unwrap().start(),
);
for item in &with_stmt.items {
joiner.entry(
item,
&item.format().with_options(
WithItemLayout::ParenthesizedContextManagers {
single: with_stmt.items.len() == 1,
},
),
);
}
joiner.finish()
}),
")",
)
.with_dangling_comments(parenthesized_comments)
.fmt(f),
match layout {
WithItemsLayout::SingleWithTarget(single) => {
optional_parentheses(&single.format().with_options(
WithItemLayout::ParenthesizedContextManagers { single: true },
))
.fmt(f)
}
})
),
clause_body(&with_stmt.body, SuiteKind::other(true), colon_comments)
]
WithItemsLayout::SingleWithoutTarget(single) => single
.format()
.with_options(WithItemLayout::SingleWithoutTarget)
.fmt(f),
WithItemsLayout::SingleParenthesizedContextManager(single) => single
.format()
.with_options(WithItemLayout::SingleParenthesizedContextManager)
.fmt(f),
WithItemsLayout::ParenthesizeIfExpands => {
parenthesize_if_expands(&format_with(|f| {
let mut joiner =
f.join_comma_separated(with_stmt.body.first().unwrap().start());
for item in &with_stmt.items {
joiner.entry_with_line_separator(
item,
&item.format().with_options(
WithItemLayout::ParenthesizedContextManagers {
single: with_stmt.items.len() == 1,
},
),
soft_line_break_or_space(),
);
}
joiner.finish()
}))
.fmt(f)
}
WithItemsLayout::Python38OrOlder => f
.join_with(format_args![token(","), space()])
.entries(with_stmt.items.iter().map(|item| {
item.format().with_options(WithItemLayout::Python38OrOlder {
single: with_stmt.items.len() == 1,
})
}))
.finish(),
WithItemsLayout::Parenthesized => parenthesized(
"(",
&format_with(|f: &mut PyFormatter| {
let mut joiner =
f.join_comma_separated(with_stmt.body.first().unwrap().start());
for item in &with_stmt.items {
joiner.entry(
item,
&item.format().with_options(
WithItemLayout::ParenthesizedContextManagers {
single: with_stmt.items.len() == 1,
},
),
);
}
joiner.finish()
}),
")",
)
.with_dangling_comments(parenthesized_comments)
.fmt(f),
}
}),
colon_comments,
&with_stmt.body,
SuiteKind::other(true),
)]
)
}
}

View File

@@ -20,24 +20,16 @@ b = [c for c in "A very long string that would normally generate some kind of co
```diff
--- Black
+++ Ruff
@@ -1,8 +1,14 @@
-def foo(): return "mock" # fmt: skip
-if True: print("yay") # fmt: skip
-for i in range(10): print(i) # fmt: skip
+def foo():
+ return "mock" # fmt: skip
@@ -1,8 +1,10 @@
def foo(): return "mock" # fmt: skip
+
+
+if True:
+ print("yay") # fmt: skip
+for i in range(10):
+ print(i) # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
-j = 1 # fmt: skip
-while j < 10: j += 1 # fmt: skip
+j = 1 # fmt: skip
+while j < 10:
+ j += 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
-b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
+b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
@@ -46,18 +38,14 @@ b = [c for c in "A very long string that would normally generate some kind of co
## Ruff Output
```python
def foo():
return "mock" # fmt: skip
def foo(): return "mock" # fmt: skip
if True:
print("yay") # fmt: skip
for i in range(10):
print(i) # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
j = 1 # fmt: skip
while j < 10:
j += 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
```

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py
snapshot_kind: text
---
## Input
```python
@@ -200,6 +199,61 @@ def foo():
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
):
pass
# Regression tests for https://github.com/astral-sh/ruff/issues/19226
if '' and (not #
0):
pass
if '' and (not #
(0)
):
pass
if '' and (not
( #
0
)):
pass
if (
not
# comment
(a)):
pass
if not ( # comment
a):
pass
if not (
# comment
(a)):
pass
if not (
# comment
a):
pass
not (# comment
(a))
(-#comment
(a))
if ( # a
# b
not # c
# d
( # e
# f
a # g
# h
) # i
# j
):
pass
```
## Output
@@ -250,31 +304,35 @@ if +(
pass
if (
not
# comment
not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
~
# comment
~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
-
# comment
-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
+
# comment
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
@@ -283,8 +341,9 @@ if (
if (
# unary comment
not
# operand comment
not (
(
# comment
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
@@ -318,31 +377,28 @@ if (
## Trailing operator comments
if ( # comment
not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
if (
not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
# comment
~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
# comment
-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
# comment
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
@@ -362,14 +418,13 @@ if (
pass
if (
not
# comment
not a
a
):
pass
if ( # comment
not a
):
if not a: # comment
pass
# Regression test for: https://github.com/astral-sh/ruff/issues/7423
@@ -385,9 +440,9 @@ if True:
# Regression test for: https://github.com/astral-sh/ruff/issues/7448
x = (
# a
# b
not # b
# c
not ( # d
( # d
# e
True
)
@@ -415,4 +470,68 @@ def foo():
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
):
pass
# Regression tests for https://github.com/astral-sh/ruff/issues/19226
if "" and (
not 0 #
):
pass
if "" and (
not (0) #
):
pass
if "" and (
not ( #
0
)
):
pass
if (
not
# comment
(a)
):
pass
if not ( # comment
a
):
pass
if not (
# comment
a
):
pass
if not (
# comment
a
):
pass
not ( # comment
a
)
(
-(a) # comment
)
if ( # a
# b
not # c
# d
( # e
# f
a # g
# h
) # i
# j
):
pass
```

View File

@@ -0,0 +1,341 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/compound_one_liners.py
---
## Input
```python
# Test cases for fmt: skip on compound statements that fit on one line
# Basic single-line compound statements
def simple_func(): return "hello" # fmt: skip
if True: print("condition met") # fmt: skip
for i in range(5): print(i) # fmt: skip
while x < 10: x += 1 # fmt: skip
# With expressions that would normally trigger formatting
def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip
if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip
# Nested compound statements (outer should be preserved)
if True:
for i in range(10): print(i) # fmt: skip
# Multiple statements in body (should not apply - multiline)
if True:
x = 1
y = 2 # fmt: skip
# With decorators - decorated function on one line
@overload
def decorated_func(x: int) -> str: return str(x) # fmt: skip
@property
def prop_method(self): return self._value # fmt: skip
# Class definitions on one line
class SimpleClass: pass # fmt: skip
class GenericClass(Generic[T]): pass # fmt: skip
# Try/except blocks
try: risky_operation() # fmt: skip
except ValueError: handle_error() # fmt: skip
except: handle_any_error() # fmt: skip
else: success_case() # fmt: skip
finally: cleanup() # fmt: skip
# Match statements (Python 3.10+)
match value:
case 1: print("one") # fmt: skip
case _: print("other") # fmt: skip
# With statements
with open("file.txt") as f: content = f.read() # fmt: skip
with context_manager() as cm: result = cm.process() # fmt: skip
# Async variants
async def async_func(): return await some_call() # fmt: skip
async for item in async_iterator(): await process(item) # fmt: skip
async with async_context() as ctx: await ctx.work() # fmt: skip
# Complex expressions that would normally format
def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip
if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip
# Edge case: comment positioning
def func_with_comment(): # some comment
return "value" # fmt: skip
# Edge case: multiple fmt: skip (only last one should matter)
def multiple_skip(): return "test" # fmt: skip # fmt: skip
# Should NOT be affected (already multiline)
def multiline_func():
return "this should format normally"
if long_condition_that_spans \
and continues_on_next_line:
print("multiline condition")
# Mix of skipped and non-skipped
for i in range(10): print(f"item {i}") # fmt: skip
for j in range(5):
print(f"formatted item {j}")
# With trailing comma that would normally be removed
def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip
# Dictionary/list comprehensions
def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip
def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip
# Lambda in one-liner
def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip
# String formatting that would normally be reformatted
def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip
# loop else clauses
for i in range(2): print(i) # fmt: skip
else: print("this") # fmt: skip
while foo(): print(i) # fmt: skip
else: print("this") # fmt: skip
# again but only the first skip
for i in range(2): print(i) # fmt: skip
else: print("this")
while foo(): print(i) # fmt: skip
else: print("this")
# again but only the second skip
for i in range(2): print(i)
else: print("this") # fmt: skip
while foo(): print(i)
else: print("this") # fmt: skip
# multiple statements in body
if True: print("this"); print("that") # fmt: skip
# Examples with more comments
try: risky_operation() # fmt: skip
# leading 1
except ValueError: handle_error() # fmt: skip
# leading 2
except: handle_any_error() # fmt: skip
# leading 3
else: success_case() # fmt: skip
# leading 4
finally: cleanup() # fmt: skip
# trailing
# multi-line before colon (should remain as is)
if (
long_condition
): a + b # fmt: skip
# over-indented comment example
# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910
# and https://github.com/astral-sh/ruff/pull/21185
for x in it: foo()
# comment
else: bar() # fmt: skip
if this(
'is a long',
# commented
'condition'
): with_a_skip # fmt: skip
```
## Output
```python
# Test cases for fmt: skip on compound statements that fit on one line
# Basic single-line compound statements
def simple_func(): return "hello" # fmt: skip
if True: print("condition met") # fmt: skip
for i in range(5): print(i) # fmt: skip
while x < 10: x += 1 # fmt: skip
# With expressions that would normally trigger formatting
def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip
if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip
# Nested compound statements (outer should be preserved)
if True:
for i in range(10): print(i) # fmt: skip
# Multiple statements in body (should not apply - multiline)
if True:
x = 1
y = 2 # fmt: skip
# With decorators - decorated function on one line
@overload
def decorated_func(x: int) -> str: return str(x) # fmt: skip
@property
def prop_method(self): return self._value # fmt: skip
# Class definitions on one line
class SimpleClass: pass # fmt: skip
class GenericClass(Generic[T]): pass # fmt: skip
# Try/except blocks
try: risky_operation() # fmt: skip
except ValueError: handle_error() # fmt: skip
except: handle_any_error() # fmt: skip
else: success_case() # fmt: skip
finally: cleanup() # fmt: skip
# Match statements (Python 3.10+)
match value:
case 1: print("one") # fmt: skip
case _: print("other") # fmt: skip
# With statements
with open("file.txt") as f: content = f.read() # fmt: skip
with context_manager() as cm: result = cm.process() # fmt: skip
# Async variants
async def async_func(): return await some_call() # fmt: skip
async for item in async_iterator(): await process(item) # fmt: skip
async with async_context() as ctx: await ctx.work() # fmt: skip
# Complex expressions that would normally format
def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip
if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip
# Edge case: comment positioning
def func_with_comment(): # some comment
return "value" # fmt: skip
# Edge case: multiple fmt: skip (only last one should matter)
def multiple_skip(): return "test" # fmt: skip # fmt: skip
# Should NOT be affected (already multiline)
def multiline_func():
return "this should format normally"
if long_condition_that_spans and continues_on_next_line:
print("multiline condition")
# Mix of skipped and non-skipped
for i in range(10): print(f"item {i}") # fmt: skip
for j in range(5):
print(f"formatted item {j}")
# With trailing comma that would normally be removed
def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip
# Dictionary/list comprehensions
def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip
def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip
# Lambda in one-liner
def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip
# String formatting that would normally be reformatted
def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip
# loop else clauses
for i in range(2): print(i) # fmt: skip
else: print("this") # fmt: skip
while foo(): print(i) # fmt: skip
else: print("this") # fmt: skip
# again but only the first skip
for i in range(2): print(i) # fmt: skip
else:
print("this")
while foo(): print(i) # fmt: skip
else:
print("this")
# again but only the second skip
for i in range(2):
print(i)
else: print("this") # fmt: skip
while foo():
print(i)
else: print("this") # fmt: skip
# multiple statements in body
if True: print("this"); print("that") # fmt: skip
# Examples with more comments
try: risky_operation() # fmt: skip
# leading 1
except ValueError: handle_error() # fmt: skip
# leading 2
except: handle_any_error() # fmt: skip
# leading 3
else: success_case() # fmt: skip
# leading 4
finally: cleanup() # fmt: skip
# trailing
# multi-line before colon (should remain as is)
if (
long_condition
): a + b # fmt: skip
# over-indented comment example
# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910
# and https://github.com/astral-sh/ruff/pull/21185
for x in it:
foo()
# comment
else: bar() # fmt: skip
if this(
'is a long',
# commented
'condition'
): with_a_skip # fmt: skip
```

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/docstrings.py
snapshot_kind: text
---
## Input
```python

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/parentheses/expression_parentheses_comments.py
snapshot_kind: text
---
## Input
```python
@@ -179,13 +178,13 @@ nested_parentheses4 = [
x = (
# unary comment
not
# in-between comment
not (
(
# leading inner
"a"
),
# in-between comment
not (
not ( # in-between comment
# leading inner
"b"
),
@@ -194,8 +193,7 @@ x = (
"c"
),
# 1
# 2
not ( # 3
not ( # 2 # 3
# 4
"d"
),
@@ -203,8 +201,9 @@ x = (
if (
# unary comment
not
# in-between comment
not (
(
# leading inner
1
)

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,27 +283,30 @@ 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,
code_description: diagnostic.to_ruff_url().and_then(|url| {
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()?,
href: lsp_types::Url::parse(url).ok()?,
})
}),
source: Some(DIAGNOSTIC_NAME.into()),

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#L120" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L126" 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#L164" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L170" 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#L190" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L196" 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#L215" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L221" 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#L241" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L247" 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#L306" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L312" 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#L327" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L333" 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#L531" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L537" 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#L555" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L561" 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#L359" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L365" 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#L609" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L615" 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#L649" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L655" 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#L1808" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1814" 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#L671" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L677" 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#L701" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L707" 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#L752" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L758" 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#L773" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L779" 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#L796" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L802" 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#L832" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L838" 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#L576" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L582" 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#L858" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L864" 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#L955" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L961" 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#L1942" 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#L505" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L511" 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#L931" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L937" 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#L982" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L988" 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#L1081" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1087" 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#L886" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L892" 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#L441" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L447" 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#L1101" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1107" 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#L630" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L636" 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#L1144" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1150" 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#L910" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L916" 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#L1183" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1189" 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#L1207" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1213" 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#L1259" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1265" 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#L1231" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1237" 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#L1287" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1293" 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#L1316" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1322" 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#L1909" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1915" 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#L1335" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1341" 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#L1358" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1364" 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#L1376" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1382" 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#L1427" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1433" 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#L1662" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1668" 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#L1784" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1790" 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#L1518" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1524" 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#L1563" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1569" 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#L1541" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1547" 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#L1584" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1590" 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#L1641" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1647" 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#L1683" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1689" 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#L1705" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1711" 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#L1724" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1730" 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#L1396" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1402" 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#L1743" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1749" 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#L1765" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1771" 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#L470" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L476" 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#L285" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L291" 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#L1448" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1454" 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#L138" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L144" 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#L1470" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1476" 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#L1836" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1842" 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#L1623" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1629" 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#L1857" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1863" 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#L719" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L725" 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#L1025" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1031" 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#L267" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L273" 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#L1496" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1502" target="_blank">View source</a>
</small>

View File

@@ -5,6 +5,7 @@ mod python_version;
mod version;
pub use args::Cli;
use ty_project::metadata::settings::TerminalSettings;
use ty_static::EnvVars;
use std::fmt::Write;
@@ -21,7 +22,9 @@ use clap::{CommandFactory, Parser};
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use rayon::ThreadPoolBuilder;
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, DisplayDiagnostics, Severity};
use ruff_db::diagnostic::{
Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity,
};
use ruff_db::files::File;
use ruff_db::max_parallelism;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
@@ -193,6 +196,12 @@ pub enum ExitStatus {
InternalError = 101,
}
impl ExitStatus {
pub const fn is_internal_error(self) -> bool {
matches!(self, ExitStatus::InternalError)
}
}
impl Termination for ExitStatus {
fn report(self) -> ExitCode {
ExitCode::from(self as u8)
@@ -334,11 +343,8 @@ impl MainLoop {
let diagnostics_count = result.len();
let mut stdout = self.printer.stream_for_details().lock();
let max_severity = result
.iter()
.map(Diagnostic::severity)
.max()
.unwrap_or(Severity::Info);
let exit_status =
exit_status_from_diagnostics(&result, terminal_settings);
// Only render diagnostics if they're going to be displayed, since doing
// so is expensive.
@@ -359,25 +365,14 @@ impl MainLoop {
)?;
}
if max_severity.is_fatal() {
if exit_status.is_internal_error() {
tracing::warn!(
"A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."
);
}
if self.watcher.is_none() {
return Ok(match max_severity {
Severity::Info => ExitStatus::Success,
Severity::Warning => {
if terminal_settings.error_on_warning {
ExitStatus::Failure
} else {
ExitStatus::Success
}
}
Severity::Error => ExitStatus::Failure,
Severity::Fatal => ExitStatus::InternalError,
});
return Ok(exit_status);
}
}
} else {
@@ -410,6 +405,40 @@ impl MainLoop {
}
}
fn exit_status_from_diagnostics(
diagnostics: &[Diagnostic],
terminal_settings: &TerminalSettings,
) -> ExitStatus {
if diagnostics.is_empty() {
return ExitStatus::Success;
}
let mut max_severity = Severity::Info;
let mut io_error = false;
for diagnostic in diagnostics {
max_severity = max_severity.max(diagnostic.severity());
io_error = io_error || matches!(diagnostic.id(), DiagnosticId::Io);
}
if !max_severity.is_fatal() && io_error {
return ExitStatus::Error;
}
match max_severity {
Severity::Info => ExitStatus::Success,
Severity::Warning => {
if terminal_settings.error_on_warning {
ExitStatus::Failure
} else {
ExitStatus::Success
}
}
Severity::Error => ExitStatus::Failure,
Severity::Fatal => ExitStatus::InternalError,
}
}
/// A progress reporter for `ty check`.
struct IndicatifReporter {
collector: CollectReporter,

View File

@@ -41,22 +41,24 @@ fn test_quiet_output() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", "x: int = 'foo'")?;
// By default, we emit a diagnostic
assert_cmd_snapshot!(case.command(), @r###"
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int`
--> test.py:1:1
--> test.py:1:4
|
1 | x: int = 'foo'
| ^
| --- ^^^^^ Incompatible value of type `Literal["foo"]`
| |
| Declared type
|
info: rule `invalid-assignment` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
"#);
// With `quiet`, the diagnostic is not displayed, just the summary message
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"
@@ -560,9 +562,9 @@ fn check_non_existing_path() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case.command().arg("project/main.py").arg("project/tests"),
@r###"
@r"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
error[io]: `<temp_dir>/project/main.py`: No such file or directory (os error 2)
@@ -572,7 +574,7 @@ fn check_non_existing_path() -> anyhow::Result<()> {
----- stderr -----
WARN No python files found under the given path(s)
"###
"
);
Ok(())

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,13 +8,12 @@ use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
///
/// Returns symbols from all files in the workspace and dependencies, filtered
/// by the query.
pub fn all_symbols<'db>(db: &'db dyn Db, query: &str) -> Vec<AllSymbolInfo<'db>> {
pub fn all_symbols<'db>(db: &'db dyn Db, query: &QueryPattern) -> Vec<AllSymbolInfo<'db>> {
// If the query is empty, return immediately to avoid expensive file scanning
if query.is_empty() {
if query.will_match_everything() {
return Vec::new();
}
let query = QueryPattern::new(query);
let results = std::sync::Mutex::new(Vec::new());
{
let modules = all_modules(db);
@@ -144,7 +143,7 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
impl CursorTest {
fn all_symbols(&self, query: &str) -> String {
let symbols = all_symbols(&self.db, query);
let symbols = all_symbols(&self.db, &QueryPattern::new(query));
if symbols.is_empty() {
return "No symbols found".to_string();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::types::Type;
use ty_python_semantic::types::ide_support::{
call_signature_details, definitions_for_keyword_argument,
call_signature_details, call_type_simplified_by_overloads, definitions_for_keyword_argument,
};
use ty_python_semantic::{
HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol,
@@ -61,6 +61,7 @@ pub(crate) enum GotoTarget<'a> {
/// ```
ImportModuleComponent {
module_name: String,
level: u32,
component_index: usize,
component_range: TextRange,
},
@@ -213,6 +214,7 @@ impl<'db> DefinitionsOrTargets<'db> {
| ty_python_semantic::types::TypeDefinition::Function(definition)
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)
| ty_python_semantic::types::TypeDefinition::SpecialForm(definition)
| ty_python_semantic::types::TypeDefinition::NewType(definition) => {
ResolvedDefinition::Definition(definition)
}
@@ -301,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 { .. }
@@ -326,6 +337,18 @@ impl GotoTarget<'_> {
Some(ty)
}
/// Try to get a simplified display of this callable type by resolving overloads
pub(crate) fn call_type_simplified_by_overloads(
&self,
model: &SemanticModel,
) -> Option<String> {
if let GotoTarget::Call { call, .. } = self {
call_type_simplified_by_overloads(model.db(), model, call)
} else {
None
}
}
/// Gets the definitions for this goto target.
///
/// The `alias_resolution` parameter controls whether import aliases
@@ -340,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 {
@@ -391,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(
@@ -431,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)),
]))
}
@@ -465,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() {
@@ -478,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))
}
@@ -619,6 +625,7 @@ impl GotoTarget<'_> {
{
return Some(GotoTarget::ImportModuleComponent {
module_name: full_name.to_string(),
level: 0,
component_index,
component_range,
});
@@ -659,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,
});
@@ -863,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))
@@ -934,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())
@@ -942,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
@@ -970,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();
@@ -991,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]
}

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