100 Commits
Author SHA1 Message Date
Alex Waygood 10056500e0 Update uv to 0.13.5 (#29659) 2026-10-22 23:29:36 +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
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
Alex Waygood b3d4603308 Update rustup in the Docker builder (#29599)
Docker builds can reuse an older rustup installer layer. Pin rustup to
[1.29.2](https://blog.rust-lang.org/2026/10/20/Rustup-1.29.2/) so the
builder uses a release that blocks the compromised eMudhra intermediate
certificates. The Rust toolchain version is unchanged.

Run the Docker image build for pull requests that change the Dockerfile.
2026-10-22 11:20:59 -07:00
Alex Waygood acba99d527 Consolidate dependency pins in root Cargo.toml (#29595) 2026-10-21 17:18:57 +00:00
Alex Waygood 84e3f4f790 Update fflate (#29528)
The playground's locked `fflate` 0.8.2 has a [security
advisory](https://github.com/advisories/GHSA-px8p-9vwx-vf98). Update it
to 0.8.3, which is outside the affected range and allowed by the
playground's declared dependency range.

Version 0.8.3 was [published on May 16,
2026](https://github.com/101arrowz/fflate/releases/tag/v0.8.3). The
advisory was published on July 22, 2026 and reviewed by GitHub on
September 3, when [Ruff's Dependabot
alert](https://github.com/astral-sh/ruff/security/dependabot/235) was
created. The fix was therefore available when the alert was created.

Ruff's Renovate configuration enables vulnerability alerts, but Codex
could not confirm why Renovate did not open a security update. An [open
Renovate
discussion](https://github.com/renovatebot/renovate/discussions/45331)
reports that Renovate can fail to read the locked version of an npm
workspace dependency when it is hoisted to the workspace root. The
playground declares `fflate` in `playground/shared/package.json` and
hoists it to the workspace root, with its locked entry under
`node_modules/fflate` in `playground/package-lock.json`. If the same
problem prevents Renovate from matching a GitHub alert to the locked
version, it might not open a security-update PR. Codex has not confirmed
whether this happened in Ruff; the linked discussion concerns OSV-based
alerts.

Related to #29518.
2026-10-20 16:10:11 -07:00
Alex Waygood 96c8d444c6 Update ws (#29526)
The playground's locked `ws` 8.18.2 has security advisories. Update this
transitive dependency to 8.22.0, which is outside the affected ranges.

Codex's analysis is that Renovate's missing [support for targeted
updates of transitive dependencies in
`package-lock.json`](https://github.com/renovatebot/renovate/issues/46379)
may explain why it did not open a security update for the locked `ws`
version.

Split from #29518.
2026-10-20 16:10:06 -07:00
Alex Waygood 61104fce09 Update picomatch (#29525)
The playground's locked `picomatch` 2.3.1 has a security advisory.
Update it to 2.3.2, which is outside the affected range. Updating
`picomatch` also refreshes the other locked version from 4.0.4 to 4.0.7.

Codex's analysis is that Renovate's missing [support for targeted
updates of transitive dependencies in
`package-lock.json`](https://github.com/renovatebot/renovate/issues/46379)
may explain why it did not open a security update for the vulnerable
`picomatch` version.

Split from #29518.
2026-10-20 16:10:01 -07:00
Alex Waygood b50b6833ea Update tar (#29524)
The playground's locked `tar` 7.5.15 has security advisories. Update
this transitive dependency to 7.5.22, which is outside the affected
ranges. Regenerating the lockfile also moves `tar` and several of its
dependencies to top-level `node_modules` entries.

Codex's analysis is that Renovate's missing [support for targeted
updates of transitive dependencies in
`package-lock.json`](https://github.com/renovatebot/renovate/issues/46379)
may explain why it did not open a security update for the locked `tar`
version.

Split from #29518.
2026-10-20 16:09:53 -07:00
Alex Waygood 7e8a18d49e Update source-map-js (#29523)
The playground's locked `source-map-js` 1.2.1 has a [security
advisory](https://github.com/advisories/GHSA-68fv-2mgg-jv7q). Update
this transitive dependency to 1.2.2, which is outside the affected
range.

Codex's analysis is that Renovate's missing [support for targeted
updates of transitive dependencies in
`package-lock.json`](https://github.com/renovatebot/renovate/issues/46379)
may explain why it did not open a security update for the locked
`source-map-js` version.

Split from #29518.
2026-10-20 16:09:47 -07:00
Alex Waygood 9e9ad51c27 Update smol-toml (#29521)
The playground's locked `smol-toml` 1.4.1 has security advisories.
Update it to 1.9.1, which is outside the affected ranges and allowed by
the playgrounds' declared dependency ranges.

Version 1.9.1 was [published on October 9,
2026](https://github.com/squirrelchat/smol-toml/releases/tag/v1.9.1).
The first release that fixes all three advisories was 1.9.0. Each
advisory had a fixed version available when Ruff's Dependabot alert was
created. Dates below are in 2026 and UTC:

| Advisory | Published in smol-toml repository | Published and reviewed
by GitHub | Ruff alert created | First fixed version |
| --- | --- | --- | --- | --- |
|
[GHSA-v3rj-xjv7-4jmq](https://github.com/advisories/GHSA-v3rj-xjv7-4jmq)
| March 23 | March 25 | [March
25](https://github.com/astral-sh/ruff/security/dependabot/126) |
[1.6.1](https://github.com/squirrelchat/smol-toml/releases/tag/v1.6.1)
(March 23) |
|
[GHSA-7w5x-hrqm-74c2](https://github.com/advisories/GHSA-7w5x-hrqm-74c2)
| August 9 | September 9 | [September
9](https://github.com/astral-sh/ruff/security/dependabot/238) |
[1.7.1](https://github.com/squirrelchat/smol-toml/releases/tag/v1.7.1)
(July 26) |
|
[GHSA-r4xh-jqrq-34v2](https://github.com/advisories/GHSA-r4xh-jqrq-34v2)
| September 22 | October 5 | [October
6](https://github.com/astral-sh/ruff/security/dependabot/270) |
[1.9.0](https://github.com/squirrelchat/smol-toml/releases/tag/v1.9.0)
(September 22) |

Ruff's Renovate configuration enables vulnerability alerts, but Codex
could not confirm why Renovate did not open a security-update PR for
these alerts. An [open Renovate
discussion](https://github.com/renovatebot/renovate/discussions/45331)
reports that Renovate can fail to read the locked version of an npm
workspace dependency when it is hoisted to the workspace root. Both
playground packages declare `smol-toml` in
`playground/ruff/package.json` and `playground/ty/package.json`, but the
lockfile records it at the workspace root under `node_modules/smol-toml`
in `playground/package-lock.json`. If the same problem prevents Renovate
from matching a GitHub alert to the locked version, it might not open a
security-update PR. Codex has not confirmed whether this happened in
Ruff; the linked discussion concerns OSV-based alerts rather than
GitHub's Dependabot alerts.

Split from #29518.
2026-10-20 16:09:41 -07:00
Alex Waygood 29057bdbe0 Update playground API Wrangler dependencies (#29529)
The playground API lockfile contains vulnerable versions of `sharp` and
`undici` through Wrangler and directly declared Miniflare packages. The
older Miniflare packages also bring in vulnerable `picomatch` and
`cross-spawn` versions. Remove the unused direct Miniflare packages and
update Wrangler within its existing version range. This removes those
`picomatch` and `cross-spawn` copies and updates `sharp` and `undici` to
fixed versions.

Change the API's deploy script to `wrangler deploy` because the updated
Wrangler no longer supports `wrangler publish`.

Codex's analysis is that Renovate's missing [support for targeted
updates of transitive dependencies in
`package-lock.json`](https://github.com/renovatebot/renovate/issues/46379)
may explain why it did not open security updates for the vulnerable
transitive dependencies.

Split from #29518.
2026-10-20 16:09:30 -07:00
Alex Waygood e296d6f684 Update shared release-signing actions (#29556)
The shared release-signing actions use uv versions that trust an eMudhra
root associated with compromised intermediate CA keys. The macOS signing
action also installs a certifi version containing the same root in its
Azure CLI environment. Certificates issued with the compromised keys
could allow an attacker to impersonate network services to clients that
trust the root. See the [uv security
advisory](https://github.com/astral-sh/uv/security/advisories/GHSA-hq85-5cpj-mr62)
and [certifi security
advisory](https://github.com/certifi/python-certifi/security/advisories/GHSA-6q48-6c7w-9gw2).

Update all action pins to
[astral-sh/github-actions@06f6008](https://github.com/astral-sh/github-actions/commit/06f600897bfe79995c8b1df8eaa41bcf195d5793),
which uses uv 0.13.4 throughout the signing actions and upgrades certifi
to 2026.10.19 in the macOS signing environment. Both updates remove the
affected root from their bundled certificate stores.
2026-10-20 22:19:55 +00:00
Alex Waygood 29c3a12911 Update Monaco and its DOMPurify dependency (#29527)
The playgrounds declared Monaco 0.56.0, which pins DOMPurify 3.4.8. That
version is affected by
[GHSA-55q2-fjhq-7xh7](https://github.com/advisories/GHSA-55q2-fjhq-7xh7).
Update Monaco to 0.57.0, which pins DOMPurify 3.4.15 and is outside the
advisory's affected range.

This security update is only partial: DOMPurify 3.4.15 is affected by
[GHSA-p98j-92pf-mc4p](https://github.com/advisories/GHSA-p98j-92pf-mc4p)
and
[GHSA-6688-9rhm-gjv2](https://github.com/advisories/GHSA-6688-9rhm-gjv2),
which list 3.4.16 as a patched version. It is also affected by
[GHSA-c6gr-qvv4-5v3f](https://github.com/cure53/DOMPurify/security/advisories/GHSA-c6gr-qvv4-5v3f),
[GHSA-mrv2-3pqr-j5jr](https://github.com/cure53/DOMPurify/security/advisories/GHSA-mrv2-3pqr-j5jr),
and
[GHSA-cg2g-xh64-v4q7](https://github.com/cure53/DOMPurify/security/advisories/GHSA-cg2g-xh64-v4q7),
which list 3.4.17 as a patched version. Monaco 0.57.0 is the latest
release and still pins DOMPurify 3.4.15.

The `@monaco-editor/react` loader defaults to a CDN build of Monaco
0.52.2, which bundles DOMPurify 3.1.7. Bundle the installed Monaco
package and its editor and JSON workers, so the browser uses the
installed version instead of the CDN build.

Codex's analysis is that Renovate's missing [support for targeted
updates of transitive dependencies in
`package-lock.json`](https://github.com/renovatebot/renovate/issues/46379)
may explain why it did not open a security update for the vulnerable
DOMPurify version.

Split from #29518.
2026-10-20 14:55:35 -07:00
Alex Waygood 89d80d30d2 Exempt uv from the dependency cooldown (#29554)
Exempt the Astral-maintained uv package from the seven-day dependency
cooldown, replacing the temporary uv cutoff added in #29551. This allows
lockfile updates to select new uv releases without waiting seven days.

Codex prepared this update.
2026-10-20 21:14:21 +00:00
Alex Waygood 63b423619c Update Go to 1.27.3 (#29552)
The [eMudhra incident](https://emudhra.we-have-a-problem.com/)
compromised intermediate CA keys that could be used to impersonate HTTPS
sites. Go 1.27.3 rejects certificate chains containing the corresponding
intermediate public keys.

Update the Go version used by the ShellCheck CI job to 1.27.3. The job
uses Go to download and run go-shellcheck.

Codex prepared this update.
2026-10-20 13:47:38 -07:00
Alex Waygood f71f169f6f Update uv after the eMudhra incident (#29551)
The [eMudhra incident](https://emudhra.we-have-a-problem.com/)
compromised intermediate CA keys that could be used to impersonate HTTPS
sites. uv 0.13.4 removes the emSign root that anchors the affected
intermediates from its bundled trust store.

Update the uv versions used in CI and by the uv-lock hook, and the
locked uv dependency. A uv-specific cutoff allows the updated release
through the seven-day dependency waiting period.

Codex prepared this update.
2026-10-20 20:44:34 +00:00
Alex Waygood 355cda387a Update Wrangler for playground deployment (#29530) 2026-10-19 17:57:07 -07:00
Alex Waygood 34266aa483 Update transitive urllib3 dependency (#29522) 2026-10-19 17:44:58 -07:00
Alex Waygood 24bba09655 Update transitive anyio dependency (#29519) 2026-10-19 17:44:34 -07:00
Alex Waygood 8e768d1fbb Update transitive rand dependency (#29520) 2026-10-19 17:44:16 -07:00
Alex Waygood 15d1c4116f Disable fail-fast in cargo-test-other job (#29514) 2026-10-19 23:09:14 +00:00
Alex Waygood 53cab65177 [ty] Rename the ty review skill (#29510)
Rename the ty review skill from `review-ty-pr` to `review-ty-changes` to
reflect its coverage of local changes. Update the skill's directory,
metadata name, and default prompt.

Stacked on #29509.
2026-10-19 15:25:40 -07:00
Alex Waygood 59920bd3e5 [ty] Extend the review skill to local changes (#29509)
`review-ty-pr` currently assumes every review targets a pull request.
Extend it to cover local changes, including staged and unstaged edits,
branches compared with their base, and changes just written by a user or
an agent.

To avoid executing untrusted code on the host, require untrusted code
and code of uncertain trust to be vetted and run offline in a disposable
container. Allow host testing when the checkout, dependencies, inputs,
and commands are trusted; if vetting or isolation is unavailable, the
affected review work remains static.

The skill also clarifies how to assess typing-spec conformance and
ecosystem impacts, judge whether tests are necessary, and handle
test-generated snapshot updates.
2026-10-19 15:25:39 -07:00
Alex Waygood 2e34f5ba22 Use a shared time budget for fuzzing (#29280)
Our `py-fuzzer` script currently limits each failing seed to have a
maximum of 60 seconds for minimization, even when the CI job has time
remaining. For example, in [this fuzz job on
#29025](https://github.com/astral-sh/ruff/actions/runs/37632264340/job/112829497328),
minimization timed out for seed 57 even though there would have been
another 15 minutes before GitHub cancelled the CI job. Running the
script locally demonstrated that pysource_minimize was [easily
able](https://github.com/astral-sh/ruff/pull/29025#issuecomment-6064567417)
to minimize the panic if only given slightly more time to do so.

This PR replaces the per-seed limit in CI with a global, shared budget
for checking seeds and minimizing failures. Local runs have no overall
budget by default. CI, however, records the job start time in its first
step and passes it and the job timeout to the fuzzer, which subtracts
elapsed setup and build time and reserves an additional 90 seconds for
other job overhead and reporting. Individual calls to ty or Ruff still
have a five-second timeout, allowing us to minimize hangs or
pathological performance issues.

By default, the fuzzer reports failing seeds as they are found and
prints completed minimizations. When the deadline is reached, it stops
starting new seed and minimizer checks, reports the original source for
unfinished minimizations, and identifies unchecked seeds. Checks already
in progress can finish after the deadline. The logs and, when requested,
the GitHub issue body include a command to rerun failing and unchecked
seeds; the script exits with status 2 if any seeds were left unchecked.

Here are some mockups of what bot-opened issues could look like, using
fictional data:

<details>
<summary>Issue with minimized, unminimized, and unchecked
seeds</summary>

Run listed here: https://github.com/astral-sh/ty/actions/runs/1234567890

Each snippet caused ty to exit with a status other than 0, 1, or 2, or
time out after five seconds during fuzzing.

Build ty from commit `0123456789abcdef0123456789abcdef01234567` with
`cargo build --locked --bin=ty`. Save a snippet to `repro.py`, then run:

```
target/debug/ty check repro.py --python-version=3.10 --python-platform=linux
```


### Seed: 78

```python
class Example:
    pass
```

<details>
<summary>Seed: 93 (original source; minimization did not
complete)</summary>

```python
class Original:
    value = [1, 2, 3]
    def method(self):
        return self.value
```

</details>

### Unchecked seeds

The time budget expired before 2 seeds could be checked.

- `123`
- `124`

To check all seeds that failed or were left unchecked, check out Ruff
commit `0123456789abcdef0123456789abcdef01234567` and run from the
repository root:

```shell
uv run --project=./python/py-fuzzer fuzz --bin=ty 78 93 123 124
```

</details>

<details>
<summary>Issue with unchecked seeds and no failures</summary>

Run listed here:
https://github.com/astral-sh/ruff/actions/runs/1234567890

Test executable: ruff at commit
`0123456789abcdef0123456789abcdef01234567`.


### Unchecked seeds

The time budget expired before 3 seeds could be checked.

- `10`
- `11`
- `12`

To check all seeds that failed or were left unchecked, check out Ruff
commit `0123456789abcdef0123456789abcdef01234567` and run from the
repository root:

```shell
uv run --project=./python/py-fuzzer fuzz --bin=ruff 10 11 12
```

</details>

## Rejected alternatives

1. Keep a per-minimization budget, but just increase it (by a lot?).

We could do this, and it fixes the theoretical problem that codex raised
where a couple of seeds that take a very long time to minimize stop the
script from even discovering bugs in later seeds (because it never even
gets to those later seeds).

I decided against this approach, however, because in practice I think it
will only be hangs that take a _long_ time to minimize. We've seen in
the past that a ty hang can cause the script as a whole to timeout in
CI, but whenever it's done so in the past, it's never caused the script
to fail to get onto non-hanging seeds. It's always been the case that
the script has completed all seeds except the problematic ones that were
causing it to hang. So I decided that per-minimization budgets were a
solution looking for a problem.

2. Get rid of all budgets altogether, go back to something more similar
to what the script was like prior to
https://github.com/astral-sh/ruff/pull/29045.

This is very attractive, because it avoids a lot of the new complexity
added in this PR to do with calculating budgets, interrupting
subprocesses that have already been started, etc.
   
I decided against this, however, because I do think the fact that the
script now minimizes hangs substantially increases the risk that the
script might finish without having finished minimizing all seeds. In the
event that this happens, I _do_ think it's quite useful (though not
essential) for the script to report which seeds it didn't get to, before
GitHub cancels the CI job altogether.
2026-10-14 16:03:48 +01:00
Alex Waygood d47bd6a76f Group phf and phf_codegen updates (#29369)
## Summary

Ruff uses `phf_codegen` to generate a Unicode alias map and `phf` to
look up values in it. The generated map must be compatible with the
lookup implementation; updating `phf` alone can make lookups fail. For
example, [#29362](https://github.com/astral-sh/ruff/pull/29362) fails
Unicode alias tests.

Group Renovate updates to both crates when both are available so they
can be reviewed together, following the existing rule for `strum` and
`strum_macros`. Renovate can still propose an update to one crate on its
own if the other update is unavailable.
2026-10-14 12:53:17 +00:00
Alex Waygood 01b89f596f Pin Python for py-fuzzer and scripts CI checks (#29370)
After [#29364](https://github.com/astral-sh/ruff/pull/29364) upgraded uv
to 0.13.1, the [`cargo test (linux)` job on
`main`](https://github.com/astral-sh/ruff/actions/runs/38555989744/job/115825271718)
failed. uv 0.13 defaults to downloading Python 3.15 when no version is
requested and no compatible interpreter is already installed. In the
failing job, `Run tests` used Python 3.15, and the later py-fuzzer
dogfooding step failed to install locked MarkupSafe because the pinned
version of MarkupSafe doesn't have published wheels for 3.15, but source
builds are disabled by py-fuzzer's `pyproject.toml` file:


https://github.com/astral-sh/ruff/blob/9ff6b0f0d27dcbac5a1e6447a336ad8562720f3c/python/py-fuzzer/pyproject.toml#L100-L101

In the [corresponding PR
job](https://github.com/astral-sh/ruff/actions/runs/38553508773/job/115817479045),
the preceding "annotation mdtests" step ran, and `Run tests` used Python
3.13. The mdtests can install a Python version that uv later reuses,
which may explain why the py-fuzzer step passed on the PR. The
annotation mdtests are skipped on pushes to `main`.

This PR fixes the issue by setting `PYTHON_VERSION` (currently 3.14)
explicitly for py-fuzzer's dogfooding, lint, format, and type checks. We
set `UV_PYTHON` for the scripts dogfooding step too, so that both its uv
command and the uv subprocesses ty starts for script dependencies use
the configured version. This also avoids selecting Python 3.15 for
locked script dependencies that lack wheels for it.
2026-10-14 14:44:20 +02:00
Alex Waygood ae8ec37531 [ty] Fix the mdtest README examples and snapshot guidance (#29096)
The mdtest README's snapshot example uses an assertion that cannot match
its diagnostic, and its displayed output does not correspond to the
code. Its introductory example also fails to parse as a standalone
mdtest.

Correct the examples and explain how to generate inline snapshots.
Clarify that the proposed full-output syntax would work without the
inline assertions used by the existing snapshot feature.
2026-10-13 19:48:47 +01:00
Alex Waygood ef1dfb18ac [ty] Use the Black release for typeshed formatting (#29115)
Black 26.10.0’s preview style adds missing blank lines after stub
functions whose bodies contain only a docstring, without inserting blank
lines within overload groups. Previously, this formatting behavior
required installing Black from Git. Switch typeshed formatting to the
released wheel so that Black no longer needs an exception to `no-build`,
and remove its unused build constraints.
2026-10-13 19:07:10 +01: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
Alex Waygood 88886a423a [ty] Make microbenchmarks easier to read (#29224) 2026-10-08 18:35:48 +01:00
Alex Waygood 1e84ec0bbd [ty] [minor] Reduce indentation in relation.rs (#29220) 2026-10-08 14:58:10 +01:00
Alex Waygood 7c6f8fc8fc Exclude file-watcher tests from sandbox testing guidance (#29190) 2026-10-07 23:01:25 +01:00
Alex Waygood 3949a4e9d6 [ty] Show documentation for annotation operators on hover (#29185) 2026-10-07 17:40:09 +01:00
Alex Waygood 9943418153 [ty] Set a fixed release cutoff for uv integration tests (#29188)
ty's uv integration tests clear the inherited environment before passing
a small set of variables to uv subprocesses.

Set `UV_EXCLUDE_NEWER` to `2026-09-22T00:00:00Z` for these tests,
limiting package and build-dependency resolution to distribution
artifacts uploaded before that time. This keeps newly uploaded artifacts
from changing which packages the tests can resolve.
2026-10-07 15:46:30 +00:00
Alex Waygood ad08d94e05 [ty] Separate typeshed snapshot generation from PR creation (#29187)
Move snapshot generation into a job that uploads its changes as an
artifact. The PR creation job then commits and pushes those changes
before creating the PR. If snapshot generation fails or times out, the
workflow still creates a PR with the other changes, provided the
required sync steps succeeded.

Use GitHub-hosted runners and disable shared caching for the workflow.
2026-10-07 15:38:04 +00:00
Alex Waygood 40cf3ae82c Clarify PR labeling and update Ruff changelog exclusions (#29130) 2026-10-06 16:28:32 +01:00
Alex Waygood 57a6e86342 [ty] Improve display of string literal types that include unicode characters or double quotes (#29089)
ty currently displays some string-literal types with Rust-style escapes
that are invalid in Python. For example, a string containing U+200B is
shown as `Literal["\u{200b}"]`; Python uses `\u200b` for this character.
ty also always uses double quotes, so a value such as `'a"b'` is shown
as `Literal["a\"b"]`.

This PR uses Python-style escapes and prefers double quotes, switching
to single quotes when the value contains double quotes but no single
quotes.

It escapes default-ignorable characters, which can be invisible, and
combining marks that would otherwise attach to an opening quote or
escape sequence. For example, `"\u0301"` is displayed as
`Literal["\u0301"]`, preventing the mark from attaching to the opening
quote. It also escapes certain characters in parts of a string that are
not in [Unicode Normalization Form C (NFC)][NFC], so visually identical
values such as `"é"` and `"e\u0301"` are displayed distinctly as
`Literal["é"]` and `Literal["e\u0301"]`.

The following table compares the displayed types for the string values
`'a"b'`, `"\u200b"` (a zero-width space), `"é"`, `"e\u0301"`, and
`"\u0301"`, targeting Python 3.12:

The `"\u200b"` and `"\u0301"` columns use screenshots because some
checkers display an invisible zero-width space or leave a standalone
combining mark unescaped, which can render differently across browsers.

| Checker | `'a"b'` | `"\u200b"` | `"é"` | `"e\u0301"` | `"\u0301"` |
|---|---|---|---|---|---|
| ty on `main` | `Literal["a\"b"]` | <img width="194" height="30"
alt="Literal[&quot;\u{200b}&quot;]"
src="https://github.com/user-attachments/assets/0199b02e-5049-425e-8b37-5c0fc602c0c9"
/> | `Literal["é"]` | `Literal["e\u{301}"]` | <img width="184"
height="30" alt="Literal[&quot;\u{301}&quot;]"
src="https://github.com/user-attachments/assets/ce711f02-eda4-469a-8c86-51adf2665b99"
/> |
| ty with this PR | `Literal['a"b']` | <img width="174" height="30"
alt="Literal[&quot;\u200b&quot;]"
src="https://github.com/user-attachments/assets/777ac4df-418f-467b-b3dd-141e45bd1ea4"
/> | `Literal["é"]` | `Literal["e\u0301"]` | <img width="174"
height="30" alt="Literal[&quot;\u0301&quot;]"
src="https://github.com/user-attachments/assets/bb40d8e5-8257-487c-926c-709bc0ca3063"
/> |
| mypy 2.4.0 | `Literal['a"b']` | <img width="174" height="30"
alt="Literal['\u200b']"
src="https://github.com/user-attachments/assets/fa80c519-aa27-44a8-b622-2ee6aa6b2f04"
/> | `Literal['é']` | `Literal['é']` | <img width="116" height="30"
alt="Literal['&lt;unescaped combining acute accent&gt;']"
src="https://github.com/user-attachments/assets/863a132d-ba19-4bff-a8aa-623fb59d548b"
/> |
| Pyrefly 1.3.2 | `Literal['a"b']` | <img width="116" height="30"
alt="Literal['&lt;unescaped zero-width space&gt;']"
src="https://github.com/user-attachments/assets/fec1f052-06ad-4e20-b973-403c8b67eb48"
/> | `Literal['é']` | `Literal['é']` | <img width="116" height="30"
alt="Literal['&lt;unescaped combining acute accent&gt;']"
src="https://github.com/user-attachments/assets/9981d961-47e3-478d-8750-be68c4415242"
/> |
| Pyright 1.1.414 | `Literal['a"b']` | <img width="116" height="30"
alt="Literal['&lt;unescaped zero-width space&gt;']"
src="https://github.com/user-attachments/assets/5dd25b1e-72cb-4975-ad00-9b1fad3d4a28"
/> | `Literal['é']` | `Literal['é']` | <img width="116" height="30"
alt="Literal['&lt;unescaped combining acute accent&gt;']"
src="https://github.com/user-attachments/assets/d9d6148f-8135-485e-9767-7a5f967a9faa"
/> |
| Zuban 0.10.0 | `Literal['a"b']` | <img width="116" height="30"
alt="Literal['&lt;unescaped zero-width space&gt;']"
src="https://github.com/user-attachments/assets/9bb47bf3-e148-415a-828e-ce84887cffaa"
/> | `Literal['é']` | `Literal['é']` | <img width="116" height="30"
alt="Literal['&lt;unescaped combining acute accent&gt;']"
src="https://github.com/user-attachments/assets/4edfdb70-4c1f-4e5e-b227-0ed039751c2a"
/> |
| Pycroscope 0.5.0 | `Literal['a"b']` | <img width="174" height="30"
alt="Literal['\u200b']"
src="https://github.com/user-attachments/assets/b3af3d85-64e5-4172-8265-5109b9055bd1"
/> | `Literal['é']` | `Literal['é']` | <img width="116" height="30"
alt="Literal['&lt;unescaped combining acute accent&gt;']"
src="https://github.com/user-attachments/assets/98efdbe1-753c-42d6-bd00-bfb839711eb9"
/> |

This rendering will also be used by the proposed [`non-exhaustive-match`
rule](https://github.com/astral-sh/ruff/pull/29053), whose diagnostics
show uncovered values and their types.

[NFC]: https://www.unicode.org/reports/tr15/#Norm_Forms
2026-10-06 10:28:46 +01:00
Alex Waygood 97ef64a116 AGENTS.md: remove a load-bearing "necessarily" (#29120) 2026-10-05 18:57:32 +00:00
Alex Waygood 4ab929d988 Enable color in Hawk's CI output (#29117) 2026-10-05 17:36:38 +00:00
Alex Waygood e958591a34 Isolate daily fuzz issue creation from the fuzzer (#29113) 2026-10-05 18:00:23 +01:00
Alex Waygood 5f34507931 Group uv workflow and pre-commit updates (#29111) 2026-10-05 14:01:42 +00:00
Alex Waygood c79d773317 Include minimized reproducers in daily fuzz issues (#29100) 2026-10-05 14:41:20 +01:00
Alex Waygood df78bcfc14 Enable Renovate updates for the cargo-deny uv version (#29108) 2026-10-05 12:40:38 +00:00
Alex Waygood f52de0dcbd [ty] Enable additional ty checks for standalone scripts (#29093) 2026-10-03 18:51:56 +00:00
Alex Waygood 0a7f89cdd8 [ty] Minimize timeouts in py-fuzzer (#29045) 2026-10-02 16:56:21 +01:00
Alex Waygood 8ecfc92734 Check script lockfiles in CI (#29021) 2026-10-02 14:28:56 +01:00
Alex Waygood ccba26cc3a [ty] Bump docstring-adder pin (#29038) 2026-10-01 12:08:41 +00:00
Alex Waygood 8b90665aa7 [ty] Narrow later match cases after always-true guards (#28960) 2026-10-01 11:37:53 +00:00
Alex Waygood 7acc0eb6f6 Modernize the py-fuzzer Ruff configuration (#29020) 2026-09-30 12:15:46 +00:00
Alex Waygood f7446a5970 [ty] Update ecosystem pins and refresh script lockfiles (#29017) 2026-09-30 10:38:07 +00:00
Alex Waygood c7815feaf0 [ty] Update agent guidance for rules and mdtests (#28961) 2026-09-28 15:12:46 +00:00
Alex Waygood 869cb447ff [ty] Refactor suspicious condition classification and annotation lookup (#28915) 2026-09-26 14:54:40 +01:00
Alex Waygood 7d8e43a1e5 [ty] Clarify ecosystem summary count placement (#28894) 2026-09-25 12:21:16 +00:00
Alex Waygood 46ef6d4aa6 [ty] Avoid disjoint-cast diagnostics in situations where the value could have been inferred as being compatible with the casted type if it had only been inferred with the right type context (#28851) 2026-09-25 10:53:54 +00:00
Alex Waygood 29ebdf8035 Cleanup ecosystem summary report template (#28891) 2026-09-25 10:37:00 +00:00
Alex Waygood 0401be8069 [ty] Fix ecosystem dependency installation (#28883) 2026-09-24 21:55:11 +01:00
Alex Waygood eedfc62a75 [ty] Propagate outer type context through cast calls (#28855) 2026-09-24 16:09:18 +00:00
Alex Waygood d1cbb6f5e2 [ty] Use shallow fetches in primer setup (#28865) 2026-09-24 12:35:47 +00:00
Alex Waygood f3097a0328 Improve ecosystem summary skill (#28859) 2026-09-24 10:49:29 +00:00
Alex Waygood b4b8d517d1 [ty] Improve ecosystem summary and minimization workflows (#28789) 2026-09-22 19:41:57 +00:00
Alex Waygood 68073852bf [ty] Add rules that detect suspicious uses of Callable, Iterable, Iterator or Generator types in a boolean context (#28554) 2026-09-21 17:42:00 +01:00
Alex Waygood 6cde6bfb72 [ty] Generalize receiver binding for wrapped callables (#28725) 2026-09-21 11:23:31 +01:00
Alex Waygood 43c47c6972 [ty] Fix failing tests on main (#28689) 2026-09-18 11:51:49 +00:00
Alex Waygood 97489a5a44 [ty] Improve primary diagnostic annotations for redundant-condition(-strict) diagnostics (#28666)
## Summary

This addresses @sharkdp's feedback in
https://github.com/astral-sh/ruff/pull/28521#discussion_r4013438765.

Before:

<details>
<summary>Screenshot</summary>

<img width="1178" height="246" alt="image"
src="https://github.com/user-attachments/assets/fd06566d-b938-42bb-8af7-491feba5e1a8"
/>

</details>

On this PR:

<details>
<summary>Screenshot</summary>

<img width="1318" height="254" alt="image"
src="https://github.com/user-attachments/assets/10cac243-6de4-4d60-a7ee-ea79aec1e8a2"
/>

</details>

I'm still not _totally_ sold that this is better. I can see that the
reader's eye is naturally drawn towards the coloured text in the primary
annotation, but this change also makes our primary annotations for these
rules longer, and screen width is a scarce commodity. It also duplicates
information that's already presented in the diagnostic summary line. I'm
curious for what other folks think here!

## Test Plan

Inline snapshots.
2026-09-18 12:24:25 +01:00
Alex Waygood 53b6d44b3a [ty] Expand unreachable-code annotations for redundant conditions (#28674) 2026-09-17 20:52:44 +01:00
Alex Waygood 731e27aa53 [ty] Add an autofix for unused awaitables (#28668) 2026-09-17 18:34:29 +01:00
Alex Waygood d3c71baa79 [ty] redundant-condition(-strict): Add secondary annotations indicating unreachable code caused by boolean operands (#28521)
## Summary

When a `redundant-condition` diagnostic points to an operand of `and` or
`or`, that operand can still determine the reachability of the enclosing
test. For example, `if flag or nonempty:` with `nonempty: tuple[str,
str]` always enters the `if` body, so its `else` branch is unreachable.

Trace the reported operand through `and`, `or`, and `not`, treating
other operands as unknown. Add an unreachable-code annotation only when
that operand alone determines the enclosing test's truthiness. This
covers negated conditions, `if`/`elif`, loops, `match` guards, and
assertions without attributing a result that requires several operands
to any one of them.

Clarify the mdtest guidance so short independent definitions can be
repeated to keep inline snapshots close to their examples. (Codex tried
to tell me off about "repeated setup" when it reviewed my tests here.)

Stacked on #28290.
2026-09-17 13:04:53 +01:00
Alex Waygood b0320e4175 [ty] [redundant-condition rules] Extend unreachable-code annotations to while, assert and match statements (#28290)
## Summary

If we see that a `while` test is always true, we now annotate any
nontrivial statements following the `while` loop as unreachable.
Similarly, if we see it is always false, we now annotate the `while`
body as unreachable.

If we see that an `assert` statement is always `False`, we annotate any
nontrivial statements below it as unreachable.

And if we see that a `match` guard is always `False`, we annotate that
branch as always being unreachable.

## Test Plan

Inline snapshots
2026-09-16 18:57:17 +01:00
Alex Waygood 94be8866e7 [ty] Add subdiagnostics to redundant-condition rules warning about reachability implications (#28263)
Adds secondary annotations to `redundant-condition` and
`redundant-condition-strict` diagnostics to show when an always-truthy
or always-falsy `if`/`elif` condition makes a branch or following
statement unreachable. Annotations stop at the first line of the first
nontrivial statement, keeping multiline bodies readable.

For exhaustive chains whose branches all exit, suppress the
`assert_never` or defensive-assertion fix when the diagnostic identifies
unreachable code after the chain. Only attach reachability annotations
when the diagnostic covers the complete condition.

<details><summary>Screenshot</summary>

<img width="1182" height="422" alt="image"
src="https://github.com/user-attachments/assets/533851c3-0fd7-4e40-9293-a974fe288f8c"
/>

</details>
<details><summary>Screenshot</summary>

<img width="1204" height="434" alt="image"
src="https://github.com/user-attachments/assets/69585174-f863-4465-8215-3a0feb2ca424"
/>

</details>
<details><summary>Screenshot</summary>

<img width="1254" height="324" alt="image"
src="https://github.com/user-attachments/assets/bc3a79c3-ff0c-41ce-b692-a328205f26a1"
/>

</details>
2026-09-16 18:35:02 +01:00
Alex Waygood 50020fb1e8 [ty] Avoid storing constraint nodes twice (#28375) 2026-09-16 15:07:39 +01:00
Alex Waygood d27167b85c [ty] Show suggestions while typing in strings in the playground (#28611) 2026-09-15 14:10:41 +01:00
Alex Waygood 779c68aa6c [ty] Add a constructor for top callable types (#28579) 2026-09-14 17:10:19 +00:00
Alex Waygood 3a8fb30e6f [ty] Infer tuple variance from the full tuple spec (#28446) 2026-09-09 16:48:09 +01:00
Alex Waygood 94dd9702a2 [ty] Preserve wrapped functions in precise functools.partial relations (#28460) 2026-09-09 16:47:55 +01:00
Alex Waygood cb5c69f50b [ty] Preserve callable identity across specialized types (#28409) 2026-09-09 14:58:58 +00:00
Alex Waygood ab2963f21a [ty] Give existing autofixes descriptive titles (#28456) 2026-09-09 14:29:35 +01:00
Alex Waygood 4cdd004a71 [ty] Preserve redundant-condition diagnostics with unreachable operands (#28374) 2026-09-08 17:21:40 +00:00
Alex WaygoodandMicha Reiser 12132db288 [ty] Isolate uv integration tests from inherited environment settings (#28342)
Co-authored-by: Micha Reiser <micha@reiser.io>
2026-09-08 13:09:55 +02:00
Alex Waygood 321eebc2d6 [ty] Fix disjointness of type guards and boolean literals (#28363) 2026-09-08 11:00:10 +01:00
Alex WaygoodandDavid Peter eb9396423e [ty] Ensure f is identity(f) evaluates to Literal[True] (#28360)
Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
2026-09-07 16:19:15 +00:00
Alex Waygood 519fe8ff08 [ty] Cache exception suppression for context managers (#28376) 2026-09-07 12:48:53 +01:00
Alex Waygood cbb1cdc35e [ty] Dogfood more ty rules on our own scripts (#28387) 2026-09-07 10:04:25 +01: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
Alex Waygood 4fc78f66fd [ty] Add Literal fixes for diagnostics flagging invalid type annotations (#28185)
## Summary

Add unsafe fixes to diagnostics that suggest `Literal`: wrap integer,
bytes, and boolean literals and strings that cannot be parsed as forward
annotations in `Literal[...]`, and replace parameterized `LiteralString`
with `Literal` when its arguments are valid literal parameters.

The fixes preserve literal spelling and reuse unshadowed import aliases.
They import from `typing` on Python 3.8+, or from `typing_extensions` on
Python 3.7 when it is a declared direct dependency and the installed
module exports `Literal`. The typing-module availability checks are
shared with the `reveal_type` and `assert_never` fixes.
2026-09-06 08:35:58 +00:00
Alex Waygood 5804ffddcd [ty] Add defensive assertion fallback for redundant elif fixes (#28179)
When the final `elif` condition is always true but ty cannot add an
`assert_never` call, offer an unsafe fix for
`redundant-condition-strict` that replaces the `elif` with `else` and
moves the condition into a defensive assertion.

For example, on Python 3.10 in a project without a direct dependency on
`typing_extensions`, the fix makes this change:

```diff
 def handle(value: str | int):
     if isinstance(value, str):
         print(value)
-    elif isinstance(value, int):
+    else:
+        assert isinstance(value, int)
         print(value)
```

After the `str` branch, `value` must be an `int`, so the final
`isinstance` check is statically always true. The assertion retains that
check at runtime while making the exhaustive branching explicit.

The fallback also applies when the tested value cannot be reused as an
`assert_never` argument or import-name conflicts prevent a usable
import. It preserves condition parentheses, comments, indentation, and
line endings, and parenthesizes bare assignment expressions. No edit is
offered when the body cannot accommodate an indented assertion or
rewriting the header would discard a comment.

The fix is unsafe because an incorrect static assumption can make the
assertion fail at runtime.

Stacked on #28178.
2026-09-06 08:35:57 +00: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 089d8871b1 [ty] Require a direct dependency for reveal_type backport fixes (#28177)
On Python versions before 3.11, the `undefined-reveal` fix currently
offers to import `reveal_type` from `typing_extensions` even when the
project does not declare the backport. Only offer that fix when uv
dependency metadata identifies it as a direct dependency available to
the importing file. The Python 3.11+ fix continues to import from
`typing`.

Introduce a positive dependency query that shares project selection and
runtime module ownership with `missing-direct-dependency`. It respects
nested projects and dependency groups, and does not mistake a local
module shadowing `typing_extensions` for the declared distribution.
Missing metadata does not establish a dependency.

Stacked on #28170.
2026-09-06 00:37:24 +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
Alex Waygood 7cc9f1ed53 [ty] Document CallableTypeKind variants (#28277) 2026-09-03 16:28:53 +00:00
Alex Waygood 80b4891b64 [ty] Temporarily disable tuple types in property tests (#28275) 2026-09-03 14:21:48 +01:00
Alex Waygood 455e396582 [ty] lambda functions are always truthy (#28254)
## Summary

Function-like `Callable`s are ones where we know that the `Callable` is
also an instance of `FunctionType` at runtime. Since `FunctionType`
instances are always truthy, we should therefore treat function-like
`Callable` types as always truthy too. This fixes a false positive in
the ecosystem and will also reduce false negatives on
https://github.com/astral-sh/ruff/pull/28034.

## Test Plan

mdtests
2026-09-02 20:29:56 +01:00
Alex Waygood 7de38e7e2a [ty] Add a new disjoint-cast rule (#28129)
## Summary

`typing.cast()` leaves its argument unchanged at runtime, but accepts
casts even when the inferred source type and destination type cannot
overlap. Add a disabled-by-default `disjoint-cast` rule to flag these
cases:

```python
from typing import cast


def f(value: int) -> str:
    return cast(str, value)  # disjoint-cast
```

Casts between overlapping types remain accepted. The rule exempts stub
files and casts to or from `Never`.

The documentation explains less obvious disjoint casts involving
invariant containers and `NewType`, and discusses `TypeGuard` as a
possible alternative. Enable the rule in ecosystem analysis to assess
its impact.

## Error-tree diagnostics

Add error-tree explanations for disjointness checks involving invariant
generic arguments, unions, tuples, protocol members, final classes, and
TypedDict fields. Secondary annotations identify the inferred source
type, the destination, and available type definitions, including
`@final` decorators.

For example, casting a class whose `values()` method returns `list[str]`
to a protocol requiring `list[int]` produces:

```text
info: protocol member `values` is incompatible
info: └── return types `list[int]` and `list[str]` are disjoint
info:     └── `int` and `str` are not mutual subtypes of each other
```

## Parallels in other type checkers

| Checker | Rule | Comparison with `disjoint-cast` |
|---|---|---|
| **basedpyright** |
[`reportInvalidCast`](https://docs.basedpyright.com/latest/benefits-over-pyright/new-diagnostic-rules/#reportinvalidcast)
| Same high-level purpose: reject casts between non-overlapping types.
Its docs explicitly demonstrate `int` → `str`, and discuss rejecting
`dict` → `TypedDict` casts. |
| **Pyrefly** |
[`invalid-cast`](https://pyrefly.org/en/docs/error-kinds/#invalid-cast)
| Disabled by default, but deliberately narrower than `disjoint-cast`:
checks disjoint **runtime classes**. It accepts `list[int]` →
`list[str]`, ignores tuple-element differences, treats TypedDicts as
overlapping with `dict`, and excludes protocols. |

Mypy and upstream Pyright do not have direct equivalents. Both can
report redundant casts, but their current `cast` implementations do not
check source/destination overlap: [mypy
implementation](https://github.com/python/mypy/blob/master/mypy/checkexpr.py#L4546),
[Pyright
implementation](https://github.com/microsoft/pyright/blob/main/packages/pyright-internal/src/analyzer/typeEvaluator.ts#L10917).

### TypeScript

TypeScript has a similar restriction on type assertions: **TS2352**
rejects assertions between types that do not sufficiently overlap. For
example:

```typescript
const value = "hello" as number; // TS2352
```

Like `typing.cast()`, TypeScript's type assertions do not perform
runtime validation. An explicit two-step assertion through `unknown` or
`any`, such as `value as unknown as Target`, can bypass the restriction.
The overlap criterion differs from ty's disjointness relation, so this
is a precedent for checking suspicious casts rather than an exact
semantic equivalent. See the [TypeScript handbook on type
assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions).
2026-09-01 21:06:04 +00:00
Alex Waygood 79e32050dd [ty] Expand tests for upcoming redundant-condition rules (#28182)
## Summary

Builds on #28032.

Expand the test suite introduced in #28030 for the upcoming
`redundant-condition` and `redundant-condition-strict` rules in #28034.
The new examples cover short-circuiting, chained comparisons,
conditional expressions, nested boolean tests, required `TypedDict` keys
established by narrowing, and exemptions for environment-dependent
values and defensive runtime checks.

Only `redundant_condition.md` changes. Expected diagnostics remain
marked with TODOs, while `reveal_type` assertions document the
distinction between an expression's inferred value type and its
truthiness when used directly as a condition.
2026-08-31 08:49:34 +01:00
Alex Waygood 942e781b96 [ty] Share definition resolution between type inference and IDE features (#28032)
## Summary

This is a behaviour-preserving refactor of definition resolution:
navigation targets, docstrings, and existing type-checking results are
preserved. The changes are to the API boundaries and the inference work
performed internally.

## LSP benefits

Go-to-definition and go-to-declaration for ordinary builtins such as
`isinstance` no longer request completed inference of the enclosing
scope just to check whether numeric-annotation expansion applies. Only
`float` and `complex` need that check. Completing scope inference
computes and caches types for the scope's expressions, along with
diagnostics and other inference data, even though navigation only needs
the symbol's definition.

The expected benefit is lower navigation latency when scope inference is
uncached or has been invalidated by an edit, plus fewer temporary
allocations and fewer inference results retained solely for navigation.
The savings depend on the workload: cached inference leaves little
computation to avoid, and diagnostics may still require the same cached
data. This does not guarantee lower steady-state memory usage for the
server.

## Reuse during type inference

The shared API will support inference-side consumers such as
[#27634](https://github.com/astral-sh/ruff/pull/27634), whose
redundant-condition analysis needs source definitions as well as
inferred types:

- It follows assignments and imports to recognize conditions derived
from `sys.version_info`, `sys.platform`, `os.name`, or
`typing.TYPE_CHECKING`. These can be constant for the configured target
while still being deliberate compatibility guards, including when
accessed through aliases or attributes.
- It finds the annotation behind an always-truthy tuple so the
diagnostic can point to `tuple[T]` and suggest `tuple[T, ...]` when a
tuple of arbitrary length may have been intended.

These checks run while the enclosing scope is still being inferred.
Using the IDE API to obtain expression types at that point can re-enter
inference of the same scope, causing avoidable Salsa query cycles and
repeated work. The shared helpers instead accept an explicit scope or an
already-inferred receiver type, allowing these checks to reuse the
existing name, import, and member-resolution rules without requesting
completed inference of the current scope. Pydantic's existing
annotation-alias lookup is also migrated to this API.

## Diff overview

- **Moved:** Existing scope traversal, class/MRO member lookup, import
resolution, and resolved-definition/docstring helpers move into
`definition_resolution`, preserving their lookup algorithms.
Numeric-annotation expansion and stub-to-implementation mapping remain
in `ide_support`.
- **Added:** Shared lookup entry points and three regression tests that
assert definition lookup does not request completed scope inference.
- **Changed:** IDE wrappers and the Pydantic caller delegate to the
shared helpers; builtin name lookup gets the `float`/`complex` inference
guard described above. The remaining edits adapt parameters, imports,
and visibility, and document the shared API's contracts.
2026-08-31 07:24:21 +01:00