[ty] Use annotated parameters as type context#20635
Conversation
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-10-03 20:48:40.319496259 +0000
+++ new-output.txt 2025-10-03 20:48:43.574517278 +0000
@@ -1,6 +1,6 @@
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/29ab321/src/function/execute.rs:217:25 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_type_statement.py`: `PEP695TypeAliasType < 'db >::value_type_(Id(cc17)): execute: too many cycle iterations`
-fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/29ab321/src/function/execute.rs:217:25 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(15c32)): execute: too many cycle iterations`
+fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/29ab321/src/function/execute.rs:217:25 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(16432)): execute: too many cycle iterations`
_directives_deprecated_library.py:15:31: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
_directives_deprecated_library.py:30:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `str`
_directives_deprecated_library.py:36:41: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Self@__add__` |
|
a90814a to
49661f2
Compare
|
It looks like a lot of these errors are duplicated diagnostics now that we infer argument expressions multiple times for callable unions/overloads, e.g., if flag:
def f(_: int): ...
else:
def f(_: int): ...
if flag:
x = 0
# warning[possibly-unresolved-reference] Name `x` used when possibly not defined
# warning[possibly-unresolved-reference] Name `x` used when possibly not defined
f(x)(In this example, multi-inference is not strictly necessary because the signatures are identical, but that's a separate performance concern). I'm not sure there's a great way around this other than adding checks around all diagnostics that aren't type-context specific? |
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
missing-typed-dict-key |
162 | 0 | 0 |
unresolved-attribute |
2 | 109 | 0 |
type-assertion-failure |
30 | 3 | 0 |
invalid-argument-type |
4 | 0 | 9 |
invalid-context-manager |
0 | 13 | 0 |
non-subscriptable |
9 | 0 | 0 |
unsupported-operator |
2 | 2 | 2 |
possibly-missing-attribute |
3 | 0 | 1 |
redundant-cast |
3 | 1 | 0 |
invalid-assignment |
3 | 0 | 0 |
invalid-return-type |
2 | 0 | 0 |
possibly-missing-implicit-call |
1 | 0 | 0 |
unused-ignore-comment |
0 | 1 | 0 |
| Total | 221 | 129 | 12 |
Instead of adding checks for every non-type-context-specific context, can we use state on |
b68d4ca to
f9a4ae1
Compare
CodSpeed Instrumentation Performance ReportMerging #20635 will degrade performances by 17.12%Comparing Summary
Benchmarks breakdown
|
d8b1f25 to
f5e0cb1
Compare
CodSpeed WallTime Performance ReportMerging #20635 will degrade performances by 21.13%Comparing Summary
Benchmarks breakdown
|
|
|
I've decided to remove the idea of "lints that may be affected by bidirectional type context", as this seemed quite. Instead, the new implementation does the following for each argument:
This does mean we get worse diagnostics for I also went through the ecosystem report, and most diagnostic changes seem to be correct, or the result of an unrelated known limitation (namely |
e8b8f6d to
ecd6491
Compare
| match call_expression_tcx { | ||
| // A type variable is not a useful type-context for expression inference, and applying it | ||
| // to the return type can lead to confusing unions in nested generic calls. | ||
| Type::TypeVar(_) => {} |
There was a problem hiding this comment.
This is mostly for better diagnostics. Without it, reveal_type(identity(1)) is revealed as T@reveal_type | int. It also doesn't quite work for parameters annotated as T | None — we still might union the inferred type with T@f in that case.
ecd6491 to
af00ebe
Compare
|
|
||
| fn get_argument_node( | ||
| node: ast::AnyNodeRef<'_>, | ||
| argument_index: Option<usize>, |
There was a problem hiding this comment.
nit: I think you can update this helper to take in argument_index: usize, and use argument_index? up above when you call the helper. Then you only need to match on node below.
There was a problem hiding this comment.
We also use get_argument_node in the diagnostic code with an Option<usize>, so I'm not sure the change is worth it.
| let val_ty = | ||
| BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Invariant); |
There was a problem hiding this comment.
Were there tests that were failing with the old signature? I can see that the new signature means that there's now a return value, which will have the same type as the value being checked, but I don't see any tests that rely on that behavior.
There was a problem hiding this comment.
The issue that showed up in the ecosystem report was that the f(1) in assert_type(f(1), int) was inferred as int | Any, because the signature of assert_type was (Any, ..) -> ... I added a test.
af00ebe to
6227fca
Compare
|
Deduplicating the parameter types helps the performance regression somewhat, but outside of that I think it's unavoidable. |
Summary
Use the type annotation of function parameters as bidirectional type context when inferring the argument expression. For example, the following example now type-checks:
Part of astral-sh/ty#168.