190 Commits
Author SHA1 Message Date
Charlie Marsh 691b60a3f3 [ty] Fix a panic when checking truthiness in a loop (#29759)
## Summary

Ty could panic on `assert predicate` inside a loop where `predicate`
starts as a lambda and a later iteration rebinds it to a class. When
cycle recovery added the class to the operand type, ty looked up its
metaclass to determine its truthiness. That lookup could introduce a new
inference cycle while Salsa was recovering the first one.

Reachability now evaluates the predicate from the operand types that
expression inference has already normalized. Outside loops, it caches
the result so repeated walks of the same condition can reuse it. Inside
a loop, a condition can depend on an assignment it guards, so the caller
reads the inferred operands directly. Expression inference no longer
stores predicate truthiness or recomputes it during recovery.

The 163-project ecosystem run produced identical diagnostics. Checking
isort took 291 ms, up from 248 ms on the base revision, which gives back
some of the speedup from #29706. The average timing factor across
projects was 1.02×. CodSpeed found no significant changes among the 214
benchmarks it compared. [Ecosystem
diff](https://dqhfo45wdv0ek.cloudfront.net/ruff/pull-requests/29759/ty-ecosystem/40467940782/1/diff.html)
·
[Timings](https://dqhfo45wdv0ek.cloudfront.net/ruff/pull-requests/29759/ty-ecosystem/40467940782/1/timing.html)
·
[CodSpeed](https://github.com/astral-sh/ruff/pull/29759#issuecomment-6435864937)

Closes https://github.com/astral-sh/ty/issues/4834.
2026-10-27 07:30:58 -07:00
Charlie Marsh fbf83e2268 [ty] Avoid replaying conditions in long boolean expressions (#29754)
## Summary

Checking Home Assistant’s Anthropic integration took 283.1 ms before
this change and 252.6 ms after (10.8% less time, with a 95% paired
interval of 7.8–13.7% less). Each of 24 blocks ran `ty check
homeassistant/components/anthropic` twice with the baseline build and
once with the changed build, in balanced, randomized order. All runs
emitted the same 41 lines of diagnostics. The full Home Assistant
timings did not resolve this patch’s individual contribution.

The Anthropic SDK’s synchronous and asynchronous streaming functions
each test 53 event names in one `or` expression. At each operand, ty
used to reapply every condition leading up to it, recording 1,378
conditions per expression. The continuation already carries those
earlier conditions, so adding just the preceding operand’s condition
brings the count down to 52.

Checking a file that imports the two original function bodies went from
95.9 ms to 19.3 ms. Changing just those two `or` chains to tuple
membership checks made both builds take about 12 ms. Toggling prefix
replay in a single release executable also reproduced the integration
improvement (270.1 ms to 237.5 ms).

An assignment expression (`:=`) can introduce a new binding partway
through a chain. Those chains keep the earlier conditions so the
reassigned name gets the same reachability information.
2026-10-26 23:14:59 -07:00
Charlie Marsh 8aeb8e2ebc [ty] Reuse exception checkpoints when a new place is read (#29750)
## Summary

Cold CLI checks of Home Assistant’s KNX integration took 14% less time,
and checks of its Reolink integration took 4.5% less time. Each row
averages 12 paired runs of `ty check
homeassistant/components/<integration>` with the project’s dependencies
installed. Both integrations emitted the same diagnostics before and
after.

| Integration | Before | After |
| --- | ---: | ---: |
| KNX | 389.5 ms | 334.9 ms |
| Reolink | 396.0 ms | 378.1 ms |

The KNX decoder has a long sequence of branches inside a `try` block.
Reading a name like `GroupValueRead` for the first time used to trigger
another snapshot of the bindings visible to the exception handler.
Restoring an earlier snapshot already gives that name the same undefined
state. We now leave the checkpoint’s binding revision alone on that
first read and advance it when the place gets a definition.

In isolation, a cold CLI check importing the original
`xknx.APCI.from_knx` body went from 84.5 ms to 36.4 ms (57%). For
`reolink_aio.Host.map_channel_json_response`, it went from 41.9 ms to
32.4 ms (23%).
2026-10-26 21:48:18 -07:00
astral-automations-bot[bot]andBrent Westbrook ba65abf8ba Bump version to 0.17.3 (#29745)
Co-authored-by: astral-automations-bot[bot] <305554984+astral-automations-bot[bot]@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2026-10-26 20:14:27 -04:00
Carl Meyer 95888b2edf [ty] Preserve narrowing after guarded match subject captures (#29558)
When a match case captures the subject under its own name and its guard
fails, later cases now narrow that capture. Previously, those cases
could retain a wider type because narrowing targeted only the binding
read before the match.

Retain bindings that capture the entire subject under its original name,
including `as` patterns and OR alternatives. Captures of extracted
values and assignments made by guards remain distinct from the saved
subject.

Fixes astral-sh/ty#4761.
2026-10-26 15:21:49 -07:00
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
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
astral-automations-bot[bot]andBrent Westbrook 30516b42ab Bump version to 0.17.2 (#29637)
Co-authored-by: astral-automations-bot[bot] <305554984+astral-automations-bot[bot]@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2026-10-22 13:54:24 -04:00
astral-automations-bot[bot] 4683270c69 Bump version to 0.17.1 (#29420)
Co-authored-by: astral-automations-bot[bot] <305554984+astral-automations-bot[bot]@users.noreply.github.com>
2026-10-15 10:56:41 -04:00
Alex Waygood a7b52a1565 [ty] Preserve distinct string literal displays without deprecated ICU data (#29206)
Canonical equivalence can make distinct string values look the same. ty
escapes their displays to distinguish them, but the current approach
relies on ICU4X's deprecated `NfcInert` property, which [ICU4X plans to
remove](https://github.com/unicode-org/icu4x/issues/7892), and can
escape characters that do not need escaping.

Replace inertness checks with NFC checks on runs of unescaped text. When
adding a character would make a run non-NFC, escape that character and
continue checking after it. The resulting string literal is in NFC and
evaluates to the original value, so canonically equivalent values remain
distinguishable.

For example, `\u212b` (the Angstrom sign) is canonically equivalent to
`Å`. ty now keeps the unrelated `é` visible while still distinguishing
the two values:

```py
reveal_type("éÅ")
# main:    Literal["éÅ"]
# this PR: Literal["éÅ"]

reveal_type("é\u212b")
# main:    Literal["\xe9\u212b"]
# this PR: Literal["é\u212b"]
```

The change also uses Unicode grapheme-break properties to keep
characters visually separate from the quotes and escape sequences around
them. It escapes the braille blank because it can look like an ordinary
space.
2026-10-13 15:04:47 +00:00
David Peter 350c634ad1 [ty] Extract small is_end_of_scope_reachable helper (#29320)
Extract `is_end_of_scope_reachable(db, ScopeId)` into the reachability
module and use it when checking ordinary and generator functions for
implicit `None` returns.

This will allow us to use this helper in the return type inference
implementation (#29253).
2026-10-13 10:26:05 +02:00
astral-automations-bot[bot]andBrent Westbrook 5b27c1ccfa Bump version to 0.17.0 (#29259)
Co-authored-by: astral-automations-bot[bot] <305554984+astral-automations-bot[bot]@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2026-10-09 13:45:29 -04:00
Charlie Marsh 3ac6b4c312 [ty] Represent uninhabited truthiness explicitly (#28843)
## Summary

We now mark both branches of a condition with type `Never` as
unreachable:

```python
from typing import Never

def f(value: Never):
    if value:
        result = 1  # Unreachable.
    else:
        result = 2  # Also unreachable.
```

Previously, we treated `Never` as having ambiguous truthiness, allowing
either assignment to affect inference. In the reported loop, the
variables depend on each other, so including these impossible
assignments made their inferred types depend on which file we checked
first.

We add `Truthiness::Uninhabited` to represent the absence of either
boolean outcome. `Type::bool` and `Type::try_bool` return this variant
for `Never`, replacing the opt-in `bool_if_inhabited` API and making
callers handle the distinction explicitly.

Closes https://github.com/astral-sh/ty/issues/4577.
2026-10-08 20:46:56 -04:00
Zsolt Dollenstein e0da489892 [ty] Invalidate dependency metadata when environments change (#29126)
`Project::dependency_metadata` resolves Python environments inside a
Salsa query without tracking those inputs. This can leave
invalid-environment, missing-environment, and environment-mismatch
diagnostics stale after an environment changes.

Store the selected environment's resolution result in `ProgramSettings`
and track uv's environment directory through Salsa. Resolve uv's
canonical path whenever the query runs so it observes symlink changes
even when uv metadata is unchanged.

After a failed selection, retain the last usable environment for
watching and recovery, and preserve the checking program's search paths.
Dependency metadata reports the current environment error.

Fixes astral-sh/ty#4604.
2026-10-07 14:27:03 +00:00
Gore WilliamsandCharlie Marsh 433704ea75 [ty] Narrow tuple unions through truthiness and type checks (#29125)
## Summary

We now narrow unions of tuples when an element is checked for truthiness
or with `isinstance`, `issubclass`, or `TypeIs`. Eliminating an
alternative also narrows the tuple’s other elements, preserving the
relationship between a tag and its associated value. For example,
checking a boolean literal tag selects the corresponding callable
signature in either branch.

We reuse the tuple-union filtering used by equality checks and register
the containing tuple as a possible narrowing target for call predicates.
We keep uncertain alternatives and skip filtering for non-literal or
out-of-bounds indices. `TypeGuard` checks retain their existing
behavior.

For truthiness checks and calls, we skip the additional tuple narrowing
if the expression contains an assignment (`:=`). Evaluating the
expression can replace the tuple after it has been read. This
conservatively includes harmless assignments to other names; a TODO test
records that limitation.

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

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2026-10-06 14:22:17 -04:00
Charlie Marsh f0214e0c6f [ty] Limit wildcard exports using literal __all__ (#28972)
## Summary

When a module imports a large namespace and then defines a small
`__all__`, we currently include every imported name when another module
uses a wildcard import. Packages with many such modules can create
hundreds of thousands of unnecessary definitions and reachability
constraints.

This PR instead recognizes an unchanged literal `__all__` before
semantic indexing and limits the possible exports to its names. Other
uses of `__all__`, including mutations and later wildcard imports, fall
back to the existing analysis. So this fixes narrow (but non-contrived)
cases.

For the `pulumi-cloudflare` reproduction in
[astral-sh/ty#4606](https://github.com/astral-sh/ty/issues/4606), peak
memory goes from 5,649-6,344 MiB to 453-459 MiB.

This also changes some diagnostics by avoiding cycles during import
resolution. Previously, determining the contents of `__all__` could
require type inference that depended on imports we were still resolving,
leaving some types as `Divergent`. Reading the literal directly breaks
those cycles, so we resolve more types successfully. This removes
spurious warnings and enables checks that were previously skipped
because the relevant types were unresolved.
2026-10-05 07:00:34 -04:00
astral-automations-bot[bot]andBrent Westbrook 3265ed1f94 Bump version to 0.16.10 (#29055)
Co-authored-by: astral-automations-bot[bot] <305554984+astral-automations-bot[bot]@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2026-10-01 13:40:09 -04:00
Micha Reiser 226201fef0 [ty] Refresh Python settings when virtual environments change (#28650)
## Summary

This PR is a first step towards updating the project's uv metadata when
the virtual environment changes. The scope of this PR is to add the
non-uv specific virtual environment file watching. That is, it adds the
necessary file watching handling to update the detected virtual
environment and its Python version when:

* a `pyenv.cfg` changes
* the default `.venv` or the explicitly specified `environment.python`
virtual environment gets created or deleted.


Unfortunately, this does require storing the path to the resolved
virtual environment in `ProgramSettings`. Otherwise, we'd have to
resolve it every time we see a related file watcher event. It's
unfortunate because it doesn't feel like a setting. But there isn't
really any better to carry forward this information.


I recommend reviewing this PR commit by commit.


## Test Plan

Native file-watching regression tests pass.
2026-09-27 18:48:09 +02:00
Charlie Marsh a1f59fb6a3 [ty] Index nonlocal bindings when sweeping snapshots (#28935)
## Summary

The nonlocal snapshot sweep searches later scopes separately for every
enclosing snapshot. We now index the last scope that binds each nonlocal
name and reuse it to decide which lazy snapshots to discard. This
preserves the scope-order check and retains eager and member snapshots.
2026-09-27 10:47:22 -04:00
Charlie Marsh 5b05844104 [ty] Skip storing default definition states (#28934)
## Summary

During semantic indexing, we clone and store default definition states
only to discard them when finalizing the map. We now skip those entries
up front, removing any prior entry when a definition is recorded again
with a default state to preserve overwrite behavior.
2026-09-27 10:46:35 -04:00
astral-automations-bot[bot]andBrent Westbrook 0be08a206f Bump version to 0.16.9 (#28882)
Co-authored-by: astral-automations-bot[bot] <305554984+astral-automations-bot[bot]@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2026-09-24 16:21:50 -04:00
Zsolt DollensteinandTamir Duberstein 6cddbe8ee3 [ty] Complete string keys from dictionary initializers (#28820)
For `d = {"host": "localhost", "port": 8000}`, ty currently infers a
broad `str` key type and offers no key completions for `d["<CURSOR>"]`.
This adds `host` and `port` suggestions by following dictionary
initializers through reaching assignments, simple aliases, imports, and
nested dictionaries.

Existing completions from finite expected string types remain
authoritative, including `TypedDict` keys. Initializer candidates are
filtered against the expected type. These suggestions are a heuristic
for mutable dictionaries; the initializer does not define a complete
schema.

The completion evaluator also opens the cursor file in `ProjectDatabase`
so inference retains the expected types used for string completions. The
new fixtures cover dictionary initializers, `Literal`-typed function
arguments, and `TypedDict` precedence.

Adapted from [Tamir Duberstein’s original
change](https://github.com/tamird/ruff/commit/3cd0176407b78dd882691ef5d08d12925e6aa0e8).

---------

Co-authored-by: Tamir Duberstein <tamird@gmail.com>
2026-09-24 11:48:44 +01:00
Carl Meyer 5dabb44304 [ty] Use reaching definitions for redundant-condition exemptions (#28788)
Redundant-condition exemptions currently consider assignments that do
not reach the condition. For example, `value = 1; if value: ...` can
lose its `redundant-condition-strict` diagnostic merely because `value =
sys.platform` appears later in the function. An overwritten
platform-dependent value can suppress the diagnostic in the same way.

Use the existing place-load resolution to follow the definitions that
reach each read, including reads through aliases and definite attribute
assignments. Preserve environment-dependent assignment guards, lazy
closure/global reads, and conservative member lookup when an attribute
has no definite assignment at the use. Cache shared provenance and guard
summaries so repeated conditions do not repeatedly scan all assignments.

## Test plan

Add mdtests covering later and overwritten assignments, aliases,
separate and merged branches, assignment guards versus guards around a
use, eager and lazy scopes, global/nonlocal overwrites, comprehension
assignments, star imports, augmented assignments, and definite versus
implicit attribute bindings. Preserve annotation hints for captured
tuple variables and extend the existing query-reuse regression test to
cover repeated closure reads.


## Ecosystem report

For head `6487736580b552faad10bb382e3e088ee28af1f8`, all four added
diagnostics are expected: parso's annotation omits `None`, schema_salad
disables the tested branch, spack already asserts the tested value is
`None`, and steam.py tests an always-truthy TypedDict. Each change was
reproduced and minimized against the exact CI revisions. CI is green
with no unresolved ecosystem concerns. [Full report and minimized
examples](https://github.com/astral-sh/ruff/pull/28788#issuecomment-5782857703).
2026-09-22 15:43:41 -07:00
Charlie MarshandCarl Meyer 3d834a01bf [ty] Treat imports as bindings instead of declarations (#27325)
## Summary

Prior to this change, we treated imports as declarations, incorrectly
requiring later assignments to preserve the imported type:

```py
import os
from types import ModuleType


def normalize(module: ModuleType) -> ModuleType:
    return module


os = normalize(os)
```

We now model regular imports, `from` imports, and wildcard imports as
bindings rather than declarations. This allows ordinary reassignment,
context-manager targets, and optional-import fallbacks while preserving
existing local annotations.

Imported `Final` qualifiers are tracked separately from declared types
through use-def flow. This preserves the qualifier across assignments,
re-exports, and branches without replacing explicit annotations or
treating imports as protocol-member declarations. The implementation
reuses declaration-flow storage and reachability information, with
separate queries for imported qualifiers.

Deprecation checks use the original imported value before assignment
recovery, so a conflicting local annotation does not suppress the
import's deprecation warning.

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

## Test plan

- Added mdtests for import reassignment, optional-import fallbacks,
existing annotations, wildcard imports, and cyclic loop/match bindings.
- Added imported-`Final` coverage for re-exports, conditional and
unreachable imports, nested and forwarding scopes, class and protocol
members, annotated stub re-exports, and replacement by later imports or
annotations. Core tests check that imported qualifiers remain separate
from genuine declarations and participate in branch merging.
- Added a conflicting-annotation regression that checks both the
assignment error and import deprecation warning while preserving the
recovered binding type.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2026-09-17 07:03:59 -04:00
astral-automations-bot[bot]andBrent Westbrook 62914c4b9b Bump version to 0.16.8 (#28648)
Co-authored-by: astral-automations-bot[bot] <305554984+astral-automations-bot[bot]@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2026-09-16 11:31:02 -04:00
Charlie Marsh 94efeaa286 [ty] Compact reachable binding and declaration histories (#28349)
## Summary

We retain duplicate binding and declaration lists because reachable
histories include implicit unbound/undeclared entries that end-of-scope
histories can discard. We now store those initial entries as one
reachability constraint per place and intern the remaining definitions,
reusing the corresponding end-of-scope declaration list when it matches.
2026-09-16 10:14:40 -04:00
Alex Waygood 50020fb1e8 [ty] Avoid storing constraint nodes twice (#28375) 2026-09-16 15:07:39 +01:00
Charlie Marsh 852e8409d6 [ty] Skip inferring concrete methods during abstract-method discovery (#28613)
## Summary

The abstract-class checks added in #28167 infer full signatures for
concrete methods. We now use cached decorator information to skip
methods that cannot introduce abstractness, without reloading superclass
ASTs.

Overrides of abstract methods, implicitly abstract protocol methods, and
unknown decorators still use full inference.
2026-09-15 11:58:45 -04:00
Charlie Marsh e0a77db7b7 Add accessors for comparison expressions (#28485)
## Summary

We add accessors and iteration methods to `ExprCompare` and use them
throughout AST visitors, formatting, lint rules, and type inference. For
`a < b <= c`, `iter` yields `(a, Lt, b)` and `(b, LtE, c)`. `as_single`
returns a triple only when there is exactly one operator and two
operands, so callers can match an operator and both operands together
while rejecting comparison chains. Rules that only inspect operands use
the existing operand slice directly.

The `first_operand`, `first_operator`, and `second_operand` accessors
assert the parser's existing guarantee that a comparison has at least
one operator and two operands. Error recovery synthesizes missing
operands; manually constructed ASTs must uphold the same invariant.
2026-09-10 15:55:08 -04:00
Charlie Marsh 7b13f01ba1 [ty] Reduce retained AST memory by shrinking expressions (#28335)
## Summary

We reduce `Expr` from 64 to 56 bytes on 64-bit targets. The largest
variants determine the enum's size, so shrinking those variants saves
eight bytes in every `Expr` value, including expressions in the parsed
modules retained by ty.

The changes are:

- Store positional call arguments in `ThinVec`, reducing `Arguments` and
`ExprCall` by eight bytes.
- Store all comparison operands in one boxed slice. This removes the
separate left-operand allocation and reduces `ExprCompare` by eight
bytes while keeping operators in a boxed slice.
- Store dictionary-comprehension generators in a boxed slice, reducing
`ExprDictComp` by eight bytes.
- Store f-string and t-string elements in boxed slices, and represent a
single f-string directly as `FString`. This keeps single parts inline
and reduces `ExprFString` and `ExprTString` by eight bytes.

The parser, AST visitors, formatter, lint rules, and type inference use
the updated layouts. Size assertions cover the 56-byte enum and its
affected variants.
2026-09-10 15:39:14 -04:00
astral-automations-bot[bot]andBrent Westbrook b5dba861cc Bump version to 0.16.7 (#28496)
Co-authored-by: astral-automations-bot[bot] <305554984+astral-automations-bot[bot]@users.noreply.github.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2026-09-10 13:47:30 -04:00
Charlie Marsh 83c3487de4 [ty] Share equivalent place tables within a file (#28319)
## Summary

We retain a separate place table for each scope even when multiple
scopes in a file have identical symbols and members. Share equivalent
nonempty tables within the file, checking structural equality after
fingerprint matching so collisions cannot merge different tables.
2026-09-09 10:53:44 -04:00
Charlie Marsh 7d1432f809 [ty] Avoid excess capacity in multi-binding tables (#28412)
## Summary

`MultiBindingsByUse` currently builds and sorts a temporary `Vec`, then
allocates again when collecting into a `ThinVec`. For tables with one to
three entries, that final collection also retains four slots.

We now build the `ThinVec` directly with capacity reserved to the map's
length and sort it in place, removing the intermediate allocation and
excess capacity.
2026-09-08 10:52:26 -04:00
Alex Waygood e579b84aba [ty] Intern statement call predicates (#28373)
## Summary

Repeated statement-call reachability checks reconstruct the `(callable,
call_expr, is_await)` key for `analyze_non_terminal_call`. Intern that
information when constructing the predicate and pass its ID directly to
the tracked query, avoiding repeated argument interning. The same fields
continue to identify each call, including awaited calls.

## Performance

Median CPU time across four runs of profiling CLI builds on macOS arm64,
using the existing microbenchmark inputs:

| Benchmark | Before | After | Reduction |
| --- | ---: | ---: | ---: |
| `repeated_statement_calls` | 29.7 ms | 28.0 ms | 5.8% |
| `repeated_statement_calls_pre_narrowed` | 30.4 ms | 28.4 ms | 6.6% |
| `repeated_statement_calls_fixed_reachability` | 29.5 ms | 27.1 ms |
8.1% |
| `repeated_statement_calls_in_try_with_if_branches` | 82.0 ms | 73.6 ms
| 10.3% |
| `repeated_suppressing_context_managers_interleaved_calls` | 68.7 ms |
59.5 ms | 13.4% |

For `repeated_statement_calls`, the retained-memory report falls from
10.36 MB to 8.84 MB (14.6%).
2026-09-06 22:22:15 +01:00
Charlie Marsh cd60f34ffb [ty] Reduce repeated work in use-def merges (#28365)
## Summary

Branch merges repeat reachability work for consecutive places that share
the same last-applied reachability node. We now reuse the most recently
merged path constraint within each merge, where both branch tips remain
fixed. We also skip the member-state merge when there are no member
states.
2026-09-06 16:55:04 -04:00
Alex Waygood a5ef1f43fc [ty] Add assert_never fixes for redundant final elif branches (#28178)
When the final `elif` condition is always true, offer an unsafe fix for
`redundant-condition-strict` that adds an `else` branch calling
`assert_never`. This makes the code explicit that it intends to be
exhaustive.

The argument to `assert_never` is a plain variable that narrows to
`Never` when the condition is false. The existing condition and body are
preserved.

**Python 3.11+: add a new import.** After the `str` branch, `value` must
be an `int`, so the final condition is statically always true. The fix
adds an explicit exhaustiveness check:

```diff
+from typing import assert_never
 def handle(value: str | int):
     if isinstance(value, str):
         print(value)
     elif isinstance(value, int):
         print(value)
+    else:
+        assert_never(value)
```

**Reuse an existing runtime import.** An unshadowed alias can be used
without adding another import:

```diff
 from typing import assert_never as unreachable
 
 def handle(value: str | int):
     if isinstance(value, str):
         print(value)
     elif isinstance(value, int):
         print(value)
+    else:
+        unreachable(value)
```

An existing qualified import such as `import typing as t` can similarly
supply `t.assert_never(value)`.

**Avoid a shadowed function name.** If `assert_never` already names a
parameter, the fix uses a qualified reference instead:

```diff
+import typing
 def handle(value: str | int, assert_never: int):
     if isinstance(value, str):
         print(value)
     elif isinstance(value, int):
         print(value, assert_never)
+    else:
+        typing.assert_never(value)
```

**Python 3.10: use an available backport.** When uv dependency metadata
identifies `typing_extensions` as a direct dependency available to this
file, and the installed runtime module exports `assert_never`, the fix
imports from the backport:

```diff
+from typing_extensions import assert_never
 def handle(value: str | int):
     if isinstance(value, str):
         print(value)
     elif isinstance(value, int):
         print(value)
+    else:
+        assert_never(value)
```

No fix is offered if there is no suitable plain variable, if assignment
expressions occur in the condition, or if neither an unqualified nor a
qualified reference can avoid existing bindings. On older Python
versions, missing dependency metadata, an indirect dependency, a missing
runtime module, or a backport without `assert_never` also prevent the
fix. The new branch raises if the static assumptions fail at runtime, so
the fix is unsafe.

The shared importer checks bindings without re-entering type inference
and only reuses runtime imports that have not been reassigned or
deleted.

Stacked on #28177.
2026-09-06 00:37:25 +01:00
Alex Waygood 71b790029c [ty] Move ty_ide's importer to ty_python_semantic (#28170)
Future autofixes will need to introduce imports alongside code changes.
In particular, there are cases where inserting `assert_never(...)` would
be useful, but the fix often also needs to import `assert_never`. The
importer currently lives in `ty_ide`, so diagnostics emitted by
`ty_python_semantic` cannot use it.

Move the importer into `ty_python_semantic` and reuse it from
completions and inlay hints. As its first use in a diagnostic, attach
the `reveal_type` import fix directly to `undefined-reveal`, replacing
the custom LSP handling. The fix imports from `typing` on Python 3.11+
and `typing_extensions` on older Python versions.

Cache source-style detection and top-level import discovery in Salsa per
file, so diagnostics in separate inference regions and IDE features can
share the setup. The cached data does not retain the parsed AST or a
copy of the source text.

Stacked on #28169.
2026-09-06 00:29:01 +01:00
Alex Waygood 368db99969 [ty] Add await fixes for redundant conditions (#28169)
`redundant-condition` already catches conditions that test a coroutine
object instead of its result. Add an unsafe fix that awaits the
expression, inserting parentheses when needed to preserve precedence.
For an uncalled function or method known to return a coroutine, extend
the existing call fix to call and await it.

Only suggest `await` where it is syntactically valid, accounting for
annotations, nested scopes, comprehensions, Python-version differences,
and top-level await in notebooks. A return type of `Any`, `Unknown`, or
`Never` does not by itself justify adding `await`.

Stacked on #28168.
2026-09-06 00:13:14 +01:00
Alex Waygood 13020b80d7 [ty] Suggest variable-length tuple annotations for redundant conditions (#28168)
`tuple[T]` describes a tuple with exactly one element, so its instances
are always truthy. When `redundant-condition` flags such a value, the
intended annotation may instead be `tuple[T, ...]`.

This adds a hint pointing to the parameter or variable annotation that
establishes the one-element type and suggests the arbitrary-length form.
Tuple subclasses are excluded, replacement suggestions that cannot be
expressed in Python annotation syntax are omitted, and hints for
dependency annotations refer to the dependency's author.

Stacked on #28034.
2026-09-05 23:25:18 +01:00
Alex Waygood 689a33fd36 [ty] Add rules to detect always-truthy and always-falsy conditions (#28034)
## Summary

Builds on #28032.

Add two rules for boolean tests whose truthiness is statically known:

- `redundant-condition`, enabled by default at warning severity, catches
cases such as an uncalled function or method, an unawaited coroutine, a
generator tested for emptiness, or a tuple whose length makes its
truthiness constant.
- `redundant-condition-strict`, disabled by default, covers
boolean/integer conditions, conditions whose outcome is fixed only by
short-circuit evaluation, and conditions containing walrus expressions.
These can expose real bugs, but are also much more likely to be
intentional.

The checks reuse ty's truthiness inference and apply to `if`/`elif`,
`while`, assertions, match guards, conditional expressions,
comprehension filters, and unary `not`. The operands of `and`/`or` are
checked when the expression is used as a condition. The main design
question is which statically redundant tests are likely to represent
mistakes. The implementation deliberately prefers missing some redundant
conditions to warning about common defensive or compatibility patterns.

## Avoiding false positives

### Separate likely mistakes from more opinionated checks

Types assignable to `int`, including `bool`, use the strict rule. So do
conditions whose outcome is fixed by short-circuit evaluation even
though their inferred value type has ambiguous truthiness, and tests
containing walrus expressions. Other values with fixed truthiness use
the default rule.

This keeps checks of ordinary boolean/integer flags quiet by default,
because these are extremely common and it is difficult to distinguish
deliberately constant flags from mistakes:

```python
DEBUG = False

if DEBUG:  # Only reported by redundant-condition-strict.
    print("debugging")
```

The strict rule is a sibling of the default rule: it handles a separate
set of expressions rather than reporting the same expressions more
aggressively.

### Distinguish a condition's outcome from its value type

Short-circuit evaluation can establish a fixed outcome even when the
expression's value type does not:

```python
def check(value: object):
    if value and False:  # Always false; reported by redundant-condition-strict.
        print("unreachable")
```

The condition fails whether `value` is truthy or falsy. However, saving
`value and False` to a variable and testing that variable later is
different: the saved value could be the original object, whose
`__bool__` result may change between calls. The redundant-condition
checks use short-circuit truthiness APIs introduced in
https://github.com/astral-sh/ruff/pull/28082 for direct conditions, and
value-type truthiness for tests of computed values. This also handles
chained comparisons whose intermediate results are objects with mutable
truthiness.

### Recognize explicitly constant tests

Both rules exempt direct boolean, integer and `None` literals in the
AST, such as `if False:`, `if 0:`, `while True:`, `while 1:`, and
`assert None`. These spellings are strong evidence that the constant
truthiness is deliberate. A name _inferred_ as `Literal[False]` is
different: its value may not be apparent where it is tested, so it
remains eligible for the strict rule.

### Treat walrus expressions as evidence of intentional side effects

An always-truthy operand can be useful because evaluating it assigns a
value:

```python
if should_continue() and (message := "continuing") and is_ready():
    print(message)
```

The middle operand uses the strict rule, so it does not warn by default.

We does not attempt to classify arbitrary side effects. For example, `if
items.append(value) or fallback:` can still trigger the default rule
because `append` returns `None`.

An `and`/`or` expression used to compute a value (rather than one that
is used directly in an `if` test or similar), such as `result =
items.append(value) or fallback`, is not flagged by either rule.

### Follow the origins of environment-dependent conditions

Both rules exempt compatibility checks involving `sys.version_info`,
`sys.platform`, `os.name`, and `TYPE_CHECKING`. A condition being
constant for the configured environment does not make it redundant
across the environments supported by the project.

The exemption also follows names and attributes through their
definitions, including aliases and imports across modules:

```python
import sys

IS_WINDOWS = sys.platform == "win32"
LINE_PREFIX = "\n" if IS_WINDOWS else ""

if LINE_PREFIX:  # Exempt even though its truthiness is known on this platform.
    print(LINE_PREFIX)
```

The traversal follows assignment values, augmented assignments, walrus
bindings, loop and comprehension iterables, context-manager expressions,
and the subjects of captured `match` patterns.

This lookup is deliberately not flow-sensitive: if any resolved
definition depends on one of these symbols, the use is exempt. This is
so that we err on the side of avoiding false positives.

The shared definition-resolution machinery from #28032 allows this
traversal during inference without requesting completed inference of the
same scope. Name lookups are cached per scope and name; attribute
lookups are cached per receiver type and attribute name. Definition
traversal has its own Salsa query boundary and handles cyclic aliases,
so repeated conditions can share the provenance analysis.

### Preserve defensive assertions and exhaustive dispatch

Defensive boolean/integer assertions such as `assert isinstance(value,
int)` remain useful at runtime even when `value` is annotated as `int`,
since the runtime does not enforce type annotations. These are therefore
exempt from both rules.

Non-boolean mistakes such as `assert function` (where the user meant
`assert function()`) are still reported by the default rule. A complete
assertion whose value type has ambiguous truthiness but whose outcome is
fixed by short-circuit evaluation remains eligible for the strict rule.

For `if` and `elif` statements, the strict rule also recognizes
defensive exits. An always-false condition is exempt when its body ends
in a `raise`, an assertion that might fail, a call returning `Never`, or
a return assignable to `NotImplementedType`:

```python
def increment(value: int) -> int:
    if not isinstance(value, int):
        raise TypeError("expected an int")
    return value + 1
```

For an always-true condition, the same heuristic examines an immediately
following `else` suite, or the remainder of the surrounding suite when
there are no later clauses. This accommodates exhaustive dispatch
followed by `assert_never`, and early-return dispatch followed by a
defensive exception.

These defensive-exit exemptions apply to boolean/integer conditions and
conditions requiring short-circuit analysis. An uncalled function is
still suspicious even when an otherwise unreachable branch raises.

### Check compound conditions once

In `result = flag and function`, Python does not test the truthiness of
the final operand: it simply returns that value if evaluation reaches
it. The rule therefore does not flag `function`, even though it is an
always-truthy object in a boolean expression. In `if flag and
predicate:`, the surrounding `if` does test the truthiness of
`predicate`, so the rule reports the uncalled function even though the
complete condition has ambiguous truthiness.

Unlike `and`/`or` expressions used to compute values, `not` explicitly
tests its operand's truthiness even when it used to computer a value, so
`not` expressions are checked even outside the context of
`if`/`while`/`elif`/`assert` tests.

For compound conditions, the redundant-condition checks visit operands
before deciding whether to report the complete expression. Reporting an
uncalled function in `if not function:` suppresses a second duplicate
diagnostic on the `not function` expression as a whole. Boolean/integer
operands and operands requiring short-circuit analysis are instead
suppressed so that the complete condition can be reported once. This
applies consistently to statement conditions, conditional expressions,
and comprehension filters.

Boolean tests inside value expressions, such as `if consume(not
function):`, are checked independently. Tests in nested scopes are
checked when those scopes are inferred.

Neither rule runs in stub files or files excluded from checking.
Diagnostic and provenance analysis is skipped entirely when both rules
are disabled.

## Diagnostics and fixes

Special-case diagnostics are provided to help explain some of the more
common errors we see in the ecosystem: an uncalled function or method
being always truthy, a fixed-length tuple being always truthy or always
falsy, a `TypedDict` always being truthy due to the presence of
`Required` fields, a `@final` class without `__bool__` or `__len__`
being always truthy, or an unevaluated generator being always truthy.

For simple function and method references, the diagnostic carries a call
fix. A parameterless signature gets an unsafe `()` fix; signatures with
parameters get a display-only `(...)` suggestion. Generator diagnostics
suggest `any()` and carry a display-only wrapping fix when the standard
`any` builtin is known not to be shadowed, including by a custom
`__builtins__.pyi`.

These fixes are deliberately not classified as safe: calling a function
or consuming a generator can change runtime behavior, and the intended
correction may be different.

### Default-rule diagnostics

<details>
<summary>Uncalled function</summary>

![Uncalled
function](https://github.com/user-attachments/assets/9538e4a9-b716-4fff-a4c7-24fc6deb7994)

</details>

<details>
<summary>Uncalled method</summary>

![Uncalled
method](https://github.com/user-attachments/assets/3c327ada-eec0-4bcc-9588-e5df50b3455b)

</details>

<details>
<summary>Generator: suggest <code>any()</code></summary>

![Generator: suggest
any()](https://github.com/user-attachments/assets/83d500fb-d69a-4753-94d6-4008379af8d8)

</details>

<details>
<summary>Tuple with a fixed length</summary>

![Tuple with a fixed
length](https://github.com/user-attachments/assets/e37ade52-22a6-4946-aa29-71f5328bfa79)

</details>

<details>
<summary>Tuple with a minimum length</summary>

![Tuple with a minimum
length](https://github.com/user-attachments/assets/6fc71bd5-6a52-417f-9a9b-db4e760752c3)

</details>

<details>
<summary>Empty tuple</summary>

![Empty
tuple](https://github.com/user-attachments/assets/6c8e8242-fd8b-410d-b56b-e6a49737ea0f)

</details>

<details>
<summary>Nonempty string literals</summary>

![Nonempty string
literals](https://github.com/user-attachments/assets/139f9f53-5128-4d85-8901-01937f312986)

</details>

<details>
<summary>Empty string</summary>

![Empty
string](https://github.com/user-attachments/assets/261fc16d-dc67-44b8-bf51-58f2fcb12e2f)

</details>

<details>
<summary><code>None</code></summary>


![None](https://github.com/user-attachments/assets/c04b0308-b294-4125-bba8-3603fd5bda02)

</details>

<details>
<summary><code>TypedDict</code>: required fields and their
declarations</summary>

![TypedDict: required fields and their
declarations](https://github.com/user-attachments/assets/46367c59-665f-4b40-893b-227add4ca3df)

</details>

<details>
<summary>Final class without <code>__bool__</code> or
<code>__len__</code></summary>

![Final class without __bool__ or
__len__](https://github.com/user-attachments/assets/4e8e92e7-0645-485c-ab30-4d0807943005)

</details>

<details>
<summary>Enum: implicit finality</summary>

![Enum: implicit
finality](https://github.com/user-attachments/assets/6596a69b-a122-40bb-80fc-b011c054e15d)

</details>

### Strict-rule diagnostics

These examples enable `redundant-condition-strict` explicitly.

<details>
<summary>Indexing <code>bytes</code> returns <code>int</code></summary>

![Indexing bytes returns
int](https://github.com/user-attachments/assets/e6f33bc9-c998-4be5-b1ca-36c32b073d99)

</details>

<details>
<summary>Comparing <code>str</code> with <code>bytes</code></summary>

![Comparing str with
bytes](https://github.com/user-attachments/assets/48e15215-d6ae-4779-ab44-244c21dabf55)

</details>

<details>
<summary>Length comparison: explain the fixed tuple length</summary>

![Length comparison: explain the fixed tuple
length](https://github.com/user-attachments/assets/556ef251-ac2f-4048-b7f4-b545a8b87a69)

</details>

<details>
<summary>Comparison: omit an obvious literal's type annotation</summary>

![Comparison: omit an obvious literal's type
annotation](https://github.com/user-attachments/assets/95a0e73f-38f5-457b-9726-8169016b991d)

</details>

## Implementation

The semantic index records the outermost boolean tests in each scope, so
one check owns the diagnostics for a complete condition and its
subexpressions. A shared checker separates classification, context
exemptions, and duplicate suppression from diagnostic rendering. The
main implementation is in
`types/infer/builder/redundant_conditions/mod.rs`; its `diagnostic.rs`
submodule constructs messages and fixes, and `exemptions.rs` handles
environment provenance and defensive exits.
2026-09-05 20:45:59 +01:00
Charlie MarshandCharlie Marsh 69781a3cd8 [ty] Share retained use-def tables across scopes (#28313)
## Summary

We retain separate binding and declaration tables for every scope in a
file, even when those tables contain identical scope-local IDs. The
duplicate storage accounts for a measurable share of the semantic index
in representative projects.

We now share equivalent immutable tables across scopes while keeping
each scope's definitions and constraints in its own `UseDefMap`. The
interner is local to semantic-index construction, so its lookup sets do
not remain in the retained index.

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2026-09-04 12:17:11 -04:00
Brent Westbrook 22f65a2ab5 Bump 0.16.6 (#28280) 2026-09-03 12:36:42 -04:00
Charlie Marsh f10245402a [ty] Validate type variable scopes in constructors and aliases (#28130)
## Summary

Previously, we allowed constructor calls to use unbound type variables
and legacy type aliases to capture their enclosing class's type
parameters. We now report `unbound-type-variable` for the former and
`invalid-type-form` for the latter.

```python
from typing import Generic, TypeAlias, TypeVar

T = TypeVar("T")

# Before: no errors. After: unbound-type-variable on each line.
list[T]()
items = list[T]()

class Box(Generic[T]):
    # Before: no error. After: invalid-type-form.
    Items: TypeAlias = list[T]
```

A constructor call cannot introduce a generic context: `items =
list[T]()` creates an instance, whereas `Items = list[T]` defines a
generic alias. Constructor type arguments must therefore be bound by an
enclosing generic function or class. We preserve this distinction for
standalone calls, assigned calls, and calls in stubs, including when the
callable is inferred independently for reachability analysis.

Legacy aliases bind their own type parameters and cannot reuse
parameters already bound to the enclosing class. Aliases with
independent parameters, such as `Items: TypeAlias = list[S]` inside
`Box[T]`, remain valid. PEP 695 `type` statements can capture enclosing
class parameters and are unchanged. The alias check also covers
stringified annotations, `ParamSpec`, and `TypeVarTuple`.

This moves
[`generics_scoping`](https://github.com/python/typing/blob/cf943ccbea5596ef969eec6e2260097ff697b7ba/conformance/tests/generics_scoping.py)
from Partial to Pass by reporting its two missing errors. No other
conformance file's outcome changes.

<details>
<summary>Case analysis</summary>

Mypy 2.1.0, pyright 1.1.410, and pyrefly 1.3.0-dev.1 reject both unbound
constructor calls and the class-scoped `TypeVar` alias above. They also
accept constructor calls inside functions or methods where the type
variable is bound, and aliases that introduce independent parameters.
Pyrefly comparisons use its `default` preset.

For variadic parameters, pyright and pyrefly reject both captures below.
Mypy rejects the `TypeVarTuple` capture but accepts the `ParamSpec`
capture:

```python
from typing import Callable, Generic, ParamSpec, TypeAlias, TypeVarTuple

P = ParamSpec("P")
Ts = TypeVarTuple("Ts")

class Callbacks(Generic[P]):
    # ty, pyright, pyrefly: error; mypy: OK.
    Callback: TypeAlias = Callable[P, None]

class Tuples(Generic[*Ts]):
    # All four: error.
    Items: TypeAlias = tuple[*Ts]
```

The distinction between legacy aliases and PEP 695 aliases matters here.
We continue to accept this alias, as do pyright and pyrefly. Mypy
rejects it because `T` is not declared in the alias's own parameter
list:

```python
class Modern[T]:
    # ty, pyright, pyrefly: OK; mypy: error.
    type Items = list[T]
```

</details>
2026-09-01 10:30:52 -04:00
Carl Meyer 44713bd0f9 [ty] Invalidate member narrowing across loop iterations (#28009)
Reassigning an object inside a loop leaves narrowing of its attributes
or indexed elements in effect from before the loop. This can make a
`while` condition appear permanently true and cause ty to treat
reachable code after the loop as unreachable.

Create loop-header definitions for places invalidated by reassignment,
and include their fallback member types when computing narrowing across
loop-back paths. Preserve narrowing established after reassignment and
the initial narrowing when a reassignment cannot reach another
iteration. Propagate invalidation through nested loop headers with
memoization.

Exclude synthetic loop headers from attribute-assignment discovery:
rebinding a receiver does not write its attributes or initialize `Final`
values. Treat discarded dictionary-key assignments as invalidations so
rejected replacements and property-setter inputs do not become inferred
key values on subsequent iterations.

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

## Ecosystem

The ecosystem run removes 55 false-positive Pydantic errors and one
Pyodide warning. It adds three expected websockets diagnostics by making
a previously unreachable `join` reachable; the existing annotations
allow mixed text and binary fragments. Minimized reproductions confirm
these changes are expected, with no regressions found.

## Test plan

- Extend `while` and `for` mdtests to cover condition, body, and
normal-exit narrowing after object replacement, explicit `continue`,
nested-loop early exits, nested attributes, and subscripts.
- Verify that break-only paths preserve initial narrowing and that
assertions on replacement objects narrow the next iteration.
- Verify method binding before receiver-rebinding loops and at external
calls, and distinguish actual `Final` initialization inside loops from
receiver rebinding.
- Cover accepted and rejected dictionary replacements, narrowing after
rejected replacements, and property setters whose input types differ
from their getters, including loop-carried dictionary values.
2026-08-28 22:26:38 +00:00
Carl Meyer 4e4e9f5e73 [ty] Rename Truthiness::and_else to and_then (#28137)
The lazy conjunction helper's name, `Truthiness::and_else`, does not
follow Rust's `Option::and_then` convention. Rename it to `and_then` and
update its chained-comparison caller and existing tests, without
changing evaluation behavior.

Addresses [the review feedback on
#28082](https://github.com/astral-sh/ruff/pull/28082#discussion_r3878774178).

## Test plan

Updated the existing truthiness test to use the new name. It covers all
nine input combinations and verifies that the closure runs exactly once
when the left side may be true, and never when it is always false.
2026-08-28 10:07:24 -07:00
Carl Meyer 17761270a5 [ty] Preserve short-circuit reachability for direct conditions (#28082)
ty can incorrectly treat a branch guarded by an impossible compound
condition as reachable when an operand has potentially mutable
truthiness. For example, `if value and False:` always skips its body in
CPython; the first falsy operand selects the control-flow path
immediately, it doesn't fully evaluate `value and False` and then
re-evaluate the truthiness of the result. Saving `value and False` to a
variable and then testing the saved object can instead call
`value.__bool__` twice and get different answers.

Distinguish expressions consumed directly as conditions from expressions
that produce values. Evaluate direct boolean and conditional expressions
structurally, and retain the combined truthiness of chained comparisons
even when a comparison returns an arbitrary object. Operands with types
equivalent to `Never` cannot produce a result; preserve that distinction
from ambiguous truthiness while composing conditions. Value contexts
continue to allow repeated truthiness checks to disagree.

To limit retained memory, comparison truthiness uses sparse overrides in
optional inference metadata, stored as boxed slices. Ordinary comparison
chains use the truthiness of their inferred value type, and root
chained-comparison conditions reuse expression inference without a
separate condition-analysis memo. Cycle widening preserves overrides
when needed to keep the effective truthiness monotonic. Tracking
operands that cannot produce a result adds only transient evaluation
state.

Prerequisite for #28045, which addresses astral-sh/ty#4380.

## Ecosystem results

Five expected diagnostic removals across pwndbg, pytest, and Spark; no
additions. Each comes from a branch that is unreachable under the
checked platform or declared types. See the [Codex ecosystem
analysis](https://github.com/astral-sh/ruff/pull/28082#issuecomment-5434341797)
for details and minimized reproducers.

## Test plan

- Mdtests cover compound conditions, nested boolean operations and
`not`, conditional expressions, and chained comparisons across branches,
loops, assertions, comprehension filters, and match guards.
- Reachability assertions use revealed variable types, including
reachable saved results, assignment expressions, nested value
expressions, and call arguments whose truthiness can be tested again.
- Cases with operands narrowed to `Never`, aliases and unions of aliases
of `Never`, and type variables bounded by `Never` assert branch
reachability using unrelated variables and check precise
conditional-expression result types.
- Additional cases cover unreachable loop assignments and conditional
expressions in stringified `Annotated` metadata.
- A Rust invariant test covers sparse comparison-truthiness overrides
appearing or disappearing across inference-cycle results, including
preservation of previous-only overrides.
2026-08-27 17:03:22 -07:00
Brent Westbrook 9e4938c4a6 Bump 0.16.5 (#28110) 2026-08-27 12:09:07 -04:00
Charlie Marsh 50a3eae8ee Run Hawk in CI (#27372)
## Summary

We now run Hawk 0.1.13 with Rust 1.98.0 in CI, following the pattern
used by uv. The configuration preserves intentional public APIs and
generated interfaces while checking all configured production binaries
and workspace non-production targets with `-D warnings`.

The accompanying cleanup narrows internal visibility, removes unused
benchmark classification state and accessors, and records intentional
public interfaces as audited Hawk expectations. The resulting
configuration reports zero Hawk findings across the configured targets.
2026-08-26 13:51:16 -06:00
Carl Meyer 423b9fbf19 [ty] Fix unsound narrowing through branch-assigned conditions (#28006)
Our support for aliased narrowing conditions (i.e. `thing_is_not_none =
thing is not None` followed later by `if thing_is_not_none:`) didn't
properly account for control flow, meaning an aliased condition assigned
only in a branch could wrongly be treated as definitely narrowing, and a
check assigned on a returning branch could wrongly be treated as
relevant to a later narrowing check.

Validate the condition's reaching bindings when generating narrowing
constraints, after reachability is known. Apply alias narrowing only
when every binding that can produce the condition's outcome evaluates
the recorded alias expression. This accounts for loop-carried bindings
and distinguishes unbound locals, which raise, from names that can fall
back to another scope.

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

## Test plan

Adds mdtests for aliases assigned in either branch, conditional
reassignment, returning branches, and always-taken branches. Also covers
possibly-unbound locals, class/global fallbacks, outcome-specific
narrowing, cached aliases across loop iterations (including conditions
initialized to `False` or `True`), and unrelated loop-carried
assignments that prevent narrowing.
2026-08-24 16:55:45 -07:00
Charlie Marsh f42a9e0b99 [ty] Avoid duplicate bindings in multi-target assignments (#27938)
## Summary

Previously, chained assignments could panic when their shared right-hand
side contained both an assignment expression and a lambda:

```python
first = second = (named := lambda: 0)
```

Each target independently imported the same nested binding, violating
the uniqueness invariant for inferred bindings.

We now record whether an assignment's value bindings belong to its
definition or its enclosing statement during semantic indexing.
Statements claim shared bindings once while individual targets retain
contextual expression inference. The same ownership rule handles
unpacking, subscript targets, and assignment expressions in lambda
defaults:

```python
first = second = lambda value=(named := 1): value
```
2026-08-22 12:42:54 +00:00