[ty] Avoid stack overflow when calculating inferable typevars (#21971)

When we calculate which typevars are inferable in a generic context, the
result might include more than the typevars bound by the generic
context. The canonical example is a generic method of a generic class:

```py
class C[A]:
    def method[T](self, t: T): ...
```

Here, the inferable typevar set of `method` contains `Self` and `T`, as
you'd expect. (Those are the typevars bound by the method.) But it also
contains `A@C`, since the implicit `Self` typevar is defined as `Self:
C[A]`. That means when we call `method`, we need to mark `A@C` as
inferable, so that we can determine the correct mapping for `A@C` at the
call site.

Fixes https://github.com/astral-sh/ty/issues/1874
This commit is contained in:
Douglas Creager 2025-12-15 10:25:33 -05:00 committed by GitHub
parent 8f530a7ab0
commit cbfecfaf41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 41 additions and 3 deletions

View File

@ -800,6 +800,29 @@ def func(x: D): ...
func(G()) # error: [invalid-argument-type]
```
### Self-referential protocol with different specialization
This is a minimal reproduction for [ty#1874](https://github.com/astral-sh/ty/issues/1874).
```py
from __future__ import annotations
from typing import Protocol
from ty_extensions import generic_context
class A[S, R](Protocol):
def get(self, s: S) -> R: ...
def set(self, s: S, r: R) -> S: ...
def merge[R2](self, other: A[S, R2]) -> A[S, tuple[R, R2]]: ...
class Impl[S, R](A[S, R]):
def foo(self, s: S) -> S:
return self.set(s, self.get(s))
reveal_type(generic_context(A.get)) # revealed: ty_extensions.GenericContext[Self@get]
reveal_type(generic_context(A.merge)) # revealed: ty_extensions.GenericContext[Self@merge, R2@merge]
reveal_type(generic_context(Impl.foo)) # revealed: ty_extensions.GenericContext[Self@foo]
```
## Tuple as a PEP-695 generic class
Our special handling for `tuple` does not break if `tuple` is defined as a PEP-695 generic class in

View File

@ -23,7 +23,7 @@ use crate::types::{
KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Signature, Type,
TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity,
TypeVarInstance, TypeVarKind, TypeVarVariance, UnionType, declaration_type,
walk_bound_type_var_type,
walk_type_var_bounds,
};
use crate::{Db, FxOrderMap, FxOrderSet};
@ -290,6 +290,18 @@ impl<'db> GenericContext<'db> {
)
}
/// Returns the typevars that are inferable in this generic context. This set might include
/// more typevars than the ones directly bound by the generic context. For instance, consider a
/// method of a generic class:
///
/// ```py
/// class C[A]:
/// def method[T](self, t: T):
/// ```
///
/// In this example, `method`'s generic context binds `Self` and `T`, but its inferable set
/// also includes `A@C`. This is needed because at each call site, we need to infer the
/// specialized class instance type whose method is being invoked.
pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> {
#[derive(Default)]
struct CollectTypeVars<'db> {
@ -299,7 +311,7 @@ impl<'db> GenericContext<'db> {
impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> {
fn should_visit_lazy_type_attributes(&self) -> bool {
true
false
}
fn visit_bound_type_var_type(
@ -310,7 +322,10 @@ impl<'db> GenericContext<'db> {
self.typevars
.borrow_mut()
.insert(bound_typevar.identity(db));
walk_bound_type_var_type(db, bound_typevar, self);
let typevar = bound_typevar.typevar(db);
if let Some(bound_or_constraints) = typevar.bound_or_constraints(db) {
walk_type_var_bounds(db, bound_or_constraints, self);
}
}
fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {