Compare commits

..

242 Commits

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

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

## Test Plan

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

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

## Test Plan

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

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

## Test Plan

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

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

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

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

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

## Ecosystem impact

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

## Test Plan

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

This is a follow-up to #19321.

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

## Test Plan

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

This is a follow-up to #19321.

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

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

## Test Plan

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

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

## Test Plan

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

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

```python
# somefile.py

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

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

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

Support `as` patterns in reachability analysis:

```py
from typing import assert_never


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

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

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

## Test Plan

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

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

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

## Test Plan

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

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

---

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

---

### Release Notes

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

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

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

#### What's Changed

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

#### New Contributors

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

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

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

---

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

---

### Release Notes

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

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

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

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

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

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

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

---

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

---

### Release Notes

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

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

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

Initial release

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

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

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

---

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

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

---

### Release Notes

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

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

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

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

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

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

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

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

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

---

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

---

### Release Notes

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

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

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

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

##### Preview features

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

##### Bug fixes

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

##### Rule changes

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

##### Performance

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

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

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

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

---

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

---

### Release Notes

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

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

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

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

##### In this release:

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

##### Other changes:

- Upgrade dependencies

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

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

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

---

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

---

### Release Notes

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

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

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

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

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

Fixes #19577

---------

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

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

I make the change in this PR.
2025-08-03 16:30:56 +00:00
cristian64
bc6e105c18 Include column numbers in GitLab output format. (#19708) 2025-08-03 12:37:01 +00:00
Micha Reiser
6bd413df6c [ty] Update salsa (#19710) 2025-08-03 09:18:10 +00:00
Nathaniel Roman
85bd961fd3 [ty] resolve file symlinks in src walk (#19674)
Co-authored-by: Nathaniel Roman <nroman@openai.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-08-01 22:52:04 +02:00
Douglas Creager
d37911685f [ty] Correctly instantiate generic class that inherits __init__ from generic base class (#19693)
This is subtle, and the root cause became more apparent with #19604,
since we now have many more cases of superclasses and subclasses using
different typevars. The issue is easiest to see in the following:

```py
class C[T]:
    def __init__(self, t: T) -> None: ...

class D[U](C[T]):
    pass

reveal_type(C(1))  # revealed: C[int]
reveal_type(D(1))  # should be: D[int]
```

When instantiating a generic class, the `__init__` method inherits the
generic context of that class. This lets our call binding machinery
infer a specialization for that context.

Prior to this PR, the instantiation of `C` worked just fine. Its
`__init__` method would inherit the `[T]` generic context, and we would
infer `{T = int}` as the specialization based on the argument
parameters.

It didn't work for `D`. The issue is that the `__init__` method was
inheriting the generic context of the class where `__init__` was defined
(here, `C` and `[T]`). At the call site, we would then infer `{T = int}`
as the specialization — but that wouldn't help us specialize `D[U]`,
since `D` does not have `T` in its generic context!

Instead, the `__init__` method should inherit the generic context of the
class that we are performing the lookup on (here, `D` and `[U]`). That
lets us correctly infer `{U = int}` as the specialization, which we can
successfully apply to `D[U]`.

(Note that `__init__` refers to `C`'s typevars in its signature, but
that's okay; our member lookup logic already applies the `T = U`
specialization when returning a member of `C` while performing a lookup
on `D`, transforming its signature from `(Self, T) -> None` to `(Self,
U) -> None`.)

Closes https://github.com/astral-sh/ty/issues/588
2025-08-01 15:29:18 -04:00
GiGaGon
580577e667 [refurb] Make example error out-of-the-box (FURB157) (#19695)
## Summary

Part of #18972

This PR makes [verbose-decimal-constructor
(FURB157)](https://docs.astral.sh/ruff/rules/verbose-decimal-constructor/#verbose-decimal-constructor-furb157)'s
example error out-of-the-box.

[Old example](https://play.ruff.rs/0930015c-ad45-4490-800e-66ed057bfe34)
```py
Decimal("0")
Decimal(float("Infinity"))
```

[New example](https://play.ruff.rs/516e5992-322f-4203-afe7-46d8cad53368)
```py
from decimal import Decimal

Decimal("0")
Decimal(float("Infinity"))
```

Imports were also added to the "Use Instead" section.
2025-08-01 12:55:48 -05:00
Dan Parizher
dce25da19a [flake8-errmsg] Exclude typing.cast from EM101 (#19656)
## Summary

Fixes #19596
2025-08-01 13:37:44 -04:00
Douglas Creager
06cd249a9b [ty] Track different uses of legacy typevars, including context when rendering typevars (#19604)
This PR introduces a few related changes:

- We now keep track of each time a legacy typevar is bound in a
different generic context (e.g. class, function), and internally create
a new `TypeVarInstance` for each usage. This means the rest of the code
can now assume that salsa-equivalent `TypeVarInstance`s refer to the
same typevar, even taking into account that legacy typevars can be used
more than once.

- We also go ahead and track the binding context of PEP 695 typevars.
That's _much_ easier to track since we have the binding context right
there during type inference.

- With that in place, we can now include the name of the binding context
when rendering typevars (e.g. `T@f` instead of `T`)
2025-08-01 12:20:32 -04:00
David Peter
48d5bd13fa [ty] Initial test suite for TypedDict (#19686)
## Summary

Adds an initial set of tests based on the highest-priority items in
https://github.com/astral-sh/ty/issues/154. This is certainly not yet
exhaustive (required/non-required, `total`, and other things are
missing), but will be useful to measure progress on this feature.

## Test Plan

Checked intended behavior against runtime and other type checkers.
2025-08-01 16:56:02 +02:00
Alex Waygood
e7e7b7bf21 [ty] Improve debuggability of protocol types (#19662) 2025-08-01 15:16:13 +01:00
Alex Waygood
57e2e8664f [ty] Simplify lifetime requirements for PySlice trait (#19687) 2025-08-01 15:13:47 +01:00
Alex Waygood
18aae21b9a [ty] Improve isinstance() truthiness analysis for generic types (#19668) 2025-08-01 14:44:22 +01:00
GiGaGon
d8151f0239 [refurb] Make example error out-of-the-box (FURB164) (#19673)
<!--
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

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

Part of #18972

This PR makes [unnecessary-from-float
(FURB164)](https://docs.astral.sh/ruff/rules/unnecessary-from-float/#unnecessary-from-float-furb164)'s
example error out-of-the-box.

[Old example](https://play.ruff.rs/807ef72f-9671-408d-87ab-8b8bad65b33f)
```py
Decimal.from_float(4.2)
Decimal.from_float(float("inf"))
Fraction.from_float(4.2)
Fraction.from_decimal(Decimal("4.2"))
```

[New example](https://play.ruff.rs/303680d1-8a68-4b6c-a5fd-d79c56eb0f88)
```py
from decimal import Decimal
from fractions import Fraction

Decimal.from_float(4.2)
Decimal.from_float(float("inf"))
Fraction.from_float(4.2)
Fraction.from_decimal(Decimal("4.2"))
```

The "Use instead" section also had imports added, and one of the fixed
examples was slightly wrong and needed modification.

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
2025-08-01 07:49:58 -05:00
Hunter Hogan
2ee56735e2 Fix link: unused_import.rs (#19648) 2025-08-01 08:47:52 +00:00
David Peter
ade6a4262a [ty] Remove Specialization::display (full) (#19682)
## Summary

This seems to be unused
2025-08-01 10:44:11 +02:00
David Peter
d43e6fb9c6 [ty] Remove KnownModule::is_enum (#19681)
## Summary

Changes the visibility of `KnownModule` and removes an unneeded
function.
2025-08-01 10:31:12 +02:00
Matthew Mckee
b30d97e5e0 [ty] Support __setitem__ and improve __getitem__ related diagnostics (#19578)
## Summary

Adds validation to subscript assignment expressions.

```py
class Foo: ...

class Bar:
    __setattr__ = None

class Baz:
    def __setitem__(self, index: str, value: int) -> None:
        pass

# We now emit a diagnostic on these statements
Foo()[1] = 2
Bar()[1] = 2
Baz()[1] = 2

```

Also improves error messages on invalid `__getitem__` expressions

## Test Plan

Update mdtests and add more to `subscript/instance.md`

---------

Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
Co-authored-by: David Peter <mail@david-peter.de>
2025-08-01 09:23:27 +02:00
github-actions[bot]
5c5d50d57a [ty] Sync vendored typeshed stubs (#19676)
Close and reopen this PR to trigger CI

---------

Co-authored-by: typeshedbot <>
2025-08-01 08:43:42 +02:00
Dan Parizher
b3a26a50ad [flake8-use-pathlib] Expand PTH201 to check all PurePath subclasses (#19440)
## Summary

Fixes #19437
2025-07-31 22:18:07 -04:00
GiGaGon
6a2d358d7a [refurb] Make example error out-of-the-box (FURB180) (#19672)
<!--
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

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

Part of #18972

This PR makes [meta-class-abc-meta
(FURB180)](https://docs.astral.sh/ruff/rules/meta-class-abc-meta/#meta-class-abc-meta-furb180)'s
example error out-of-the-box.

[Old example](https://play.ruff.rs/6beca1be-45cd-4e5a-aafa-6a0584c10d64)
```py
class C(metaclass=ABCMeta):
    pass
```

[New example](https://play.ruff.rs/bbad34da-bf07-44e6-9f34-53337e8f57d4)
```py
import abc


class C(metaclass=abc.ABCMeta):
    pass
```

The "Use instead" section as also modified similarly.

## Test Plan

<!-- How was it tested? -->

N/A, no functionality/tests affected
2025-07-31 17:14:15 -04:00
Dan Parizher
b07def07c9 [pyupgrade] Prevent infinite loop with I002 (UP010, UP035) (#19413)
## Summary

Fixes #18729 and fixes #16802

## Test Plan

Manually verified via CLI that Ruff no longer enters an infinite loop by
running:
```sh
echo 1 | ruff --isolated check - --select I002,UP010 --fix
```
with `required-imports = ["from __future__ import generator_stop"]` set
in the config, confirming “All checks passed!” and no snapshots were
generated.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-07-31 15:17:27 -04:00
Alex Waygood
2ab1502e51 [ty] Improve the Display for generic type[] types (#19667) 2025-07-31 19:45:01 +01:00
Alex Waygood
a3f28baab4 [ty] Refactor TypeInferenceBuilder::infer_subscript_expression_types (#19658) 2025-07-31 13:38:43 +00:00
Brent Westbrook
a71513bae1 Fix tests on 32-bit architectures (#19652)
Summary
--

Fixes #19640. I'm not sure these are the exact fixes we really want, but
I
reproduced the issue in a 32-bit Docker container and tracked down the
causes,
so I figured I'd open a PR.

As I commented on the issue, the `goto_references` test depends on the
iteration
order of the files in an `FxHashSet` in `Indexed`. In this case, we can
just
sort the output in test code.

Similarly, the tuple case depended on the order of overloads inserted in
an
`FxHashMap`. `FxIndexMap` seemed like a convenient drop-in replacement,
but I
don't know if that will have other detrimental effects. I did have to
change the
assertion for the tuple test, but I think it should now be stable across
architectures.

Test Plan
--

Running the tests in the aforementioned Docker container
2025-07-31 08:52:19 -04:00
Alex Waygood
d2d4b115e3 [ty] Move pandas-stubs to bad.txt (#19659) 2025-07-31 12:33:24 +01:00
Alex Waygood
27b03a9d7b [ty] Remove special casing for string-literal-in-tuple __contains__ (#19642) 2025-07-31 11:28:03 +01:00
Harshil
32c454bb56 Update pre-commit's ruff id (#19654) 2025-07-31 07:17:04 +02:00
Ibraheem Ahmed
f6b7418def Update salsa (#19449)
## Summary

Pulls in a bunch of salsa micro-optimizations.
2025-07-30 15:31:46 -04:00
Ibraheem Ahmed
8f8c39c435 Simplify get_size2 usage (#19643)
## Summary

These were added in the 0.5.0 release.
2025-07-30 15:31:37 -04:00
Matthew Mckee
4739bc8d14 [ty] Fix incorrect diagnostic when calling __setitem__ (#19645)
## Summary

Resolves https://github.com/astral-sh/ty/issues/862 by not emitting a
diagnostic.

## Test Plan

Add test to show we don't emit the diagnostic
2025-07-30 20:34:52 +02:00
Alex Waygood
7b4103bcb6 [ty] Remove special casing for tuple addition (#19636) 2025-07-30 16:25:42 +00:00
Jim Hoekstra
38049aae12 fix missing-required-imports introducing syntax error after dosctring ending with backslash (#19505)
Issue: https://github.com/astral-sh/ruff/issues/19498

## Summary


[missing-required-import](https://docs.astral.sh/ruff/rules/missing-required-import/)
inserts the missing import on the line immediately following the last
line of the docstring. However, if the dosctring is immediately followed
by a continuation token (i.e. backslash) then this leads to a syntax
error because Python interprets the docstring and the inserted import to
be on the same line.

The proposed solution in this PR is to check if the first token after a
file docstring is a continuation character, and if so, to advance an
additional line before inserting the missing import.

## Test Plan

Added a unit test, and the following example was verified manually:

Given this simple test Python file:

```python
"Hello, World!"\

print(__doc__)
```

and this ruff linting configuration in the `pyproject.toml` file:

```toml
[tool.ruff.lint]
select = ["I"]

[tool.ruff.lint.isort]
required-imports = ["import sys"]
```

Without the changes in this PR, the ruff linter would try to insert the
missing import in line 2, resulting in a syntax error, and report the
following:

`error: Fix introduced a syntax error. Reverting all changes.`

With the changes in this PR, ruff correctly advances one more line
before adding the missing import, resulting in the following output:

```python
"Hello, World!"\

import sys

print(__doc__)
```

---------

Co-authored-by: Jim Hoekstra <jim.hoekstra@pacmed.nl>
2025-07-30 12:12:46 -04:00
Alex Waygood
ec3d5ebda2 [ty] Upcast heterogeneous and mixed tuples to homogeneous tuples where it's necessary to solve a TypeVar (#19635)
## Summary

This PR improves our generics solver such that we are able to solve the
`TypeVar` in this snippet to `int | str` (the union of the elements in
the heterogeneous tuple) by upcasting the heterogeneous tuple to its
pure-homogeneous-tuple supertype:

```py
def f[T](x: tuple[T, ...]) -> T:
    return x[0]

def g(x: tuple[int, str]):
    reveal_type(f(x))
```

## Test Plan

Mdtests. Some TODOs remain in the mdtest regarding solving `TypeVar`s
for mixed tuples, but I think this PR on its own is a significant step
forward for our generics solver when it comes to tuple types.

---------

Co-authored-by: Douglas Creager <dcreager@dcreager.net>
2025-07-30 17:12:21 +01:00
Micha Reiser
d797592f70 [ty] Fix server panic in workspace diagnostics request handler when typing (#19631) 2025-07-30 16:40:42 +01:00
David Peter
eb02aa5676 [ty] Async for loops and async iterables (#19634)
## Summary

Add support for `async for` loops and async iterables.

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

## Ecosystem impact

```diff
- boostedblob/listing.py:445:54: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
```

This is correct. We now find a true positive in the `# type: ignore`'d
code.

All of the other ecosystem hits are of the type

```diff
trio (https://github.com/python-trio/trio)
+ src/trio/_core/_tests/test_guest_mode.py:532:24: error[not-iterable] Object of type `MemorySendChannel[int] | MemoryReceiveChannel[int]` may not be iterable
```

The message is correct, because only `MemoryReceiveChannel` has an
`__aiter__` method, but `MemorySendChannel` does not. What's not correct
is our inferred type here. It should be `MemoryReceiveChannel[int]`, not
the union of the two. This is due to missing unpacking support for tuple
subclasses, which @AlexWaygood is working on. I don't think this should
block merging this PR, because those wrong types are already there,
without this PR.

## Test Plan

New Markdown tests and snapshot tests for diagnostics.
2025-07-30 17:40:24 +02:00
Dan Parizher
e593761232 [ruff] Parenthesize generator expressions in f-strings (RUF010) (#19434)
## Summary

Fixes #19433

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-07-30 15:02:31 +00:00
Brent Westbrook
8979271ea8 Always expand tabs to four spaces in diagnostics (#19618)
## Summary

I was a bit stuck on some snapshot differences I was seeing in #19415,
but @BurntSushi pointed out that `annotate-snippets` already normalizes
tabs on its own, which was very helpful! Instead of applying this change
directly to the other branch, I wanted to try applying it in
`ruff_linter` first. This should very slightly reduce the number of
changes in #19415 proper.

It looks like `annotate-snippets` always expands a tab to four spaces,
whereas I think we were aligning to tab stops:

```diff
  6 | spam(ham[1], { eggs: 2})
  7 | #: E201:1:6
- 8 | spam(   ham[1], {eggs: 2})
-   |      ^^^ E201
+ 8 | spam(    ham[1], {eggs: 2})
+   |      ^^^^ E201
```

```diff
61 | #: E203:2:15 E702:2:16
 62 | if x == 4:
-63 |     print(x, y) ; x, y = y, x
-   |                ^ E203
+63 |     print(x, y)    ; x, y = y, x
+   |                ^^^^ E203
```

```diff
 E27.py:15:6: E271 [*] Multiple spaces after keyword
    |
-13 | True        and False
+13 | True        and    False
 14 | #: E271
 15 | a and  b
    |      ^^ E271
```

I don't think this is too bad and has the major benefit of allowing us
to pass the non-tab-expanded range to `annotate-snippets` in #19415,
where it's also displayed in the header. Ruff doesn't have this problem
currently because it uses its own concise diagnostic output as the
header for full diagnostics, where the pre-expansion range is used
directly.

## Test Plan

Existing tests with a few snapshot updates
2025-07-30 11:00:36 -04:00
Andrew Gallant
d1a286226c [ty] Update module resolution diagram to account for typeshed VERSIONS file
This does unfortunately add a fair bit of complexity to the flow
diagram.

Ref https://github.com/astral-sh/ruff/pull/19620#issuecomment-3133684294
2025-07-30 10:34:58 -04:00
Micha Reiser
1ba32684da Fix copy and line separator colors in dark mode (#19630) 2025-07-30 15:08:31 +01:00
Micha Reiser
70d4b271da [ty] Remove AssertUnwindSafe requirement from ProgressReporter (#19637) 2025-07-30 12:46:44 +00:00
Alex Waygood
feaedb1812 [ty] Synthesize precise __getitem__ overloads for tuple subclasses (#19493) 2025-07-30 11:25:44 +00:00
Micha Reiser
6237ecb4db [ty] Add progress reporting to workspace diagnostics (#19616) 2025-07-30 10:27:34 +00:00
Micha Reiser
2a5ace6e55 [ty] Implement diagnostic caching (#19605) 2025-07-30 11:04:34 +01:00
David Peter
4ecf1d205a [ty] Support async/await, async with and yield from (#19595)
## Summary

- Add support for the return types of `async` functions
- Add type inference for `await` expressions
- Add support for `async with` / async context managers
- Add support for `yield from` expressions

This PR is generally lacking proper error handling in some cases (e.g.
illegal `__await__` attributes). I'm planning to work on this in a
follow-up.

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

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

## Ecosystem

There are a lot of true positives on `prefect` which look similar to:
```diff
prefect (https://github.com/PrefectHQ/prefect)
+ src/integrations/prefect-aws/tests/workers/test_ecs_worker.py:406:12: error[unresolved-attribute] Type `str` has no attribute `status_code`
```

This is due to a wrong return type annotation
[here](e926b8c4c1/src/integrations/prefect-aws/tests/workers/test_ecs_worker.py (L355-L391)).

```diff
mitmproxy (https://github.com/mitmproxy/mitmproxy)
+ test/mitmproxy/addons/test_clientplayback.py:18:1: error[invalid-argument-type] Argument to function `asynccontextmanager` is incorrect: Expected `(...) -> AsyncIterator[Unknown]`, found `def tcp_server(handle_conn, **server_args) -> Unknown | tuple[str, int]`
```


[This](a4d794c59a/test/mitmproxy/addons/test_clientplayback.py (L18-L19))
is a true positive. That function should return
`AsyncIterator[Address]`, not `Address`.

I looked through almost all of the other new diagnostics and they all
look like known problems or true positives.

## Typing conformance

The typing conformance diff looks good.

## Test Plan

New Markdown tests
2025-07-30 11:51:21 +02:00
Brent Westbrook
c5ac998892 Bump 0.12.7 (#19627)
## Test Plan

- [x] Download the [sdist
artifact](https://github.com/astral-sh/ruff/actions/runs/16608501774/artifacts/3643617012)
and check that the LICENSE is present
2025-07-29 18:18:42 -04:00
Brent Westbrook
04a8f64cd7 Revert license and license-files changes in pyproject.toml (#19624)
Summary
--

This partially reverts commit 13634ff433
after issues in the release today.

Test Plan
--

```shell
uv build --sdist
tar -tzf dist/ruff-0.12.6.tar.gz | grep ruff-0.12.6/LICENSE
```

which finds the license now.
2025-07-29 17:27:55 -04:00
Brent Westbrook
6e00adf308 Bump 0.12.6 (#19622) 2025-07-29 16:31:01 -04:00
Brent Westbrook
864196b988 Add Checker::context method, deduplicate Unicode checks (#19609)
Summary
--

This PR adds a `Checker::context` method that returns the underlying
`LintContext` to unify `Candidate::into_diagnostic` and
`Candidate::report_diagnostic` in our ambiguous Unicode character
checks. This avoids some duplication and also avoids collecting a `Vec`
of `Candidate`s only to iterate over it later.

Test Plan
--

Existing tests
2025-07-29 16:07:55 -04:00
Thomas Mattone
ae26fa020c [flake8-pyi] Preserve inline comment in ellipsis removal (PYI013) (#19399)
## Summary

Fixes #19385.

Based on [unnecessary-placeholder
(PIE790)](https://docs.astral.sh/ruff/rules/unnecessary-placeholder/)
behavior, [ellipsis-in-non-empty-class-body
(PYI013)](https://docs.astral.sh/ruff/rules/ellipsis-in-non-empty-class-body/)
now safely preserve inline comment on ellipsis removal.

## Test Plan

A new test class was added:

```python
class NonEmptyChildWithInlineComment:
    value: int
    ... # preserve me
```

with the following expected fix:

```python
class NonEmptyChildWithInlineComment:
    value: int
    # preserve me
```
2025-07-29 15:06:04 -04:00
Andrew Gallant
88a679945c [ty] Add flow diagram for import resolution
The diagram is written in the Dot language, which can
be converted to SVG (or any other image) by GraphViz.

I thought it was a good idea to write this down in
preparation for adding routines that list modules.
Code reuse is likely to be difficult and I wanted to
be sure I understood how it worked.
2025-07-29 14:49:20 -04:00
Andrew Gallant
941be52358 [ty] Add comments to some core resolver functions
Some of the contracts were a little tricky to discover from just the
parameter types, so I added some docs (and fixed what I believe was one
typo).
2025-07-29 14:49:20 -04:00
Andrew Gallant
13624ce17f [ty] Add missing ticks and use consistent quoting
This irked me while I was reading the code, so I just tried to fix what
I could see.
2025-07-29 14:49:20 -04:00
Andrew Gallant
edb2f8e997 [ty] Reflow some long lines
I mostly just did this because the long string literals were annoying
me. And these can make rustfmt give up on formatting.

I also re-flowed some long comment lines while I was here.
2025-07-29 14:49:20 -04:00
Andrew Gallant
5e6ad849ff [ty] Unexport helper function
I'm not sure if this used to be used elsewhere, but it no longer is.
And it looks like an internal-only helper function, so just un-export
it.

And note that `ModuleNameIngredient` is also un-exported, so this
function isn't really usable outside of its defining module anyway.
2025-07-29 14:49:20 -04:00
Andrew Gallant
865a9b3424 [ty] Remove offset from CompletionTargetTokens::Unknown
At some point, the surrounding code was refactored so that the
cursor offset was always passed around, so storing it here is
no longer necessary.
2025-07-29 14:49:20 -04:00
Dan Parizher
d449c541cb [pyupgrade] Fix UP030 to avoid modifying double curly braces in format strings (#19378)
## Summary

Fixes #19348

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-07-29 18:35:54 +00:00
हिमांशु
f7c6a6b2d0 [ty] fix a typo (#19621)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-07-29 17:53:44 +00:00
justin
656273bf3d [ty] synthesize __replace__ for dataclasses (>=3.13) (#19545)
## Summary
https://github.com/astral-sh/ty/issues/111

adds support for the new `copy.replace` and `__replace__` protocol
[added in 3.13](https://docs.python.org/3/whatsnew/3.13.html#copy)

- docs: https://docs.python.org/3/library/copy.html#object.__replace__
- some discussion on pyright/mypy implementations:
https://discuss.python.org/t/dataclass-transform-and-replace/69067



### Burndown
- [x] add tests
- [x] implement `__replace__`
- [ ]
[collections.namedtuple()](https://docs.python.org/3/library/collections.html#collections.namedtuple)
- [x]
[dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass)

## Test Plan
new mdtests

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-29 17:32:01 +02:00
Alex Waygood
81867ea7ce [ty] Discard Definitions when normalizing Signatures (#19615) 2025-07-29 14:37:47 +01:00
Brent Westbrook
a54061e757 [ty] Fix empty spans following a line terminator and unprintable character spans in diagnostics (#19535)
## Summary

This was previously the last commit in #19415, split out to make it
easier to review. This applies the fixes from c9b99e4, 5021f32, and
2922490cb8 to the new rendering code in `ruff_db`. I initially intended
only to fix the empty span after a line terminator (as you can see in
the branch name), but the two fixes were tied pretty closely together,
and my initial fix for the empty spans needed a big change after trying
to handle unprintable characters too. I can still split this up if it
would help with review. I would just start with the unprintable
characters first.

The implementation here is essentially copy-pasted from
`ruff_linter::message::text.rs`, with the `SourceCode` struct renamed to
`EscapedSourceCode` since there's already a `SourceCode` in scope in
`render.rs`. It's also updated slightly to account for the multiple
annotations for a single snippet. The original implementation used some
types from the `line_width` module from `ruff_linter`. I copied over
heavily stripped-down versions of these instead of trying to import
them. We could inline the remaining code entirely, if we want, but I
thought it was nice enough to keep.

I also moved over `ceil_char_boundary`, which is unchanged except to
make it a free function taking a `&str` instead of a `Locator` method.
All of this code could be deleted from `ruff_linter` if we also move
over the `grouped` output format, which will be the last user after
#19415.

## Test Plan

I added new tests in `ruff_linter` that call into the new rendering code
to snapshot the diagnostics for the affected cases. These are copies of
existing snapshots in Ruff, so it's helpful to compare them. These are a
bit noisy because of the other rendering differences in the header, but
all of the `^^^` indicators should be the same.

<details><summary>`empty_span_after_line_terminator` diff</summary>

```diff
diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__empty_span_after_line_terminator.snap
index 5ade4346e0..6df75c16f0 100644
--- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap
+++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__empty_span_after_line_terminator.snap
@@ -1,17 +1,20 @@
 ---
-source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
+source: crates/ruff_linter/src/message/text.rs
+expression: value.to_string()
 ---
-E11.py:9:1: E112 Expected an indented block
+error[no-indented-block]: Expected an indented block
+  --> E11.py:9:1
    |
  7 | #: E112
  8 | if False:
  9 | print()
-   | ^ E112
+   | ^
 10 | #: E113
 11 | print()
    |
 
-E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
+error[invalid-syntax]: SyntaxError: Expected an indented block after `if` statement
+  --> E11.py:9:1
    |
  7 | #: E112
  8 | if False:
@@ -21,7 +24,8 @@ E11.py:9:1: SyntaxError: Expected an indented block after `if` statement
 11 | print()
    |
 
-E11.py:12:1: SyntaxError: Unexpected indentation
+error[invalid-syntax]: SyntaxError: Unexpected indentation
+  --> E11.py:12:1
    |
 10 | #: E113
 11 | print()
@@ -31,7 +35,8 @@ E11.py:12:1: SyntaxError: Unexpected indentation
 14 | mimetype = 'application/x-directory'
    |
 
-E11.py:14:1: SyntaxError: Expected a statement
+error[invalid-syntax]: SyntaxError: Expected a statement
+  --> E11.py:14:1
    |
 12 |     print()
 13 | #: E114 E116
@@ -41,17 +46,19 @@ E11.py:14:1: SyntaxError: Expected a statement
 16 | create_date = False
    |
 
-E11.py:45:1: E112 Expected an indented block
+error[no-indented-block]: Expected an indented block
+  --> E11.py:45:1
    |
 43 | #: E112
 44 | if False:  #
 45 | print()
-   | ^ E112
+   | ^
 46 | #:
 47 | if False:
    |
 
-E11.py:45:1: SyntaxError: Expected an indented block after `if` statement
+error[invalid-syntax]: SyntaxError: Expected an indented block after `if` statement
+  --> E11.py:45:1
    |
 43 | #: E112
 44 | if False:  #
```

</details>

<details><summary>`unprintable_characters` diff</summary>

```diff
diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__unprintable_characters.snap
index 52cfdf9cce..fcfa1ac9f1 100644
--- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap
+++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__unprintable_characters.snap
@@ -1,161 +1,115 @@
 ---
-source: crates/ruff_linter/src/rules/pylint/mod.rs
+source: crates/ruff_linter/src/message/text.rs
+expression: value.to_string()
 ---
-invalid_characters.py:24:12: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:24:12
    |
 22 | cr_ok = f'\\r'
 23 |
 24 | sub = 'sub '
-   |            ^ PLE2512
+   |            ^
 25 | sub = f'sub '
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-ℹ Safe fix
-21 21 | cr_ok = '\\r'
-22 22 | cr_ok = f'\\r'
-23 23 | 
-24    |-sub = 'sub '
-   24 |+sub = 'sub \x1A'
-25 25 | sub = f'sub '
-26 26 | 
-27 27 | sub_ok = '\x1a'
-
-invalid_characters.py:25:13: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:25:13
    |
 24 | sub = 'sub '
 25 | sub = f'sub '
-   |             ^ PLE2512
+   |             ^
 26 |
 27 | sub_ok = '\x1a'
    |
-   = help: Replace with escape sequence
-
-ℹ Safe fix
-22 22 | cr_ok = f'\\r'
-23 23 | 
-24 24 | sub = 'sub '
-25    |-sub = f'sub '
-   25 |+sub = f'sub \x1A'
-26 26 | 
-27 27 | sub_ok = '\x1a'
-28 28 | sub_ok = f'\x1a'
+help: Replace with escape sequence
 
-invalid_characters.py:55:25: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:55:25
    |
 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ ​​"
 54 |
 55 | nested_fstrings = f'␈{f'{f'␛'}'}'
-   |                         ^ PLE2512
+   |                         ^
 56 |
 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
    |
-   = help: Replace with escape sequence
-
-ℹ Safe fix
-52 52 | zwsp_after_multicharacter_grapheme_cluster = "ಫ್ರಾನ್ಸಿಸ್ಕೊ ​​"
-53 53 | zwsp_after_multicharacter_grapheme_cluster = f"ಫ್ರಾನ್ಸಿಸ್ಕೊ ​​"
-54 54 | 
-55    |-nested_fstrings = f'␈{f'{f'␛'}'}'
-   55 |+nested_fstrings = f'␈{f'\x1A{f'␛'}'}'
-56 56 | 
-57 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
-58 58 | x = f"""}}ab"""
+help: Replace with escape sequence
 
-invalid_characters.py:58:12: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:58:12
    |
 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
 58 | x = f"""}}ab"""
-   |            ^ PLE2512
+   |            ^
 59 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998256
 60 | x = f"""}}a␛b"""
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-ℹ Safe fix
-55 55 | nested_fstrings = f'␈{f'{f'␛'}'}'
-56 56 | 
-57 57 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998106
-58    |-x = f"""}}ab"""
-   58 |+x = f"""}}a\x1Ab"""
-59 59 | # https://github.com/astral-sh/ruff/issues/7455#issuecomment-1741998256
-60 60 | x = f"""}}a␛b"""
-61 61 | 
-
-invalid_characters.py:64:12: PLE2512 Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:64:12
    |
 63 | # https://github.com/astral-sh/ruff/issues/13294
 64 | print(r"""␈␛�​
-   |            ^ PLE2512
+   |            ^
 65 | """)
 66 | print(fr"""␈␛�​
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-invalid_characters.py:66:13: PLE2512 Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:66:13
    |
 64 | print(r"""␈␛�​
 65 | """)
 66 | print(fr"""␈␛�​
-   |             ^ PLE2512
+   |             ^
 67 | """)
 68 | print(Rf"""␈␛�​
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-invalid_characters.py:68:13: PLE2512 Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:68:13
    |
 66 | print(fr"""␈␛�​
 67 | """)
 68 | print(Rf"""␈␛�​
-   |             ^ PLE2512
+   |             ^
 69 | """)
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-invalid_characters.py:73:9: PLE2512 Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:73:9
    |
 71 | # https://github.com/astral-sh/ruff/issues/18815
 72 | b = "\␈"
 73 | sub = "\"
-   |         ^ PLE2512
+   |         ^
 74 | esc = "\␛"
 75 | zwsp = "\​"
    |
-   = help: Replace with escape sequence
+help: Replace with escape sequence
 
-invalid_characters.py:80:25: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:80:25
    |
 78 | # tstrings
 79 | esc = t'esc esc ␛'
 80 | nested_tstrings = t'␈{t'{t'␛'}'}'
-   |                         ^ PLE2512
+   |                         ^
 81 | nested_ftstrings = t'␈{f'{t'␛'}'}'
    |
-   = help: Replace with escape sequence
-
-ℹ Safe fix
-77 77 | 
-78 78 | # tstrings
-79 79 | esc = t'esc esc ␛'
-80    |-nested_tstrings = t'␈{t'{t'␛'}'}'
-   80 |+nested_tstrings = t'␈{t'\x1A{t'␛'}'}'
-81 81 | nested_ftstrings = t'␈{f'{t'␛'}'}'
-82 82 | 
+help: Replace with escape sequence
 
-invalid_characters.py:81:26: PLE2512 [*] Invalid unescaped character SUB, use "\x1A" instead
+error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
+  --> invalid_characters.py:81:26
    |
 79 | esc = t'esc esc ␛'
 80 | nested_tstrings = t'␈{t'{t'␛'}'}'
 81 | nested_ftstrings = t'␈{f'{t'␛'}'}'
-   |                          ^ PLE2512
+   |                          ^
    |
-   = help: Replace with escape sequence
-
-ℹ Safe fix
-78 78 | # tstrings
-79 79 | esc = t'esc esc ␛'
-80 80 | nested_tstrings = t'␈{t'{t'␛'}'}'
-81    |-nested_ftstrings = t'␈{f'{t'␛'}'}'
-   81 |+nested_ftstrings = t'␈{f'\x1A{t'␛'}'}'
-82 82 |
+help: Replace with escape sequence
```

</details>
2025-07-29 08:25:58 -04:00
Brent Westbrook
19569bf838 Add LinterContext::settings to avoid passing separate settings (#19608)
Summary
--

I noticed while reviewing #19390 that in `check_tokens` we were still
passing
around an extra `LinterSettings`, despite all of the same functions also
receiving a `LintContext` with its own settings.

This PR adds the `LintContext::settings` method and calls that instead
of using
the separate `LinterSettings`.

Test Plan
--

Existing tests
2025-07-29 08:13:22 -04:00
Charlie Marsh
e0f4f25d28 Support .pyi files in ruff analyze graph (#19611)
## Summary

We now return both the `.pyi` and `.py` files. Previously, we only
returned the `.pyi` file.
2025-07-28 22:00:27 -04:00
github-actions[bot]
c6a123290d [ty] Sync vendored typeshed stubs (#19607)
Co-authored-by: typeshedbot <>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-07-28 22:06:33 +00:00
Alex Waygood
d4f64cd474 [ty] Bump docstring-adder pin (#19606) 2025-07-28 22:59:56 +01:00
Igor Drokin
e4f64480da [perflint] Ignore rule if target is global or nonlocal (PERF401) (#19539)
## Summary

Resolves #19531

I've implemented a check to determine whether the for_stmt target is
declared as global or nonlocal. I believe we should skip the rule in all
such cases, since variables declared this way are intended for use
outside the loop scope, making value changes expected behavior.

## Test Plan

Added two test cases for global and nonlocal variable to snapshot.
2025-07-28 17:03:22 -04:00
Micha Reiser
4016aff057 Add license classifier back to pyproject.toml (#19599)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-07-28 20:58:16 +01:00
UnboundVariable
24134837f3 [ty] Add stub mapping support to signature help (#19570)
This PR improves the "signature help" language server feature in two
ways:
1. It adds support for the recently-introduced "stub mapper" which maps
symbol declarations within stubs to their implementation counterparts.
This allows the signature help to display docstrings from the original
implementation.
2. It incorporates a more robust fix to a bug that was addressed in a
[previous PR](https://github.com/astral-sh/ruff/pull/19542). It also
adds more comprehensive tests to cover this case.

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-28 10:57:18 -07:00
Douglas Creager
130d4e1135 [ty] Don't panic with argument that doesn't actually implement Iterable (#19602)
This eliminates the panic reported in
https://github.com/astral-sh/ty/issues/909, though it doesn't address
the underlying cause, which is that we aren't yet checking the types of
the fields of a protocol when checking whether a class implements the
protocol. And in particular, if a class explictly opts out of iteration
via

```py
class NotIterable:
    __iter__ = None
```

we currently treat that as "having an `__iter__`" member, and therefore
implementing `Iterable`.

Note that the assumption that was in the comment before is still
correct: call binding will have already checked that the argument
satisfies `Iterable`, and so it shouldn't be an error to iterate over
said argument. But arguably, the new logic in this PR is a better way to
discharge that assumption — instead of panicking if we happen to be
wrong, fall back on an unknown iteration result.
2025-07-28 12:09:54 -04:00
Dan Parizher
e63dfa3d18 [flake8-commas] Add support for trailing comma checks in type parameter lists (COM812,COM819) (#19390)
## Summary

Fixes #18844

I'm not too sure if the solution is as simple as the way I implemented
it, but I'm curious to see if we are covering all cases correctly here.

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-07-28 10:53:04 -04:00
Junhson Jean-Baptiste
6d0f3ef3a5 [pylint] Implement auto-fix for missing-maxsplit-arg (PLC0207) (#19387)
<!--
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
As a follow-up to #18949 (suggested
[here](https://github.com/astral-sh/ruff/pull/18949#pullrequestreview-2998417889)),
this PR implements auto-fix logic for `PLC0207`.

## Test Plan

<!-- How was it tested? -->

Existing tests pass, with updates to the snapshot so that it expects the
new output that comes along with the auto-fix.
2025-07-28 10:45:26 -04:00
Dan Parizher
201b079084 [refurb] Mark int and bool cases for Decimal.from_float as safe fixes in FURB164 tests (#19468)
## Summary

Fixes #19460

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-07-28 14:21:38 +00:00
David Peter
2680f2ed81 [ty] Minor: test isolation (#19597)
## Summary

Split the "Generator functions" tests into two parts. The first part
(synchronous) refers to a function called `i` from a function `i2`. But
`i` is later redeclared in the asynchronous part, which was probably not
intended.
2025-07-28 15:52:59 +02:00
Micha Reiser
afdfa042f3 [ty] Remove AssertUnwindSafe from BackgroundRequestHandler api (#19598) 2025-07-28 13:28:09 +00:00
Micha Reiser
8c0743df97 [ty] Fix "peek definition" in playground (#19592) 2025-07-28 09:13:00 +01:00
Dimitri Papadopoulos Orfanos
13634ff433 Use PEP 639 license information for Ruff itself instead of classifier (#19499)
## Summary

Declare licenses using only these two fields, as per PEP 639:
* `license`: SPDX license expression consisting of one or more license
identifiers
* `license-files`: list of license file glob patterns

Supported by maturin ≥ 1.9.0:
https://www.maturin.rs/changelog.html

## Test Plan

N/A
2025-07-28 09:43:50 +02:00
renovate[bot]
7a541f597f Update NPM Development dependencies (#19588)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 07:55:22 +01:00
renovate[bot]
2deb50f4e3 Update Rust crate get-size2 to 0.6.0 (#19590)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 07:55:03 +01:00
renovate[bot]
85e22645aa Update pre-commit hook astral-sh/ruff-pre-commit to v0.12.5 (#19585)
This PR contains the following updates:

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

---

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

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

---

### Release Notes

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

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

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

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 08:48:14 +02:00
renovate[bot]
d3f6de8b0e Update cargo-bins/cargo-binstall action to v1.14.2 (#19583)
This PR contains the following updates:

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

---

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

---

### Release Notes

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

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

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

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

##### In this release:

- Upgrade dependencies

</details>

---

### Configuration

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

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

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

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

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 08:47:33 +02:00
renovate[bot]
9eb8174209 Update CodSpeedHQ/action action to v3.8.0 (#19587)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [CodSpeedHQ/action](https://redirect.github.com/CodSpeedHQ/action) |
action | minor | `v3.7.0` -> `v3.8.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>CodSpeedHQ/action (CodSpeedHQ/action)</summary>

###
[`v3.8.0`](https://redirect.github.com/CodSpeedHQ/action/releases/tag/v3.8.0)

[Compare
Source](https://redirect.github.com/CodSpeedHQ/action/compare/v3.7.0...v3.8.0)

##### What's Changed

##### <!-- 1 -->🐛 Bug Fixes

- Adjust offset for symbols of module loaded at preferred base by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias) in
[#&#8203;97](https://redirect.github.com/CodSpeedHQ/runner/pull/97)
- Run with --scope to allow perf to trace the benchmark process by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)
- Run with bash to support complex scripts by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)
- Execute pre- and post-bench scripts for non-perf walltime runner by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias) in
[#&#8203;96](https://redirect.github.com/CodSpeedHQ/runner/pull/96)

##### <!-- 2 -->🏗️ Refactor

- Process memory mappings in separate function by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)

##### <!-- 7 -->⚙️ Internals

- Add debug logs for perf.map collection by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)
- Add complex cmd and env tests by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)

**Full Changelog**:
https://github.com/CodSpeedHQ/action/compare/v3.7.0...v3.8.0
**Full Runner Changelog**:
https://github.com/CodSpeedHQ/runner/blob/main/CHANGELOG.md

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 08:47:05 +02:00
renovate[bot]
9c68616d91 Update Rust crate criterion to 0.7.0 (#19589)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [criterion](https://bheisler.github.io/criterion.rs/book/index.html)
([source](https://redirect.github.com/bheisler/criterion.rs)) |
workspace.dependencies | minor | `0.6.0` -> `0.7.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>bheisler/criterion.rs (criterion)</summary>

###
[`v0.7.0`](https://redirect.github.com/bheisler/criterion.rs/blob/HEAD/CHANGELOG.md#070---2025-07-25)

[Compare
Source](https://redirect.github.com/bheisler/criterion.rs/compare/0.6.0...0.7.0)

- Bump version of criterion-plot to align dependencies.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 08:35:36 +02:00
renovate[bot]
9280c7e945 Update astral-sh/setup-uv action to v6.4.3 (#19586)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | minor | `v6.3.1` -> `v6.4.3` |

---

> [!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>

###
[`v6.4.3`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.4.3):
🌈 fix relative paths starting with dots

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.4.2...v6.4.3)

#### 🐛 Bug fixes

- fix relative paths starting with dots
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;500](https://redirect.github.com/astral-sh/setup-uv/issues/500))

###
[`v6.4.2`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.4.2):
🌈 Interpret relative inputs as under working-directory

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.4.1...v6.4.2)

#### Changes

This release will interpret relative paths in inputs as relative
to the value of `working-directory` (default is `${{ github.workspace
}}`) .
This means the following configuration

```yaml
- uses: astral-sh/setup-uv@v6
   with:
     working-directory: /my/path
     cache-dependency-glob: uv.lock
```

will look for the `cache-dependency-glob` under `/my/path/uv.lock`

#### 🐛 Bug fixes

- interpret relative inputs as under working-directory
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;498](https://redirect.github.com/astral-sh/setup-uv/issues/498))

#### 🧰 Maintenance

- chore: update known versions for 0.8.1/0.8.2
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;497](https://redirect.github.com/astral-sh/setup-uv/issues/497))
- chore: update known versions for 0.8.0
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;491](https://redirect.github.com/astral-sh/setup-uv/issues/491))

###
[`v6.4.1`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.4.1):
🌈 Hotfix: Ignore deps starting with uv when finding uv version

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.4.0...v6.4.1)

##### Changes

Thank you [@&#8203;phpmypython](https://redirect.github.com/phpmypython)
for raising a PR to fix this issue!

##### 🐛 Bug fixes

- Ignore deps starting with uv when finding uv version
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;492](https://redirect.github.com/astral-sh/setup-uv/issues/492))

###
[`v6.4.0`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v6.4.0):
🌈 Add input `version-file`

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v6.3.1...v6.4.0)

##### Changes

You can now use the `version-file` input to specify a file that contains
the version of uv to install.
This can either be a `pyproject.toml` or `uv.toml` file which defines a
`required-version` or
uv defined as a dependency in `pyproject.toml` or `requirements.txt`.

```yaml
- name: Install uv based on the version defined in requirements.txt
  uses: astral-sh/setup-uv@v6
  with:
    version-file: "requirements.txt"
```

##### 🚀 Enhancements

- Add input version-file
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;486](https://redirect.github.com/astral-sh/setup-uv/issues/486))

##### 🧰 Maintenance

- chore: update known versions for 0.7.22
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;488](https://redirect.github.com/astral-sh/setup-uv/issues/488))
- Bump dependencies
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;487](https://redirect.github.com/astral-sh/setup-uv/issues/487))
- chore: update known versions for 0.7.21
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;483](https://redirect.github.com/astral-sh/setup-uv/issues/483))
- chore: update known versions for 0.7.20
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;480](https://redirect.github.com/astral-sh/setup-uv/issues/480))
- chore: update known versions for 0.7.19
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;475](https://redirect.github.com/astral-sh/setup-uv/issues/475))
- chore: update known versions for 0.7.18
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;473](https://redirect.github.com/astral-sh/setup-uv/issues/473))
- chore: update known versions for 0.7.17
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;468](https://redirect.github.com/astral-sh/setup-uv/issues/468))
- chore: update known versions for 0.7.16
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;466](https://redirect.github.com/astral-sh/setup-uv/issues/466))
- chore: update known versions for 0.7.15
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;463](https://redirect.github.com/astral-sh/setup-uv/issues/463))

##### 📚 Documentation

- Add FAQ on changed cache and cache upload behavior
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;477](https://redirect.github.com/astral-sh/setup-uv/issues/477))

##### ⬆️ Dependency updates

- Bump dependencies
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;487](https://redirect.github.com/astral-sh/setup-uv/issues/487))

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 08:33:44 +02:00
Dhruv Manilawala
e19145040f [ty] Add workflow to comment conformance tests diff (#19555)
## Summary

Follow-up to https://github.com/astral-sh/ruff/pull/19556, this PR adds
the workflow that computes the diagnostic diff which the workflow
introduced in the linked PR will add as a comment.

This workflow is similar to the [ty ecosystem-analyzer
workflow](d781a6ab3f/.github/workflows/ty-ecosystem-analyzer.yaml).

Closes: astral-sh/ty#212

## Test Plan

1. Initially there's no diff to show
2. This
[commit](d0db9937df)
comments out a rule which updates the comment with the diff
3. Later, that commit is reverted and the diff goes away

Use the comment history to look at the diff output where the order of
the history corresponds to the steps mentioned above in reverse order
i.e., the edit in the middle will contain the diff output:

<img width="1082" height="313" alt="Screenshot 2025-07-25 at 21 09 26"
src="https://github.com/user-attachments/assets/6aceb60c-1987-4b9a-9063-e3999844f035"
/>
2025-07-28 11:33:22 +05:30
renovate[bot]
ef3a195f28 Update dependency ruff to v0.12.5 (#19584) 2025-07-27 22:22:23 -04:00
Dylan
008bbfdf5a Disallow implicit concatenation of t-strings and other string types (#19485)
As of [this cpython PR](https://github.com/python/cpython/pull/135996),
it is not allowed to concatenate t-strings with non-t-strings,
implicitly or explicitly. Expressions such as `"foo" t"{bar}"` are now
syntax errors.

This PR updates some AST nodes and parsing to reflect this change.

The structural change is that `TStringPart` is no longer needed, since,
as in the case of `BytesStringLiteral`, the only possibilities are that
we have a single `TString` or a vector of such (representing an implicit
concatenation of t-strings). This removes a level of nesting from many
AST expressions (which is what all the snapshot changes reflect), and
simplifies some logic in the implementation of visitors, for example.

The other change of note is in the parser. When we meet an implicit
concatenation of string-like literals, we now count the number of
t-string literals. If these do not exhaust the total number of
implicitly concatenated pieces, then we emit a syntax error. To recover
from this syntax error, we encode any t-string pieces as _invalid_
string literals (which means we flag them as invalid, record their
range, and record the value as `""`). Note that if at least one of the
pieces is an f-string we prefer to parse the entire string as an
f-string; otherwise we parse it as a string.

This logic is exactly the same as how we currently treat
`BytesStringLiteral` parsing and error recovery - and carries with it
the same pros and cons.

Finally, note that I have not implemented any changes in the
implementation of the formatter. As far as I can tell, none are needed.
I did change a few of the fixtures so that we are always concatenating
t-strings with t-strings.
2025-07-27 12:41:03 +00:00
Alex Waygood
df5eba7583 [ty] Mark all_type_assignable_to_iterable_are_iterable as flaky (#19574) 2025-07-27 11:04:13 +00:00
Micha Reiser
469c50b0b7 [ty] Support stdlib files in playground (#19557) 2025-07-26 19:33:38 +01:00
UnboundVariable
738246627f [ty] Implemented support for "selection range" language server feature (#19567)
This PR adds support for the "selection range" language server feature.
This feature was recently requested by a ty user in [this feature
request](https://github.com/astral-sh/ty/issues/882).

This feature allows a client to implement "smart selection expansion"
based on the structure of the parse tree. For example, if you type
"shift-ctrl-right-arrow" in VS Code, the current selection will be
expanded to include the parent AST node. Conversely,
"shift-ctrl-left-arrow" shrinks the selection.

We will probably need to tune the granularity of selection expansion
based on user feedback. The initial implementation includes most AST
nodes, but users may find this to be too fine-grained. We have the
option of skipping some AST nodes that are not as meaningful when
editing code.

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-26 09:08:36 -07:00
Douglas Creager
e867830848 [ty] Don't include already-bound legacy typevars in function generic context (#19558)
We now correctly exclude legacy typevars from enclosing scopes when
constructing the generic context for a generic function.

more detail:

A function is generic if it refers to legacy typevars in its signature:

```py
from typing import TypeVar

T = TypeVar("T")

def f(t: T) -> T:
    return t
```

Generic functions are allowed to appear inside of other generic
contexts. When they do, they can refer to the typevars of those
enclosing generic contexts, and that should not rebind the typevar:

```py
from typing import TypeVar, Generic

T = TypeVar("T")
U = TypeVar("U")

class C(Generic[T]):
    @staticmethod
    def method(t: T, u: U) -> None: ...

# revealed: def method(t: int, u: U) -> None
reveal_type(C[int].method)
```

This substitution was already being performed correctly, but we were
also still including the enclosing legacy typevars in the method's own
generic context, which can be seen via `ty_extensions.generic_context`
(which has been updated to work on generic functions and methods):

```py
from ty_extensions import generic_context

# before: tuple[T, U]
# after: tuple[U]
reveal_type(generic_context(C[int].method))
```

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-07-25 18:14:19 -04:00
Elliot Simpson
72fdb7d439 [flake8-blind-except] Change BLE001 to permit logging.critical(..., exc_info=True). (#19520)
## Summary

Changing `BLE001` (blind-except) so that it does not flag `except`
clauses which include `logging.critical(..., exc_info=True)`.

## Test Plan

It passes the following (whereas the `main` branch does not):
```sh
$ cargo run -p ruff -- check somefile.py --no-cache --select=BLE001
```
```python
# somefile.py

import logging


try:
    print("Hello world!")
except Exception:
    logging.critical("Did not run.", exc_info=True)
```
Related: https://github.com/astral-sh/ruff/issues/19519
2025-07-25 17:52:58 -04:00
Dylan
fbf1dfc782 Reword preview warning for target-version Python 3.14 (#19563)
Small rewording to indicate that core development is done but that we
may add breaking changes.

Feel free to bikeshed!

Test:

```console
❯ echo "t''" | cargo run -p ruff -- check --no-cache --isolated --target-version py314 -
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/ruff check --no-cache --isolated --target-version py314 -`
warning: Support for Python 3.14 is in preview and may undergo breaking changes. Enable `preview` to remove this warning.
All checks passed!
```
2025-07-25 16:09:45 -05:00
UnboundVariable
a0d8ff51dd [ty] Added support for "document symbols" and "workspace symbols" (#19521)
This PR adds support for "document symbols" and "workspace symbols"
language server features. Most of the logic to implement these features
is shared.

The "document symbols" feature returns a list of all symbols within a
specified source file. Clients can specify whether they want a flat or
hierarchical list. Document symbols are typically presented by a client
in an "outline" form. Here's what this looks like in VS Code, for
example.

<img width="240" height="249" alt="image"
src="https://github.com/user-attachments/assets/82b11f4f-32ec-4165-ba01-d6496ad13bdf"
/>


The "workspace symbols" feature returns a list of all symbols across the
entire workspace that match some user-supplied query string. This allows
the user to quickly find and navigate to any symbol within their code.

<img width="450" height="134" alt="image"
src="https://github.com/user-attachments/assets/aac131e0-9464-4adf-8a6c-829da028c759"
/>

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-25 13:07:38 -07:00
Brent Westbrook
165091a31c Add TextEmitter::with_color and disable colors in unreadable_files test (#19562)
Summary
--

I looked at other uses of `TextEmitter`, and I think this should be the
only one affected by this. The other integration tests must work
properly since they're run with `assert_cmd_snapshot!`, which I assume
triggers the `SHOULD_COLORIZE` case, and the `cfg!(test)` check will
work for uses in `ruff_linter`.


4a4dc38b5b/crates/ruff_linter/src/message/text.rs (L36-L44)

Alternatively, we could probably move this to a CLI test instead.

Test Plan
--

`cargo test -p ruff`, which was failing on `main` with color codes in
the output before this
2025-07-25 15:47:49 -04:00
UnboundVariable
4a4dc38b5b [ty] Added support for document highlights in playground. (#19540)
This PR adds support for the "document highlights" feature in the ty
playground.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-25 08:55:40 -07:00
Dan Parizher
3e366fdf13 [refurb] Ignore decorated functions for FURB118 (#19339)
## Summary

Fixes #19305
2025-07-25 10:43:17 -05:00
Dhruv Manilawala
53e9e4421c [ty] Add workflow to comment diagnostic diff for conformance tests (#19556)
## Summary

This PR adds a workflow to comment the diff of diagnostics when running
ty between `main` and a pull request on the [typing conformance test
suite](https://github.com/python/typing/tree/main/conformance/tests).

The main workflow is introduced in
https://github.com/astral-sh/ruff/pull/19555 which this workflow depends
on. This workflow is similar to the [mypy primer
comment](d781a6ab3f/.github/workflows/mypy_primer_comment.yaml)
workflow.

## Test Plan

I cannot test this workflow without merging it on `main` unless anyone
knows a way to do this.
2025-07-25 20:54:28 +05:30
Alex Waygood
859262bd49 [ty] Move zope.interface to good.txt for primer runs (#19208) 2025-07-25 14:12:17 +01:00
David Peter
c0768dfd96 [ty] Attribute access on intersections with negative parts (#19524)
## Summary

We currently infer a `@Todo` type whenever we access an attribute on an
intersection type with negative components. This can happen very
naturally. Consequently, this `@Todo` type is rather pervasive and hides
a lot of true positives that ty could otherwise detect:

```py
class Foo:
    attr: int = 1

def _(f: Foo | None):
    if f:
        reveal_type(f)  # Foo & ~AlwaysFalsy

        reveal_type(f.attr)  # now: int, previously: @Todo
```

The changeset here proposes to handle member access on these
intersection types by simply ignoring all negative contributions. This
is not always ideal: a negative contribution like `~<Protocol with
members 'attr'>` could be a hint that `.attr` should not be accessible
on the full intersection type. The behavior can certainly be improved in
the future, but this seems like a reasonable initial step to get rid of
this unnecessary `@Todo` type.

## Ecosystem analysis

There are quite a few changes here. I spot-checked them and found one
bug where attribute access on pure negation types (`~P == object & ~P`)
would not allow attributes on `object` to be accessed. After that was
fixed, I only see true positives and known problems. The fact that a lot
of `unused-ignore-comment` diagnostics go away are also evidence for the
fact that this touches a sensitive area, where static analysis clashes
with dynamically adding attributes to objects:
```py
… # type: ignore # Runtime attribute access
```

## Test Plan

Updated tests.
2025-07-25 14:56:14 +02:00
David Peter
d4eb4277ad [ty] Add basic support for dataclasses.field (#19553)
## Summary

Add basic support for `dataclasses.field`:
* remove fields with `init=False` from the signature of the synthesized
`__init__` method
* infer correct default value types from `default` or `default_factory`
arguments

```py
from dataclasses import dataclass, field

def default_roles() -> list[str]:
    return ["user"]

@dataclass
class Member:
    name: str
    roles: list[str] = field(default_factory=default_roles)
    tag: str | None = field(default=None, init=False)

# revealed: (self: Member, name: str, roles: list[str] = list[str]) -> None
reveal_type(Member.__init__)
```

Support for `kw_only` has **not** been added.

part of https://github.com/astral-sh/ty/issues/111

## Test Plan

New Markdown tests
2025-07-25 14:56:04 +02:00
Micha Reiser
b033fb6bfd [ty] Split ScopedPlaceId into ScopedSymbolId and ScopedMemberId (#19497) 2025-07-25 13:54:33 +02:00
Alex Waygood
f722bfa9e6 [ty] Do not consider a type T to satisfy a method member on a protocol unless the method is available on the meta-type of T (#19187) 2025-07-25 11:16:04 +01:00
Shunsuke Shibayama
b124e182ca [ty] improve lazy scope place lookup (#19321)
Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
Co-authored-by: Carl Meyer <carl@oddbird.net>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-07-25 07:11:11 +00:00
Dhruv Manilawala
57373a7e4d [ty] Derive Serialize unconditionally on client options (#19549) 2025-07-25 04:03:03 +00:00
Carl Meyer
ae9d450b5f [ty] Fallback to Unknown if no type is stored for an expression (#19517)
## Summary

See discussion at
https://github.com/astral-sh/ruff/pull/19478/files#r2223870292

Fixes https://github.com/astral-sh/ty/issues/865

## Test Plan

Added one mdtest for invalid Callable annotation; removed `pull-types:
skip` from that test file.

Co-authored-by: lipefree <willy.ngo.2000@gmail.com>
2025-07-25 02:05:32 +00:00
UnboundVariable
c8c80e054e [ty] Fix bug #879 in signature help (#19542)
This PR fixes bug [#879](https://github.com/astral-sh/ty/issues/879)
where the signature help popup remains visible after typing the closing
paren in a call expression.

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-24 16:26:14 -07:00
UnboundVariable
4bc34b82ef [ty] Added support for "document highlights" language server feature. (#19515)
This PR adds support for the "document highlights" language server
feature.

This feature allows a client to highlight all instances of a selected
name within a document. Without this feature, editors perform
highlighting based on a simple text match. This adds semantic knowledge.

The implementation of this feature largely overlaps that of the
recently-added "references" feature. This PR refactors the existing
"references.rs" module, separating out the functionality and tests that
are specific to the other language feature into a "goto_references.rs"
module. The "references.rs" module now contains the functionality that
is common to "goto references", "document highlights" and "rename"
(which is not yet implemented).

As part of this PR, I also created a new `ReferenceTarget` type which is
similar to the existing `NavigationTarget` type but better suited for
references. This idea was suggested by @MichaReiser in [this code review
feedback](https://github.com/astral-sh/ruff/pull/19475#discussion_r2224061006)
from a previous PR. Notably, this new type contains a field that
specifies the "kind" of the reference (read, write or other). This
"kind" is needed for the document highlights feature.

Before: all textual instances of `foo` are highlighted
<img width="156" height="126" alt="Screenshot 2025-07-23 at 12 51 09 PM"
src="https://github.com/user-attachments/assets/37ccdb2f-d48a-473d-89d5-8e89cb6c394e"
/>

After: only semantic matches are highlighted
<img width="164" height="157" alt="Screenshot 2025-07-23 at 12 52 05 PM"
src="https://github.com/user-attachments/assets/2efadadd-4691-4815-af04-b031e74c81b7"
/>

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-24 13:06:25 -07:00
Charlie Marsh
d9cab4d242 Add support for specifying minimum dots in detected string imports (#19538)
## Summary

Defaults to requiring two dots, which matches the Pants default.
2025-07-24 15:48:23 -04:00
David Peter
d77b7312b0 [ty] Minor: fix incomplete docstring (#19534) 2025-07-24 21:01:15 +02:00
Dhruv Manilawala
f9091ea8bb [ty] Move server tests as integration tests (#19522)
## Summary

Reference:
https://github.com/astral-sh/ruff/pull/19391#discussion_r2222780892
2025-07-24 16:10:17 +00:00
Robsdedude
1d2181623c [ruff] Offer fixes for RUF039 in more cases (#19065)
## Summary
Expand cases in which ruff can offer a fix for `RUF039` (some of which
are unsafe).

While turning `"\n"` (== `\n`) into `r"\n"` (== `\\n`) is not equivalent
at run-time, it's still functionally equivalent to do so in the context
of [regex
patterns](https://docs.python.org/3/library/re.html#regular-expression-syntax)
as they themselves interpret the escape sequence. Therefore, an unsafe
fix can be offered.

Further, this PR also makes ruff offer fixes for byte string literals,
not only strings literals as before.

## Test Plan
Tests for all escape sequences have been added.

## Related
Closes: https://github.com/astral-sh/ruff/issues/16713

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-07-24 11:45:45 -04:00
David Peter
dc6be457b5 [ty] Support dataclasses.InitVar (#19527)
## Summary

I saw that this creates a lot of false positives in the ecosystem, and
it seemed to be relatively easy to add basic support for this.

Some preliminary work on this was done by @InSyncWithFoo — thank you.

part of https://github.com/astral-sh/ty/issues/111

## Ecosystem analysis

The results look good.

## Test Plan

New Markdown tests

---------

Co-authored-by: InSync <insyncwithfoo@gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-07-24 16:33:33 +02:00
Robsdedude
1079975b35 [ruff] Fix RUF033 breaking with named default expressions (#19115)
## Summary
The generated fix for `RUF033` would cause a syntax error for named
expressions as parameter defaults.
```python
from dataclasses import InitVar, dataclass
@dataclass
class Foo:
    def __post_init__(self, bar: int = (x := 1)) -> None:
        pass
```
would be turned into
```python
from dataclasses import InitVar, dataclass
@dataclass
class Foo:
    x: InitVar[int] = x := 1
    def __post_init__(self, bar: int = (x := 1)) -> None:
        pass
```
instead of the syntactically correct
```python
# ...
x: InitVar[int] = (x := 1)
# ...
```

## Test Plan
Test reproducer (plus some extra tests) have been added to the test
suite.

## Related
Fixes: https://github.com/astral-sh/ruff/issues/18950
2025-07-24 09:45:49 -04:00
Brent Westbrook
39eb0f6c6c Update pre-commit hook name (#19530)
## Summary

A couple of months ago now
(https://github.com/astral-sh/ruff-pre-commit/pull/124) we changed the
hook ID from just `ruff` to `ruff-check` to mirror `ruff-format`. I
noticed the `ruff (legacy alias)` when running pre-commit on the release
today and realized we should probably update.

## Test Plan

Commit on this PR:

```shell
> git commit -m "Update pre-commit hook name"
check for merge conflicts................................................Passed
Validate pyproject.toml..............................(no files to check)Skipped
mdformat.............................................(no files to check)Skipped
markdownlint-fix.....................................(no files to check)Skipped
blacken-docs.........................................(no files to check)Skipped
typos....................................................................Passed
cargo fmt............................................(no files to check)Skipped
ruff format..........................................(no files to check)Skipped
ruff check...........................................(no files to check)Skipped  <-- 
prettier.................................................................Passed
zizmor...............................................(no files to check)Skipped
Validate GitHub Workflows............................(no files to check)Skipped
shellcheck...........................................(no files to check)Skipped
```

Compared to the release branch:

```shell
> pre-commit run
...
cargo fmt............................................(no files to check)Skipped
ruff format..........................................(no files to check)Skipped
ruff (legacy alias)..................................(no files to check)Skipped
...
```
2025-07-24 09:44:47 -04:00
Brent Westbrook
d13228ab85 Bump 0.12.5 (#19528) 2025-07-24 09:12:50 -04:00
David Peter
9461d3076f [ty] Rename type_api => ty_extensions (#19523) 2025-07-24 08:24:26 +00:00
UnboundVariable
63d1d332b3 [ty] Added support for "go to references" in ty playground. (#19516)
This PR adds support for "go to references" in the ty playground.

<img width="393" height="168" alt="image"
src="https://github.com/user-attachments/assets/ce3ae1bf-c17c-4510-9f77-20b10f6170c4"
/>

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-23 22:46:42 -07:00
Douglas Creager
e0149cd9f3 [ty] Return a tuple spec from the iterator protocol (#19496)
This PR updates our iterator protocol machinery to return a tuple spec
describing the elements that are returned, instead of a type. That
allows us to track heterogeneous iterators more precisely, and
consolidates the logic in unpacking and splatting, which are the two
places where we can take advantage of that more precise information.
(Other iterator consumers, like `for` loops, have to collapse the
iterated elements down to a single type regardless, and we provide a new
helper method on `TupleSpec` to perform that summarization.)
2025-07-23 17:11:44 -04:00
David Peter
2a00eca66b [ty] Exhaustiveness checking & reachability for match statements (#19508)
## Summary

Implements proper reachability analysis and — in effect — exhaustiveness
checking for `match` statements. This allows us to check the following
code without any errors (leads to *"can implicitly return `None`"* on
`main`):

```py
from enum import Enum, auto

class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

def hex(color: Color) -> str:
    match color:
        case Color.RED:
            return "#ff0000"
        case Color.GREEN:
            return "#00ff00"
        case Color.BLUE:
            return "#0000ff"
```

Note that code like this already worked fine if there was a
`assert_never(color)` statement in a catch-all case, because we would
then consider that `assert_never` call terminal. But now this also works
without the wildcard case. Adding a member to the enum would still lead
to an error here, if that case would not be handled in `hex`.

What needed to happen to support this is a new way of evaluating match
pattern constraints. Previously, we would simply compare the type of the
subject expression against the patterns. For the last case here, the
subject type would still be `Color` and the value type would be
`Literal[Color.BLUE]`, so we would infer an ambiguous truthiness.

Now, before we compare the subject type against the pattern, we first
generate a union type that corresponds to the set of all values that
would have *definitely been matched* by previous patterns. Then, we
build a "narrowed" subject type by computing `subject_type &
~already_matched_type`, and compare *that* against the pattern type. For
the example here, `already_matched_type = Literal[Color.RED] |
Literal[Color.GREEN]`, and so we have a narrowed subject type of `Color
& ~(Literal[Color.RED] | Literal[Color.GREEN]) = Literal[Color.BLUE]`,
which allows us to infer a reachability of `AlwaysTrue`.

<details>

<summary>A note on negated reachability constraints</summary>

It might seem that we now perform duplicate work, because we also record
*negated* reachability constraints. But that is still important for
cases like the following (and possibly also for more realistic
scenarios):

```py
from typing import Literal

def _(x: int | str):
    match x:
        case None:
            pass # never reachable
        case _:
            y = 1

    y
```

</details>

closes https://github.com/astral-sh/ty/issues/99

## Test Plan

* I verified that this solves all examples from the linked ticket (the
first example needs a PEP 695 type alias, because we don't support
legacy type aliases yet)
* Verified that the ecosystem changes are all because of removed false
positives
* Updated tests
2025-07-23 22:45:45 +02:00
David Peter
3d17897c02 [ty] Fix narrowing and reachability of class patterns with arguments (#19512)
## Summary

I noticed that our type narrowing and reachability analysis was
incorrect for class patterns that are not irrefutable. The test cases
below compare the old and the new behavior:

```py
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

class Other: ...

def _(target: Point):
    y = 1

    match target:
        case Point(0, 0):
            y = 2
        case Point(x=0, y=1):
            y = 3
        case Point(x=1, y=0):
            y = 4
    
    reveal_type(y)  # revealed: Literal[1, 2, 3, 4]    (previously: Literal[2])


def _(target: Point | Other):
    match target:
        case Point(0, 0):
            reveal_type(target)  # revealed: Point
        case Point(x=0, y=1):
            reveal_type(target)  # revealed: Point    (previously: Never)
        case Point(x=1, y=0):
            reveal_type(target)  # revealed: Point    (previously: Never)
        case Other():
            reveal_type(target)  # revealed: Other    (previously: Other & ~Point)
```

## Test Plan

New Markdown test
2025-07-23 18:45:03 +02:00
UnboundVariable
fa1df4cedc [ty] Implemented partial support for "find references" language server feature. (#19475)
This PR adds basic support for the "find all references" language server feature.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-23 09:16:22 -07:00
chiri
89258f1938 [flake8-use-pathlib] Add autofix for PTH101, PTH104, PTH105, PTH121 (#19404)
## Summary

Part of https://github.com/astral-sh/ruff/issues/2331

## Test Plan

`cargo nextest run flake8_use_pathlib`
2025-07-23 12:13:43 -04:00
हिमांशु
1dcef1a011 [perflint] Parenthesize generator expressions (PERF401) (#19325)
## Summary
closes #19204 

## Test Plan
1. test case is added in dedicated file
2. locally tested the code manually

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: CodeMan62 <sharmahimanshu150082007@gmail.com>
2025-07-23 12:08:15 -04:00
Dan Parizher
ba629fe262 [pep8-naming] Fix N802 false positives for CGIHTTPRequestHandler and SimpleHTTPRequestHandler (#19432)
## Summary

Fixes #19422
2025-07-23 12:04:11 -04:00
frank
bb3a05f92b [pylint] Handle empty comments after line continuation (PLR2044) (#19405)
fixes #19326
2025-07-23 11:56:49 -04:00
Brent Westbrook
4daf59e5e7 Move concise diagnostic rendering to ruff_db (#19398)
## Summary

This PR moves most of the work of rendering concise diagnostics in Ruff
into `ruff_db`, where the code is shared with ty. To accomplish this
without breaking backwards compatibility in Ruff, there are two main
changes on the `ruff_db`/ty side:
- Added the logic from Ruff for remapping notebook line numbers to cells
- Reordered the fields in the diagnostic to match Ruff and rustc
  ```text
  # old
error[invalid-assignment] try.py:3:1: Object of type `Literal[1]` is not
assignable to `str`
  # new
try.py:3:1: error[invalid-assignment]: Object of type `Literal[1]` is
not assignable to `str`
  ```

I don't think the notebook change failed any tests on its own, and only
a handful of snaphots changed in ty after reordering the fields, but
this will obviously affect any other uses of the concise format, outside
of tests, too.

The other big change should only affect Ruff:

- Added three new `DisplayDiagnosticConfig` options
Micha and I hoped that we could get by with one option
(`hide_severity`), but Ruff also toggles `show_fix_status` itself,
independently (there are cases where we want neither severity nor the
fix status), and during the implementation I realized we also needed
access to an `Applicability`. The main goal here is to suppress the
severity (`error` above) because ruff only uses the `error` severity and
to use the secondary/noqa code instead of the line name
(`invalid-assignment` above).
  ```text
  # ty - same as "new" above
try.py:3:1: error[invalid-assignment]: Object of type `Literal[1]` is
not assignable to `str`
  # ruff
try.py:3:1: RUF123 [*] Object of type `Literal[1]` is not assignable to
`str`
  ```

This part of the concise diagnostic is actually shared with the `full`
output format in Ruff, but with the settings above, there are no
snapshot changes to either format.

## Test Plan

Existing tests with the handful of updates mentioned above, as well as
some new tests in the `concise` module.

Also this PR. Swapping the fields might have broken mypy_primer, unless
it occasionally times out on its own.

I also ran this script in the root of my Ruff checkout, which also has
CPython in it:

```shell
flags=(--isolated --no-cache --no-respect-gitignore --output-format concise .)
diff <(target/release/ruff check ${flags[@]} 2> /dev/null) \
     <(ruff check ${flags[@]} 2> /dev/null)
```

This yielded an expected diff due to some t-string error changes on main
since 0.12.4:
```diff
33622c33622
< crates/ruff_python_parser/resources/inline/err/f_string_lambda_without_parentheses.py:1:15: SyntaxError: Expected an element of or the end of the f-string
---
> crates/ruff_python_parser/resources/inline/err/f_string_lambda_without_parentheses.py:1:15: SyntaxError: Expected an f-string or t-string element or the end of the f-string or t-string
33742c33742
< crates/ruff_python_parser/resources/inline/err/implicitly_concatenated_unterminated_string_multiline.py:4:1: SyntaxError: Expected an element of or the end of the f-string
---
> crates/ruff_python_parser/resources/inline/err/implicitly_concatenated_unterminated_string_multiline.py:4:1: SyntaxError: Expected an f-string or t-string element or the end of the f-string or t-string
34131c34131
< crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py:2:15: SyntaxError: Expected an element of or the end of the t-string
---
> crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py:2:15: SyntaxError: Expected an f-string or t-string element or the end of the f-string or t-string
```

So modulo color, the results are identical on 38,186 errors in our test
suite and CPython 3.10.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-23 11:43:32 -04:00
Jack O'Connor
88bd82938f [ty] highlight the argument in static_assert error messages (#19426)
Closes https://github.com/astral-sh/ty/issues/209.

Before:
```
error[static-assert-error]: Static assertion error: custom message
 --> test.py:2:1
  |
1 | from ty_extensions import static_assert
2 | static_assert(3 > 4, "custom message")
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
```

After:
```
error[static-assert-error]: Static assertion error: custom message
 --> test.py:2:1
  |
1 | from ty_extensions import static_assert
2 | static_assert(3 > 4, "custom message")
  | ^^^^^^^^^^^^^^-----^^^^^^^^^^^^^^^^^^^
  |               |
  |               Inferred type of argument is `Literal[False]`
  |
```
2025-07-23 08:24:12 -07:00
David Peter
5a55bab3f3 [ty] Infer single-valuedness for enums based on int/str (#19510)
## Summary

We previously didn't recognize `Literal[Color.RED]` as single-valued, if
the enum also derived from `str` or `int`:
```py
from enum import Enum

class Color(str, Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

def _(color: Color):
    if color == Color.RED:
        reveal_type(color)  # previously: Color, now: Literal[Color.RED]
```

The reason for that was that `int` and `str` have "custom" `__eq__` and
`__ne__` implementations that return `bool`. We do not treat enum
literals from classes with custom `__eq__` and `__ne__` implementations
as single-valued, but of course we know that `int.__eq__` and
`str.__eq__` are well-behaved.

## Test Plan

New Markdown tests.
2025-07-23 15:55:42 +02:00
Andrew Gallant
cc5885e564 [ty] Restructure submodule query around File dependency
This makes caching of submodules independent of whether `Module`
is itself a Salsa ingredient. In fact, this makes the work done in
the prior commit superfluous. But we're possibly keeping it as an
ingredient for now since it's a bit of a tedious change and we might
need it in the near future.

Ref https://github.com/astral-sh/ruff/pull/19495#pullrequestreview-3045736715
2025-07-23 09:46:40 -04:00
Andrew Gallant
4573a0f6a0 [ty] Make Module a Salsa ingredient
We want to write queries that depend on `Module` for caching. While it
seems it can be done without making `Module` an ingredient, it seems it
is best practice to do so.

[best practice to do so]: https://github.com/astral-sh/ruff/pull/19408#discussion_r2215867301
2025-07-23 09:46:40 -04:00
David Peter
905b9d7f51 [ty] Reachability analysis for isinstance(…) branches (#19503)
## Summary

Add more precise type inference for a limited set of `isinstance(…)`
calls, i.e. return `Literal[True]` if we can be sure that this is the
correct result. This improves exhaustiveness checking / reachability
analysis for if-elif-else chains with `isinstance` checks. For example:

```py
def is_number(x: int | str) -> bool:  # no "can implicitly return `None` error here anymore
    if isinstance(x, int):
        return True
    elif isinstance(x, str):
        return False

    # code here is now detected as being unreachable
```

This PR also adds a new test suite for exhaustiveness checking.

## Test Plan

New Markdown tests

### Ecosystem analysis

The removed diagnostics look good. There's [one
case](f52c4f1afd/torchvision/io/video_reader.py (L125-L143))
where a "true positive" is removed in unreachable code. `src` is
annotated as being of type `str`, but there is an `elif isinstance(src,
bytes)` branch, which we now detect as unreachable. And so the
diagnostic inside that branch is silenced. I don't think this is a
problem, especially once we have a "graying out" feature, or a lint that
warns about unreachable code.
2025-07-23 13:06:30 +02:00
David Peter
b605c3e232 [ty] Normalize single-member enums to their instance type (#19502)
## Summary

Fixes https://github.com/astral-sh/ty/issues/874

Labeling this as `internal`, since we haven't released the
enum-expansion feature.

## Test Plan

New Markdown tests
2025-07-23 10:14:20 +02:00
Micha Reiser
c281891b5c [ty] Invert ty_ide and ty_project dependency (#19501) 2025-07-23 07:37:46 +00:00
Dhruv Manilawala
53d795da67 [ty] Implement mock language server for testing (#19391)
## Summary

Closes: astral-sh/ty#88

This PR implements an initial version of a mock language server that can
be used to write e2e tests using the real server running in the
background.

The way it works is that you'd use the `TestServerBuilder` to help
construct the `TestServer` with the setup data. This could be the
workspace folders, populating the file and it's content in the memory
file system, setting the right client capabilities to make the server
respond correctly, etc. This can be expanded as we write more test
cases.

There are still a few things to follow-up on:
- ~In the `Drop` implementation, we should assert that there are no
pending notification, request and responses from the server that the
test code hasn't handled yet~ Implemented in [`afd1f82`
(#19391)](afd1f82bde)
- Reduce the setup boilerplate in any way we can
- Improve the final assertion, currently I'm just snapshotting the final
output

## Test Plan

Written a few test cases.
2025-07-23 12:26:58 +05:30
David Peter
385d6fa608 [ty] Detect enums if metaclass is a subtype of EnumType/EnumMeta (#19481)
## Summary

This PR implements the following section from the [typing spec on
enums](https://typing.python.org/en/latest/spec/enums.html#enum-definition):

> Enum classes can also be defined using a subclass of `enum.Enum` **or
any class that uses `enum.EnumType` (or a subclass thereof) as a
metaclass**. Note that `enum.EnumType` was named `enum.EnumMeta` prior
to Python 3.11.

part of https://github.com/astral-sh/ty/issues/183

## Test Plan

New Markdown tests
2025-07-23 08:46:51 +02:00
Jack O'Connor
ba070bb6d5 [ty] perform type narrowing for places marked global too (#19381)
Fixes https://github.com/astral-sh/ty/issues/311.
2025-07-22 16:42:10 -07:00
Micha Reiser
dc10ab81bd [ty] Use ThinVec for sub segments in PlaceExpr (#19470) 2025-07-22 20:39:39 +02:00
Douglas Creager
7673d46b71 [ty] Splat variadic arguments into parameter list (#18996)
This PR updates our call binding logic to handle splatted arguments.

Complicating matters is that we have separated call bind analysis into
two phases: parameter matching and type checking. Parameter matching
looks at the arity of the function signature and call site, and assigns
arguments to parameters. Importantly, we don't yet know the type of each
argument! This is needed so that we can decide whether to infer the type
of each argument as a type form or value form, depending on the
requirements of the parameter that the argument was matched to.

This is an issue when splatting an argument, since we need to know how
many elements the splatted argument contains to know how many positional
parameters to match it against. And to know how many elements the
splatted argument has, we need to know its type.

To get around this, we now make the assumption that splatted arguments
can only be used with value-form parameters. (If you end up splatting an
argument into a type-form parameter, we will silently pass in its
value-form type instead.) That allows us to preemptively infer the
(value-form) type of any splatted argument, so that we have its arity
available during parameter matching. We defer inference of non-splatted
arguments until after parameter matching has finished, as before.

We reuse a lot of the new tuple machinery to make this happen — in
particular resizing the tuple spec representing the number of arguments
passed in with the tuple length representing the number of parameters
the splat was matched with.

This work also shows that we might need to change how we are performing
argument expansion during overload resolution. At the moment, when we
expand parameters, we assume that each argument will still be matched to
the same parameters as before, and only retry the type-checking phase.
With splatted arguments, this is no longer the case, since the inferred
arity of each union element might be different than the arity of the
union as a whole, which can affect how many parameters the splatted
argument is matched to. See the regression test case in
`mdtest/call/function.md` for more details.
2025-07-22 14:33:08 -04:00
frank
9d5ecacdc5 [flake8-pyi] Skip fix if all Union members are None (PYI016) (#19416)
patches #19403

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-07-22 17:03:14 +00:00
Brent Westbrook
9af8597608 Skip notebook with errors in ecosystem check (#19491)
Summary
--

I've been noticing this failure in the formatter ecosystem check and
decided to
look into it. We fail to parse the
[notebook](https://github.com/openai/openai-cookbook/blob/main/examples/mcp/databricks_mcp_cookbook.ipynb)
because some of the `code` cells
have non-Python code in them. `ruff format` only reports one of these,
corresponding to a shell snippet, but `ruff check` emits some additional
errors
about JS code later in the file too:

```
databricks_mcp_cookbook.ipynb:cell 21:1:11: SyntaxError: Simple statements must be separated by newlines or semicolons
databricks_mcp_cookbook.ipynb:cell 21:1:19: SyntaxError: Simple statements must be separated by newlines or semicolons
databricks_mcp_cookbook.ipynb:cell 21:1:50: SyntaxError: Simple statements must be separated by newlines or semicolons
databricks_mcp_cookbook.ipynb:cell 30:4:7: SyntaxError: Simple statements must be separated by newlines or semicolons
databricks_mcp_cookbook.ipynb:cell 30:4:41: E703 Statement ends with an unnecessary semicolon
databricks_mcp_cookbook.ipynb:cell 30:5:14: SyntaxError: Expected ':', found '{'
databricks_mcp_cookbook.ipynb:cell 30:6:9: SyntaxError: Expected ',', found '{'
databricks_mcp_cookbook.ipynb:cell 30:6:25: SyntaxError: Expected ',', found '='
databricks_mcp_cookbook.ipynb:cell 30:6:46: SyntaxError: Expected ',', found ';'
databricks_mcp_cookbook.ipynb:cell 30:6:47: SyntaxError: Expected '}', found newline
databricks_mcp_cookbook.ipynb:cell 30:7:1: SyntaxError: Unexpected indentation
databricks_mcp_cookbook.ipynb:cell 30:7:13: SyntaxError: Expected ':', found 'break'
databricks_mcp_cookbook.ipynb:cell 30:7:18: E703 Statement ends with an unnecessary semicolon
databricks_mcp_cookbook.ipynb:cell 30:8:28: SyntaxError: Simple statements must be separated by newlines or semicolons
databricks_mcp_cookbook.ipynb:cell 30:8:55: E703 Statement ends with an unnecessary semicolon
databricks_mcp_cookbook.ipynb:cell 30:9:18: SyntaxError: Expected an expression
databricks_mcp_cookbook.ipynb:cell 30:10:11: SyntaxError: Expected ',', found name
databricks_mcp_cookbook.ipynb:cell 30:10:16: SyntaxError: Expected ',', found '='
databricks_mcp_cookbook.ipynb:cell 30:10:22: SyntaxError: Expected ',', found name
databricks_mcp_cookbook.ipynb:cell 30:10:24: SyntaxError: Expected ',', found ';'
databricks_mcp_cookbook.ipynb:cell 30:11:27: SyntaxError: Expected ',', found '='
databricks_mcp_cookbook.ipynb:cell 30:11:34: SyntaxError: Expected ',', found name
databricks_mcp_cookbook.ipynb:cell 30:11:48: SyntaxError: Expected ',', found ';'
databricks_mcp_cookbook.ipynb:cell 30:11:49: SyntaxError: Expected '}', found NonLogicalNewline
databricks_mcp_cookbook.ipynb:cell 30:12:1: SyntaxError: Unexpected indentation
databricks_mcp_cookbook.ipynb:cell 30:12:16: E703 Statement ends with an unnecessary semicolon
databricks_mcp_cookbook.ipynb:cell 30:13:3: SyntaxError: Expected a statement
databricks_mcp_cookbook.ipynb:cell 30:13:4: SyntaxError: Expected a statement
databricks_mcp_cookbook.ipynb:cell 30:13:5: SyntaxError: Expected a statement
databricks_mcp_cookbook.ipynb:cell 30:13:5: E703 Statement ends with an unnecessary semicolon
databricks_mcp_cookbook.ipynb:cell 30:13:6: SyntaxError: Expected a statement
databricks_mcp_cookbook.ipynb:cell 30:14:1: SyntaxError: Expected a statement
databricks_mcp_cookbook.ipynb:cell 30:14:2: SyntaxError: Expected a statement
```

Test Plan
--

This PR
2025-07-22 12:29:38 -04:00
David Peter
64e5780037 [ty] Consistent use of American english (in rules) (#19488)
## Summary

Just noticed this as a minor inconsistency in our rules, and had Claude
do a few more automated replacements.
2025-07-22 16:10:38 +02:00
David Peter
da8aa6a631 [ty] Support iterating over enums (#19486)
## Summary

Infer the correct type in a scenario like this:

```py
class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

for color in Color:
    reveal_type(color)  # revealed: Color
```

We should eventually support this out-of-the-box when
https://github.com/astral-sh/ty/issues/501 is implemented. For this
reason, @AlexWaygood would prefer to keep things as they are (we
currently infer `Unknown`, so false positives seem unlikely). But it
seemed relatively easy to support, so I'm opening this for discussion.

part of https://github.com/astral-sh/ty/issues/183

## Test Plan

Adapted existing test.

## Ecosystem analysis

```diff
- warning[unused-ignore-comment] rotkehlchen/chain/aggregator.py:591:82: Unused blanket `type: ignore` directive
```

This `unused-ignore-comment` goes away due to a new true positive.
2025-07-22 16:09:28 +02:00
David Peter
ee69d38000 Fix panic for illegal Literal[…] annotations with inner subscript expressions (#19489)
## Summary

Fixes pull-types panics for illegal annotations like
`Literal[object[index]]`.

Originally reported by @AlexWaygood

## Test Plan

* Verified that this caused panics in the playground, when typing (and
potentially hovering over) `x: Literal[obj[0]]`.
* Added a regression test
2025-07-22 14:07:20 +00:00
Brent Westbrook
fd335eb8b7 Move fix suggestion to subdiagnostic (#19464)
Summary
--

This PR tweaks Ruff's internal usage of the new diagnostic model to more
closely
match the intended use, as I understand it. Specifically, it moves the
fix/help
suggestion from the primary annotation's message to a subdiagnostic. In
turn, it
adds the secondary/noqa code as the new primary annotation message. As
shown in
the new `ruff_db` tests, this more closely mirrors Ruff's current
diagnostic
output.

I also added `Severity::Help` to render the fix suggestion with a
`help:` prefix
instead of `info:`.

These changes don't have any external impact now but should help a bit
with #19415.

Test Plan
--

New full output format tests in `ruff_db`

Rendered Diagnostics
--

Full diagnostic output from `annotate-snippets` in this PR:

``` 
error[unused-import]: `os` imported but unused
  --> fib.py:1:8
   |
 1 | import os
   |        ^^
   |
 help: Remove unused import: `os`
```

Current Ruff output for the same code:

```
fib.py:1:8: F401 [*] `os` imported but unused
  |
1 | import os
  |        ^^ F401
  |
  = help: Remove unused import: `os`
```

Proposed final output after #19415:

``` 
F401 [*] `os` imported but unused
  --> fib.py:1:8
   |
 1 | import os
   |        ^^
   |
 help: Remove unused import: `os`
```

These are slightly updated from
https://github.com/astral-sh/ruff/pull/19464#issuecomment-3097377634
below to remove the extra noqa codes in the primary annotation messages
for the first and third cases.
2025-07-22 10:03:58 -04:00
Aria Desires
c82fa94e0a [ty] Implement non-stdlib stub mapping for classes and functions (#19471)
This implements mapping of definitions in stubs to definitions in the
"real" implementation using the approach described in
https://github.com/astral-sh/ty/issues/788#issuecomment-3097000287

I've tested this with goto-definition in vscode with code that uses
`colorama` and `types-colorama`.

Notably this implementation does not add support for stub-mapping stdlib
modules, which can be done as an essentially orthogonal followup in the
implementation of `resolve_real_module`.

Part of https://github.com/astral-sh/ty/issues/788
2025-07-22 12:42:55 +00:00
David Peter
6d4687c9af [ty] Disallow illegal uses of ClassVar (#19483)
## Summary

It was faster to implement this then to write the ticket: Disallow
`ClassVar` annotations almost everywhere outside of class body scopes.

## Test Plan

New Markdown tests
2025-07-22 14:21:29 +02:00
David Peter
9180cd094d [ty] Disallow Final in function parameter/return-type annotations (#19480)
## Summary

Disallow `Final` in function parameter- and return-type annotations.

[Typing
spec](https://typing.python.org/en/latest/spec/qualifiers.html#uppercase-final):

> `Final` may only be used in assignments or variable annotations. Using
it in any other position is an error. In particular, `Final` can’t be
used in annotations for function arguments

## Test Plan

Updated MD test
2025-07-22 13:15:19 +02:00
David Peter
9d98a66f65 [ty] Extend Final test suite (#19476)
## Summary

Restructures and cleans up the `typing.Final` test suite. Also adds a
few more tests with TODOs based on the [typing spec for
`typing.Final`](https://typing.python.org/en/latest/spec/qualifiers.html#uppercase-final).
2025-07-22 12:06:47 +02:00
David Peter
cb60ecef6b [ty] Minor change to diagnostic message for invalid Literal uses (#19482) 2025-07-22 11:42:12 +02:00
David Peter
215a1c55d4 [ty] Detect illegal non-enum attribute accesses in Literal annotation (#19477)
## Summary

Detect illegal attribute accesses in `Literal[X.Y]` annotations if `X`
is not an enum class.

## Test Plan

New Markdown test
2025-07-22 11:42:03 +02:00
Micha Reiser
5e29278aa2 [ty] Reduce size of TypeInference (#19435) 2025-07-22 11:36:36 +02:00
David Peter
af62d0368f Run MD tests for Markdown-only changes (#19479)
## Summary

Exclusions in Git pathspecs [are not
order-sensitive](https://css-tricks.com/git-pathspecs-and-how-to-use-them/#aa-exclude):

> After all other pathspecs have been resolved, all pathspecs with an
exclude signature are resolved and then removed from the returned paths.

This means that we can't write chains like we had here before to exclude
Markdown file changes *unless* they are in
`crates/ty_python_semantic/resources/mdtest`. This doesn't work. The
exclude pattern will just overwrite the second pattern and all Markdown
changes will be excluded:

```bash
':!**/*.md' \
':crates/ty_python_semantic/resources/mdtest/**/*.md' \
```

The configuration we had here before meant that tests wouldn't run on
MD-test only PRs, see e.g. https://github.com/astral-sh/ruff/pull/19476.

So here, I'm proposing to remove the broad `:!**/*.md` pattern. We can
always add more fine-grained exclusion patterns, if that's needed. The
`docs` folder is already excluded.

## Test Plan

Tested with local `git diff` invocations.
2025-07-22 11:29:07 +02:00
David Peter
30683e3a93 Revert "[ty] Detect illegal non-enum attribute accesses in Literal annotation"
This reverts commit cbc8c08016.
2025-07-22 09:19:44 +02:00
David Peter
cbc8c08016 [ty] Detect illegal non-enum attribute accesses in Literal annotation 2025-07-22 09:18:50 +02:00
UnboundVariable
897889d1ce [ty] Added semantic token support for more identifiers (#19473)
I noticed that the semantic token implementation was not handling
identifiers in a few cases. This adds support for identifiers that
appear in `except`, `case`, `nonlocal`, and `global` statements.

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-21 15:39:40 -07:00
Alex Waygood
cb5a9ff8dc [ty] Make tuple subclass constructors sound (#19469) 2025-07-21 21:25:11 +00:00
David Peter
fcdffe4ac9 [ty] Pass down specialization to generic dataclass bases (#19472)
## Summary

closes https://github.com/astral-sh/ty/issues/853

## Test Plan

Regression test
2025-07-21 20:51:58 +02:00
Douglas Creager
88de5727df [ty] Garbage-collect reachability constraints (#19414)
This is a follow-on to #19410 that further reduces the memory usage of
our reachability constraints. When finishing the building of a use-def
map, we walk through all of the "final" states and mark only those
reachability constraints as "used". We then throw away the interior TDD
nodes of any reachability constraints that weren't marked as used.

(This helps because we build up quite a few intermediate TDD nodes when
constructing complex reachability constraints. These nodes can never be
accessed if they were _only_ used as an intermediate TDD node. The
marking step ensures that we keep any nodes that ended up being referred
to in some accessible use-def map state.)
2025-07-21 14:16:27 -04:00
David Peter
b8dec79182 [ty] Implicit instance attributes declared Final (#19462)
## Summary

Adds proper type inference for implicit instance attributes that are
declared with a "bare" `Final` and adds `invalid-assignment` diagnostics
for all implicit instance attributes that are declared `Final` or
`Final[…]`.

## Test Plan

New and updated MD tests.

## Ecosystem analysis

```diff
pytest (https://github.com/pytest-dev/pytest)
+ error[invalid-return-type] src/_pytest/fixtures.py:1662:24: Return type does not match returned value: expected `Scope`, found `Scope | (Unknown & ~None & ~((...) -> object) & ~str) | (((str, Config, /) -> Unknown) & ~((...) -> object) & ~str) | (Unknown & ~str)
```

The definition of the `scope` attribute is [here](

5f99385635/src/_pytest/fixtures.py (L1020-L1028)).
Looks like this is a new false positive due to missing `TypeAlias`
support that is surfaced here because we now infer a more precise type
for `FixtureDef._scope`.
2025-07-21 20:01:07 +02:00
David Peter
dc66019fbc [ty] Expansion of enums into unions of literals (#19382)
## Summary

Implement expansion of enums into unions of enum literals (and the
reverse operation). For the enum below, this allows us to understand
that `Color = Literal[Color.RED, Color.GREEN, Color.BLUE]`, or that
`Color & ~Literal[Color.RED] = Literal[Color.GREEN, Color.BLUE]`. This
helps in exhaustiveness checking, which is why we see some removed
`assert_never` false positives. And since exhaustiveness checking also
helps with understanding terminal control flow, we also see a few
removed `invalid-return-type` and `possibly-unresolved-reference` false
positives. This PR also adds expansion of enums in overload resolution
and type narrowing constructs.

```py
from enum import Enum
from typing_extensions import Literal, assert_never
from ty_extensions import Intersection, Not, static_assert, is_equivalent_to

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

type Red = Literal[Color.RED]
type Green = Literal[Color.GREEN]
type Blue = Literal[Color.BLUE]

static_assert(is_equivalent_to(Red | Green | Blue, Color))
static_assert(is_equivalent_to(Intersection[Color, Not[Red]], Green | Blue))


def color_name(color: Color) -> str:  # no error here (we detect that this can not implicitly return None)
    if color is Color.RED:
        return "Red"
    elif color is Color.GREEN:
        return "Green"
    elif color is Color.BLUE:
        return "Blue"
    else:
        assert_never(color)  # no error here
```

## Performance

I avoided an initial regression here for large enums, but the
`UnionBuilder` and `IntersectionBuilder` parts can certainly still be
optimized. We might want to use the same technique that we also use for
unions of other literals. I didn't see any problems in our benchmarks so
far, so this is not included yet.

## Test Plan

Many new Markdown tests
2025-07-21 19:37:55 +02:00
Micha Reiser
926e83323a [ty] Avoid rechecking the entire project when changing the opened files (#19463) 2025-07-21 18:05:03 +02:00
Micha Reiser
5cace28c3e [ty] Add warning for unknown TY_MEMORY_REPORT value (#19465) 2025-07-21 14:29:24 +00:00
github-actions[bot]
3785e13231 [ty] Sync vendored typeshed stubs (#19461)
Co-authored-by: typeshedbot <>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-07-21 14:01:42 +01:00
Alex Waygood
c2380fa0e2 [ty] Extend tuple __len__ and __bool__ special casing to also cover tuple subclasses (#19289)
Co-authored-by: Brent Westbrook
2025-07-21 12:50:46 +00:00
Alex Waygood
4dec44ae49 [ty] bump docstring-adder pin (#19458) 2025-07-21 13:38:40 +01:00
David Peter
b6579eaf04 [ty] Disallow assignment to Final class attributes (#19457)
## Summary

Emit errors for the following assignments:
```py
class C:
    CLASS_LEVEL_CONSTANT: Final[int] = 1

C.CLASS_LEVEL_CONSTANT = 2
C().CLASS_LEVEL_CONSTANT = 2
```

## Test Plan

Updated and new MD tests
2025-07-21 14:27:56 +02:00
renovate[bot]
f063c0e874 Update dependency ruff to v0.12.4 (#19442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:32:09 +02:00
renovate[bot]
6a65734ee3 Update pre-commit hook astral-sh/ruff-pre-commit to v0.12.4 (#19443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:31:55 +02:00
renovate[bot]
00066e094c Update rui314/setup-mold digest to 702b190 (#19441)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:31:47 +02:00
renovate[bot]
37a1958374 Update taiki-e/install-action action to v2.56.19 (#19448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:31:18 +02:00
renovate[bot]
2535d791ae Update Rust crate strum_macros to v0.27.2 (#19447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:31:07 +02:00
renovate[bot]
05c4399e7b Update Rust crate strum to v0.27.2 (#19446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:30:51 +02:00
renovate[bot]
b18434b0f6 Update Rust crate rand to v0.9.2 (#19444)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:30:07 +02:00
renovate[bot]
17779c9a17 Update Rust crate serde_json to v1.0.141 (#19445)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 08:29:51 +02:00
Dylan
53fc0614da Fix unreachable panic in parser (#19183)
Parsing the (invalid) expression `f"{\t"i}"` caused a panic because the
`TStringMiddle` character was "unreachable" due the way the parser
recovered from the line continuation (it ate the t-string start).

The cause of the issue is as follows: 

The parser begins parsing the f-string and expects to see a list of
objects, essentially alternating between _interpolated elements_ and
ordinary strings. It is happy to see the first left brace, but then
there is a lexical error caused by the line-continuation character. So
instead of the parser seeing a list of elements with just one member, it
sees a list that starts like this:

- Interpolated element with an invalid token, stored as a `Name`
- Something else built from tokens beginning with `TStringStart` and
`TStringMiddle`

When it sees the `TStringStart` error recovery says "that's a list
element I don't know what to do with, let's skip it". When it sees
`TStringMiddle` it says "oh, that looks like the middle of _some
interpolated string_ so let's try to parse it as one of the literal
elements of my `FString`". Unfortunately, the function being used to
parse individual list elements thinks (arguably correctly) that it's not
possible to have a `TStringMiddle` sitting in your `FString`, and hits
`unreachable`.

Two potential ways (among many) to solve this issue are:

1. Allow a `TStringMiddle` as a valid "literal" part of an f-string
during parsing (with the hope/understanding that this would only occur
in an invalid context)
2. Skip the `TStringMiddle` as an "unexpected/invalid list item" in the
same way that we skipped `TStringStart`.

I have opted for the second approach since it seems somehow more morally
correct, even though it loses more information. To implement this, the
recovery context needs to know whether we are in an f-string or t-string
- hence the changes to that enum. As a bonus we get slightly more
specific error messages in some cases.

Closes #18860
2025-07-20 22:04:14 +00:00
Dan Parizher
59249f483b [ruff] Support byte strings (RUF055) (#18926)
<!--
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

<!-- What's the purpose of the change? What does it do, and why? -->

Closes #18739

## Test Plan

<!-- How was it tested? -->

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-07-20 17:58:40 -04:00
Micha Reiser
84e76f4d04 [ty] Avoid second lookup for infer_maybe_standalone_expression (#19439) 2025-07-20 18:22:04 +02:00
UnboundVariable
0acc273286 [ty] Implemented "go to definition" support for import statements (#19428)
This PR extends the "go to declaration" and "go to definition"
functionality to support import statements — both standard imports and
"from" import forms.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-19 11:22:07 -07:00
Micha Reiser
93a9fabb26 [ty] Avoid secondary tree traversal to get call expression for keyword arguments (#19429) 2025-07-19 18:21:27 +02:00
Micha Reiser
98d1811dd1 [ty] Add goto definition to playground (#19425) 2025-07-19 15:44:44 +02:00
Aria Desires
06f9f52e59 [ty] Add support for @warnings.deprecated (#19376)
* [x] basic handling
  * [x] parse and discover `@warnings.deprecated` attributes
  * [x] associate them with function definitions
  * [x] associate them with class definitions
  * [x] add a new "deprecated" diagnostic
* [x] ensure diagnostic is styled appropriately for LSPs
(DiagnosticTag::Deprecated)

* [x] functions
  * [x] fire on calls
  * [x] fire on arbitrary references 
* [x] classes
  * [x] fire on initializers
  * [x] fire on arbitrary references
* [x] methods
  * [x] fire on calls
  * [x] fire on arbitrary references
* [ ] overloads
  * [ ] fire on calls
  * [ ] fire on arbitrary references(??? maybe not ???)
  * [ ] only fire if the actual selected overload is deprecated 

* [ ] dunder desugarring (warn on deprecated `__add__` if `+` is
invoked)
* [ ] alias supression? (don't warn on uses of variables that deprecated
items were assigned to)

* [ ] import logic
  * [x] fire on imports of deprecated items
* [ ] suppress subsequent diagnostics if the import diagnostic fired (is
this handled by alias supression?)
  * [x] fire on all qualified references (`module.mydeprecated`)
  * [x] fire on all references that depend on a `*` import
    


Fixes https://github.com/astral-sh/ty/issues/153
2025-07-18 23:50:29 +00:00
Jack O'Connor
e9a64e5825 [ty] make del x force local resolution of x in the current scope (#19389)
Fixes https://github.com/astral-sh/ty/issues/769.

**Updated:** The preferred approach here is to keep the SemanticIndex
simple (`del` of any name marks that name "bound" in the current scope)
and to move complexity to type inference (free variable resolution stops
when it finds a binding, unless that binding is declared `nonlocal`). As
part of this change, free variable resolution will now union the types
it finds as it walks in enclosing scopes. This approach is still
incomplete, because it doesn't consider inner scopes or sibling scopes,
but it improves the common case.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-07-18 14:58:32 -07:00
UnboundVariable
360eb7005f [ty] Added support for "go to definition" for attribute accesses and keyword arguments (#19417)
This PR builds upon #19371. It addresses a few additional code review
suggestions and adds support for attribute accesses (expressions of the
form `x.y`) and keyword arguments within call expressions.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-18 11:33:57 -07:00
Micha Reiser
630c7a3152 [ty] Reduce number of inline stored definitions per place (#19409) 2025-07-18 18:28:46 +02:00
Ibraheem Ahmed
e6e029a8b7 Update salsa (#19258)
## Summary

Pulls in https://github.com/salsa-rs/salsa/pull/934.
2025-07-18 12:14:28 -04:00
Andrew Gallant
64f9481fd0 [ty] Add caching for submodule completion suggestions (#19408)
This change makes it so we aren't doing a directory traversal every time
we ask for completions from a module. Specifically, submodules that
aren't attributes of their parent module can only be discovered by
looking at the directory tree. But we want to avoid doing a directory
scan unless we think there are changes.

To make this work, this change does a little bit of surgery to
`FileRoot`. Previously, a `FileRoot` was only used for library search
paths. Its revision was bumped whenever a file in that tree was added,
deleted or even modified (to support the discovery of `pth` files and
changes to its contents). This generally seems fine since these are
presumably dependency paths that shouldn't change frequently.

In this change, we add a `FileRoot` for the project. But having the
`FileRoot`'s revision bumped for every change in the project makes
caching based on that `FileRoot` rather ineffective. That is, cache
invalidation will occur too aggressively. To the point that there is
little point in adding caching in the first place. To mitigate this, a
`FileRoot`'s revision is only bumped on a change to a child file's
contents when the `FileRoot` is a `LibrarySearchPath`. Otherwise, we
only bump the revision when a file is created or added.

The effect is that, at least in VS Code, when a new module is added or
removed, this change is picked up and the cache is properly invalidated.
Other LSP clients with worse support for file watching (which seems to
be the case for the CoC vim plugin that I use) don't work as well. Here,
the cache is less likely to be invalidated which might cause completions
to have stale results. Unless there's an obvious way to fix or improve
this, I propose punting on improvements here for now.
2025-07-18 11:54:27 -04:00
Dhruv Manilawala
99d0ac60b4 [ty] Track open files in the server (#19264)
## Summary

This PR updates the server to keep track of open files both system and
virtual files.

This is done by updating the project by adding the file in the open file
set in `didOpen` notification and removing it in `didClose`
notification.

This does mean that for workspace diagnostics, ty will only check open
files because the behavior of different diagnostic builder is to first
check `is_file_open` and only add diagnostics for open files. So, this
required updating the `is_file_open` model to be `should_check_file`
model which validates whether the file needs to be checked based on the
`CheckMode`. If the check mode is open files only then it will check
whether the file is open. If it's all files then it'll return `true` by
default.

Closes: astral-sh/ty#619

## Test Plan

### Before

There are two files in the project: `__init__.py` and `diagnostics.py`.

In the video, I'm demonstrating the old behavior where making changes to
the (open) `diagnostics.py` file results in re-parsing the file:


https://github.com/user-attachments/assets/c2ac0ecd-9c77-42af-a924-c3744b146045

### After

Same setup as above.

In the video, I'm demonstrating the new behavior where making changes to
the (open) `diagnostics.py` file doesn't result in re-parting the file:


https://github.com/user-attachments/assets/7b82fe92-f330-44c7-b527-c841c4545f8f
2025-07-18 19:33:35 +05:30
Andrew Gallant
ba7ed3a6f9 [ty] Use as the "cut" indicator in diagnostic rendering (#19420)
This makes ty match ruff's behavior. Specifically, we want to use `…`
instead of the default `...` because `...` has special significance in
Python.
2025-07-18 07:46:48 -04:00
justin
39b41838f3 [ty] synthesize __setattr__ for frozen dataclasses (#19307)
## Summary

Synthesize a `__setattr__` method with a return type of `Never` for
frozen dataclasses.

https://docs.python.org/3/library/dataclasses.html#frozen-instances

https://docs.python.org/3/library/dataclasses.html#dataclasses.FrozenInstanceError

### Related
https://github.com/astral-sh/ty/issues/111
https://github.com/astral-sh/ruff/pull/17974#discussion_r2108527106
https://github.com/astral-sh/ruff/pull/18347#discussion_r2128174665

## Test Plan

New Markdown tests

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-07-18 11:35:05 +02:00
UnboundVariable
c7640a433e [ty] Fixed bug in semantic token provider for parameters. (#19418)
This fixes https://github.com/astral-sh/ty/issues/832.

New tests were added to prevent future regressions.

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
2025-07-18 00:02:23 -07:00
Micha Reiser
1765014be3 [ty] Shrink reachability constraints (#19410) 2025-07-18 07:36:18 +02:00
Brent Westbrook
997dc2e7cc Move JUnit rendering to ruff_db (#19370)
Summary
--

This PR moves the JUnit output format to the new rendering
infrastructure. As I
mention in a TODO in the code, there's some code that will be shared
with the
`grouped` output format. Hopefully I'll have that PR up too by the time
this one
is reviewed.

Test Plan
--

Existing tests moved to `ruff_db`

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-07-17 18:24:13 -04:00
Douglas Creager
4aee0398cb [ty] Show the raw argument type in reveal_type (#19400)
This PR is changes how `reveal_type` determines what type to reveal, in
a way that should be a no-op to most callers.

Previously, we would reveal the type of the first parameter, _after_ all
of the call binding machinery had done its work. This includes inferring
the specialization of a generic function, and then applying that
specialization to all parameter and argument types, which is relevant
since the typeshed definition of `reveal_type` is generic:

```pyi
def reveal_type(obj: _T, /) -> _T: ...
```

Normally this does not matter, since we infer `_T = [arg type]` and
apply that to the parameter type, yielding `[arg type]`. But applying
that specialization also simplifies the argument type, which makes
`reveal_type` less useful as a debugging aid when we want to see the
actual, raw, unsimplified argument type.

With this patch, we now grab the original unmodified argument type and
reveal that instead.

In addition to making the debugging aid example work, this also makes
our `reveal_type` implementation more robust to custom typeshed
definitions, such as

```py
def reveal_type(obj: Any) -> Any: ...
```

(That custom definition is probably not what anyone would want, since
you wouldn't be able to depend on the return type being equivalent to
the argument type, but still)
2025-07-17 16:50:29 -04:00
1191 changed files with 45919 additions and 17590 deletions

6
.github/CODEOWNERS vendored
View File

@@ -19,6 +19,10 @@
# ty
/crates/ty* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ruff_db/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty_project/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty_server/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty/ @carljm @MichaReiser @sharkdp @dcreager
/crates/ty_wasm/ @carljm @MichaReiser @sharkdp @dcreager
/scripts/ty_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ty_python_semantic @carljm @AlexWaygood @sharkdp @dcreager

View File

@@ -63,7 +63,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with:
images: ${{ env.RUFF_BASE_IMG }}
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
@@ -123,7 +123,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with:
images: ${{ env.RUFF_BASE_IMG }}
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
@@ -219,7 +219,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
# ghcr.io prefers index level annotations
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
@@ -266,7 +266,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
with:

View File

@@ -143,12 +143,12 @@ jobs:
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**' \
':!**/*.md' \
':crates/ty_python_semantic/resources/mdtest/**/*.md' \
# NOTE: Do not exclude all Markdown files here, but rather use
# specific exclude patterns like 'docs/**'), because tests for
# 'ty' are written in Markdown.
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
':!docs/**' \
':!assets/**' \
':.github/workflows/ci.yaml' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
@@ -238,13 +238,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-insta
- name: ty mdtests (GitHub annotations)
@@ -296,13 +296,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-insta
- name: "Run tests"
@@ -325,7 +325,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Run tests"
@@ -381,7 +381,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Build"
run: cargo build --release --locked
@@ -406,7 +406,7 @@ jobs:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- name: "Build tests"
shell: bash
env:
@@ -429,7 +429,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -451,7 +451,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download Ruff binary to test
id: download-cached-binary
@@ -652,7 +652,7 @@ jobs:
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: Fuzz
env:
FORCE_COLOR: 1
@@ -682,7 +682,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
- uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -722,7 +722,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
@@ -765,7 +765,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -897,13 +897,13 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-codspeed
@@ -911,7 +911,7 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
@@ -930,13 +930,13 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@c07504cae06f832dc8de08911c9a9c5cddb0d2d3 # v2.56.13
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-codspeed
@@ -944,7 +944,7 @@ jobs:
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -34,11 +34,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@85c79d00377f0d32cdbae595a46de6f7c2fa6599 # v1
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: Build ruff
# A debug build means the script runs slower once it gets started,

View File

@@ -38,7 +38,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
@@ -81,9 +81,9 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
workspaces: "ruff"

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: wheels-*

View File

@@ -34,6 +34,10 @@ env:
# and which all three workers push to.
UPSTREAM_BRANCH: typeshedbot/sync-typeshed
# The path to the directory that contains the vendored typeshed stubs,
# relative to the root of the Ruff repository.
VENDORED_TYPESHED: crates/ty_vendored/vendor/typeshed
jobs:
# Sync typeshed stubs, and sync all docstrings available on Linux.
# Push the changes to a new branch on the upstream repository.
@@ -61,23 +65,23 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: Sync typeshed stubs
run: |
rm -rf ruff/crates/ty_vendored/vendor/typeshed
mkdir ruff/crates/ty_vendored/vendor/typeshed
cp typeshed/README.md ruff/crates/ty_vendored/vendor/typeshed
cp typeshed/LICENSE ruff/crates/ty_vendored/vendor/typeshed
rm -rf "ruff/${VENDORED_TYPESHED}"
mkdir "ruff/${VENDORED_TYPESHED}"
cp typeshed/README.md "ruff/${VENDORED_TYPESHED}"
cp typeshed/LICENSE "ruff/${VENDORED_TYPESHED}"
# The pyproject.toml file is needed by a later job for the black configuration.
# It's deleted before creating the PR.
cp typeshed/pyproject.toml ruff/crates/ty_vendored/vendor/typeshed
cp typeshed/pyproject.toml "ruff/${VENDORED_TYPESHED}"
cp -r typeshed/stdlib ruff/crates/ty_vendored/vendor/typeshed/stdlib
rm -rf ruff/crates/ty_vendored/vendor/typeshed/stdlib/@tests
git -C typeshed rev-parse HEAD > ruff/crates/ty_vendored/vendor/typeshed/source_commit.txt
cp -r typeshed/stdlib "ruff/${VENDORED_TYPESHED}/stdlib"
rm -rf "ruff/${VENDORED_TYPESHED}/stdlib/@tests"
git -C typeshed rev-parse HEAD > "ruff/${VENDORED_TYPESHED}/source_commit.txt"
cd ruff
git checkout -b typeshedbot/sync-typeshed
git checkout -b "${UPSTREAM_BRANCH}"
git add .
git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)" --allow-empty
- name: Sync Linux docstrings
@@ -113,7 +117,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -151,7 +155,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -167,17 +171,17 @@ jobs:
# consistent with the other typeshed stubs around them.
# Typeshed formats code using black in their CI, so we just invoke
# black on the stubs the same way that typeshed does.
uvx black crates/ty_vendored/vendor/typeshed/stdlib --config crates/ty_vendored/vendor/typeshed/pyproject.toml || true
uvx black "${VENDORED_TYPESHED}/stdlib" --config "${VENDORED_TYPESHED}/pyproject.toml" || true
git commit -am "Format codemodded docstrings" --allow-empty
rm crates/ty_vendored/vendor/typeshed/pyproject.toml
rm "${VENDORED_TYPESHED}/pyproject.toml"
git commit -am "Remove pyproject.toml file"
git push
- name: Create a PR
if: ${{ success() }}
run: |
gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr list --repo "${GITHUB_REPOSITORY}" --head "${UPSTREAM_BRANCH}" --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty"
create-issue-on-failure:

View File

@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
@@ -64,7 +64,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
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@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
@@ -49,7 +49,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
ecosystem-analyzer \
--verbose \

View File

@@ -0,0 +1,113 @@
name: Run typing conformance
permissions: {}
on:
pull_request:
paths:
- "crates/ty*/**"
- "crates/ruff_db"
- "crates/ruff_python_ast"
- "crates/ruff_python_parser"
- ".github/workflows/typing_conformance.yaml"
- ".github/workflows/typing_conformance_comment.yaml"
- "Cargo.lock"
- "!**.md"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
CONFORMANCE_SUITE_COMMIT: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
jobs:
typing_conformance:
name: Compute diagnostic diff
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: python/typing
ref: ${{ env.CONFORMANCE_SUITE_COMMIT }}
path: typing
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
workspaces: "ruff"
- name: Install Rust toolchain
run: rustup show
- name: Compute diagnostic diff
shell: bash
run: |
RUFF_DIR="$GITHUB_WORKSPACE/ruff"
# Build the executable for the old and new commit
(
cd ruff
echo "new commit"
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
cargo build --release --bin ty
mv target/release/ty ty-new
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b old_commit "$MERGE_BASE"
echo "old commit (merge base)"
git rev-list --format=%s --max-count=1 old_commit
cargo build --release --bin ty
mv target/release/ty ty-old
)
(
cd typing/conformance/tests
echo "Running ty on old commit (merge base)"
"$RUFF_DIR/ty-old" check --color=never --output-format=concise . > "$GITHUB_WORKSPACE/old-output.txt" 2>&1 || true
echo "Running ty on new commit"
"$RUFF_DIR/ty-new" check --color=never --output-format=concise . > "$GITHUB_WORKSPACE/new-output.txt" 2>&1 || true
)
if ! diff -u old-output.txt new-output.txt > typing_conformance_diagnostics.diff; then
echo "Differences found between base and PR"
else
echo "No differences found"
touch typing_conformance_diagnostics.diff
fi
echo ${{ github.event.number }} > pr-number
echo "${CONFORMANCE_SUITE_COMMIT}" > conformance-suite-commit
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: typing_conformance_diagnostics_diff
path: typing_conformance_diagnostics.diff
- name: Upload pr-number
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pr-number
path: pr-number
- name: Upload conformance suite commit
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: conformance-suite-commit
path: conformance-suite-commit

View File

@@ -0,0 +1,112 @@
name: PR comment (typing_conformance)
on: # zizmor: ignore[dangerous-triggers]
workflow_run:
workflows: [Run typing conformance]
types: [completed]
workflow_dispatch:
inputs:
workflow_run_id:
description: The typing_conformance workflow that triggers the workflow run
required: true
jobs:
comment:
runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download PR number
with:
name: pr-number
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
if_no_artifact_found: ignore
allow_forks: true
- name: Parse pull request number
id: pr-number
run: |
if [[ -f pr-number ]]
then
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download typing conformance suite commit
with:
name: conformance-suite-commit
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
if_no_artifact_found: ignore
allow_forks: true
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: "Download typing_conformance results"
id: download-typing_conformance_diff
if: steps.pr-number.outputs.pr-number
with:
name: typing_conformance_diagnostics_diff
workflow: typing_conformance.yaml
pr: ${{ steps.pr-number.outputs.pr-number }}
path: pr/typing_conformance_diagnostics_diff
workflow_conclusion: completed
if_no_artifact_found: ignore
allow_forks: true
- name: Generate comment content
id: generate-comment
if: ${{ steps.download-typing_conformance_diff.outputs.found_artifact == 'true' }}
run: |
# Guard against malicious typing_conformance results that symlink to a secret
# file on this runner
if [[ -L pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff ]]
then
echo "Error: typing_conformance_diagnostics.diff cannot be a symlink"
exit 1
fi
# Note this identifier is used to find the comment to update on
# subsequent runs
echo '<!-- generated-comment typing_conformance_diagnostics_diff -->' >> comment.txt
if [[ -f conformance-suite-commit ]]
then
echo "## Diagnostic diff on [typing conformance tests](https://github.com/python/typing/tree/$(<conformance-suite-commit)/conformance)" >> comment.txt
else
echo "conformance-suite-commit file not found"
echo "## Diagnostic diff on typing conformance tests" >> comment.txt
fi
if [ -s "pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff" ]; then
echo '<details>' >> comment.txt
echo '<summary>Changes were detected when running ty on typing conformance tests</summary>' >> comment.txt
echo '' >> comment.txt
echo '```diff' >> comment.txt
cat pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff >> comment.txt
echo '```' >> comment.txt
echo '</details>' >> comment.txt
else
echo 'No changes detected when running ty on typing conformance tests ✅' >> comment.txt
fi
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.txt >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:
issue-number: ${{ steps.pr-number.outputs.pr-number }}
comment-author: "github-actions[bot]"
body-includes: "<!-- generated-comment typing_conformance_diagnostics_diff -->"
- name: Create or update comment
if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }}
body-path: comment.txt
edit-mode: replace

View File

@@ -81,10 +81,10 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.3
rev: v0.12.7
hooks:
- id: ruff-format
- id: ruff
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, pyi]
require_serial: true

View File

@@ -1,5 +1,53 @@
# Changelog
## 0.12.7
This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.
## 0.12.6
### Preview features
- \[`flake8-commas`\] Add support for trailing comma checks in type parameter lists (`COM812`, `COM819`) ([#19390](https://github.com/astral-sh/ruff/pull/19390))
- \[`pylint`\] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`) ([#19387](https://github.com/astral-sh/ruff/pull/19387))
- \[`ruff`\] Offer fixes for `RUF039` in more cases ([#19065](https://github.com/astral-sh/ruff/pull/19065))
### Bug fixes
- Support `.pyi` files in ruff analyze graph ([#19611](https://github.com/astral-sh/ruff/pull/19611))
- \[`flake8-pyi`\] Preserve inline comment in ellipsis removal (`PYI013`) ([#19399](https://github.com/astral-sh/ruff/pull/19399))
- \[`perflint`\] Ignore rule if target is `global` or `nonlocal` (`PERF401`) ([#19539](https://github.com/astral-sh/ruff/pull/19539))
- \[`pyupgrade`\] Fix `UP030` to avoid modifying double curly braces in format strings ([#19378](https://github.com/astral-sh/ruff/pull/19378))
- \[`refurb`\] Ignore decorated functions for `FURB118` ([#19339](https://github.com/astral-sh/ruff/pull/19339))
- \[`refurb`\] Mark `int` and `bool` cases for `Decimal.from_float` as safe fixes (`FURB164`) ([#19468](https://github.com/astral-sh/ruff/pull/19468))
- \[`ruff`\] Fix `RUF033` for named default expressions ([#19115](https://github.com/astral-sh/ruff/pull/19115))
### Rule changes
- \[`flake8-blind-except`\] Change `BLE001` to permit `logging.critical(..., exc_info=True)` ([#19520](https://github.com/astral-sh/ruff/pull/19520))
### Performance
- Add support for specifying minimum dots in detected string imports ([#19538](https://github.com/astral-sh/ruff/pull/19538))
## 0.12.5
### Preview features
- \[`flake8-use-pathlib`\] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` ([#19404](https://github.com/astral-sh/ruff/pull/19404))
- \[`ruff`\] Support byte strings (`RUF055`) ([#18926](https://github.com/astral-sh/ruff/pull/18926))
### Bug fixes
- Fix `unreachable` panic in parser ([#19183](https://github.com/astral-sh/ruff/pull/19183))
- \[`flake8-pyi`\] Skip fix if all `Union` members are `None` (`PYI016`) ([#19416](https://github.com/astral-sh/ruff/pull/19416))
- \[`perflint`\] Parenthesize generator expressions (`PERF401`) ([#19325](https://github.com/astral-sh/ruff/pull/19325))
- \[`pylint`\] Handle empty comments after line continuation (`PLR2044`) ([#19405](https://github.com/astral-sh/ruff/pull/19405))
### Rule changes
- \[`pep8-naming`\] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` ([#19432](https://github.com/astral-sh/ruff/pull/19432))
## 0.12.4
### Preview features

470
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -57,6 +57,9 @@ assert_fs = { version = "1.1.0" }
argfile = { version = "0.2.0" }
bincode = { version = "2.0.0" }
bitflags = { version = "2.5.0" }
bitvec = { version = "1.0.1", default-features = false, features = [
"alloc",
] }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" }
camino = { version = "1.1.7" }
@@ -70,7 +73,7 @@ console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version = "3.0.1" }
compact_str = "0.9.0"
criterion = { version = "0.6.0", default-features = false }
criterion = { version = "0.7.0", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
dir-test = { version = "0.4.0" }
@@ -80,7 +83,7 @@ etcetera = { version = "0.10.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
get-size2 = { version = "0.5.0", features = [
get-size2 = { version = "0.6.0", features = [
"derive",
"smallvec",
"hashbrown",
@@ -138,7 +141,12 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa", rev = "fc00eba89e5dcaa5edba51c41aa5f309b5cb126b" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d66fe331d546216132ace503512b94d5c68d2c50", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
"inventory",
] }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -150,7 +158,7 @@ serde_with = { version = "3.6.0", default-features = false, features = [
] }
shellexpand = { version = "3.0.0" }
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.2" }
smallvec = { version = "1.13.2", features = ["union", "const_generics", "const_new"] }
snapbox = { version = "0.6.0", features = [
"diff",
"term-svg",
@@ -163,7 +171,6 @@ strum_macros = { version = "0.27.0" }
syn = { version = "2.0.55" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }
thin-vec = { version = "0.2.14" }
thiserror = { version = "2.0.0" }
tikv-jemallocator = { version = "0.6.0" }
toml = { version = "0.9.0" }

View File

@@ -148,8 +148,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.12.4/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.4/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.7/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,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.12.4
rev: v0.12.7
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.12.4"
version = "0.12.7"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -169,6 +169,9 @@ pub struct AnalyzeGraphCommand {
/// Attempt to detect imports from string literals.
#[clap(long)]
detect_string_imports: bool,
/// The minimum number of dots in a string import to consider it a valid import.
#[clap(long)]
min_dots: Option<usize>,
/// Enable preview mode. Use `--no-preview` to disable.
#[arg(long, overrides_with("no_preview"))]
preview: bool,
@@ -808,6 +811,7 @@ impl AnalyzeGraphCommand {
} else {
None
},
string_imports_min_dots: self.min_dots,
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
target_version: self.target_version.map(ast::PythonVersion::from),
..ExplicitConfigOverrides::default()
@@ -1305,6 +1309,7 @@ struct ExplicitConfigOverrides {
show_fixes: Option<bool>,
extension: Option<Vec<ExtensionPair>>,
detect_string_imports: Option<bool>,
string_imports_min_dots: Option<usize>,
}
impl ConfigurationTransformer for ExplicitConfigOverrides {
@@ -1392,6 +1397,9 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
if let Some(detect_string_imports) = &self.detect_string_imports {
config.analyze.detect_string_imports = Some(*detect_string_imports);
}
if let Some(string_imports_min_dots) = &self.string_imports_min_dots {
config.analyze.string_imports_min_dots = Some(*string_imports_min_dots);
}
config
}

View File

@@ -454,7 +454,7 @@ impl LintCacheData {
CacheMessage {
rule,
body: msg.body().to_string(),
suggestion: msg.suggestion().map(ToString::to_string),
suggestion: msg.first_help_text().map(ToString::to_string),
range: msg.expect_range(),
parent: msg.parent(),
fix: msg.fix().cloned(),

View File

@@ -102,7 +102,7 @@ pub(crate) fn analyze_graph(
// Resolve the per-file settings.
let settings = resolver.resolve(path);
let string_imports = settings.analyze.detect_string_imports;
let string_imports = settings.analyze.string_imports;
let include_dependencies = settings.analyze.include_dependencies.get(path).cloned();
// Skip excluded files.

View File

@@ -279,6 +279,7 @@ mod test {
TextEmitter::default()
.with_show_fix_status(true)
.with_color(false)
.emit(
&mut output,
&diagnostics.inner,

View File

@@ -15,8 +15,8 @@ use ruff_db::diagnostic::{
use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel;
use ruff_linter::message::{
Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, JunitEmitter,
SarifEmitter, TextEmitter,
Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, SarifEmitter,
TextEmitter,
};
use ruff_linter::notify_user;
use ruff_linter::settings::flags::{self};
@@ -252,7 +252,11 @@ impl Printer {
write!(writer, "{value}")?;
}
OutputFormat::Junit => {
JunitEmitter.emit(writer, &diagnostics.inner, &context)?;
let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Junit)
.preview(preview);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Concise | OutputFormat::Full => {
TextEmitter::default()
@@ -260,6 +264,7 @@ impl Printer {
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
.with_show_source(self.format == OutputFormat::Full)
.with_unsafe_fixes(self.unsafe_fixes)
.with_preview(preview)
.emit(writer, &diagnostics.inner, &context)?;
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {

View File

@@ -57,33 +57,40 @@ fn dependencies() -> Result<()> {
.write_str(indoc::indoc! {r#"
def f(): pass
"#})?;
root.child("ruff")
.child("e.pyi")
.write_str(indoc::indoc! {r#"
def f() -> None: ...
"#})?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().current_dir(&root), @r###"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": [
"ruff/d.py"
],
"ruff/d.py": [
"ruff/e.py"
],
"ruff/e.py": []
}
assert_cmd_snapshot!(command().current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": [
"ruff/d.py"
],
"ruff/d.py": [
"ruff/e.py",
"ruff/e.pyi"
],
"ruff/e.py": [],
"ruff/e.pyi": []
}
----- stderr -----
"###);
----- stderr -----
"#);
});
Ok(())
@@ -197,23 +204,43 @@ fn string_detection() -> Result<()> {
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r###"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": []
}
assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [],
"ruff/c.py": []
}
----- stderr -----
"###);
----- stderr -----
"#);
});
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().arg("--detect-string-imports").arg("--min-dots").arg("1").current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": []
}
----- stderr -----
"#);
});
Ok(())

View File

@@ -2422,7 +2422,7 @@ requires-python = ">= 3.11"
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.11
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -2734,7 +2734,7 @@ requires-python = ">= 3.11"
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.10
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -3098,7 +3098,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.11
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -3478,7 +3478,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.11
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -3806,7 +3806,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.10
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -4134,7 +4134,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.9
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -4419,7 +4419,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.9
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}
@@ -4757,7 +4757,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.10
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}

View File

@@ -95,6 +95,6 @@ is stricter, which could affect the suggested fix. See [this FAQ section](https:
## References
- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)
- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)
----- stderr -----

View File

@@ -22,11 +22,17 @@ exit_code: 1
"description": "`os` imported but unused",
"fingerprint": "4dbad37161e65c72",
"location": {
"lines": {
"begin": 1,
"end": 1
},
"path": "input.py"
"path": "input.py",
"positions": {
"begin": {
"column": 8,
"line": 1
},
"end": {
"column": 10,
"line": 1
}
}
},
"severity": "major"
},
@@ -35,11 +41,17 @@ exit_code: 1
"description": "Undefined name `y`",
"fingerprint": "7af59862a085230",
"location": {
"lines": {
"begin": 2,
"end": 2
},
"path": "input.py"
"path": "input.py",
"positions": {
"begin": {
"column": 5,
"line": 2
},
"end": {
"column": 6,
"line": 2
}
}
},
"severity": "major"
},
@@ -48,11 +60,17 @@ exit_code: 1
"description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
"fingerprint": "e558cec859bb66e8",
"location": {
"lines": {
"begin": 3,
"end": 3
},
"path": "input.py"
"path": "input.py",
"positions": {
"begin": {
"column": 1,
"line": 3
},
"end": {
"column": 6,
"line": 3
}
}
},
"severity": "major"
}

View File

@@ -25,7 +25,7 @@ exit_code: 1
<testcase name="org.ruff.F821" classname="[TMP]/input" line="2" column="5">
<failure message="Undefined name `y`">line 2, col 5, Undefined name `y`</failure>
</testcase>
<testcase name="org.ruff" classname="[TMP]/input" line="3" column="1">
<testcase name="org.ruff.invalid-syntax" classname="[TMP]/input" line="3" column="1">
<failure message="SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)">line 3, col 1, SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)</failure>
</testcase>
</testsuite>

View File

@@ -95,7 +95,7 @@ exit_code: 1
"rules": [
{
"fullDescription": {
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
},
"help": {
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"

View File

@@ -392,7 +392,7 @@ formatter.docstring_code_line_width = dynamic
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.7
analyze.detect_string_imports = false
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}

View File

@@ -18,7 +18,7 @@ use ruff_python_ast::PythonVersion;
use ty_project::metadata::options::{EnvironmentOptions, Options};
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::watch::{ChangeEvent, ChangedKind};
use ty_project::{Db, ProjectDatabase, ProjectMetadata};
use ty_project::{CheckMode, Db, ProjectDatabase, ProjectMetadata};
struct Case {
db: ProjectDatabase,
@@ -102,6 +102,7 @@ fn setup_tomllib_case() -> Case {
let re = re.unwrap();
db.set_check_mode(CheckMode::OpenFiles);
db.project().set_open_files(&mut db, tomllib_files);
let re_path = re.path(&db).as_system_path().unwrap().to_owned();
@@ -237,6 +238,7 @@ fn setup_micro_case(code: &str) -> Case {
let mut db = ProjectDatabase::new(metadata, system).unwrap();
let file = system_path_to_file(&db, SystemPathBuf::from(file_path)).unwrap();
db.set_check_mode(CheckMode::OpenFiles);
db.project()
.set_open_files(&mut db, FxHashSet::from_iter([file]));
@@ -349,6 +351,41 @@ fn benchmark_many_tuple_assignments(criterion: &mut Criterion) {
});
}
fn benchmark_tuple_implicit_instance_attributes(criterion: &mut Criterion) {
setup_rayon();
criterion.bench_function("ty_micro[many_tuple_assignments]", |b| {
b.iter_batched_ref(
|| {
// This is a regression benchmark for a case that used to hang:
// https://github.com/astral-sh/ty/issues/765
setup_micro_case(
r#"
from typing import Any
class A:
foo: tuple[Any, ...]
class B(A):
def __init__(self, parent: "C", x: tuple[Any]):
self.foo = parent.foo + x
class C(A):
def __init__(self, parent: B, x: tuple[Any]):
self.foo = parent.foo + x
"#,
)
},
|case| {
let Case { db, .. } = case;
let result = db.check();
assert_eq!(result.len(), 0);
},
BatchSize::SmallInput,
);
});
}
fn benchmark_complex_constrained_attributes_1(criterion: &mut Criterion) {
setup_rayon();
@@ -525,14 +562,21 @@ impl<'a> ProjectBenchmark<'a> {
#[track_caller]
fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) {
fn check_project(db: &mut ProjectDatabase, max_diagnostics: usize) {
fn check_project(db: &mut ProjectDatabase, project_name: &str, max_diagnostics: usize) {
let result = db.check();
let diagnostics = result.len();
assert!(
diagnostics <= max_diagnostics,
"Expected <={max_diagnostics} diagnostics but got {diagnostics}"
);
if diagnostics > max_diagnostics {
let details = result
.into_iter()
.map(|diagnostic| diagnostic.concise_message().to_string())
.collect::<Vec<_>>()
.join("\n ");
assert!(
diagnostics <= max_diagnostics,
"{project_name}: Expected <={max_diagnostics} diagnostics but got {diagnostics}:\n {details}",
);
}
}
setup_rayon();
@@ -542,7 +586,7 @@ fn bench_project(benchmark: &ProjectBenchmark, criterion: &mut Criterion) {
group.bench_function(benchmark.project.config.name, |b| {
b.iter_batched_ref(
|| benchmark.setup_iteration(),
|db| check_project(db, benchmark.max_diagnostics),
|db| check_project(db, benchmark.project.config.name, benchmark.max_diagnostics),
BatchSize::SmallInput,
);
});
@@ -610,7 +654,7 @@ fn datetype(criterion: &mut Criterion) {
max_dep_date: "2025-07-04",
python_version: PythonVersion::PY313,
},
0,
2,
);
bench_project(&benchmark, criterion);
@@ -621,6 +665,7 @@ criterion_group!(
micro,
benchmark_many_string_assignments,
benchmark_many_tuple_assignments,
benchmark_tuple_implicit_instance_attributes,
benchmark_complex_constrained_attributes_1,
benchmark_complex_constrained_attributes_2,
benchmark_many_enum_members,

View File

@@ -25,7 +25,6 @@ ty_static = { workspace = true }
anstyle = { workspace = true }
arc-swap = { workspace = true }
camino = { workspace = true }
countme = { workspace = true }
dashmap = { workspace = true }
dunce = { workspace = true }
filetime = { workspace = true }
@@ -34,6 +33,7 @@ glob = { workspace = true }
ignore = { workspace = true, optional = true }
matchit = { workspace = true }
path-slash = { workspace = true }
quick-junit = { workspace = true, optional = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
schemars = { workspace = true, optional = true }
@@ -56,7 +56,13 @@ tempfile = { workspace = true }
[features]
cache = ["ruff_cache"]
junit = ["dep:quick-junit"]
os = ["ignore", "dep:etcetera"]
serde = ["camino/serde1", "dep:serde", "dep:serde_json", "ruff_diagnostics/serde"]
serde = [
"camino/serde1",
"dep:serde",
"dep:serde_json",
"ruff_diagnostics/serde",
]
# Exposes testing utilities.
testing = ["tracing-subscriber"]

View File

@@ -1,12 +1,14 @@
use std::{fmt::Formatter, path::Path, sync::Arc};
use ruff_diagnostics::Fix;
use ruff_diagnostics::{Applicability, Fix};
use ruff_source_file::{LineColumn, SourceCode, SourceFile};
use ruff_annotate_snippets::Level as AnnotateLevel;
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use self::render::{DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input};
pub use self::render::{
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
};
use crate::{Db, files::File};
mod render;
@@ -19,7 +21,7 @@ mod stylesheet;
/// characteristics in the inputs given to the tool. Typically, but not always,
/// a characteristic is a deficiency. An example of a characteristic that is
/// _not_ a deficiency is the `reveal_type` diagnostic for our type checker.
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
pub struct Diagnostic {
/// The actual diagnostic.
///
@@ -122,7 +124,14 @@ impl Diagnostic {
/// directly. If callers want or need to avoid cloning the diagnostic
/// message, then they can also pass a `DiagnosticMessage` directly.
pub fn info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
self.sub(SubDiagnostic::new(Severity::Info, message));
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Info, message));
}
/// Adds a "help" sub-diagnostic with the given message.
///
/// See the closely related [`Diagnostic::info`] method for more details.
pub fn help<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Help, message));
}
/// Adds a "sub" diagnostic to this diagnostic.
@@ -377,9 +386,15 @@ impl Diagnostic {
self.primary_message()
}
/// Returns the fix suggestion for the violation.
pub fn suggestion(&self) -> Option<&str> {
self.primary_annotation()?.get_message()
/// Returns the message of the first sub-diagnostic with a `Help` severity.
///
/// Note that this is used as the fix title/suggestion for some of Ruff's output formats, but in
/// general this is not the guaranteed meaning of such a message.
pub fn first_help_text(&self) -> Option<&str> {
self.sub_diagnostics()
.iter()
.find(|sub| matches!(sub.inner.severity, SubDiagnosticSeverity::Help))
.map(|sub| sub.inner.message.as_str())
}
/// Returns the URL for the rule documentation, if it exists.
@@ -464,7 +479,7 @@ impl Diagnostic {
}
}
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
struct DiagnosticInner {
id: DiagnosticId,
severity: Severity,
@@ -540,7 +555,7 @@ impl Eq for RenderingSortKey<'_> {}
/// Currently, the order in which sub-diagnostics are rendered relative to one
/// another (for a single parent diagnostic) is the order in which they were
/// attached to the diagnostic.
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
pub struct SubDiagnostic {
/// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
/// pointer-sized.
@@ -565,7 +580,10 @@ impl SubDiagnostic {
/// Callers can pass anything that implements `std::fmt::Display`
/// directly. If callers want or need to avoid cloning the diagnostic
/// message, then they can also pass a `DiagnosticMessage` directly.
pub fn new<'a>(severity: Severity, message: impl IntoDiagnosticMessage + 'a) -> SubDiagnostic {
pub fn new<'a>(
severity: SubDiagnosticSeverity,
message: impl IntoDiagnosticMessage + 'a,
) -> SubDiagnostic {
let inner = Box::new(SubDiagnosticInner {
severity,
message: message.into_diagnostic_message(),
@@ -641,9 +659,9 @@ impl SubDiagnostic {
}
}
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
struct SubDiagnosticInner {
severity: Severity,
severity: SubDiagnosticSeverity,
message: DiagnosticMessage,
annotations: Vec<Annotation>,
}
@@ -669,7 +687,7 @@ struct SubDiagnosticInner {
///
/// Messages attached to annotations should also be as brief and specific as
/// possible. Long messages could negative impact the quality of rendering.
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
pub struct Annotation {
/// The span of this annotation, corresponding to some subsequence of the
/// user's input that we want to highlight.
@@ -789,7 +807,7 @@ impl Annotation {
///
/// These tags are used to provide additional information about the annotation.
/// and are passed through to the language server protocol.
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
pub enum DiagnosticTag {
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
Unnecessary,
@@ -998,7 +1016,7 @@ impl std::fmt::Display for DiagnosticId {
///
/// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and
/// emitting diagnostics from both ty and ruff.
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
pub enum UnifiedFile {
Ty(File),
Ruff(SourceFile),
@@ -1062,7 +1080,7 @@ impl DiagnosticSource {
/// It consists of a `File` and an optional range into that file. When the
/// range isn't present, it semantically implies that the diagnostic refers to
/// the entire file. For example, when the file should be executable but isn't.
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
pub struct Span {
file: UnifiedFile,
range: Option<TextRange>,
@@ -1140,7 +1158,7 @@ impl From<crate::files::FileRange> for Span {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
pub enum Severity {
Info,
Warning,
@@ -1170,6 +1188,32 @@ impl Severity {
}
}
/// Like [`Severity`] but exclusively for sub-diagnostics.
///
/// This type only exists to add an additional `Help` severity that isn't present in `Severity` or
/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
/// deleted and the two combined again.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
pub enum SubDiagnosticSeverity {
Help,
Info,
Warning,
Error,
Fatal,
}
impl SubDiagnosticSeverity {
fn to_annotate(self) -> AnnotateLevel {
match self {
SubDiagnosticSeverity::Help => AnnotateLevel::Help,
SubDiagnosticSeverity::Info => AnnotateLevel::Info,
SubDiagnosticSeverity::Warning => AnnotateLevel::Warning,
SubDiagnosticSeverity::Error => AnnotateLevel::Error,
SubDiagnosticSeverity::Fatal => AnnotateLevel::Error,
}
}
}
/// Configuration for rendering diagnostics.
#[derive(Clone, Debug)]
pub struct DisplayDiagnosticConfig {
@@ -1196,6 +1240,15 @@ pub struct DisplayDiagnosticConfig {
reason = "This is currently only used for JSON but will be needed soon for other formats"
)]
preview: bool,
/// Whether to hide the real `Severity` of diagnostics.
///
/// This is intended for temporary use by Ruff, which only has a single `error` severity at the
/// moment. We should be able to remove this option when Ruff gets more severities.
hide_severity: bool,
/// Whether to show the availability of a fix in a diagnostic.
show_fix_status: bool,
/// The lowest applicability that should be shown when reporting diagnostics.
fix_applicability: Applicability,
}
impl DisplayDiagnosticConfig {
@@ -1224,6 +1277,35 @@ impl DisplayDiagnosticConfig {
..self
}
}
/// Whether to hide a diagnostic's severity or not.
pub fn hide_severity(self, yes: bool) -> DisplayDiagnosticConfig {
DisplayDiagnosticConfig {
hide_severity: yes,
..self
}
}
/// Whether to show a fix's availability or not.
pub fn show_fix_status(self, yes: bool) -> DisplayDiagnosticConfig {
DisplayDiagnosticConfig {
show_fix_status: yes,
..self
}
}
/// Set the lowest fix applicability that should be shown.
///
/// In other words, an applicability of `Safe` (the default) would suppress showing fixes or fix
/// availability for unsafe or display-only fixes.
///
/// Note that this option is currently ignored when `hide_severity` is false.
pub fn fix_applicability(self, applicability: Applicability) -> DisplayDiagnosticConfig {
DisplayDiagnosticConfig {
fix_applicability: applicability,
..self
}
}
}
impl Default for DisplayDiagnosticConfig {
@@ -1233,6 +1315,9 @@ impl Default for DisplayDiagnosticConfig {
color: false,
context: 2,
preview: false,
hide_severity: false,
show_fix_status: false,
fix_applicability: Applicability::Safe,
}
}
}
@@ -1282,6 +1367,9 @@ pub enum DiagnosticFormat {
Rdjson,
/// Print diagnostics in the format emitted by Pylint.
Pylint,
/// Print diagnostics in the format expected by JUnit.
#[cfg(feature = "junit")]
Junit,
}
/// A representation of the kinds of messages inside a diagnostic.
@@ -1340,7 +1428,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
/// In most cases, callers shouldn't need to use this. Instead, there is
/// a blanket trait implementation for `IntoDiagnosticMessage` for
/// anything that implements `std::fmt::Display`.
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
pub struct DiagnosticMessage(Box<str>);
impl DiagnosticMessage {

View File

@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::path::Path;
@@ -7,9 +8,9 @@ use ruff_annotate_snippets::{
};
use ruff_notebook::{Notebook, NotebookIndex};
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::diagnostic::stylesheet::{DiagnosticStylesheet, fmt_styled};
use crate::diagnostic::stylesheet::DiagnosticStylesheet;
use crate::{
Db,
files::File,
@@ -18,18 +19,23 @@ use crate::{
};
use super::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig, Severity,
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig,
SubDiagnostic, UnifiedFile,
};
use azure::AzureRenderer;
use concise::ConciseRenderer;
use pylint::PylintRenderer;
mod azure;
mod concise;
mod full;
#[cfg(feature = "serde")]
mod json;
#[cfg(feature = "serde")]
mod json_lines;
#[cfg(feature = "junit")]
mod junit;
mod pylint;
#[cfg(feature = "serde")]
mod rdjson;
@@ -102,48 +108,7 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.config.format {
DiagnosticFormat::Concise => {
let stylesheet = if self.config.color {
DiagnosticStylesheet::styled()
} else {
DiagnosticStylesheet::plain()
};
for diag in self.diagnostics {
let (severity, severity_style) = match diag.severity() {
Severity::Info => ("info", stylesheet.info),
Severity::Warning => ("warning", stylesheet.warning),
Severity::Error => ("error", stylesheet.error),
Severity::Fatal => ("fatal", stylesheet.error),
};
write!(
f,
"{severity}[{id}]",
severity = fmt_styled(severity, severity_style),
id = fmt_styled(diag.id(), stylesheet.emphasis)
)?;
if let Some(span) = diag.primary_span() {
write!(
f,
" {path}",
path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis)
)?;
if let Some(range) = span.range() {
let diagnostic_source = span.file().diagnostic_source(self.resolver);
let start = diagnostic_source
.as_source_code()
.line_column(range.start());
write!(
f,
":{line}:{col}",
line = fmt_styled(start.line, stylesheet.emphasis),
col = fmt_styled(start.column, stylesheet.emphasis),
)?;
}
write!(f, ":")?;
}
writeln!(f, " {message}", message = diag.concise_message())?;
}
ConciseRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
}
DiagnosticFormat::Full => {
let stylesheet = if self.config.color {
@@ -156,7 +121,8 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
AnnotateRenderer::styled()
} else {
AnnotateRenderer::plain()
};
}
.cut_indicator("");
renderer = renderer
.error(stylesheet.error)
@@ -196,6 +162,10 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
DiagnosticFormat::Pylint => {
PylintRenderer::new(self.resolver).render(f, self.diagnostics)?;
}
#[cfg(feature = "junit")]
DiagnosticFormat::Junit => {
junit::JunitRenderer::new(self.resolver).render(f, self.diagnostics)?;
}
}
Ok(())
@@ -249,7 +219,7 @@ impl<'a> Resolved<'a> {
/// both.)
#[derive(Debug)]
struct ResolvedDiagnostic<'a> {
severity: Severity,
level: AnnotateLevel,
id: Option<String>,
message: String,
annotations: Vec<ResolvedAnnotation<'a>>,
@@ -274,7 +244,7 @@ impl<'a> ResolvedDiagnostic<'a> {
let id = Some(diag.inner.id.to_string());
let message = diag.inner.message.as_str().to_string();
ResolvedDiagnostic {
severity: diag.inner.severity,
level: diag.inner.severity.to_annotate(),
id,
message,
annotations,
@@ -297,7 +267,7 @@ impl<'a> ResolvedDiagnostic<'a> {
})
.collect();
ResolvedDiagnostic {
severity: diag.inner.severity,
level: diag.inner.severity.to_annotate(),
id: None,
message: diag.inner.message.as_str().to_string(),
annotations,
@@ -364,7 +334,7 @@ impl<'a> ResolvedDiagnostic<'a> {
snippets_by_input
.sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse());
RenderableDiagnostic {
severity: self.severity,
level: self.level,
id: self.id.as_deref(),
message: &self.message,
snippets_by_input,
@@ -452,7 +422,7 @@ struct Renderable<'r> {
#[derive(Debug)]
struct RenderableDiagnostic<'r> {
/// The severity of the diagnostic.
severity: Severity,
level: AnnotateLevel,
/// The ID of the diagnostic. The ID can usually be used on the CLI or in a
/// config file to change the severity of a lint.
///
@@ -471,7 +441,6 @@ struct RenderableDiagnostic<'r> {
impl RenderableDiagnostic<'_> {
/// Convert this to an "annotate" snippet.
fn to_annotate(&self) -> AnnotateMessage<'_> {
let level = self.severity.to_annotate();
let snippets = self.snippets_by_input.iter().flat_map(|snippets| {
let path = snippets.path;
snippets
@@ -479,7 +448,7 @@ impl RenderableDiagnostic<'_> {
.iter()
.map(|snippet| snippet.to_annotate(path))
});
let mut message = level.title(self.message);
let mut message = self.level.title(self.message);
if let Some(id) = self.id {
message = message.id(id);
}
@@ -552,7 +521,7 @@ impl<'r> RenderableSnippets<'r> {
#[derive(Debug)]
struct RenderableSnippet<'r> {
/// The actual snippet text.
snippet: &'r str,
snippet: Cow<'r, str>,
/// The absolute line number corresponding to where this
/// snippet begins.
line_start: OneIndexed,
@@ -612,6 +581,12 @@ impl<'r> RenderableSnippet<'r> {
.iter()
.map(|ann| RenderableAnnotation::new(snippet_start, ann))
.collect();
let EscapedSourceCode {
text: snippet,
annotations,
} = replace_unprintable(snippet, annotations).fix_up_empty_spans_after_line_terminator();
RenderableSnippet {
snippet,
line_start,
@@ -622,7 +597,7 @@ impl<'r> RenderableSnippet<'r> {
/// Convert this to an "annotate" snippet.
fn to_annotate<'a>(&'a self, path: &'a str) -> AnnotateSnippet<'a> {
AnnotateSnippet::source(self.snippet)
AnnotateSnippet::source(&self.snippet)
.origin(path)
.line_start(self.line_start.get())
.annotations(
@@ -852,12 +827,213 @@ fn relativize_path<'p>(cwd: &SystemPath, path: &'p str) -> &'p str {
path
}
/// Given some source code and annotation ranges, this routine replaces
/// unprintable characters with printable representations of them.
///
/// The source code and annotations returned are updated to reflect changes made
/// to the source code (if any).
///
/// We don't need to normalize whitespace, such as converting tabs to spaces,
/// because `annotate-snippets` handles that internally. Similarly, it's safe to
/// modify the annotation ranges by inserting 3-byte Unicode replacements
/// because `annotate-snippets` will account for their actual width when
/// rendering and displaying the column to the user.
fn replace_unprintable<'r>(
source: &'r str,
mut annotations: Vec<RenderableAnnotation<'r>>,
) -> EscapedSourceCode<'r> {
// Updates the annotation ranges given by the caller whenever a single byte (at `index` in
// `source`) is replaced with `len` bytes.
//
// When the index occurs before the start of the range, the range is
// offset by `len`. When the range occurs after or at the start but before
// the end, then the end of the range only is offset by `len`.
let mut update_ranges = |index: usize, len: u32| {
for ann in &mut annotations {
if index < usize::from(ann.range.start()) {
ann.range += TextSize::new(len - 1);
} else if index < usize::from(ann.range.end()) {
ann.range = ann.range.add_end(TextSize::new(len - 1));
}
}
};
// If `c` is an unprintable character, then this returns a printable
// representation of it (using a fancier Unicode codepoint).
let unprintable_replacement = |c: char| -> Option<char> {
match c {
'\x07' => Some('␇'),
'\x08' => Some('␈'),
'\x1b' => Some('␛'),
'\x7f' => Some('␡'),
_ => None,
}
};
let mut last_end = 0;
let mut result = String::new();
for (index, c) in source.char_indices() {
if let Some(printable) = unprintable_replacement(c) {
result.push_str(&source[last_end..index]);
let len = printable.text_len().to_u32();
update_ranges(result.text_len().to_usize(), len);
result.push(printable);
last_end = index + 1;
}
}
// No tabs or unprintable chars
if result.is_empty() {
EscapedSourceCode {
annotations,
text: Cow::Borrowed(source),
}
} else {
result.push_str(&source[last_end..]);
EscapedSourceCode {
annotations,
text: Cow::Owned(result),
}
}
}
struct EscapedSourceCode<'r> {
text: Cow<'r, str>,
annotations: Vec<RenderableAnnotation<'r>>,
}
impl<'r> EscapedSourceCode<'r> {
// This attempts to "fix up" the spans on each annotation in the case where
// it's an empty span immediately following a line terminator.
//
// At present, `annotate-snippets` (both upstream and our vendored copy)
// will render annotations of such spans to point to the space immediately
// following the previous line. But ideally, this should point to the space
// immediately preceding the next line.
//
// After attempting to fix `annotate-snippets` and giving up after a couple
// hours, this routine takes a different tact: it adjusts the span to be
// non-empty and it will cover the first codepoint of the following line.
// This forces `annotate-snippets` to point to the right place.
//
// See also: <https://github.com/astral-sh/ruff/issues/15509> and
// `ruff_linter::message::text::SourceCode::fix_up_empty_spans_after_line_terminator`,
// from which this was adapted.
fn fix_up_empty_spans_after_line_terminator(mut self) -> EscapedSourceCode<'r> {
for ann in &mut self.annotations {
let range = ann.range;
if !range.is_empty()
|| range.start() == TextSize::from(0)
|| range.start() >= self.text.text_len()
{
continue;
}
if !matches!(
self.text.as_bytes()[range.start().to_usize() - 1],
b'\n' | b'\r'
) {
continue;
}
let start = range.start();
let end = ceil_char_boundary(&self.text, start + TextSize::from(1));
ann.range = TextRange::new(start, end);
}
self
}
}
/// Finds the closest [`TextSize`] not less than the offset given for which
/// `is_char_boundary` is `true`. Unless the offset given is greater than
/// the length of the underlying contents, in which case, the length of the
/// contents is returned.
///
/// Can be replaced with `str::ceil_char_boundary` once it's stable.
///
/// # Examples
///
/// From `std`:
///
/// ```
/// use ruff_db::diagnostic::ceil_char_boundary;
/// use ruff_text_size::{Ranged, TextLen, TextSize};
///
/// let source = "❤️🧡💛💚💙💜";
/// assert_eq!(source.text_len(), TextSize::from(26));
/// assert!(!source.is_char_boundary(13));
///
/// let closest = ceil_char_boundary(source, TextSize::from(13));
/// assert_eq!(closest, TextSize::from(14));
/// assert_eq!(&source[..closest.to_usize()], "❤️🧡💛");
/// ```
///
/// Additional examples:
///
/// ```
/// use ruff_db::diagnostic::ceil_char_boundary;
/// use ruff_text_size::{Ranged, TextRange, TextSize};
///
/// let source = "Hello";
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(5)),
/// TextSize::from(5)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(6)),
/// TextSize::from(5)
/// );
///
/// let source = "α";
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(1)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(2)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(3)),
/// TextSize::from(2)
/// );
/// ```
pub fn ceil_char_boundary(text: &str, offset: TextSize) -> TextSize {
let upper_bound = offset
.to_u32()
.saturating_add(4)
.min(text.text_len().to_u32());
(offset.to_u32()..upper_bound)
.map(TextSize::from)
.find(|offset| text.is_char_boundary(offset.to_usize()))
.unwrap_or_else(|| TextSize::from(upper_bound))
}
#[cfg(test)]
mod tests {
use ruff_diagnostics::{Edit, Fix};
use ruff_diagnostics::{Applicability, Edit, Fix};
use crate::diagnostic::{Annotation, DiagnosticId, SecondaryCode, Severity, Span};
use crate::diagnostic::{
Annotation, DiagnosticId, IntoDiagnosticMessage, SecondaryCode, Severity, Span,
SubDiagnosticSeverity,
};
use crate::files::system_path_to_file;
use crate::system::{DbWithWritableSystem, SystemPath};
use crate::tests::TestDb;
@@ -1541,7 +1717,7 @@ watermelon
let mut diag = env.err().primary("animals", "3", "3", "").build();
diag.sub(
env.sub_builder(Severity::Info, "this is a helpful note")
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
.build(),
);
insta::assert_snapshot!(
@@ -1570,15 +1746,15 @@ watermelon
let mut diag = env.err().primary("animals", "3", "3", "").build();
diag.sub(
env.sub_builder(Severity::Info, "this is a helpful note")
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
.build(),
);
diag.sub(
env.sub_builder(Severity::Info, "another helpful note")
env.sub_builder(SubDiagnosticSeverity::Info, "another helpful note")
.build(),
);
diag.sub(
env.sub_builder(Severity::Info, "and another helpful note")
env.sub_builder(SubDiagnosticSeverity::Info, "and another helpful note")
.build(),
);
insta::assert_snapshot!(
@@ -2300,6 +2476,27 @@ watermelon
self.config = config;
}
/// Hide diagnostic severity when rendering.
pub(super) fn hide_severity(&mut self, yes: bool) {
let mut config = std::mem::take(&mut self.config);
config = config.hide_severity(yes);
self.config = config;
}
/// Show fix availability when rendering.
pub(super) fn show_fix_status(&mut self, yes: bool) {
let mut config = std::mem::take(&mut self.config);
config = config.show_fix_status(yes);
self.config = config;
}
/// The lowest fix applicability to show when rendering.
pub(super) fn fix_applicability(&mut self, applicability: Applicability) {
let mut config = std::mem::take(&mut self.config);
config = config.fix_applicability(applicability);
self.config = config;
}
/// Add a file with the given path and contents to this environment.
pub(super) fn add(&mut self, path: &str, contents: &str) {
let path = SystemPath::new(path);
@@ -2363,11 +2560,11 @@ watermelon
/// sub-diagnostic with "error" severity and canned values for
/// its identifier and message.
fn sub_warn(&mut self) -> SubDiagnosticBuilder<'_> {
self.sub_builder(Severity::Warning, "sub-diagnostic message")
self.sub_builder(SubDiagnosticSeverity::Warning, "sub-diagnostic message")
}
/// Returns a builder for tersely constructing diagnostics.
fn builder(
pub(super) fn builder(
&mut self,
identifier: &'static str,
severity: Severity,
@@ -2384,7 +2581,11 @@ watermelon
}
/// Returns a builder for tersely constructing sub-diagnostics.
fn sub_builder(&mut self, severity: Severity, message: &str) -> SubDiagnosticBuilder<'_> {
fn sub_builder(
&mut self,
severity: SubDiagnosticSeverity,
message: &str,
) -> SubDiagnosticBuilder<'_> {
let subdiag = SubDiagnostic::new(severity, message);
SubDiagnosticBuilder { env: self, subdiag }
}
@@ -2430,7 +2631,7 @@ watermelon
///
/// See the docs on `TestEnvironment::span` for the meaning of
/// `path`, `line_offset_start` and `line_offset_end`.
fn primary(
pub(super) fn primary(
mut self,
path: &str,
line_offset_start: &str,
@@ -2487,6 +2688,12 @@ watermelon
self.diag.set_noqa_offset(noqa_offset);
self
}
/// Adds a "help" sub-diagnostic with the given message.
fn help(mut self, message: impl IntoDiagnosticMessage) -> DiagnosticBuilder<'e> {
self.diag.help(message);
self
}
}
/// A helper builder for tersely populating a `SubDiagnostic`.
@@ -2593,7 +2800,8 @@ def fibonacci(n):
let diagnostics = vec![
env.builder("unused-import", Severity::Error, "`os` imported but unused")
.primary("fib.py", "1:7", "1:9", "Remove unused import: `os`")
.primary("fib.py", "1:7", "1:9", "")
.help("Remove unused import: `os`")
.secondary_code("F401")
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(0),
@@ -2606,12 +2814,8 @@ def fibonacci(n):
Severity::Error,
"Local variable `x` is assigned to but never used",
)
.primary(
"fib.py",
"6:4",
"6:5",
"Remove assignment to unused variable `x`",
)
.primary("fib.py", "6:4", "6:5", "")
.help("Remove assignment to unused variable `x`")
.secondary_code("F841")
.fix(Fix::unsafe_edit(Edit::deletion(
TextSize::from(94),
@@ -2658,6 +2862,25 @@ if call(foo
}
/// Create Ruff-style diagnostics for testing the various output formats for a notebook.
///
/// The concatenated cells look like this:
///
/// ```python
/// # cell 1
/// import os
/// # cell 2
/// import math
///
/// print('hello world')
/// # cell 3
/// def foo():
/// print()
/// x = 1
/// ```
///
/// The first diagnostic is on the unused `os` import with location cell 1, row 2, column 8
/// (`cell 1:2:8`). The second diagnostic is the unused `math` import at `cell 2:2:8`, and the
/// third diagnostic is an unfixable unused variable at `cell 3:4:5`.
#[allow(
dead_code,
reason = "This is currently only used for JSON but will be needed soon for other formats"
@@ -2713,7 +2936,8 @@ if call(foo
let diagnostics = vec![
env.builder("unused-import", Severity::Error, "`os` imported but unused")
.primary("notebook.ipynb", "2:7", "2:9", "Remove unused import: `os`")
.primary("notebook.ipynb", "2:7", "2:9", "")
.help("Remove unused import: `os`")
.secondary_code("F401")
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(9),
@@ -2726,12 +2950,8 @@ if call(foo
Severity::Error,
"`math` imported but unused",
)
.primary(
"notebook.ipynb",
"4:7",
"4:11",
"Remove unused import: `math`",
)
.primary("notebook.ipynb", "4:7", "4:11", "")
.help("Remove unused import: `math`")
.secondary_code("F401")
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(28),
@@ -2744,12 +2964,8 @@ if call(foo
Severity::Error,
"Local variable `x` is assigned to but never used",
)
.primary(
"notebook.ipynb",
"10:4",
"10:5",
"Remove assignment to unused variable `x`",
)
.primary("notebook.ipynb", "10:4", "10:5", "")
.help("Remove assignment to unused variable `x`")
.secondary_code("F841")
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(94),

View File

@@ -0,0 +1,195 @@
use crate::diagnostic::{
Diagnostic, DisplayDiagnosticConfig, Severity,
stylesheet::{DiagnosticStylesheet, fmt_styled},
};
use super::FileResolver;
pub(super) struct ConciseRenderer<'a> {
resolver: &'a dyn FileResolver,
config: &'a DisplayDiagnosticConfig,
}
impl<'a> ConciseRenderer<'a> {
pub(super) fn new(resolver: &'a dyn FileResolver, config: &'a DisplayDiagnosticConfig) -> Self {
Self { resolver, config }
}
pub(super) fn render(
&self,
f: &mut std::fmt::Formatter,
diagnostics: &[Diagnostic],
) -> std::fmt::Result {
let stylesheet = if self.config.color {
DiagnosticStylesheet::styled()
} else {
DiagnosticStylesheet::plain()
};
let sep = fmt_styled(":", stylesheet.separator);
for diag in diagnostics {
if let Some(span) = diag.primary_span() {
write!(
f,
"{path}",
path = fmt_styled(
span.file().relative_path(self.resolver).to_string_lossy(),
stylesheet.emphasis
)
)?;
if let Some(range) = span.range() {
let diagnostic_source = span.file().diagnostic_source(self.resolver);
let start = diagnostic_source
.as_source_code()
.line_column(range.start());
if let Some(notebook_index) = self.resolver.notebook_index(span.file()) {
write!(
f,
"{sep}cell {cell}{sep}{line}{sep}{col}",
cell = notebook_index.cell(start.line).unwrap_or_default(),
line = notebook_index.cell_row(start.line).unwrap_or_default(),
col = start.column,
)?;
} else {
write!(
f,
"{sep}{line}{sep}{col}",
line = start.line,
col = start.column,
)?;
}
}
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)
)?;
}
if self.config.show_fix_status {
if let Some(fix) = diag.fix() {
// Do not display an indicator for inapplicable fixes
if fix.applies(self.config.fix_applicability) {
write!(f, "[{fix}] ", fix = fmt_styled("*", stylesheet.separator))?;
}
}
}
} else {
let (severity, severity_style) = match diag.severity() {
Severity::Info => ("info", stylesheet.info),
Severity::Warning => ("warning", stylesheet.warning),
Severity::Error => ("error", stylesheet.error),
Severity::Fatal => ("fatal", stylesheet.error),
};
write!(
f,
"{severity}[{id}] ",
severity = fmt_styled(severity, severity_style),
id = fmt_styled(diag.id(), stylesheet.emphasis)
)?;
}
writeln!(f, "{message}", message = diag.concise_message())?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use ruff_diagnostics::Applicability;
use crate::diagnostic::{
DiagnosticFormat,
render::tests::{
TestEnvironment, create_diagnostics, create_notebook_diagnostics,
create_syntax_error_diagnostics,
},
};
#[test]
fn output() {
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
fib.py:1:8: error[unused-import] `os` imported but unused
fib.py:6:5: error[unused-variable] Local variable `x` is assigned to but never used
undef.py:1:4: error[undefined-name] Undefined name `a`
");
}
#[test]
fn show_fixes() {
let (mut env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
env.hide_severity(true);
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
fib.py:1:8: F401 [*] `os` imported but unused
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
undef.py:1:4: F821 Undefined name `a`
");
}
#[test]
fn show_fixes_preview() {
let (mut env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
env.hide_severity(true);
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
env.preview(true);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
fib.py:1:8: F401 [*] `os` imported but unused
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
undef.py:1:4: F821 Undefined name `a`
");
}
#[test]
fn show_fixes_syntax_errors() {
let (mut env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Concise);
env.hide_severity(true);
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
syntax_errors.py:3:12: SyntaxError: Expected ')', found newline
");
}
#[test]
fn syntax_errors() {
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Concise);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
syntax_errors.py:1:15: error[invalid-syntax] SyntaxError: Expected one or more symbol names after import
syntax_errors.py:3:12: error[invalid-syntax] SyntaxError: Expected ')', found newline
");
}
#[test]
fn notebook_output() {
let (env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Concise);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
notebook.ipynb:cell 1:2:8: error[unused-import] `os` imported but unused
notebook.ipynb:cell 2:2:8: error[unused-import] `math` imported but unused
notebook.ipynb:cell 3:4:5: error[unused-variable] Local variable `x` is assigned to but never used
");
}
#[test]
fn missing_file() {
let mut env = TestEnvironment::new();
env.format(DiagnosticFormat::Concise);
let diag = env.err().build();
insta::assert_snapshot!(
env.render(&diag),
@"error[test-diagnostic] main diagnostic message",
);
}
}

View File

@@ -0,0 +1,201 @@
#[cfg(test)]
mod tests {
use crate::diagnostic::{
DiagnosticFormat, Severity,
render::tests::{TestEnvironment, create_diagnostics, create_syntax_error_diagnostics},
};
#[test]
fn output() {
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Full);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r#"
error[unused-import]: `os` imported but unused
--> fib.py:1:8
|
1 | import os
| ^^
|
help: Remove unused import: `os`
error[unused-variable]: Local variable `x` is assigned to but never used
--> fib.py:6:5
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^
7 | if n == 0:
8 | return 0
|
help: Remove assignment to unused variable `x`
error[undefined-name]: Undefined name `a`
--> undef.py:1:4
|
1 | if a == 1: pass
| ^
|
"#);
}
#[test]
fn syntax_errors() {
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Full);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[invalid-syntax]: SyntaxError: Expected one or more symbol names after import
--> syntax_errors.py:1:15
|
1 | from os import
| ^
2 |
3 | if call(foo
|
error[invalid-syntax]: SyntaxError: Expected ')', found newline
--> syntax_errors.py:3:12
|
1 | from os import
2 |
3 | if call(foo
| ^
4 | def bar():
5 | pass
|
");
}
/// Check that the new `full` rendering code in `ruff_db` handles cases fixed by commit c9b99e4.
///
/// For example, without the fix, we get diagnostics like this:
///
/// ```
/// error[no-indented-block]: Expected an indented block
/// --> example.py:3:1
/// |
/// 2 | if False:
/// | ^
/// 3 | print()
/// |
/// ```
///
/// where the caret points to the end of the previous line instead of the start of the next.
#[test]
fn empty_span_after_line_terminator() {
let mut env = TestEnvironment::new();
env.add(
"example.py",
r#"
if False:
print()
"#,
);
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"no-indented-block",
Severity::Error,
"Expected an indented block",
)
.primary("example.py", "3:0", "3:0", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[no-indented-block]: Expected an indented block
--> example.py:3:1
|
2 | if False:
3 | print()
| ^
|
");
}
/// Check that the new `full` rendering code in `ruff_db` handles cases fixed by commit 2922490.
///
/// For example, without the fix, we get diagnostics like this:
///
/// ```
/// error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
/// --> example.py:1:25
/// |
/// 1 | nested_fstrings = f'␈{f'{f'␛'}'}'
/// | ^
/// |
/// ```
///
/// where the caret points to the `f` in the f-string instead of the start of the invalid
/// character (`^Z`).
#[test]
fn unprintable_characters() {
let mut env = TestEnvironment::new();
env.add("example.py", "nested_fstrings = f'{f'{f''}'}'");
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"invalid-character-sub",
Severity::Error,
r#"Invalid unescaped character SUB, use "\x1A" instead"#,
)
.primary("example.py", "1:24", "1:24", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r#"
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
--> example.py:1:25
|
1 | nested_fstrings = f'␈{f'{f'␛'}'}'
| ^
|
"#);
}
#[test]
fn multiple_unprintable_characters() -> std::io::Result<()> {
let mut env = TestEnvironment::new();
env.add("example.py", "");
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"invalid-character-sub",
Severity::Error,
r#"Invalid unescaped character SUB, use "\x1A" instead"#,
)
.primary("example.py", "1:1", "1:1", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r#"
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
--> example.py:1:2
|
1 | ␈
| ^
|
"#);
Ok(())
}
/// Ensure that the header column matches the column in the user's input, even if we've replaced
/// tabs with spaces for rendering purposes.
#[test]
fn tab_replacement() {
let mut env = TestEnvironment::new();
env.add("example.py", "def foo():\n\treturn 1");
env.format(DiagnosticFormat::Full);
let diagnostic = env.err().primary("example.py", "2:1", "2:9", "").build();
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
--> example.py:2:2
|
1 | def foo():
2 | return 1
| ^^^^^^^^
|
");
}
}

View File

@@ -87,7 +87,7 @@ pub(super) fn diagnostic_to_json<'a>(
let fix = diagnostic.fix().map(|fix| JsonFix {
applicability: fix.applicability(),
message: diagnostic.suggestion(),
message: diagnostic.first_help_text(),
edits: ExpandedEdits {
edits: fix.edits(),
notebook_index,

View File

@@ -0,0 +1,195 @@
use std::{collections::BTreeMap, ops::Deref, path::Path};
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite, XmlString};
use ruff_source_file::LineColumn;
use crate::diagnostic::{Diagnostic, SecondaryCode, render::FileResolver};
/// A renderer for diagnostics in the [JUnit] format.
///
/// See [`junit.xsd`] for the specification in the JUnit repository and an annotated [version]
/// linked from the [`quick_junit`] docs.
///
/// [JUnit]: https://junit.org/
/// [`junit.xsd`]: https://github.com/junit-team/junit-framework/blob/2870b7d8fd5bf7c1efe489d3991d3ed3900e82bb/platform-tests/src/test/resources/jenkins-junit.xsd
/// [version]: https://llg.cubic.org/docs/junit/
/// [`quick_junit`]: https://docs.rs/quick-junit/latest/quick_junit/
pub struct JunitRenderer<'a> {
resolver: &'a dyn FileResolver,
}
impl<'a> JunitRenderer<'a> {
pub fn new(resolver: &'a dyn FileResolver) -> Self {
Self { resolver }
}
pub(super) fn render(
&self,
f: &mut std::fmt::Formatter,
diagnostics: &[Diagnostic],
) -> std::fmt::Result {
let mut report = Report::new("ruff");
if diagnostics.is_empty() {
let mut test_suite = TestSuite::new("ruff");
test_suite
.extra
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
let mut case = TestCase::new("No errors found", TestCaseStatus::success());
case.set_classname("ruff");
test_suite.add_test_case(case);
report.add_test_suite(test_suite);
} else {
for (filename, diagnostics) in group_diagnostics_by_filename(diagnostics, self.resolver)
{
let mut test_suite = TestSuite::new(filename);
test_suite
.extra
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
let classname = Path::new(filename).with_extension("");
for diagnostic in diagnostics {
let DiagnosticWithLocation {
diagnostic,
start_location: location,
} = diagnostic;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(diagnostic.body());
if let Some(location) = location {
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.line,
col = location.column,
body = diagnostic.body()
));
} else {
status.set_description(diagnostic.body());
}
let code = diagnostic
.secondary_code()
.map_or_else(|| diagnostic.name(), SecondaryCode::as_str);
let mut case = TestCase::new(format!("org.ruff.{code}"), status);
case.set_classname(classname.to_str().unwrap());
if let Some(location) = location {
case.extra.insert(
XmlString::new("line"),
XmlString::new(location.line.to_string()),
);
case.extra.insert(
XmlString::new("column"),
XmlString::new(location.column.to_string()),
);
}
test_suite.add_test_case(case);
}
report.add_test_suite(test_suite);
}
}
let adapter = FmtAdapter { fmt: f };
report.serialize(adapter).map_err(|_| std::fmt::Error)
}
}
// TODO(brent) this and `group_diagnostics_by_filename` are also used by the `grouped` output
// format. I think they'd make more sense in that file, but I started here first. I'll move them to
// that module when adding the `grouped` output format.
struct DiagnosticWithLocation<'a> {
diagnostic: &'a Diagnostic,
start_location: Option<LineColumn>,
}
impl Deref for DiagnosticWithLocation<'_> {
type Target = Diagnostic;
fn deref(&self) -> &Self::Target {
self.diagnostic
}
}
fn group_diagnostics_by_filename<'a>(
diagnostics: &'a [Diagnostic],
resolver: &'a dyn FileResolver,
) -> BTreeMap<&'a str, Vec<DiagnosticWithLocation<'a>>> {
let mut grouped_diagnostics = BTreeMap::default();
for diagnostic in diagnostics {
let (filename, start_location) = diagnostic
.primary_span_ref()
.map(|span| {
let file = span.file();
let start_location =
span.range()
.filter(|_| !resolver.is_notebook(file))
.map(|range| {
file.diagnostic_source(resolver)
.as_source_code()
.line_column(range.start())
});
(span.file().path(resolver), start_location)
})
.unwrap_or_default();
grouped_diagnostics
.entry(filename)
.or_insert_with(Vec::new)
.push(DiagnosticWithLocation {
diagnostic,
start_location,
});
}
grouped_diagnostics
}
struct FmtAdapter<'a> {
fmt: &'a mut dyn std::fmt::Write,
}
impl std::io::Write for FmtAdapter<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.fmt
.write_str(std::str::from_utf8(buf).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid UTF-8 in JUnit report",
)
})?)
.map_err(std::io::Error::other)?;
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> {
self.fmt.write_fmt(args).map_err(std::io::Error::other)
}
}
#[cfg(test)]
mod tests {
use crate::diagnostic::{
DiagnosticFormat,
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
};
#[test]
fn output() {
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Junit);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
}
#[test]
fn syntax_errors() {
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Junit);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
}
}

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_linter/src/message/junit.rs
expression: content
snapshot_kind: text
source: crates/ruff_db/src/diagnostic/render/junit.rs
expression: env.render_diagnostics(&diagnostics)
---
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="ruff" tests="3" failures="3" errors="0">

View File

@@ -1,15 +1,14 @@
---
source: crates/ruff_linter/src/message/junit.rs
expression: content
snapshot_kind: text
source: crates/ruff_db/src/diagnostic/render/junit.rs
expression: env.render_diagnostics(&diagnostics)
---
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="ruff" tests="2" failures="2" errors="0">
<testsuite name="syntax_errors.py" tests="2" disabled="0" errors="0" failures="2" package="org.ruff">
<testcase name="org.ruff" classname="syntax_errors" line="1" column="15">
<testcase name="org.ruff.invalid-syntax" classname="syntax_errors" line="1" column="15">
<failure message="SyntaxError: Expected one or more symbol names after import">line 1, col 15, SyntaxError: Expected one or more symbol names after import</failure>
</testcase>
<testcase name="org.ruff" classname="syntax_errors" line="3" column="12">
<testcase name="org.ruff.invalid-syntax" classname="syntax_errors" line="3" column="12">
<failure message="SyntaxError: Expected &apos;)&apos;, found newline">line 3, col 12, SyntaxError: Expected &apos;)&apos;, found newline</failure>
</testcase>
</testsuite>

View File

@@ -41,6 +41,8 @@ pub struct DiagnosticStylesheet {
pub(crate) line_no: Style,
pub(crate) emphasis: Style,
pub(crate) none: Style,
pub(crate) separator: Style,
pub(crate) secondary_code: Style,
}
impl Default for DiagnosticStylesheet {
@@ -62,6 +64,8 @@ impl DiagnosticStylesheet {
line_no: bright_blue.effects(Effects::BOLD),
emphasis: Style::new().effects(Effects::BOLD),
none: Style::new(),
separator: AnsiColor::Cyan.on_default(),
secondary_code: AnsiColor::Red.on_default().effects(Effects::BOLD),
}
}
@@ -75,6 +79,8 @@ impl DiagnosticStylesheet {
line_no: Style::new(),
emphasis: Style::new(),
none: Style::new(),
separator: Style::new(),
secondary_code: Style::new(),
}
}
}

View File

@@ -1,7 +1,6 @@
use std::fmt;
use std::sync::Arc;
use countme::Count;
use dashmap::mapref::entry::Entry;
pub use file_root::{FileRoot, FileRootKind};
pub use path::FilePath;
@@ -232,7 +231,7 @@ impl Files {
let roots = inner.roots.read().unwrap();
for root in roots.all() {
if root.path(db).starts_with(&path) {
if path.starts_with(root.path(db)) {
root.set_revision(db).to(FileRevision::now());
}
}
@@ -312,11 +311,6 @@ pub struct File {
/// the file has been deleted is to change the status to `Deleted`.
#[default]
status: FileStatus,
/// Counter that counts the number of created file instances and active file instances.
/// Only enabled in debug builds.
#[default]
count: Count<File>,
}
// The Salsa heap is tracked separately.
@@ -375,12 +369,25 @@ impl File {
}
/// Refreshes the file metadata by querying the file system if needed.
///
/// This also "touches" the file root associated with the given path.
/// This means that any Salsa queries that depend on the corresponding
/// root's revision will become invalidated.
pub fn sync_path(db: &mut dyn Db, path: &SystemPath) {
let absolute = SystemPath::absolute(path, db.system().current_directory());
Files::touch_root(db, &absolute);
Self::sync_system_path(db, &absolute, None);
}
/// Refreshes *only* the file metadata by querying the file system if needed.
///
/// This specifically does not touch any file root associated with the
/// given file path.
pub fn sync_path_only(db: &mut dyn Db, path: &SystemPath) {
let absolute = SystemPath::absolute(path, db.system().current_directory());
Self::sync_system_path(db, &absolute, None);
}
/// Increments the revision for the virtual file at `path`.
pub fn sync_virtual_path(db: &mut dyn Db, path: &SystemVirtualPath) {
if let Some(virtual_file) = db.files().try_virtual_file(path) {
@@ -486,7 +493,7 @@ impl fmt::Debug for File {
///
/// This is a wrapper around a [`File`] that provides additional methods to interact with a virtual
/// file.
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
pub struct VirtualFile(File);
impl VirtualFile {

View File

@@ -23,7 +23,7 @@ pub struct FileRoot {
pub path: SystemPathBuf,
/// The kind of the root at the time of its creation.
kind_at_time_of_creation: FileRootKind,
pub kind_at_time_of_creation: FileRootKind,
/// A revision that changes when the contents of the source root change.
///

View File

@@ -21,7 +21,7 @@ use crate::source::source_text;
/// reflected in the changed AST offsets.
/// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires
/// for determining if a query result is unchanged.
#[salsa::tracked(returns(ref), no_eq, heap_size=get_size2::GetSize::get_heap_size)]
#[salsa::tracked(returns(ref), no_eq, heap_size=get_size2::heap_size)]
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
let _span = tracing::trace_span!("parsed_module", ?file).entered();

View File

@@ -1,8 +1,6 @@
use std::ops::Deref;
use std::sync::Arc;
use countme::Count;
use ruff_notebook::Notebook;
use ruff_python_ast::PySourceType;
use ruff_source_file::LineIndex;
@@ -11,7 +9,7 @@ use crate::Db;
use crate::files::{File, FilePath};
/// Reads the source text of a python text file (must be valid UTF8) or notebook.
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
#[salsa::tracked(heap_size=get_size2::heap_size)]
pub fn source_text(db: &dyn Db, file: File) -> SourceText {
let path = file.path(db);
let _span = tracing::trace_span!("source_text", file = %path).entered();
@@ -38,11 +36,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
};
SourceText {
inner: Arc::new(SourceTextInner {
kind,
read_error,
count: Count::new(),
}),
inner: Arc::new(SourceTextInner { kind, read_error }),
}
}
@@ -75,21 +69,21 @@ impl SourceText {
pub fn as_str(&self) -> &str {
match &self.inner.kind {
SourceTextKind::Text(source) => source,
SourceTextKind::Notebook(notebook) => notebook.source_code(),
SourceTextKind::Notebook { notebook } => notebook.source_code(),
}
}
/// Returns the underlying notebook if this is a notebook file.
pub fn as_notebook(&self) -> Option<&Notebook> {
match &self.inner.kind {
SourceTextKind::Notebook(notebook) => Some(notebook),
SourceTextKind::Notebook { notebook } => Some(notebook),
SourceTextKind::Text(_) => None,
}
}
/// Returns `true` if this is a notebook source file.
pub fn is_notebook(&self) -> bool {
matches!(&self.inner.kind, SourceTextKind::Notebook(_))
matches!(&self.inner.kind, SourceTextKind::Notebook { .. })
}
/// Returns `true` if there was an error when reading the content of the file.
@@ -114,7 +108,7 @@ impl std::fmt::Debug for SourceText {
SourceTextKind::Text(text) => {
dbg.field(text);
}
SourceTextKind::Notebook(notebook) => {
SourceTextKind::Notebook { notebook } => {
dbg.field(notebook);
}
}
@@ -125,29 +119,19 @@ impl std::fmt::Debug for SourceText {
#[derive(Eq, PartialEq, get_size2::GetSize)]
struct SourceTextInner {
#[get_size(ignore)]
count: Count<SourceText>,
kind: SourceTextKind,
read_error: Option<SourceTextError>,
}
#[derive(Eq, PartialEq)]
#[derive(Eq, PartialEq, get_size2::GetSize)]
enum SourceTextKind {
Text(String),
Notebook(Box<Notebook>),
}
impl get_size2::GetSize for SourceTextKind {
fn get_heap_size(&self) -> usize {
match self {
SourceTextKind::Text(text) => text.get_heap_size(),
// TODO: The `get-size` derive does not support ignoring enum variants.
//
// Jupyter notebooks are not very relevant for memory profiling, and contain
// arbitrary JSON values that do not implement the `GetSize` trait.
SourceTextKind::Notebook(_) => 0,
}
}
Notebook {
// Jupyter notebooks are not very relevant for memory profiling, and contain
// arbitrary JSON values that do not implement the `GetSize` trait.
#[get_size(ignore)]
notebook: Box<Notebook>,
},
}
impl From<String> for SourceTextKind {
@@ -158,7 +142,9 @@ impl From<String> for SourceTextKind {
impl From<Notebook> for SourceTextKind {
fn from(notebook: Notebook) -> Self {
SourceTextKind::Notebook(Box::new(notebook))
SourceTextKind::Notebook {
notebook: Box::new(notebook),
}
}
}
@@ -171,7 +157,7 @@ pub enum SourceTextError {
}
/// Computes the [`LineIndex`] for `file`.
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
#[salsa::tracked(heap_size=get_size2::heap_size)]
pub fn line_index(db: &dyn Db, file: File) -> LineIndex {
let _span = tracing::trace_span!("line_index", ?file).entered();

View File

@@ -43,7 +43,7 @@ pub enum IsolationLevel {
}
/// A collection of [`Edit`] elements to be applied to a source file.
#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)]
#[derive(Debug, PartialEq, Eq, Clone, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Fix {
/// The [`Edit`] elements to be applied, sorted by [`Edit::start`] in ascending order.

View File

@@ -20,6 +20,7 @@ ty_python_semantic = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true, optional = true }
memchr = { workspace = true }
salsa = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }

View File

@@ -1,3 +1,4 @@
use crate::StringImports;
use ruff_python_ast::visitor::source_order::{
SourceOrderVisitor, walk_expr, walk_module, walk_stmt,
};
@@ -10,13 +11,13 @@ pub(crate) struct Collector<'a> {
/// The path to the current module.
module_path: Option<&'a [String]>,
/// Whether to detect imports from string literals.
string_imports: bool,
string_imports: StringImports,
/// The collected imports from the Python AST.
imports: Vec<CollectedImport>,
}
impl<'a> Collector<'a> {
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: bool) -> Self {
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: StringImports) -> Self {
Self {
module_path,
string_imports,
@@ -118,7 +119,7 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
| Stmt::Continue(_)
| Stmt::IpyEscapeCommand(_) => {
// Only traverse simple statements when string imports is enabled.
if self.string_imports {
if self.string_imports.enabled {
walk_stmt(self, stmt);
}
}
@@ -126,20 +127,26 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
}
fn visit_expr(&mut self, expr: &'ast Expr) {
if self.string_imports {
if self.string_imports.enabled {
if let Expr::StringLiteral(ast::ExprStringLiteral {
value,
range: _,
node_index: _,
}) = expr
{
// Determine whether the string literal "looks like" an import statement: contains
// a dot, and consists solely of valid Python identifiers.
let value = value.to_str();
if let Some(module_name) = ModuleName::new(value) {
self.imports.push(CollectedImport::Import(module_name));
// Determine whether the string literal "looks like" an import statement: contains
// the requisite number of dots, and consists solely of valid Python identifiers.
if self.string_imports.min_dots == 0
|| memchr::memchr_iter(b'.', value.as_bytes()).count()
>= self.string_imports.min_dots
{
if let Some(module_name) = ModuleName::new(value) {
self.imports.push(CollectedImport::Import(module_name));
}
}
}
walk_expr(self, expr);
}
}

View File

@@ -87,7 +87,7 @@ impl SourceDb for ModuleDb {
#[salsa::db]
impl Db for ModuleDb {
fn is_file_open(&self, file: File) -> bool {
fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path()
}

View File

@@ -9,7 +9,7 @@ use ruff_python_parser::{Mode, ParseOptions, parse};
use crate::collector::Collector;
pub use crate::db::ModuleDb;
use crate::resolver::Resolver;
pub use crate::settings::{AnalyzeSettings, Direction};
pub use crate::settings::{AnalyzeSettings, Direction, StringImports};
mod collector;
mod db;
@@ -26,7 +26,7 @@ impl ModuleImports {
db: &ModuleDb,
path: &SystemPath,
package: Option<&SystemPath>,
string_imports: bool,
string_imports: StringImports,
) -> Result<Self> {
// Read and parse the source code.
let source = std::fs::read_to_string(path)?;
@@ -42,13 +42,11 @@ impl ModuleImports {
// Resolve the imports.
let mut resolved_imports = ModuleImports::default();
for import in imports {
let Some(resolved) = Resolver::new(db).resolve(import) else {
continue;
};
let Some(path) = resolved.as_system_path() else {
continue;
};
resolved_imports.insert(path.to_path_buf());
for resolved in Resolver::new(db).resolve(import) {
if let Some(path) = resolved.as_system_path() {
resolved_imports.insert(path.to_path_buf());
}
}
}
Ok(resolved_imports)

View File

@@ -1,5 +1,5 @@
use ruff_db::files::FilePath;
use ty_python_semantic::resolve_module;
use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
use crate::ModuleDb;
use crate::collector::CollectedImport;
@@ -16,24 +16,67 @@ impl<'a> Resolver<'a> {
}
/// Resolve the [`CollectedImport`] into a [`FilePath`].
pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> {
pub(crate) fn resolve(&self, import: CollectedImport) -> impl Iterator<Item = &'a FilePath> {
match import {
CollectedImport::Import(import) => {
let module = resolve_module(self.db, &import)?;
Some(module.file()?.path(self.db))
// Attempt to resolve the module (e.g., given `import foo`, look for `foo`).
let file = self.resolve_module(&import);
// If the file is a stub, look for the corresponding source file.
let source_file = file
.is_some_and(|file| file.extension() == Some("pyi"))
.then(|| self.resolve_real_module(&import))
.flatten();
std::iter::once(file)
.chain(std::iter::once(source_file))
.flatten()
}
CollectedImport::ImportFrom(import) => {
// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
if let Some(file) = self.resolve_module(&import) {
// If the file is a stub, look for the corresponding source file.
let source_file = (file.extension() == Some("pyi"))
.then(|| self.resolve_real_module(&import))
.flatten();
return std::iter::once(Some(file))
.chain(std::iter::once(source_file))
.flatten();
}
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
let parent = import.parent();
let file = parent
.as_ref()
.and_then(|parent| self.resolve_module(parent));
let module = resolve_module(self.db, &import).or_else(|| {
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
// If the file is a stub, look for the corresponding source file.
let source_file = file
.is_some_and(|file| file.extension() == Some("pyi"))
.then(|| {
parent
.as_ref()
.and_then(|parent| self.resolve_real_module(parent))
})
.flatten();
resolve_module(self.db, &parent?)
})?;
Some(module.file()?.path(self.db))
std::iter::once(file)
.chain(std::iter::once(source_file))
.flatten()
}
}
}
/// Resolves a module name to a module.
pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
/// Resolves a module name to a module (stubs not allowed).
fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_real_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
}

View File

@@ -11,7 +11,7 @@ pub struct AnalyzeSettings {
pub exclude: FilePatternSet,
pub preview: PreviewMode,
pub target_version: PythonVersion,
pub detect_string_imports: bool,
pub string_imports: StringImports,
pub include_dependencies: BTreeMap<PathBuf, (PathBuf, Vec<String>)>,
pub extension: ExtensionMapping,
}
@@ -26,7 +26,7 @@ impl fmt::Display for AnalyzeSettings {
self.exclude,
self.preview,
self.target_version,
self.detect_string_imports,
self.string_imports,
self.extension | debug,
self.include_dependencies | debug,
]
@@ -35,6 +35,31 @@ impl fmt::Display for AnalyzeSettings {
}
}
#[derive(Debug, Copy, Clone, CacheKey)]
pub struct StringImports {
pub enabled: bool,
pub min_dots: usize,
}
impl Default for StringImports {
fn default() -> Self {
Self {
enabled: false,
min_dots: 2,
}
}
}
impl fmt::Display for StringImports {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.enabled {
write!(f, "enabled (min_dots: {})", self.min_dots)
} else {
write!(f, "disabled")
}
}
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, CacheKey)]
#[cfg_attr(
feature = "serde",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.12.4"
version = "0.12.7"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -15,7 +15,7 @@ license = { workspace = true }
[dependencies]
ruff_annotate_snippets = { workspace = true }
ruff_cache = { workspace = true }
ruff_db = { workspace = true, features = ["serde"] }
ruff_db = { workspace = true, features = ["junit", "serde"] }
ruff_diagnostics = { workspace = true, features = ["serde"] }
ruff_notebook = { workspace = true }
ruff_macros = { workspace = true }
@@ -55,7 +55,6 @@ path-absolutize = { workspace = true, features = [
pathdiff = { workspace = true }
pep440_rs = { workspace = true }
pyproject-toml = { workspace = true }
quick-junit = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }

View File

@@ -89,3 +89,14 @@ print(1)
# ///
#
# Foobar
# Regression tests for https://github.com/astral-sh/ruff/issues/19713
# mypy: ignore-errors
# pyright: ignore-errors
# pyrefly: ignore-errors
# ty: ignore[unresolved-import]
# pyrefly: ignore[unused-import]
print(1)

View File

@@ -25,5 +25,5 @@ def my_func():
# t-strings - all ok
t"0.0.0.0"
"0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
t"0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
t"0.0.0.0" t"0.0.0.0{expr}0.0.0.0" t"0.0.0.0{expr}0.0.0.0"

View File

@@ -94,7 +94,7 @@ except Exception:
logging.error("...", exc_info=True)
from logging import error, exception
from logging import critical, error, exception
try:
pass
@@ -114,6 +114,23 @@ except Exception:
error("...", exc_info=None)
try:
pass
except Exception:
critical("...")
try:
pass
except Exception:
critical("...", exc_info=False)
try:
pass
except Exception:
critical("...", exc_info=None)
try:
pass
except Exception:
@@ -125,6 +142,13 @@ try:
except Exception:
error("...", exc_info=True)
try:
pass
except Exception:
critical("...", exc_info=True)
try:
...
except Exception as e:
@@ -138,3 +162,86 @@ except Exception:
exception("An error occurred")
else:
exception("An error occurred")
# Test tuple exceptions
try:
pass
except (Exception,):
pass
try:
pass
except (Exception, ValueError):
pass
try:
pass
except (ValueError, Exception):
pass
try:
pass
except (ValueError, Exception) as e:
print(e)
try:
pass
except (BaseException, TypeError):
pass
try:
pass
except (TypeError, BaseException):
pass
try:
pass
except (Exception, BaseException):
pass
try:
pass
except (BaseException, Exception):
pass
# Test nested tuples
try:
pass
except ((Exception, ValueError), TypeError):
pass
try:
pass
except (ValueError, (BaseException, TypeError)):
pass
# Test valid tuple exceptions (should not trigger)
try:
pass
except (ValueError, TypeError):
pass
try:
pass
except (OSError, FileNotFoundError):
pass
try:
pass
except (OSError, FileNotFoundError) as e:
print(e)
try:
pass
except (Exception, ValueError):
critical("...", exc_info=True)
try:
pass
except (Exception, ValueError):
raise
try:
pass
except (Exception, ValueError) as e:
raise e

View File

@@ -650,3 +650,17 @@ f"""This is a test. {
if True else
"Don't add a trailing comma here ->"
}"""
type X[
T
] = T
def f[
T
](): pass
class C[
T
]: pass
type X[T,] = T
def f[T,](): pass
class C[T,]: pass

View File

@@ -88,3 +88,25 @@ def f_multi_line_string2():
example="example"
)
)
def raise_typing_cast_exception():
import typing
raise typing.cast("Exception", None)
def f_typing_cast_excluded():
from typing import cast
raise cast(RuntimeError, "This should not trigger EM101")
def f_typing_cast_excluded_import():
import typing
raise typing.cast(RuntimeError, "This should not trigger EM101")
def f_typing_cast_excluded_aliased():
from typing import cast as my_cast
raise my_cast(RuntimeError, "This should not trigger EM101")

View File

@@ -39,6 +39,11 @@ class NonEmptyWithInit:
pass
class NonEmptyChildWithInlineComment:
value: int
... # preserve me
class EmptyClass:
...

View File

@@ -38,6 +38,10 @@ class NonEmptyWithInit:
def __init__():
pass
class NonEmptyChildWithInlineComment:
value: int
... # preserve me
# Not violations
class EmptyClass: ...

View File

@@ -142,3 +142,7 @@ field47: typing.Optional[int] | typing.Optional[dict]
# avoid reporting twice
field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
field49: typing.Optional[complex | complex] | complex
# Regression test for https://github.com/astral-sh/ruff/issues/19403
# Should throw duplicate union member but not fix
isinstance(None, typing.Union[None, None])

View File

@@ -129,4 +129,35 @@ print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))
print(" x ".rsplit(maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))
print(" x ".rsplit(sep=None, maxsplit=0))
# https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
r"""simple@example.com
very.common@example.com
FirstName.LastName@EasierReading.org
x@example.com
long.email-address-with-hyphens@and.subdomains.example.com
user.name+tag+sorting@example.com
name/surname@example.com
xample@s.example
" "@example.org
"john..doe"@example.org
mailhost!username@example.org
"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
user%example.com@example.org
user-@example.org
I❤CHOCOLATE@example.com
this\ still\"not\\allowed@example.com
stellyamburrr985@example.com
Abc.123@example.com
user+mailbox/department=shipping@example.com
!#$%&'*+-/=?^_`.{|}~@example.com
"Abc@def"@example.com
"Fred\ Bloggs"@example.com
"Joe.\\Blow"@example.com""".split("\n")
r"""first
'no need' to escape
"swap" quote style
"use' ugly triple quotes""".split("\n")

View File

@@ -1,4 +1,4 @@
from pathlib import Path, PurePath
from pathlib import Path, PurePath, PosixPath, PurePosixPath, WindowsPath, PureWindowsPath
from pathlib import Path as pth
@@ -68,3 +68,11 @@ Path(".", "folder")
PurePath(".", "folder")
Path()
from importlib.metadata import PackagePath
_ = PosixPath(".")
_ = PurePosixPath(".")
_ = WindowsPath(".")
_ = PureWindowsPath(".")
_ = PackagePath(".")

View File

@@ -47,3 +47,19 @@ def _():
from builtin import open
with open(p) as _: ... # No error
file = "file_1.py"
rename(file, "file_2.py")
rename(
# commment 1
file, # comment 2
"file_2.py"
,
# comment 3
)
rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
rename(file, "file_2.py", src_dir_fd=1)

View File

@@ -0,0 +1,3 @@
"""Hello, world!"""\
x = 1; y = 2

View File

@@ -84,3 +84,25 @@ class MyRequestHandler(BaseHTTPRequestHandler):
def dont_GET(self):
pass
from http.server import CGIHTTPRequestHandler
class MyCGIRequestHandler(CGIHTTPRequestHandler):
def do_OPTIONS(self):
pass
def dont_OPTIONS(self):
pass
from http.server import SimpleHTTPRequestHandler
class MySimpleRequestHandler(SimpleHTTPRequestHandler):
def do_OPTIONS(self):
pass
def dont_OPTIONS(self):
pass

View File

@@ -278,3 +278,30 @@ def f():
for i in src:
if lambda: 0:
dst.append(i)
def f():
i = "xyz"
result = []
for i in range(3):
result.append(x for x in [i])
def f():
i = "xyz"
result = []
for i in range(3):
result.append((x for x in [i]))
G_INDEX = None
def f():
global G_INDEX
result = []
for G_INDEX in range(3):
result.append(G_INDEX)
def f():
NL_INDEX = None
def x():
nonlocal NL_INDEX
result = []
for NL_INDEX in range(3):
result.append(NL_INDEX)

View File

@@ -0,0 +1,5 @@
#
x = 0 \
#
+1
print(x)

View File

@@ -59,3 +59,7 @@ kwargs = {x: x for x in range(10)}
"{1}_{0}".format(1, 2, *args)
"{1}_{0}".format(1, 2)
r"\d{{1,2}} {0}".format(42)
"{{{0}}}".format(123)

View File

@@ -143,3 +143,23 @@ class NotAMethodButHardToDetect:
# without risking false positives elsewhere or introducing complex heuristics
# that users would find surprising and confusing
FOO = sorted([x for x in BAR], key=lambda x: x.baz)
# https://github.com/astral-sh/ruff/issues/19305
import pytest
@pytest.fixture
def my_fixture_with_param(request):
return request.param
@pytest.fixture()
def my_fixture_with_param2(request):
return request.param
# Decorated function (should be ignored)
def custom_decorator(func):
return func
@custom_decorator
def add(x, y):
return x + y

View File

@@ -55,3 +55,12 @@ _ = Decimal(0.1)
_ = Decimal(-0.5)
_ = Decimal(5.0)
_ = decimal.Decimal(4.2)
# Cases with int and bool - should produce safe fixes
_ = Decimal.from_float(1)
_ = Decimal.from_float(True)
# Cases with non-finite floats - should produce safe fixes
_ = Decimal.from_float(float("-nan"))
_ = Decimal.from_float(float("\x2dnan"))
_ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))

View File

@@ -52,3 +52,7 @@ f"{repr(lambda: 1)}"
f"{repr(x := 2)}"
f"{str(object=3)}"
f"{str(x for x in [])}"
f"{str((x for x in []))}"

View File

@@ -79,3 +79,8 @@ def in_type_def():
from typing import cast
a = 'int'
cast('f"{a}"','11')
# Regression test for parser bug
# https://github.com/astral-sh/ruff/issues/18860
def fuzz_bug():
c('{\t"i}')

View File

@@ -65,3 +65,62 @@ class Foo:
bar = "should've used attrs"
def __post_init__(self, bar: str = "ahhh", baz: str = "hmm") -> None: ...
# https://github.com/astral-sh/ruff/issues/18950
@dataclass
class Foo:
def __post_init__(self, bar: int = (x := 1)) -> None:
pass
@dataclass
class Foo:
def __post_init__(
self,
bar: int = (x := 1) # comment
,
baz: int = (y := 2), # comment
foo = (a := 1) # comment
,
faz = (b := 2), # comment
) -> None:
pass
@dataclass
class Foo:
def __post_init__(
self,
bar: int = 1, # comment
baz: int = 2, # comment
) -> None:
pass
@dataclass
class Foo:
def __post_init__(
self,
arg1: int = (1) # comment
,
arg2: int = ((1)) # comment
,
arg2: int = (i for i in range(10)) # comment
,
) -> None:
pass
# makes little sense, but is valid syntax
def fun_with_python_syntax():
@dataclass
class Foo:
def __post_init__(
self,
bar: (int) = (yield from range(5)) # comment
,
) -> None:
...
return Foo

View File

@@ -53,3 +53,16 @@ regex.subn(br"""eak your machine with rm -""", rf"""/""")
regex.splititer(both, non_literal)
regex.subf(f, lambda _: r'means', '"format"')
regex.subfn(fn, f'''a$1n't''', lambda: "'function'")
# https://github.com/astral-sh/ruff/issues/16713
re.compile("\a\f\n\r\t\u27F2\U0001F0A1\v\x41") # with unsafe fix
re.compile("\b") # without fix
re.compile("\"") # without fix
re.compile("\'") # without fix
re.compile('\"') # without fix
re.compile('\'') # without fix
re.compile("\\") # without fix
re.compile("\101") # without fix
re.compile("a\
b") # without fix

View File

@@ -91,3 +91,20 @@ regex.subf(
br''br""br''
)
regex.subfn(br'I\s\nee*d\s[O0o]me\x20\Qoffe\E, ' br'b')
# https://github.com/astral-sh/ruff/issues/16713
re.compile(
"["
"\U0001F600-\U0001F64F" # emoticons
"\U0001F300-\U0001F5FF" # symbols & pictographs
"\U0001F680-\U0001F6FF" # transport & map symbols
"\U0001F1E0-\U0001F1FF" # flags (iOS)
"\U00002702-\U000027B0"
"\U000024C2-\U0001F251"
"\u200d" # zero width joiner
"\u200c" # zero width non-joiner
"\\u200c" # must not be escaped in a raw string
"]+",
flags=re.UNICODE,
)

View File

@@ -0,0 +1,3 @@
import re
re.compile("\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix

View File

@@ -0,0 +1,24 @@
import re
b_src = b"abc"
# Should be replaced with `b_src.replace(rb"x", b"y")`
re.sub(rb"x", b"y", b_src)
# Should be replaced with `b_src.startswith(rb"abc")`
if re.match(rb"abc", b_src):
pass
# Should be replaced with `rb"x" in b_src`
if re.search(rb"x", b_src):
pass
# Should be replaced with `b_src.split(rb"abc")`
re.split(rb"abc", b_src)
# Patterns containing metacharacters should NOT be replaced
re.sub(rb"ab[c]", b"", b_src)
re.match(rb"ab[c]", b_src)
re.search(rb"ab[c]", b_src)
re.fullmatch(rb"ab[c]", b_src)
re.split(rb"ab[c]", b_src)

View File

@@ -1039,14 +1039,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
}
if checker.any_rule_enabled(&[
Rule::OsChmod,
Rule::OsMkdir,
Rule::OsMakedirs,
Rule::OsRename,
Rule::OsReplace,
Rule::OsStat,
Rule::OsPathJoin,
Rule::OsPathSamefile,
Rule::OsPathSplitext,
Rule::BuiltinOpen,
Rule::PyPath,
@@ -1112,6 +1108,18 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::OsGetcwd) {
flake8_use_pathlib::rules::os_getcwd(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsChmod) {
flake8_use_pathlib::rules::os_chmod(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsRename) {
flake8_use_pathlib::rules::os_rename(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsReplace) {
flake8_use_pathlib::rules::os_replace(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathSamefile) {
flake8_use_pathlib::rules::os_path_samefile(checker, call, segments);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(
checker, call, segments,

View File

@@ -590,6 +590,16 @@ impl<'a> Checker<'a> {
member,
})
}
/// Return the [`LintContext`] for the current analysis.
///
/// Note that you should always prefer calling methods like `settings`, `report_diagnostic`, or
/// `is_rule_enabled` directly on [`Checker`] when possible. This method exists only for the
/// rare cases where rules or helper functions need to be accessed by both a `Checker` and a
/// `LintContext` in different analysis phases.
pub(crate) const fn context(&self) -> &'a LintContext<'a> {
self.context
}
}
pub(crate) struct TypingImporter<'a, 'b> {
@@ -3216,6 +3226,11 @@ impl<'a> LintContext<'a> {
pub(crate) fn iter(&mut self) -> impl Iterator<Item = &Diagnostic> {
self.diagnostics.get_mut().iter()
}
/// The [`LinterSettings`] for the current analysis, including the enabled rules.
pub(crate) const fn settings(&self) -> &LinterSettings {
self.settings
}
}
/// An abstraction for mutating a diagnostic.

View File

@@ -16,7 +16,6 @@ use crate::rules::{
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
flake8_pyi, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff,
};
use crate::settings::LinterSettings;
use super::ast::LintContext;
@@ -27,7 +26,6 @@ pub(crate) fn check_tokens(
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
settings: &LinterSettings,
source_type: PySourceType,
cell_offsets: Option<&CellOffsets>,
context: &mut LintContext,
@@ -42,15 +40,8 @@ pub(crate) fn check_tokens(
Rule::BlankLinesAfterFunctionOrClass,
Rule::BlankLinesBeforeNestedDefinition,
]) {
BlankLinesChecker::new(
locator,
stylist,
settings,
source_type,
cell_offsets,
context,
)
.check_lines(tokens);
BlankLinesChecker::new(locator, stylist, source_type, cell_offsets, context)
.check_lines(tokens);
}
if context.is_rule_enabled(Rule::BlanketTypeIgnore) {
@@ -58,17 +49,17 @@ pub(crate) fn check_tokens(
}
if context.is_rule_enabled(Rule::EmptyComment) {
pylint::rules::empty_comments(context, comment_ranges, locator);
pylint::rules::empty_comments(context, comment_ranges, locator, indexer);
}
if context.is_rule_enabled(Rule::AmbiguousUnicodeCharacterComment) {
for range in comment_ranges {
ruff::rules::ambiguous_unicode_character_comment(context, locator, range, settings);
ruff::rules::ambiguous_unicode_character_comment(context, locator, range);
}
}
if context.is_rule_enabled(Rule::CommentedOutCode) {
eradicate::rules::commented_out_code(context, locator, comment_ranges, settings);
eradicate::rules::commented_out_code(context, locator, comment_ranges);
}
if context.is_rule_enabled(Rule::UTF8EncodingDeclaration) {
@@ -110,7 +101,7 @@ pub(crate) fn check_tokens(
Rule::SingleLineImplicitStringConcatenation,
Rule::MultiLineImplicitStringConcatenation,
]) {
flake8_implicit_str_concat::rules::implicit(context, tokens, locator, indexer, settings);
flake8_implicit_str_concat::rules::implicit(context, tokens, locator, indexer);
}
if context.any_rule_enabled(&[

View File

@@ -920,11 +920,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-use-pathlib
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath),
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsChmod),
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsChmod),
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir),
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs),
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRename),
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReplace),
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRename),
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReplace),
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir),
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove),
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink),
@@ -940,7 +940,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin),
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename),
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname),
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSamefile),
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathSamefile),
(Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext),
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen),
(Flake8UsePathlib, "124") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::PyPath),

View File

@@ -56,13 +56,19 @@ impl<'a> Insertion<'a> {
stylist: &Stylist,
) -> Insertion<'static> {
// Skip over any docstrings.
let mut location = if let Some(location) = match_docstring_end(body) {
let mut location = if let Some(mut location) = match_docstring_end(body) {
// If the first token after the docstring is a semicolon, insert after the semicolon as
// an inline statement.
if let Some(offset) = match_semicolon(locator.after(location)) {
return Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";");
}
// If the first token after the docstring is a continuation character (i.e. "\"), advance
// an additional row to prevent inserting in the same logical line.
if match_continuation(locator.after(location)).is_some() {
location = locator.full_line_end(location);
}
// Otherwise, advance to the next row.
locator.full_line_end(location)
} else {
@@ -363,6 +369,16 @@ mod tests {
Insertion::own_line("", TextSize::from(20), "\n")
);
let contents = r#"
"""Hello, world!"""\
"#
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(22), "\n")
);
let contents = r"
x = 1
"

View File

@@ -188,7 +188,6 @@ pub fn check_path(
locator,
indexer,
stylist,
settings,
source_type,
source_kind.as_ipy_notebook().map(Notebook::cell_offsets),
&mut context,
@@ -473,7 +472,7 @@ pub fn lint_only(
&& !is_py314_support_enabled(settings)
{
warn_user_once!(
"Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning."
"Support for Python 3.14 is in preview and may undergo breaking changes. Enable `preview` to remove this warning."
);
}
@@ -584,7 +583,7 @@ pub fn lint_fix<'a>(
&& !is_py314_support_enabled(settings)
{
warn_user_once!(
"Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning."
"Support for Python 3.14 is in preview and may undergo breaking changes. Enable `preview` to remove this warning."
);
}

View File

@@ -118,86 +118,6 @@ impl<'a> Locator<'a> {
}
}
/// Finds the closest [`TextSize`] not less than the offset given for which
/// `is_char_boundary` is `true`. Unless the offset given is greater than
/// the length of the underlying contents, in which case, the length of the
/// contents is returned.
///
/// Can be replaced with `str::ceil_char_boundary` once it's stable.
///
/// # Examples
///
/// From `std`:
///
/// ```
/// use ruff_text_size::{Ranged, TextSize};
/// use ruff_linter::Locator;
///
/// let locator = Locator::new("❤️🧡💛💚💙💜");
/// assert_eq!(locator.text_len(), TextSize::from(26));
/// assert!(!locator.contents().is_char_boundary(13));
///
/// let closest = locator.ceil_char_boundary(TextSize::from(13));
/// assert_eq!(closest, TextSize::from(14));
/// assert_eq!(&locator.contents()[..closest.to_usize()], "❤️🧡💛");
/// ```
///
/// Additional examples:
///
/// ```
/// use ruff_text_size::{Ranged, TextRange, TextSize};
/// use ruff_linter::Locator;
///
/// let locator = Locator::new("Hello");
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(5)),
/// TextSize::from(5)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(6)),
/// TextSize::from(5)
/// );
///
/// let locator = Locator::new("α");
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(1)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(2)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(3)),
/// TextSize::from(2)
/// );
/// ```
pub fn ceil_char_boundary(&self, offset: TextSize) -> TextSize {
let upper_bound = offset
.to_u32()
.saturating_add(4)
.min(self.text_len().to_u32());
(offset.to_u32()..upper_bound)
.map(TextSize::from)
.find(|offset| self.contents.is_char_boundary(offset.to_usize()))
.unwrap_or_else(|| TextSize::from(upper_bound))
}
/// Take the source code between the given [`TextRange`].
#[inline]
pub fn slice<T: Ranged>(&self, ranged: T) -> &'a str {

View File

@@ -61,22 +61,17 @@ impl Serialize for SerializedMessages<'_> {
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
for diagnostic in self.diagnostics {
let start_location = diagnostic.expect_ruff_start_location();
let end_location = diagnostic.expect_ruff_end_location();
let filename = diagnostic.expect_ruff_filename();
let lines = if self.context.is_notebook(&filename) {
let (start_location, end_location) = if self.context.is_notebook(&filename) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
json!({
"begin": 1,
"end": 1
})
Default::default()
} else {
json!({
"begin": start_location.line,
"end": end_location.line
})
(
diagnostic.expect_ruff_start_location(),
diagnostic.expect_ruff_end_location(),
)
};
let path = self.project_dir.as_ref().map_or_else(
@@ -111,8 +106,11 @@ impl Serialize for SerializedMessages<'_> {
"fingerprint": format!("{:x}", message_fingerprint),
"location": {
"path": path,
"lines": lines
}
"positions": {
"begin": start_location,
"end": end_location,
},
},
});
s.serialize_element(&value)?;

View File

@@ -1,117 +0,0 @@
use std::io::Write;
use std::path::Path;
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite, XmlString};
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::LineColumn;
use crate::message::{Emitter, EmitterContext, MessageWithLocation, group_diagnostics_by_filename};
#[derive(Default)]
pub struct JunitEmitter;
impl Emitter for JunitEmitter {
fn emit(
&mut self,
writer: &mut dyn Write,
diagnostics: &[Diagnostic],
context: &EmitterContext,
) -> anyhow::Result<()> {
let mut report = Report::new("ruff");
if diagnostics.is_empty() {
let mut test_suite = TestSuite::new("ruff");
test_suite
.extra
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
let mut case = TestCase::new("No errors found", TestCaseStatus::success());
case.set_classname("ruff");
test_suite.add_test_case(case);
report.add_test_suite(test_suite);
} else {
for (filename, messages) in group_diagnostics_by_filename(diagnostics) {
let mut test_suite = TestSuite::new(&filename);
test_suite
.extra
.insert(XmlString::new("package"), XmlString::new("org.ruff"));
for message in messages {
let MessageWithLocation {
message,
start_location,
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.body());
let location = if context.is_notebook(&message.expect_ruff_filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
LineColumn::default()
} else {
start_location
};
status.set_description(format!(
"line {row}, col {col}, {body}",
row = location.line,
col = location.column,
body = message.body()
));
let mut case = TestCase::new(
if let Some(code) = message.secondary_code() {
format!("org.ruff.{code}")
} else {
"org.ruff".to_string()
},
status,
);
let file_path = Path::new(&*filename);
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
let classname = file_path.parent().unwrap().join(file_stem);
case.set_classname(classname.to_str().unwrap());
case.extra.insert(
XmlString::new("line"),
XmlString::new(location.line.to_string()),
);
case.extra.insert(
XmlString::new("column"),
XmlString::new(location.column.to_string()),
);
test_suite.add_test_case(case);
}
report.add_test_suite(test_suite);
}
}
report.serialize(writer)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::JunitEmitter;
use crate::message::tests::{
capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
};
#[test]
fn output() {
let mut emitter = JunitEmitter;
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = JunitEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
assert_snapshot!(content);
}
}

View File

@@ -14,7 +14,6 @@ use ruff_db::files::File;
pub use github::GithubEmitter;
pub use gitlab::GitlabEmitter;
pub use grouped::GroupedEmitter;
pub use junit::JunitEmitter;
use ruff_notebook::NotebookIndex;
use ruff_source_file::{LineColumn, SourceFile};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -28,7 +27,6 @@ mod diff;
mod github;
mod gitlab;
mod grouped;
mod junit;
mod sarif;
mod text;
@@ -77,12 +75,13 @@ where
);
let span = Span::from(file).with_range(range);
let mut annotation = Annotation::primary(span);
if let Some(suggestion) = suggestion {
annotation = annotation.message(suggestion);
}
let annotation = Annotation::primary(span);
diagnostic.annotate(annotation);
if let Some(suggestion) = suggestion {
diagnostic.help(suggestion);
}
if let Some(fix) = fix {
diagnostic.set_fix(fix);
}

View File

@@ -8,11 +8,17 @@ expression: redact_fingerprint(&content)
"description": "`os` imported but unused",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 1,
"end": 1
},
"path": "fib.py"
"path": "fib.py",
"positions": {
"begin": {
"column": 8,
"line": 1
},
"end": {
"column": 10,
"line": 1
}
}
},
"severity": "major"
},
@@ -21,11 +27,17 @@ expression: redact_fingerprint(&content)
"description": "Local variable `x` is assigned to but never used",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 6,
"end": 6
},
"path": "fib.py"
"path": "fib.py",
"positions": {
"begin": {
"column": 5,
"line": 6
},
"end": {
"column": 6,
"line": 6
}
}
},
"severity": "major"
},
@@ -34,11 +46,17 @@ expression: redact_fingerprint(&content)
"description": "Undefined name `a`",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 1,
"end": 1
},
"path": "undef.py"
"path": "undef.py",
"positions": {
"begin": {
"column": 4,
"line": 1
},
"end": {
"column": 5,
"line": 1
}
}
},
"severity": "major"
}

View File

@@ -8,11 +8,17 @@ expression: redact_fingerprint(&content)
"description": "Expected one or more symbol names after import",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 1,
"end": 2
},
"path": "syntax_errors.py"
"path": "syntax_errors.py",
"positions": {
"begin": {
"column": 15,
"line": 1
},
"end": {
"column": 1,
"line": 2
}
}
},
"severity": "major"
},
@@ -21,11 +27,17 @@ expression: redact_fingerprint(&content)
"description": "Expected ')', found newline",
"fingerprint": "<redacted>",
"location": {
"lines": {
"begin": 3,
"end": 4
},
"path": "syntax_errors.py"
"path": "syntax_errors.py",
"positions": {
"begin": {
"column": 12,
"line": 3
},
"end": {
"column": 1,
"line": 4
}
}
},
"severity": "major"
}

View File

@@ -81,7 +81,7 @@ expression: value
"rules": [
{
"fullDescription": {
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
},
"help": {
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"

View File

@@ -6,14 +6,13 @@ use bitflags::bitflags;
use colored::Colorize;
use ruff_annotate_snippets::{Level, Renderer, Snippet};
use ruff_db::diagnostic::{Diagnostic, SecondaryCode};
use ruff_db::diagnostic::{
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, SecondaryCode, ceil_char_boundary,
};
use ruff_notebook::NotebookIndex;
use ruff_source_file::{LineColumn, OneIndexed};
use ruff_source_file::OneIndexed;
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::Locator;
use crate::fs::relativize_path;
use crate::line_width::{IndentWidth, LineWidthBuilder};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext};
use crate::settings::types::UnsafeFixes;
@@ -21,8 +20,6 @@ use crate::settings::types::UnsafeFixes;
bitflags! {
#[derive(Default)]
struct EmitterFlags: u8 {
/// Whether to show the fix status of a diagnostic.
const SHOW_FIX_STATUS = 1 << 0;
/// Whether to show the diff of a fix, for diagnostics that have a fix.
const SHOW_FIX_DIFF = 1 << 1;
/// Whether to show the source code of a diagnostic.
@@ -30,17 +27,27 @@ bitflags! {
}
}
#[derive(Default)]
pub struct TextEmitter {
flags: EmitterFlags,
unsafe_fixes: UnsafeFixes,
config: DisplayDiagnosticConfig,
}
impl Default for TextEmitter {
fn default() -> Self {
Self {
flags: EmitterFlags::default(),
config: DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Concise)
.hide_severity(true)
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()),
}
}
}
impl TextEmitter {
#[must_use]
pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self {
self.flags
.set(EmitterFlags::SHOW_FIX_STATUS, show_fix_status);
self.config = self.config.show_fix_status(show_fix_status);
self
}
@@ -58,7 +65,21 @@ impl TextEmitter {
#[must_use]
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
self.unsafe_fixes = unsafe_fixes;
self.config = self
.config
.fix_applicability(unsafe_fixes.required_applicability());
self
}
#[must_use]
pub fn with_preview(mut self, preview: bool) -> Self {
self.config = self.config.preview(preview);
self
}
#[must_use]
pub fn with_color(mut self, color: bool) -> Self {
self.config = self.config.color(color);
self
}
}
@@ -71,51 +92,10 @@ impl Emitter for TextEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in diagnostics {
write!(writer, "{}", message.display(context, &self.config))?;
let filename = message.expect_ruff_filename();
write!(
writer,
"{path}{sep}",
path = relativize_path(&filename).bold(),
sep = ":".cyan(),
)?;
let start_location = message.expect_ruff_start_location();
let notebook_index = context.notebook_index(&filename);
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let diagnostic_location = if let Some(notebook_index) = notebook_index {
write!(
writer,
"cell {cell}{sep}",
cell = notebook_index
.cell(start_location.line)
.unwrap_or(OneIndexed::MIN),
sep = ":".cyan(),
)?;
LineColumn {
line: notebook_index
.cell_row(start_location.line)
.unwrap_or(OneIndexed::MIN),
column: start_location.column,
}
} else {
start_location
};
writeln!(
writer,
"{row}{sep}{col}{sep} {code_and_body}",
row = diagnostic_location.line,
col = diagnostic_location.column,
sep = ":".cyan(),
code_and_body = RuleCodeAndBody {
message,
show_fix_status: self.flags.intersects(EmitterFlags::SHOW_FIX_STATUS),
unsafe_fixes: self.unsafe_fixes,
}
)?;
if self.flags.intersects(EmitterFlags::SHOW_SOURCE) {
// The `0..0` range is used to highlight file-level diagnostics.
if message.expect_range() != TextRange::default() {
@@ -186,7 +166,7 @@ pub(super) struct MessageCodeFrame<'a> {
impl Display for MessageCodeFrame<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let suggestion = self.message.suggestion();
let suggestion = self.message.first_help_text();
let footers = if let Some(suggestion) = suggestion {
vec![Level::Help.title(suggestion)]
} else {
@@ -248,7 +228,7 @@ impl Display for MessageCodeFrame<'_> {
let start_offset = source_code.line_start(start_index);
let end_offset = source_code.line_end(end_index);
let source = replace_whitespace_and_unprintable(
let source = replace_unprintable(
source_code.slice(TextRange::new(start_offset, end_offset)),
self.message.expect_range() - start_offset,
)
@@ -291,16 +271,20 @@ impl Display for MessageCodeFrame<'_> {
}
/// Given some source code and an annotation range, this routine replaces
/// tabs with ASCII whitespace, and unprintable characters with printable
/// representations of them.
/// unprintable characters with printable representations of them.
///
/// The source code returned has an annotation that is updated to reflect
/// changes made to the source code (if any).
fn replace_whitespace_and_unprintable(source: &str, annotation_range: TextRange) -> SourceCode {
///
/// We don't need to normalize whitespace, such as converting tabs to spaces,
/// because `annotate-snippets` handles that internally. Similarly, it's safe to
/// modify the annotation ranges by inserting 3-byte Unicode replacements
/// because `annotate-snippets` will account for their actual width when
/// rendering and displaying the column to the user.
fn replace_unprintable(source: &str, annotation_range: TextRange) -> SourceCode {
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut line_width = LineWidthBuilder::new(IndentWidth::default());
// Updates the range given by the caller whenever a single byte (at
// `index` in `source`) is replaced with `len` bytes.
@@ -329,19 +313,7 @@ fn replace_whitespace_and_unprintable(source: &str, annotation_range: TextRange)
};
for (index, c) in source.char_indices() {
let old_width = line_width.get();
line_width = line_width.add_char(c);
if matches!(c, '\t') {
let tab_width = u32::try_from(line_width.get() - old_width)
.expect("small width because of tab size");
result.push_str(&source[last_end..index]);
for _ in 0..tab_width {
result.push(' ');
}
last_end = index + 1;
update_range(index, tab_width);
} else if let Some(printable) = unprintable_replacement(c) {
if let Some(printable) = unprintable_replacement(c) {
result.push_str(&source[last_end..index]);
result.push(printable);
last_end = index + 1;
@@ -396,9 +368,8 @@ impl<'a> SourceCode<'a> {
if self.text.as_bytes()[self.annotation_range.start().to_usize() - 1] != b'\n' {
return self;
}
let locator = Locator::new(&self.text);
let start = self.annotation_range.start();
let end = locator.ceil_char_boundary(start + TextSize::from(1));
let end = ceil_char_boundary(&self.text, start + TextSize::from(1));
SourceCode {
annotation_range: TextRange::new(start, end),
..self

View File

@@ -134,6 +134,26 @@ pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) ->
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19404
pub(crate) const fn is_fix_os_chmod_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19404
pub(crate) const fn is_fix_os_rename_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19404
pub(crate) const fn is_fix_os_replace_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19404
pub(crate) const fn is_fix_os_path_samefile_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19245
pub(crate) const fn is_fix_os_getcwd_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
@@ -205,3 +225,8 @@ pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSet
pub(crate) const fn is_add_future_annotations_imports_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19390
pub(crate) const fn is_trailing_comma_type_params_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -21,6 +21,7 @@ static ALLOWLIST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
(?:
# Case-sensitive
pyright
| pyrefly
| mypy:
| type:\s*ignore
| SPDX-License-Identifier:

View File

@@ -5,7 +5,6 @@ use ruff_text_size::TextRange;
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::settings::LinterSettings;
use crate::{Edit, Fix, FixAvailability, Violation};
use crate::rules::eradicate::detection::comment_contains_code;
@@ -51,7 +50,6 @@ pub(crate) fn commented_out_code(
context: &LintContext,
locator: &Locator,
comment_ranges: &CommentRanges,
settings: &LinterSettings,
) {
let mut comments = comment_ranges.into_iter().peekable();
// Iterate over all comments in the document.
@@ -65,7 +63,9 @@ pub(crate) fn commented_out_code(
}
// Verify that the comment is on its own line, and that it contains code.
if is_own_line_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
if is_own_line_comment(line)
&& comment_contains_code(line, &context.settings().task_tags[..])
{
if let Some(mut diagnostic) =
context.report_diagnostic_if_enabled(CommentedOutCode, range)
{

View File

@@ -47,9 +47,10 @@ use crate::checkers::ast::Checker;
/// raise
/// ```
///
/// Exceptions that are logged via `logging.exception()` or `logging.error()`
/// with `exc_info` enabled will _not_ be flagged, as this is a common pattern
/// for propagating exception traces:
/// Exceptions that are logged via `logging.exception()` or are logged via
/// `logging.error()` or `logging.critical()` with `exc_info` enabled will
/// _not_ be flagged, as this is a common pattern for propagating exception
/// traces:
/// ```python
/// try:
/// foo()
@@ -74,6 +75,22 @@ impl Violation for BlindExcept {
}
}
fn contains_blind_exception<'a>(
semantic: &'a SemanticModel,
expr: &'a Expr,
) -> Option<(&'a str, ruff_text_size::TextRange)> {
match expr {
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
.iter()
.find_map(|elt| contains_blind_exception(semantic, elt)),
_ => {
let builtin_exception_type = semantic.resolve_builtin_symbol(expr)?;
matches!(builtin_exception_type, "BaseException" | "Exception")
.then(|| (builtin_exception_type, expr.range()))
}
}
}
/// BLE001
pub(crate) fn blind_except(
checker: &Checker,
@@ -86,12 +103,9 @@ pub(crate) fn blind_except(
};
let semantic = checker.semantic();
let Some(builtin_exception_type) = semantic.resolve_builtin_symbol(type_) else {
let Some((builtin_exception_type, range)) = contains_blind_exception(semantic, type_) else {
return;
};
if !matches!(builtin_exception_type, "BaseException" | "Exception") {
return;
}
// If the exception is re-raised, don't flag an error.
let mut visitor = ReraiseVisitor::new(name);
@@ -109,9 +123,9 @@ pub(crate) fn blind_except(
checker.report_diagnostic(
BlindExcept {
name: builtin_exception_type.to_string(),
name: builtin_exception_type.into(),
},
type_.range(),
range,
);
}
@@ -201,7 +215,7 @@ impl<'a> StatementVisitor<'a> for LogExceptionVisitor<'a> {
) {
if match attr.as_str() {
"exception" => true,
"error" => arguments
"error" | "critical" => arguments
.find_keyword("exc_info")
.is_some_and(|keyword| is_const_true(&keyword.value)),
_ => false,
@@ -214,7 +228,7 @@ impl<'a> StatementVisitor<'a> for LogExceptionVisitor<'a> {
if self.semantic.resolve_qualified_name(func).is_some_and(
|qualified_name| match qualified_name.segments() {
["logging", "exception"] => true,
["logging", "error"] => arguments
["logging", "error" | "critical"] => arguments
.find_keyword("exc_info")
.is_some_and(|keyword| is_const_true(&keyword.value)),
_ => false,

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_blind_except/mod.rs
snapshot_kind: text
---
BLE.py:25:8: BLE001 Do not catch blind exception: `BaseException`
|
@@ -121,3 +120,120 @@ BLE.py:113:8: BLE001 Do not catch blind exception: `Exception`
| ^^^^^^^^^ BLE001
114 | error("...", exc_info=None)
|
BLE.py:119:8: BLE001 Do not catch blind exception: `Exception`
|
117 | try:
118 | pass
119 | except Exception:
| ^^^^^^^^^ BLE001
120 | critical("...")
|
BLE.py:125:8: BLE001 Do not catch blind exception: `Exception`
|
123 | try:
124 | pass
125 | except Exception:
| ^^^^^^^^^ BLE001
126 | critical("...", exc_info=False)
|
BLE.py:131:8: BLE001 Do not catch blind exception: `Exception`
|
129 | try:
130 | pass
131 | except Exception:
| ^^^^^^^^^ BLE001
132 | critical("...", exc_info=None)
|
BLE.py:169:9: BLE001 Do not catch blind exception: `Exception`
|
167 | try:
168 | pass
169 | except (Exception,):
| ^^^^^^^^^ BLE001
170 | pass
|
BLE.py:174:9: BLE001 Do not catch blind exception: `Exception`
|
172 | try:
173 | pass
174 | except (Exception, ValueError):
| ^^^^^^^^^ BLE001
175 | pass
|
BLE.py:179:21: BLE001 Do not catch blind exception: `Exception`
|
177 | try:
178 | pass
179 | except (ValueError, Exception):
| ^^^^^^^^^ BLE001
180 | pass
|
BLE.py:184:21: BLE001 Do not catch blind exception: `Exception`
|
182 | try:
183 | pass
184 | except (ValueError, Exception) as e:
| ^^^^^^^^^ BLE001
185 | print(e)
|
BLE.py:189:9: BLE001 Do not catch blind exception: `BaseException`
|
187 | try:
188 | pass
189 | except (BaseException, TypeError):
| ^^^^^^^^^^^^^ BLE001
190 | pass
|
BLE.py:194:20: BLE001 Do not catch blind exception: `BaseException`
|
192 | try:
193 | pass
194 | except (TypeError, BaseException):
| ^^^^^^^^^^^^^ BLE001
195 | pass
|
BLE.py:199:9: BLE001 Do not catch blind exception: `Exception`
|
197 | try:
198 | pass
199 | except (Exception, BaseException):
| ^^^^^^^^^ BLE001
200 | pass
|
BLE.py:204:9: BLE001 Do not catch blind exception: `BaseException`
|
202 | try:
203 | pass
204 | except (BaseException, Exception):
| ^^^^^^^^^^^^^ BLE001
205 | pass
|
BLE.py:210:10: BLE001 Do not catch blind exception: `Exception`
|
208 | try:
209 | pass
210 | except ((Exception, ValueError), TypeError):
| ^^^^^^^^^ BLE001
211 | pass
|
BLE.py:215:22: BLE001 Do not catch blind exception: `BaseException`
|
213 | try:
214 | pass
215 | except (ValueError, (BaseException, TypeError)):
| ^^^^^^^^^^^^^ BLE001
216 | pass
|

View File

@@ -27,4 +27,23 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("COM81.py"))]
#[test_case(Path::new("COM81_syntax_error.py"))]
fn preview_rules(path: &Path) -> Result<()> {
let snapshot = format!("preview__{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_commas").join(path).as_path(),
&settings::LinterSettings {
preview: crate::settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rules(vec![
Rule::MissingTrailingComma,
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
])
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -5,6 +5,8 @@ use ruff_text_size::{Ranged, TextRange};
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::preview::is_trailing_comma_type_params_enabled;
use crate::settings::LinterSettings;
use crate::{AlwaysFixableViolation, Violation};
use crate::{Edit, Fix};
@@ -24,6 +26,8 @@ enum TokenType {
Def,
For,
Lambda,
Class,
Type,
Irrelevant,
}
@@ -69,6 +73,8 @@ impl From<(TokenKind, TextRange)> for SimpleToken {
TokenKind::Lbrace => TokenType::OpeningCurlyBracket,
TokenKind::Rbrace => TokenType::ClosingBracket,
TokenKind::Def => TokenType::Def,
TokenKind::Class => TokenType::Class,
TokenKind::Type => TokenType::Type,
TokenKind::For => TokenType::For,
TokenKind::Lambda => TokenType::Lambda,
// Import treated like a function.
@@ -98,6 +104,8 @@ enum ContextType {
Dict,
/// Lambda parameter list, e.g. `lambda a, b`.
LambdaParameters,
/// Type parameter list, e.g. `def foo[T, U](): ...`
TypeParameters,
}
/// Comma context - described a comma-delimited "situation".
@@ -290,7 +298,7 @@ pub(crate) fn trailing_commas(
}
// Update the comma context stack.
let context = update_context(token, prev, prev_prev, &mut stack);
let context = update_context(token, prev, prev_prev, &mut stack, lint_context.settings());
check_token(token, prev, prev_prev, context, locator, lint_context);
@@ -326,6 +334,7 @@ fn check_token(
ContextType::No => false,
ContextType::FunctionParameters => true,
ContextType::CallArguments => true,
ContextType::TypeParameters => true,
// `(1)` is not equivalent to `(1,)`.
ContextType::Tuple => context.num_commas != 0,
// `x[1]` is not equivalent to `x[1,]`.
@@ -408,6 +417,7 @@ fn update_context(
prev: SimpleToken,
prev_prev: SimpleToken,
stack: &mut Vec<Context>,
settings: &LinterSettings,
) -> Context {
let new_context = match token.ty {
TokenType::OpeningBracket => match (prev.ty, prev_prev.ty) {
@@ -417,6 +427,17 @@ fn update_context(
}
_ => Context::new(ContextType::Tuple),
},
TokenType::OpeningSquareBracket if is_trailing_comma_type_params_enabled(settings) => {
match (prev.ty, prev_prev.ty) {
(TokenType::Named, TokenType::Def | TokenType::Class | TokenType::Type) => {
Context::new(ContextType::TypeParameters)
}
(TokenType::ClosingBracket | TokenType::Named | TokenType::String, _) => {
Context::new(ContextType::Subscript)
}
_ => Context::new(ContextType::List),
}
}
TokenType::OpeningSquareBracket => match prev.ty {
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
Context::new(ContextType::Subscript)

View File

@@ -0,0 +1,30 @@
---
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
---
COM81_syntax_error.py:3:5: SyntaxError: Starred expression cannot be used here
|
1 | # Check for `flake8-commas` violation for a file containing syntax errors.
2 | (
3 | *args
| ^^^^^
4 | )
|
COM81_syntax_error.py:6:9: SyntaxError: Type parameter list cannot be empty
|
4 | )
5 |
6 | def foo[(param1='test', param2='test',):
| ^
7 | pass
|
COM81_syntax_error.py:6:38: COM819 Trailing comma prohibited
|
4 | )
5 |
6 | def foo[(param1='test', param2='test',):
| ^ COM819
7 | pass
|
= help: Remove trailing comma

Some files were not shown because too many files have changed in this diff Show More