Commit Graph
17802 Commits
Author SHA1 Message Date
Charlie Marsh 2f0a29ece1 [ty] Record predicate truthiness alongside expression inference (#29706)
## Summary

A full ty check of isort went from 204 to 172 ms in the ecosystem run.
Across all 163 projects, the average timing factor was 1.00×. Local
checks of two isort revisions on macOS took 16–23% less time. [Ecosystem
timings](https://dqhfo45wdv0ek.cloudfront.net/ruff/pull-requests/29706/ty-ecosystem/40342132072/1/timing.html)

In `if bool(stop()):`, the call to `bool` has type `bool`, but neither
branch can run if `stop()` returns `Never`. Ty uses the operand types to
work this out. In a loop, those types can depend on an assignment
guarded by the same condition. Computing types and truthiness in
separate queries can make ty revisit that loop repeatedly.

We now record each predicate’s evaluation context when we index it.
Expression inference computes the predicate’s truthiness alongside its
operand types and recomputes it if cycle recovery widens them. Ordinary
conditions and match guards both read that result.

The only warning that changed consistently across ecosystem runs was in
psycopg. It now lists the `int` class separately even though `type[str |
Any | int]` already includes it. [Ecosystem
diff](https://dqhfo45wdv0ek.cloudfront.net/ruff/pull-requests/29706/ty-ecosystem/40342132072/1/diff.html)
2026-10-26 15:11:17 -07:00
Carl Meyer 60f104a1a2 [ty] Honor @override on overload implementations in stubs (#29470)
In stub files, ty required `@override` on the first overload even when
an implementation was present, while overload validation required the
decorator on the implementation. Honor `@override` on the implementation
whenever one exists, matching Pyright and Pyrefly. Continue using the
first overload when there is no implementation.

The implementation also becomes the location for
`invalid-explicit-override` diagnostics in these stubs.

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

## Test plan

- Checked the reported `.pyi` example, an ellipsis-only minimized
example, and a stub without an implementation using Python 3.13 with all
rules enabled; all passed without diagnostics.
- Compared decorator placement with mypy 2.4.0, Pyright 1.1.414,
basedpyright 1.40.2, and Pyrefly 1.4.0. Pyright, basedpyright, and
Pyrefly honor the implementation's decorator in stubs; mypy rejects stub
overload implementations. All four accept the decorator on the first
overload when no implementation exists.

## Ecosystem report

For head `5690fe75333fb6755662fda0cec8d6a192905d2b`, the ecosystem run
removes 27 false `missing-override-decorator` errors from pyochain stubs
whose overload implementations already have `@override`. All changes
were reproduced and classified as expected; CI passed and no ecosystem
concern blocks review. [Full ecosystem
report](https://github.com/astral-sh/ruff/pull/29470#issuecomment-6260122936).
2026-10-26 15:10:48 -07:00
Micha Reiser 207ec3affe [ty] Add diagnostic baseline support (#27636) 2026-10-26 13:34:01 -07:00
Carl Meyer 6840db323c [ty] Speed up match narrowing over large unions (#29733)
## Summary

Reachability and capture inference both walk alternatives such as `case
A(value=v) | B(value=v)` from left to right. They can compute the same
type remaining after an alternative fails. For a wide union, each
subtraction distributes an exclusion over the union members and
normalizes the resulting union of intersections.

Cache `subject & ~definitely_matched` by its ordered operands and
program. The query applies the exclusion directly to the subject so the
intersection builder can discard impossible branches as it distributes.

Refs astral-sh/ty#4815. The benchmark is in #29726.

## Test plan

Compared optimized dev builds on Apple Silicon, reversing the order of
the two builds in alternating blocks. Criterion results are medians of
six process estimates with ten samples each. CLI results are medians of
five runs per build after warmup.

| Input | Parent | This change | Time change |
| --- | ---: | ---: | ---: |
| Criterion, 45 classes | 76.8 ms | 57.5 ms | −25% |
| Criterion, 90 classes | 235.4 ms | 146.4 ms | −38% |
| CLI, 180 classes, four class alternatives followed by a single-class
case | 330 ms | 252 ms | −24% |
| CLI, 180 classes, twelve groups of three alternatives | 3.82 s | 2.55
s | −33% |

The 90-class CLI input retired 25% fewer instructions, with peak memory
footprint within 1% of the parent. On the eight-case tuple inputs from
astral-sh/ty#4676, retired instructions changed by less than 1% for both
generic and non-generic classes.

Also applied the benchmark and this change to the tuple-pattern stack at
`a08de583` (#29203). Across four paired Criterion runs on that stack,
the 45-class case fell from 63.9 to 53.4 ms and the 90-class case fell
from 195.9 to 131.8 ms.
2026-10-26 12:07:38 -07:00
Carl Meyer 002d4c8f1d [ty] Normalize runtime unions on demand (#29727)
When ty checks a value assignment such as `Alias = A[int] | B[int] |
C[int]`, it currently normalizes `A[int] | B[int]` and then compares
those same members again while building the final union. Longer chains
repeat that work for every prefix, even when nothing ever uses the value
as a type.

Instead, this PR just stores the runtime operands of `|` in the
runtime-union object, and only builds and normalizes the actual type
union lazily, if or when the runtime union value needs to be understood
as a type. For example, checking a stored union against `TypeForm[Base]`
walks the operands once and caches the result. Checks such as
`isinstance` can still inspect the original operands.

This is an alternative to #29304, which keeps the type of every
intermediate union available. For `(A | B) | C`, it normalizes `A | B`,
then seeds a fresh `UnionBuilder` with those members. If the
construction policy and inference context match, the builder only needs
to compare `C` with the existing members. This PR stores the `A | B`
object as the left operand of the outer union. If only the final type is
requested, one union builder visits `A`, `B`, and `C`. A second request
for that same union uses the cached type. The two approaches take about
the same time when a 256-class union is consumed once.

- For a chain of assignments like `U2 = U1 | Literal[2]`, each
assignment provides a new type-variable binding context. In #29304 that
context is part of the cache key, so these repeated assignments defeat
the caching, and extending the union can revisit its prefixes even when
nothing ever asks for their types. This accounts for the slowness of
#29304 on the "Extend a literal union across 1024 assignments" case
below.
- If `Prefix` is a long union and `(Prefix | X, Prefix | Y, ...)` is
checked against `tuple[TypeForm[Base], ...]`, every extension needs its
type. #29304 can reuse the normalized `Prefix` across those extensions.
This PR walks all its members for each distinct result. With 32
extensions, #29304 is nearly twice as fast. If this case turns out to
occur commonly in practice, we could add intermediate prefix caching as
well.

This PR is larger than #29304, which leaves the runtime union
representation largely intact and adds a path in `UnionBuilder` for
reconstructing an already normalized prefix. This PR needs extra
complexity in `UnionTypeInstance`, where specialization, cycle recovery,
and runtime checks need to distinguish the original operands from the
denoted type. But I prefer keeping the complexity of these
runtime-unions in the runtime-union implementation, rather than allowing
it to leak into the core `UnionBuilder`, as it does in #29304.

This also fixes a bug (as seen in xarray in the ecosystem report) with a
runtime check list `isinstance(x, float | int)`. Previously we would
simplify this union, eliminating `int`, but `int` and `float` are
separate classes at runtime, so we would then miss that the `isinstance`
check catches integers as well. Now the `isinstance` narrowing can look
at the original runtime operands instead.

## Test plan

I timed optimized builds on Apple silicon using the same main revision
(`157caa55a5`) for all three. The #29304 build applies its reviewed
patch (`a0f998e2b4`) to that base. Each entry is the median elapsed time
in milliseconds for six fresh, single-worker CLI checks with warm
filesystem caches.

| Workload | Main | #29304 | This change |
| --- | ---: | ---: | ---: |
| Construct an unused union of 256 specialized classes | 326 | 56 | 34 |
| Check the stored union against `TypeForm[CommonBase]` | 328 | 57 | 60
|
| Extend a literal union across 1,024 assignments | 46 | 3,789 | 29 |
| Extend a union across 128 assignments and consume each prefix | 77 |
95 | 79 |
| Check 32 extensions of the same 128-class prefix as type forms | 112 |
57 | 105 |

## Ecosystem report

At `dcc4ce9890`, xarray loses two false positives because `isinstance`
now recognizes integer chunk sizes. Three existing xarray diagnostics
include the types that actually reach those branches. Two truthiness
warnings in aioredis and dd-trace-py display the same union members in a
different order. All seven changes are expected. [Full ecosystem
report](https://github.com/astral-sh/ruff/pull/29727#issuecomment-6422364992)
2026-10-26 11:52:50 -07:00
Charlie Marsh d20ca8e9bf [ty] Skip predicates that cannot affect narrowing (#29736)
## Summary

A full isort check went from 218 ms to 184 ms in the ecosystem run, a
16% reduction. Across all 163 projects the average timing factor was
1.00x, and no diagnostics changed.

In this example, `ready` can be true for either type of `value`, so the
path that skips `isinstance` already accounts for both possibilities.

```python
from typing import reveal_type

def f(value: str | None, ready: bool) -> None:
    if ready or isinstance(value, str):
        reveal_type(value)  # str | None
```

| isort | Before | After | Wall time saved | CPU time saved | Runs per
build |
| --- | ---: | ---: | ---: | ---: | ---: |
| This patch alone | 336.0 ms | 296.9 ms | 11.6% | 6.8% | 20 |
| With #29701 and #29706 | 227.1 ms | 215.6 ms | 5.1% | 2.9% | 60 |
2026-10-26 11:38:59 -07:00
Charlie Marsh 9525a0c4e8 [suspicious] Add shared-clock-mock (#29074)
## Summary

This PR adds a `shared-clock-mock` rule in preview. Patching a clock or
sleep function on a shared module can affect unrelated threads or tasks,
including when the target is qualified with the consuming module:

```python
from unittest.mock import Mock, patch

from example import worker

sleep = Mock()
with patch.object(worker.time, "sleep", sleep):
    worker.retry()
```

Instead, you should replace the consuming module's binding to keep the
fake local:

```python
from types import SimpleNamespace
from unittest.mock import Mock, patch

from example import worker

sleep = Mock()
with patch.object(worker, "time", SimpleNamespace(sleep=sleep)):
    worker.retry()
```

We also flag patches of wall-clock, monotonic, and performance-counter
readings, plus `asyncio.sleep`. The rule recognizes `patch`,
`patch.object`, pytest-mock fixtures, and `monkeypatch.setattr` /
`delattr`. (Nested `.time` and `.asyncio` targets use a naming
heuristic, so a fixture that already installed a private clock can
produce a false positive.)

## Case analysis

The ecosystem run adds 601 diagnostics across eight projects.

### Counting sleep calls in LangChain

This reduced version of LangChain’s device-login test records the
polling interval before and after the server asks the client to slow
down.

```python
sleeps = []
monkeypatch.setattr(oauth_module.time, "sleep", sleeps.append)
login_chatgpt_device(store_path=tmp_path / "x.json", poll_interval=2.0)
assert sleeps[:2] == [2.0, 7.0]
```

The OAuth module does `import time`, so `oauth_module.time` and `time`
are the same object. If another thread calls `time.sleep(1)` between the
two polls, the recorder sees `[2.0, 1, 7.0]`. Installing
`SimpleNamespace(sleep=sleeps.append)` on `oauth_module.time` keeps that
call out of the list.

### Running out of clock readings in Superset

Superset tests how long a screenshot request waits for a lock. It
patches `superset.dashboards.api.time.monotonic` with
`side_effect=[10.0, 10.5, 11.0]` and patches `time.sleep` on the same
module. An unrelated `time.monotonic()` can consume the next reading.
The request can then see the deadline early or exhaust the iterator and
raise `StopIteration`. A private clock for `superset.dashboards.api`
needs both `monotonic` and `sleep`.

### Retrying an async request in Prefect

Prefect simulates three failed SQS requests and expects one two-second
backoff. The test uses
`@patch("prefect_aws.observers.ecs.asyncio.sleep")`, but the observer
does `import asyncio`. A second task awaiting `asyncio.sleep(0)` would
also call the mock and break `assert_called_once_with(2)`.

### A fixture that already installed a private clock

Ruff can see the same patch in two tests even though only one reaches
the shared module. For example, an autouse fixture can replace
`worker.time` before the test starts.

```python
@pytest.fixture(autouse=True)
def private_clock(monkeypatch):
    monkeypatch.setattr(worker, "time", SimpleNamespace(sleep=lambda _: None))


def test_retry(monkeypatch):
    sleep = Mock()
    monkeypatch.setattr(worker.time, "sleep", sleep)  # shared-clock-mock
    worker.retry()
```

The warning in `test_retry` is a false positive. At runtime the receiver
is the fixture’s `SimpleNamespace`. If the fixture returns that
namespace, the test can use `monkeypatch.setattr(private_clock, "sleep",
sleep)` and Ruff accepts it.

### A package with its own `time` module

Gevent ships a `gevent.time` module with its own `sleep` binding. The
dotted string is enough for Ruff to flag this reduced example.

```python
import time
from unittest.mock import patch

original_sleep = time.sleep
with patch("gevent.time.sleep"):
    assert time.sleep is original_sleep
```

The assertion passes. Callers that look up `gevent.time.sleep` see the
patch. A caller of standard-library `time.sleep` still gets the original
function. Ruff infers the owner from the `.time.sleep` suffix in a
string target, so it reports the wrong module here.
2026-10-26 11:15:49 -07:00
vkandBrent Westbrook 538b55873b [flake8-bandit] Describe what the rule actually checks for httpx (S113) (#27936)
Hi :)

## Summary

The S113 docs say it checks `requests` or `httpx` calls that omit
`timeout`, but that hasn't been true for httpx sincee #12213

httpx applies a default timeout, so leaving the argument out is fine
there -- only an explicit `timeout=None` gets reported

In the code the implicit case is only reported when the module is
`requests`

So this rewrites "What it does" & "Why is this bad?" to match what the
rule actually does

Doc comment only, no behaviour change -- closes #27868 

## Test Plan

Ran ruff 0.16.3 over three files:

- `httpx.get(url)` -> nothing reported
- `httpx.get(url, timeout=None)` -> S113, "timeout set to `None`"
- `requests.get(url)` -> S113, "without timeout"

`docs/rules` is generated rather than committed, so there's nothing to
regenerate here

Drafted with AI assistance (Claude Fable 5)

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2026-10-26 17:21:49 +00:00
David Peter 8d25f7d53b [ty] Django: minor test coverage improvements (#29729)
## Summary

Extend the Django foreign key example to cover passing the target model
by keyword and assigning `None` to a relation.
2026-10-26 17:55:55 +01:00
Carl Meyer 34cc89ea43 [ty] Benchmark narrowing large unions with class OR patterns (#29726)
## Summary

Add two benchmarks for the cost of narrowing a `match` subject whose
type is a large union. Both check the same three OR patterns against a
union of 45 or 90 classes. Each of those arms returns `v` from a pattern
such as `C0(value=v) | C1(value=v) | C2(value=v)`. Using
`typing.Union[...]` for the subject keeps the separate cost of inferring
a long chain of `|` expressions out of the measurement.

Refs astral-sh/ty#4815

## Test plan

Timed both inputs with ty targeting Python 3.12, then changed only the
match subject to `cast(object, subject)` and timed them again. All four
inputs checked without diagnostics. The table shows median CLI wall time
over five runs.

| Classes in the union | `match subject` | `match cast(object, subject)`
|
| --- | ---: | ---: |
| 45 | 76.7 ms | 32.9 ms |
| 90 | 180.5 ms | 36.7 ms |
2026-10-26 09:47:07 -07:00
Charlie Marsh e139fbd6e8 [ty] Reuse each generic context's inferable type variables (#29700)
## Summary

Checking 4,800 calls to a generic function with four parameters took 108
→ 100 ms of CPU time. Each parameter uses the same set of type
variables, but ty rebuilt that set for every argument. Ty now computes
it once per generic context.

Calls with plain `int` parameters were essentially unchanged. Checking
all of
[PyTorch](https://github.com/pytorch/pytorch/tree/ab92945cea42e47ae398aead69af0872d340ea0f)
showed no consistent speedup.
2026-10-25 19:17:52 -04:00
Charlie Marsh 229cb6ebdd [ty] Skip method signature inference for concrete decorator results (#29708)
## Summary

Checking
[`discord/state.py`](https://github.com/Rapptz/discord.py/blob/65232c38702be5844cf2ce865a4777eb1928b5d0/discord/state.py)
opened 100 Python files instead of 112. To decide whether `Interaction`
was abstract, we used to follow the return annotation on
[`Interaction.command`](https://github.com/Rapptz/discord.py/blob/65232c38702be5844cf2ce865a4777eb1928b5d0/discord/interactions.py#L355)
into `discord.ext.commands`.

```python
def cached_slot_property[Owner, Value](
    name: str,
) -> Callable[[Callable[[Owner], Value]], CachedSlotProperty[Owner, Value]]: ...

class Interaction:
    @cached_slot_property("_cs_command")
    def command(self) -> Command[Any, ..., Any] | ContextMenu | None: ...
```

The decorator always returns a `CachedSlotProperty`. The return
annotation on `command` determines the type of the cached value, but it
cannot make the descriptor abstract. We now skip the method’s signature
when its sole decorator rules out an abstract method or property.

Checking all of discord.py showed no measurable speedup.
2026-10-25 16:50:01 -04:00
Charlie Marsh b07f24cb66 [ty] Reuse class member lookups when checking overrides (#29716)
## Summary

A full [Home
Assistant](https://github.com/home-assistant/core/tree/c19ae285ede74cd64c3045a3a4aa43290c224f1b)
check averaged 20.47 s instead of 23.59 s across five paired runs on the
[measured
revision](https://github.com/astral-sh/ruff/commit/a3d3ecdc0229e1c046cab0f7cf6ad543d8d950e2).

When two bases define the same method, ty binds both methods to the
subclass to compare their signatures. A separate attribute check used to
bind them again, even though it accepts any pair of ordinary methods. Ty
now skips that second binding and reuses each base’s declarations across
subclasses. The [ecosystem
run](https://github.com/astral-sh/ruff/pull/29716#issuecomment-6397258602)
found no stable diagnostic changes.
2026-10-25 16:49:54 -04:00
Charlie Marsh 67db4ca68f [ty] Check for instance storage without inferring method assignments (#29709)
## Summary

Checking Home Assistant’s [HomeKit light
module](https://github.com/home-assistant/core/blob/a0162d2ff0ed061501405a617961be149b15bd1f/homeassistant/components/homekit_controller/light.py#L53)
opened 38 Python files instead of 45 and used 10% fewer instructions.

When a class declares `value: int = 0`, ty may still need to know
whether a method writes `self.value`. It used to infer `make()` in
`self.value = make()` just to answer that question. Ty now stops once it
finds a reachable assignment. The annotation supplies the type; checking
the method still validates the assigned value.
2026-10-25 16:17:03 -04:00
Charlie Marsh bfa2407e98 [ty] Skip script discovery when uv script environments are disabled (#29697)
## Summary

With two files open in Home Assistant, skipping script discovery reduced
the language server’s mean time to first diagnostics from 465 ms to 354
ms on Linux (24%). Previously, ty indexed 10,768 files looking for PEP
723 scripts, even though uv integration was off by default and ty had no
script environments to prepare.

We now scan for scripts to prepare uv environments at startup and on
reload only when uv script environments are enabled (`TY_UV=scripts` or
`TY_UV=1`). With uv off, ty still reads an open script’s inline
settings.

The benchmark client doesn’t register file watchers. With dynamic file
watching enabled, ty still scans for script-specific import paths; five
paired runs showed no startup gain in that configuration.
2026-10-25 07:11:44 -04:00
Charlie Marsh 157caa55a5 [ty] Avoid redundant work when checking inherited attributes (#29699)
## Summary

When a class inherits from multiple bases, ty resolves every inherited
attribute on the subclass before it knows whether another base defines
the same name. Binding a descriptor can specialize a method signature or
infer a property's read and write types. None of that is needed if
there’s nothing to compare.

This defers binding until we find a second attribute contract. It also
deduplicates bases that appear both in the MRO and through the explicit
bases. Distinct generic specializations are still checked in order.
2026-10-25 02:52:44 +00:00
Charlie Marsh 115138631c [ty] Collect receiver annotations once per class (#29703)
## Summary

Home Assistant’s `Entity` class reads or writes 106 different `self`
attributes across 70 methods, but none of those methods annotate one.
When checking overrides, ty could search all 70 methods for each name
just to determine that there was no declaration.

Collect receiver annotations once per class and reuse that list when
checking overrides. When inferring an implicit attribute, skip names
that the class only reads: a read of `self.x` does not define `x`.

On a class with 256 methods that each read eight different attributes,
median CPU time dropped from 896 ms to 126 ms over six runs. ty still
reported the one incompatible annotation. Two full Home Assistant checks
on Linux went from 47.0 to 45.4 s and from 49.4 to 49.2 s, with about 21
MiB less peak memory and the same 13,419 diagnostics.
2026-10-24 22:13:44 -04:00
Charlie MarshandCharlie Marsh fff823d2a2 [ty] Skip inferred writes when a declaration determines an instance member (#29702)
## Summary

In a reduced example from
[PyTorch](https://github.com/pytorch/pytorch/blob/ab92945cea42e47ae398aead69af0872d340ea0f/torch/_dynamo/variables/misc.py#L2755),
ty now opens 8 Python files instead of 68 to resolve
`tx.output.side_effects`. Across four profiling runs with a single
worker, median CPU time fell from 1.20 s to 0.25 s for that example and
from 1.99 s to 1.54 s for the full `misc.py` file. Diagnostics were
unchanged.

When looking up an instance attribute, ty inferred assignments in method
bodies before checking whether an annotation already determined the
type. Those assignments could pull in modules that the lookup never
needed. We now return the declared type first. If a subclass default
shadows an inherited declaration, augmented assignments such as `self.x
+= 1` in a base class can still affect the attribute’s type, so we
continue to collect them.

Co-authored-by: Charlie Marsh <charliemarsh@openai.com>
2026-10-24 22:13:17 -04:00
Carl Meyer 047476ab16 [ty] Avoid expanding declared bounds in captured ParamSpecs (#29649)
Passing overloaded generic functions to `Callable[P, object]` can cause
the constraint solver to enumerate combinations of signatures
specialized to their declared type-variable bounds. The captured
signatures already retain those bounds, so this adds substantial work
without new call-site evidence.

Track the replacement constraint's provenance and skip this
specialization for locally bound variables in captured ParamSpec
signatures when the replacement comes solely from a validity constraint.
Evidence and mixed constraints still specialize local variables, and
free variables retain normal substitution, including expansion of free
ParamSpecs into overloads.

Preserving the generic parameters also lets a callback constrained to
`(str, bytes)` receive either type when forwarded through a `ParamSpec`,
instead of being restricted to its first constraint.

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

## Test plan

Compared against `90bcf19c58b4` using the same Linux/aarch64 environment
and optimized dev profile (Rust 1.99.0, opt-level 1, LTO off):

- The issue's NumPy reproducer, using NumPy 2.5.4 and Python 3.12
semantics, improved from an 11.03 s mean across three runs to 0.101 s
across ten runs (about 110× faster). Every run completed without
diagnostics.
- The six-overload benchmark improved from 225 ms to 22.5 ms across ten
Criterion samples (about 10× faster), with no diagnostics before or
after.

## Ecosystem report

Analyzed head `5c07a3b83f78d45ccf9994f0bfa681ee25840b9a`: one expected
stable `invalid-argument-type` diagnostic rewrite in torchvision, now
describing both constraints of the generic callback. The invalid
argument remains rejected, and no regression was found in the reproduced
case. [Full ecosystem
report](https://github.com/astral-sh/ruff/pull/29649#issuecomment-6351757125).
2026-10-24 16:48:31 -07:00
Charlie Marsh 9d3c89e9d8 [ty] Bound tuple expansion in type displays (#29691)
## Summary

With 18 assignments of the form `b = (a, a)`, `ty check --output-format
concise` used to print 3 MiB for a single `call-non-callable` error. It
now prints 1.7 KiB.

Each assignment reuses the previous tuple type twice, so spelling out
the type in the error message doubles the number of `int`s at each step:

```python
def f(x: int):
    a = (x, x)
    b = (a, a)
    b()  # Object of type `tuple[tuple[int, int], tuple[int, int]]` is not callable
```

We now share a budget of 256 elements across a tuple and its nested
tuples. If a tuple's elements exceed the remaining budget, we display it
as `tuple[...]`. Deep tuples still display in full when they fit. Inlay
hints omit the text edit when the type contains an abbreviation, so
editors won't insert `tuple[...]` as an annotation.
2026-10-24 19:22:47 -04:00
Carl Meyer ea1865842b [ty] Explain invalid aliases at their definitions (#29654)
Builds on #29200.

When an invalid implicit alias is used as a type, the error now explains
why its original right-hand side is invalid and points to that
expression. For example:

```py
Called = type(1)

# snapshot: invalid-type-form
def inspect(called: Called):
    reveal_type(called)  # revealed: Unknown
```

```text
error[invalid-type-form]: Variable is not allowed in parameter annotations
  --> src/mdtest_snippet.py:14:21
   |
14 | def inspect(called: Called):
   |                     ^^^^^^
info: Function calls are not allowed in type alias values
  --> src/mdtest_snippet.py:11:10
   |
11 | Called = type(1)
   |          ^^^^^^^
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
```

No other type checker provides this kind of detailed origin explanation
when a value is not usable as a type alias.

Follow imports and forwarding assignments to the original definition.
Collect these explanations independently of diagnostics in the defining
file, so unchecked dependencies and local suppressions do not hide them.
Ordinary assignments remain valid at runtime; the error is reported only
where the value is used as a type.
2026-10-23 16:32:27 -07:00
Carl Meyer c08d12bfc9 [ty] Infer implicit aliases from their definition as type expressions (#29200)
https://github.com/astral-sh/ruff/pull/28425 added a new way to infer
implicit and PEP 613 type aliases, which I'd previously described in
https://github.com/astral-sh/ty/issues/1711. Rather than using the value
type of the alias and converting it back to its type-annotation meaning,
this system goes back to the original definition and infers it directly
as a type expression. This gives us more robust validation that implicit
type aliases are valid type expressions, and allows us to emit accurate
diagnostics both for invalid-as-type-expression and for
invalid-at-runtime.

https://github.com/astral-sh/ruff/pull/28425 used this new approach only
for recursive type aliases; this PR expands it to all implicit and PEP
613 type aliases, so that we consistently get its benefits. This also
significantly improves our conformance results for implicit type
aliases; we now pass two more conformance files.

This is an example of the sort of thing that we previously accepted as a
type alias, and now reject (as the conformance suite says we should):

```py
BadAlias = (lambda: int)()

def f(value: BadAlias):  # Main: int, Now: invalid-type-form
    ...
```

This also makes us more consistent (with ourselves, other type checkers
,and the conformance suite) when a generic type is used "bare" in an
alias. The core rule we use is that the RHS of an implicit or PEP 613
type alias is a type expression, therefore a use of a bare generic alias
in that type expression should be implicitly default-specialized.

```py
from typing import TypeVar

T = TypeVar("T", default=str)

GenAlias = list[T]
AnotherAlias = GenAlias | int

# this is now an error: AnotherAlias is not generic
def _(x: AnotherAlias[bytes]): ...

# The bare reference to GenAlias inside AnotherAlias is default-specialized:
def _(x: AnotherAlias):
    reveal_type(x)  # list[str] | int
```

We do make one carve-out from this rule: we consider a plain assignment
like `A = B` or `A = module.B` to be a direct "rename" or "forward" that
doesn't really treat the RHS as a type expression, and thus doesn't
default-specialize anything; instead `A` just becomes a new name for
`B`. This feels more intuitive, and matches pyrefly and pyright. This
carve-out does not apply to PEP 613 type aliases: `A: TypeAlias = B` is
explicitly a type alias, not a rename, so it does default-specialize
`B`.

Reject computed values used as types, including values that evaluate to
classes or `Any`, and preserve that validation when `invalid-type-form`
is disabled. Definition lookup stays behind a tracked query so unrelated
edits in an imported module do not invalidate consumers.

Fixes https://github.com/astral-sh/ty/issues/1711.
Fixes https://github.com/astral-sh/ty/issues/1846.
Prepares https://github.com/astral-sh/ty/issues/4683. Runtime `|`
behavior is not changed here.

Source-location diagnostic explanations for invalid aliases are added in
the follow-up #29654.

## Ecosystem

[Full ecosystem
analysis](https://github.com/astral-sh/ruff/pull/29200#issuecomment-6353595871)
for `b175c73` covers all 150 diagnostic changes across 21 projects: 61
additions, 58 removals, and 31 message changes. New diagnostics
primarily validate computed values used as types and default-specialized
explicit aliases. Improved alias inference removes cascading errors and
preserves forward references and generic parameters.

The nine new discord.py false positives match the known
[ty#4016](https://github.com/astral-sh/ty/issues/4016) callback-context
limitation and are accepted for this PR. No other new false positives
were identified.

Conformance gains six expected diagnostics (five in
`aliases_implicit.py`, one in `aliases_explicit.py`) and two fully
passing files, with no new conformance false positives.
2026-10-23 16:32:27 -07:00
Brent Westbrook 7060eed2be Clean up Emitter trait (#29680)
Summary
--

As pointed out in [#29635], we still had this `Emitter` trait, despite
almost all output formats
moving to `ruff_db` and its `Renderer` trait. This PR cleans up the last
use of `Emitter` in Ruff's
`Grouped` output format, as well as some custom settings handling in the
`GroupedEmitter` that is
now addressed by the shared `DisplayDiagnosticConfig`.

If we want to move this output format to `ruff_db` or share it with ty
in the future, this should
also help with that.

Test Plan
--

Existing tests

[#29635]:
https://github.com/astral-sh/ruff/pull/29635#discussion_r4362259048
2026-10-23 17:41:48 -04:00
Anish Giri 601192dfdb [pyupgrade] Avoid incorrect conversions to f-strings (UP032) (#25485)
## Summary

Fixes part of #15874: UP032 emitted broken fixes for str.format calls
with no f-string equivalent. Non-identifier attributes, quote-clashing
or backslash keys, unknown conversions, and mixed numbering are now
skipped. Signed indices like {+0} were a parser bug (leading +
accepted), which also affected F523/F524

## Test Plan

New mdtest at crates/ruff_linter/resources/mdtest/pyupgrade/f-string.md
plus parser unit tests
2026-10-23 16:24:46 -04:00
Charlie Marsh 5e90790d11 Compute the lexer keyword hash at compile time (#29677)
## Summary

Changing the lexer's keyword set now takes one edit to the Rust list.
The compiler finds a collision-free multiplier and builds the 64-slot
table, so we can remove the Python generator and its lockfile.

A two-byte keyword like `if` packs to `0x6669_0000_0000_0000`. Only the
low two bytes of the multiplier affect its slot. We place keywords from
shortest to longest, choosing each new byte of the multiplier without
disturbing the shorter keywords. The lexer still selects a slot with one
multiply and shift, then compares the full word.
2026-10-23 15:29:49 -04:00
Douglas Creager b4d38343dd [ty] Prioritize finite members in protocol implementation checks (#29542)
This patch has us compare members during protocol-to-protocol
comparisons using the same "structural priority" that we use during
nominal-to-protocol comparisons. This ensures that we check simple
non-recursive members first, often allowing us to determine a (failed)
comparison result before having to perform any expensive recursive
member checks.
2026-10-23 15:08:30 -04:00
Brent Westbrook 8334c5c57d [ty] Add support for SARIF output (#29635)
Summary
--

This PR fixes astral-sh/ty#2484 by moving our SARIF rendering from
`ruff_linter` into `ruff_db` and
wiring that up as an output format for ty.

The first commit introduces the metadata and makes some other
preparatory changes to the SARIF
emitter in Ruff. The second commit then moves the emitter to `ruff_db`
and removes the remaining
Ruff-specific calls. The third commit then wires that up to ty's CLI.

Test Plan
--

Existing tests, adapted to `ruff_db`, as well as a new ty CLI test
2026-10-23 14:57:03 -04:00
Charlie Marsh e3304857ec Specialize ASCII identifier lexing (#29503)
## Summary

We give ASCII identifiers a dedicated path, avoiding Unicode bookkeeping
and checking string prefixes only when a one- or two-letter name reaches
a quote. Both paths share the existing batched ASCII scanner, preserving
fast scanning for Unicode names with long ASCII suffixes.

Stacked on #29463 to measure the incremental improvement. [CodSpeed
reports](https://github.com/astral-sh/ruff/pull/29503/checks?check_run_id=118300735426)
6.6–9.8% greater efficiency across all five lexer benchmarks, with no
reported regressions. We preserve the existing scanner and add no
inlining attributes or unsafe code.
2026-10-23 13:58:34 -04:00
Charlie Marsh de9d86e245 Speed up identifier and keyword lexing (#29463)
## Summary

We scan ASCII identifier continuations in sixteen-byte batches using a
byte lookup table, avoiding per-character decoding. Keyword lookup
filters by length and initial byte, then uses a compact hash table with
a full-word comparison to reject collisions. Non-ASCII suffixes retain
the existing Unicode path.
2026-10-23 13:58:33 -04:00
Zanie Blueandzaniebot 5fabb6f32c Handle AI policy labels through Actions (#29676)
Handle `bot:ai-policy-comment` and `bot:ai-policy-close` on issues and
pull requests with the shared `ai-policy` action. The automation
dispatcher runs `ai-policy.yml` on `main`, where an STS-issued
`astral-automations-bot` token posts the policy reminder and optionally
closes the target.

Draft and fork PRs are supported. The workflow checks the current label
and state without checking out PR code.

Co-authored-by: zaniebot <242828183+zaniebot@users.noreply.github.com>
2026-10-23 16:42:22 +00:00
Douglas Creager 0f3bca2ffa [ty] Short-circuit top/bottom signature comparisons (#29537)
The bottom signature is trivially assignable _to_, and the top signature
is trivially assignable _from_, all other signatures. We can add fast
path checks for those before performing more complex signature
comparisons.
2026-10-23 11:43:25 -04:00
David Peter 005a313334 [ty] Django: synthesize <field>_id attributes for foreign keys (#29664)
## Summary

This PR adds support for the `<field>_id` attributes that Django
generates for foreign keys. Their types follow the related model's
primary key, or the field specified by `to_field`.

Example:

```py
from django.db import models

class Author(models.Model):
    pass

class Book(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

def _(book: Book):
    reveal_type(book.author)  # Author
    reveal_type(book.author_id)  # int
```

Towards https://github.com/astral-sh/ty/issues/1018.

## Test Plan

New Markdown tests. I checked against Django runtime behavior.
2026-10-23 16:42:14 +02:00
David Peter 9dfc6ee97c [ty] Django: synthesize get_{next,previous}_by_<field>() methods (#29653)
## Summary

This PR adds support for the `get_next_by_<field>()` and
`get_previous_by_<field>()` methods that Django generates for
(non-`null=True`) `DateField` and `DateTimeField` fields.

Example:

```py
from django.db import models

class Article(models.Model):
    published = models.DateField()

def _(article: Article):
    reveal_type(article.get_next_by_published())      # Article
    reveal_type(article.get_previous_by_published())  # Article
```

Towards https://github.com/astral-sh/ty/issues/1018.

## Test Plan

New Markdown tests.
2026-10-23 16:34:11 +02:00
Brent Westbrook c8589d0eb1 Move diagnostic format dispatch into Ruff and ty (#29647)
Summary
--

This addresses the suggestion from [#29635]. `DisplayDiagnosticConfig`
previously determined the
`DiagnosticFormat` for diagnostics coming from both Ruff and ty, which
meant that it had to have a
shared enum between the two, despite each program supporting a different
subset of the available
formats. This PR eliminates the shared `DiagnosticFormat` and dispatch
through `DisplayDiagnostics`,
leaving Ruff and ty to construct `Renderer`s directly and do their own
dispatch on the output
formats they actually support. This will be especially helpful in
#29635, where we want to provide
slightly different arguments to each new `SarifRenderer`.

The `Renderer` trait itself isn't strictly necessary, but it made the
calls on the Ruff side a lot
neater since the trait provides a default `write` method based on the
required `render` method.
Codex also threw in a default `display` method that helps in a couple of
places too.

Test Plan
--

Existing tests

[#29635]:
https://github.com/astral-sh/ruff/pull/29635#discussion_r4352505588
2026-10-23 09:38:24 -04:00
David Peter 2fa2ecbbf7 [ty] Django: synthesize get_<field>_display() methods (#29651)
## Summary

This PR adds support for the `get_<field>_display()` methods that Django
generates for model fields with `choices`.

Example:

```py
from django.db import models

class Article(models.Model):
    status = models.CharField(
        max_length=1,
        choices=[("D", "Draft"), ("P", "Published")],
    )

article = Article(status="P")

reveal_type(article.status)                # str
reveal_type(article.get_status_display())  # str ("Published" at runtime)
```

Towards https://github.com/astral-sh/ty/issues/1018.

## Test Plan

New Markdown test.
2026-10-23 02:40:15 +02:00
zaniebot f12318671d [ty] Remove the weekly ecosystem report (#29662) 2026-10-23 00:02:15 +00:00
Zanie Blueandzaniebot b5968c8a8d Omit the report ZIP link when HTML is available (#29661)
Report comments include a redundant ZIP download link after the hosted
HTML links. Show the ZIP link only when the HTML report is unavailable,
including when its upload fails.

Co-authored-by: zaniebot <242828183+zaniebot@users.noreply.github.com>
2026-10-22 17:00:29 -07:00
Alex Waygood 10056500e0 Update uv to 0.13.5 (#29659) 2026-10-22 23:29:36 +00:00
dependabot[bot] 0e190851d3 Bump black from 26.3.1 to 26.10.1 in /scripts/benchmarks (#29657)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 23:25:25 +00:00
7fab688f61 [ty] Host ecosystem reports from pull requests (#29625)
ty ecosystem comments currently require downloading a ZIP to view the
full reports. Add an optional `upload-html` mode to the shared report
publisher and enable it for ty, linking directly to the diagnostic diff
and timing results while retaining the downloadable artifact.

The publisher runs from `main`, uses STS for GitHub credentials, and
obtains temporary AWS upload credentials through GitHub OIDC when
hosting is enabled. Each caller supplies filenames and content types,
with optional link labels. The publisher reads only those files from a
bounded archive without extracting or executing them during publication,
and stores reports under separate paths for each PR, report, run, and
attempt. If an upload fails, the publisher still attempts to post the
summary and, when available, the artifact link.

The `automations` environment requires `REPORTS_BUCKET`, `REPORTS_URL`,
`REPORTS_ROLE_ARN`, and `REPORTS_REGION` from the report hosting
infrastructure.

---------

Co-authored-by: Zanie Blue <2586601+zanieb@users.noreply.github.com>
Co-authored-by: zaniebot <242828183+zaniebot@users.noreply.github.com>
2026-10-22 22:44:25 +00:00
Alex Waygood a51aeefc9a Remove unnecessary checkouts from release jobs (#29655) 2026-10-22 15:40:10 -07:00
Alex Waygood 86c978007e Build the Ruff playground earlier in releases (#29650) 2026-10-22 14:34:35 -07:00
David Peter 30be8844b9 [ty] Very basic Django support (#29642)
## Summary

This PR adds basic tests for Django support in ty, and adds a first
customization point where we now synthesize `.id` and `.pk` attributes
for primary keys on Django models. Django support currently relies on
`django-types`, a fork of `django-stubs` which requires less
special-casing in type checkers.

Example
```py
from django.db import models

class Book(models.Model):
    isbn = models.CharField(max_length=13, primary_key=True)
    title = models.CharField()
    pages = models.IntegerField(null=True)

def _(book: Book):
    reveal_type(book.isbn)   # str
    reveal_type(book.title)  # str
    reveal_type(book.pages)  # int | None  (because null=True)

    reveal_type(book.pk)     # str  (because `isbn` is the primary key)

    book.id  # error: no automatic `id` with a custom primary key
```

## Test Plan

I wrote new Markdown test and also verified all of our assertions
against Django's runtime behavior.
2026-10-22 20:20:48 +00:00
zaniebotandzaniebot 7d2a3c8b12 Remove unused mold setup from CI (#29639)
Linux CI installs `mold` by replacing the system linker, but the current
Rust toolchain and MSRV already select bundled `lld` for
`x86_64-unknown-linux-gnu`. Remove the installation steps to avoid
downloading `mold` and changing the system linker for these jobs.

[Eight pairs of `ruff` debug
builds](https://github.com/astral-sh/ruff/actions/runs/39841713358) on
16-vCPU CI runners averaged 28.6 s with `mold` installation and 27.5 s
without. Each pair built both variants on the same runner from empty
target directories, with build order alternated. Times include
installation when applicable and exclude dependency downloads. The mean
change was −1.1 s (two-sided 90% paired t confidence interval: −8.8 to
+6.7 s); the interval includes zero. The second build was faster in
every pair, indicating a substantial order effect.

Co-authored-by: zaniebot <242828183+zaniebot@users.noreply.github.com>
2026-10-22 13:16:51 -07:00
zaniebotandAlex Waygood a7ad638dd7 Include the release workflow in formatting checks (#27884)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2026-10-22 20:08:53 +00:00
Charlie Marsh 2db88da7aa [ty] Stabilize recursive union redundancy checks (#29589)
## Summary

We make redundancy comparisons converge when they participate in a Salsa
inference cycle. Comparing inferred callable signatures can depend on
the union being simplified: removing an alternative changes the inferred
signatures, which can make the next comparison return a different
answer.

The query still starts with `true`, but combines successive results with
`previous && current`. Once a comparison returns `false`, it remains
false for that cycle. Retaining the union alternative prevents the
comparison from oscillating; the tradeoff is that a redundant
alternative may remain in the union.

This is a prerequisite for #29553, whose stable lambda identities expose
this cycle. The regression examples belong to that PR because they
already terminate on `main`.
2026-10-22 16:01:34 -04:00
Alex Waygood b755550246 Skip compression for release archive uploads (#29643) 2026-10-22 12:56:13 -07:00
Alex Waygood 21224a7476 Build global artifacts without waiting for Docker and WASM (#29644) 2026-10-22 12:55:38 -07:00
zaniebot 356d3c9b96 Document manual maintenance of the release workflow (#27853) 2026-10-22 12:54:33 -07:00
zaniebotandAlex Waygood b54e827e1b Quote GitHub release arguments (#27888)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2026-10-22 12:49:49 -07:00