Summary
--
This PR fixesastral-sh/ty#2484 by moving our SARIF rendering from
`ruff_linter` into `ruff_db` and
wiring that up as an output format for ty.
The first commit introduces the metadata and makes some other
preparatory changes to the SARIF
emitter in Ruff. The second commit then moves the emitter to `ruff_db`
and removes the remaining
Ruff-specific calls. The third commit then wires that up to ty's CLI.
Test Plan
--
Existing tests, adapted to `ruff_db`, as well as a new ty CLI test
Use the vendored runtime and generator instead of the registry dependency,
providing Unicode 17 names with Python-compatible matching. Update workspace
dependencies and tool configuration, add parser regressions, and document how
to update Unicode data when supporting a new Python version.
This is a partial solution for #16412
I would still like to add something like
`runtime-evaluated-generic-subscripts`, but that will require further
consideration about how the presence of such subscripts should affect
the rest of the type expression, or in more extreme cases, the other
annotations in the function/class definition.
## Summary
For things like SQLAlchemy's `Mapped` the evaluation of the annotations
only requires a subset of all the symbols to be resolvable at runtime,
this is sometimes necessary to make forward references to other models
work without causing import cycles.
But this means we can neither assume that the annotations are
runtime-required, nor can we assume, that they are typing-only. So we
should assume the referenced binding is already in its correct execution
context, i.e. we should neither emit TC001-003, nor TC004 violations.
Instead of adding two additional settings now and potentially two more
settings later, the existing two settings for runtime-required
annotations now accept a table in addition to the list to flag
individual imports with individual runtime requirements.
For example:
```toml
[lint.flake8-type-checking.runtime-evaluated-base-classes]
"pydantic.BaseModel" = "required"
"sqlalchemy.orm.DeclarativeBase" = "ambiguous"
[lint.flake8-type-checking.runtime-evaluated-decorators]
"pydantic.validate_call" = "required"
"sqlalchemy.orm.declared_attr" = "ambiguous"
```
## Test Plan
`cargo nextest run`
## 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.
Summary
--
This PR fixes an issue I saw in the ecosystem report for the
stabilization PR
(https://github.com/astral-sh/ruff/pull/28761#issuecomment-5767098108).
In short, we were flagging annotations like this:
```py
__lazy_modules__ = ["pathlib"]
from pathlib import Path
class C:
path: Path
```
on Python versions before 3.14, even though they are not actually
resolved immediately when
`__lazy_modules__` makes the imports lazy (3.15 > 3.14, as it turns
out).
Because the semantic model checks the target version when setting
`TYPING_ONLY_ANNOTATION`, we
instead check the other two annotation flags:
`RUNTIME_EVALUATED_ANNOTATION` and
`RUNTIME_REQUIRED_ANNOTATION`. If an annotation is runtime-evaluated and
not runtime-required, and
we didn't already bail out at the
`semantic.execution_context().is_runtime()` check earlier in the
same function, then we know that we're in an annotation that _would_
have been typing-only on 3.14+.
Test Plan
--
New mdtests and the ecosystem check here. I expect -12 scikit-build-core
diagnostics compared to main.
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:
- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
requests.)
- Does this pull request include references to any relevant issues?
- Does this PR follow our AI policy
(https://github.com/astral-sh/.github/blob/main/AI_POLICY.md)?
-->
## Summary
<!-- What's the purpose of the change? What does it do, and why? -->
This moves the semantic model for pytest fixtures from
`dedicated/pytest.rs`[^1] to `dedicated/pytest/fixtures.rs`, where it is
now a sibling of other pytest sub-models, such as the test collection
model at `dedicated/pytest/collection.rs`.
[^1]: (in `crates/ty_python_semantic/src/types`)
## Test Plan
This is a refactor that relies on existing test coverage.
<!-- How was it tested? -->
When dependency declarations change without updating `uv.lock`, ty's
frozen metadata query returns the old dependency graph. This can leave
`missing-direct-dependency` diagnostics after the dependency has been
declared.
Drop `--frozen` so uv can refresh the lockfile. Keep `--active` without
synchronizing installed packages. Add a stale-lock regression and update
the rule documentation and its generated copies.
CLI-only `uv check --frozen` and `--isolated` policies are not forwarded
to ty's metadata subprocess. Preserving those policies needs a uv-side
follow-up.
## Summary
ty supports in-memory changes, but uv always reads the script's content
from disk. This discrepancy could lead to ty skipping a sync when,
between opening a file and saving it, the script metadata block wasn't
changed, but it changed compared to what was saved on disk when the file
was opened.
The root cause of this issue is that ty computes the script-fingerprint
from the current file content (which may include in-memory changes).
Instead, it should compute the fingerprint as it is seen on disk.
I considered a few different approaches:
* Support reading the persistent content in `ruff_db`: Add a new
`persisted_source_text` query, track the persisted `File` revision and
the in-memory revision separately. `ScriptsEnvironments` can then
compute the fingerprint based on the persisted source text.
* Use `system.read_text` in `ScriptsEnvironments`. I disregarded that
quickly because we proactively trigger the synchronization of all
scripts in a few places and we rely on this bailing quickly if the files
are unchanged (we don't want to read each script to bail).
* Introduce a new `Force` sync mode and use it in `didSave`.
I'd pick the first approach if we had multiple frontends that supported
in-memory changes. But, given that it's only the LSP, I started with the
`Force` sync mode, and I almost opened a PR for it. During the final
cleanups, I realized that there's a much simpler LSP-only fix (the CLI
doesn't have this problem).
We don't need a `persisted_source_text` query if we schedule the script
synchronization before we set the Script's in-memory content in
`didOpen`. That is, first trigger the synchronization, then open the
file and load its in-memory content. That's all we need.
Fixesastral-sh/ty#4250.
## Test Plan
**Before**
Reload VS Code with unsaved changes, hit save. Note how the script block
isn't synced (because we didn't make any changes to it)
https://github.com/user-attachments/assets/cd050b94-08a3-4c61-ba14-4a3f1b7a1e48
**This PR**
Same sequence, but the sync runs.
https://github.com/user-attachments/assets/c8acc477-7666-4411-a9b4-df33328ccb7e
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.
ty can panic when a generic protocol definition has an incomplete type
parameter list. The autofix for `invalid-generic-class` assumes that the
list contains both brackets, which can produce an inverted range or
split a Unicode identifier when the closing bracket is missing.
Check the delimiters before comparing the type parameters with the
`Protocol` subscript. Incomplete lists still produce diagnostics, but do
not receive this autofix. Targeted Hawk expectations preserve the public
saturating arithmetic helpers in `TextSize`.
Fixesastral-sh/ty#4397.
## Test plan
Add mdtests for an empty, unclosed type parameter list and an unclosed
list containing a Unicode name. Both cases produce diagnostics without
panicking. Existing protocol tests retain coverage for the autofix on
complete type parameter lists.
## 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.