Skip to content

Commit 48f1771

Browse files
mtshibaAlexWaygood
andauthored
[ty] fix infinite recursion with generic type aliases (#20969)
Co-authored-by: Alex Waygood <[email protected]>
1 parent 4ca7459 commit 48f1771

File tree

2 files changed

+103
-22
lines changed

2 files changed

+103
-22
lines changed

crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,64 @@ type X[T: X] = T
170170
def _(x: X):
171171
assert x
172172
```
173+
174+
## Recursive generic type aliases
175+
176+
```py
177+
type RecursiveList[T] = T | list[RecursiveList[T]]
178+
179+
r1: RecursiveList[int] = 1
180+
r2: RecursiveList[int] = [1, [1, 2, 3]]
181+
# error: [invalid-assignment] "Object of type `Literal["a"]` is not assignable to `RecursiveList[int]`"
182+
r3: RecursiveList[int] = "a"
183+
# error: [invalid-assignment]
184+
r4: RecursiveList[int] = ["a"]
185+
# TODO: this should be an error
186+
r5: RecursiveList[int] = [1, ["a"]]
187+
188+
def _(x: RecursiveList[int]):
189+
if isinstance(x, list):
190+
# TODO: should be `list[RecursiveList[int]]
191+
reveal_type(x[0]) # revealed: int | list[Any]
192+
if isinstance(x, list) and isinstance(x[0], list):
193+
# TODO: should be `list[RecursiveList[int]]`
194+
reveal_type(x[0]) # revealed: list[Any]
195+
```
196+
197+
Assignment checks respect structural subtyping, i.e. type aliases with the same structure are
198+
assignable to each other.
199+
200+
```py
201+
# This is structurally equivalent to RecursiveList[T].
202+
type RecursiveList2[T] = T | list[T | list[RecursiveList[T]]]
203+
# This is not structurally equivalent to RecursiveList[T].
204+
type RecursiveList3[T] = T | list[list[RecursiveList[T]]]
205+
206+
def _(x: RecursiveList[int], y: RecursiveList2[int]):
207+
r1: RecursiveList2[int] = x
208+
# error: [invalid-assignment]
209+
r2: RecursiveList3[int] = x
210+
211+
r3: RecursiveList[int] = y
212+
# error: [invalid-assignment]
213+
r4: RecursiveList3[int] = y
214+
```
215+
216+
It is also possible to handle divergent type aliases that are not actually have instances.
217+
218+
```py
219+
# The type variable `T` has no meaning here, it's just to make sure it works correctly.
220+
type DivergentList[T] = list[DivergentList[T]]
221+
222+
d1: DivergentList[int] = []
223+
# error: [invalid-assignment]
224+
d2: DivergentList[int] = [1]
225+
# error: [invalid-assignment]
226+
d3: DivergentList[int] = ["a"]
227+
# TODO: this should be an error
228+
d4: DivergentList[int] = [[1]]
229+
230+
def _(x: DivergentList[int]):
231+
d1: DivergentList[int] = [x]
232+
d2: DivergentList[int] = x[0]
233+
```

crates/ty_python_semantic/src/types.rs

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6772,7 +6772,11 @@ impl<'db> Type<'db> {
67726772
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping, tcx)),
67736773

67746774
Type::TypeAlias(alias) => {
6775-
visitor.visit(self, || alias.value_type(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor))
6775+
// Do not call `value_type` here. `value_type` does the specialization internally, so `apply_type_mapping` is performed without `visitor` inheritance.
6776+
// In the case of recursive type aliases, this leads to infinite recursion.
6777+
// Instead, call `raw_value_type` and perform the specialization after the `visitor` cache has been created.
6778+
let value_type = visitor.visit(self, || alias.raw_value_type(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor));
6779+
alias.apply_function_specialization(db, value_type).apply_type_mapping_impl(db, type_mapping, tcx, visitor)
67766780
}
67776781

67786782
Type::ModuleLiteral(_)
@@ -10716,31 +10720,12 @@ impl<'db> PEP695TypeAliasType<'db> {
1071610720
}
1071710721

1071810722
/// The RHS type of a PEP-695 style type alias with specialization applied.
10719-
#[salsa::tracked(cycle_initial=value_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
1072010723
pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> {
10721-
let value_type = self.raw_value_type(db);
10722-
10723-
if let Some(generic_context) = self.generic_context(db) {
10724-
let specialization = self
10725-
.specialization(db)
10726-
.unwrap_or_else(|| generic_context.default_specialization(db, None));
10727-
10728-
value_type.apply_specialization(db, specialization)
10729-
} else {
10730-
value_type
10731-
}
10724+
self.apply_function_specialization(db, self.raw_value_type(db))
1073210725
}
1073310726

1073410727
/// The RHS type of a PEP-695 style type alias with *no* specialization applied.
10735-
///
10736-
/// ## Warning
10737-
///
10738-
/// This uses the semantic index to find the definition of the type alias. This means that if the
10739-
/// calling query is not in the same file as this type alias is defined in, then this will create
10740-
/// a cross-module dependency directly on the full AST which will lead to cache
10741-
/// over-invalidation.
10742-
/// This method also calls the type inference functions, and since type aliases can have recursive structures,
10743-
/// we should be careful not to create infinite recursions in this method (or make it tracked if necessary).
10728+
#[salsa::tracked(cycle_initial=value_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
1074410729
pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> {
1074510730
let scope = self.rhs_scope(db);
1074610731
let module = parsed_module(db, scope.file(db)).load(db);
@@ -10750,6 +10735,17 @@ impl<'db> PEP695TypeAliasType<'db> {
1075010735
definition_expression_type(db, definition, &type_alias_stmt_node.node(&module).value)
1075110736
}
1075210737

10738+
fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> {
10739+
if let Some(generic_context) = self.generic_context(db) {
10740+
let specialization = self
10741+
.specialization(db)
10742+
.unwrap_or_else(|| generic_context.default_specialization(db, None));
10743+
ty.apply_specialization(db, specialization)
10744+
} else {
10745+
ty
10746+
}
10747+
}
10748+
1075310749
pub(crate) fn apply_specialization(
1075410750
self,
1075510751
db: &'db dyn Db,
@@ -10939,6 +10935,13 @@ impl<'db> TypeAliasType<'db> {
1093910935
}
1094010936
}
1094110937

10938+
fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> {
10939+
match self {
10940+
TypeAliasType::PEP695(type_alias) => type_alias.apply_function_specialization(db, ty),
10941+
TypeAliasType::ManualPEP695(_) => ty,
10942+
}
10943+
}
10944+
1094210945
pub(crate) fn apply_specialization(
1094310946
self,
1094410947
db: &'db dyn Db,
@@ -11799,6 +11802,9 @@ type CovariantAlias[T] = Covariant[T]
1179911802
type ContravariantAlias[T] = Contravariant[T]
1180011803
type InvariantAlias[T] = Invariant[T]
1180111804
type BivariantAlias[T] = Bivariant[T]
11805+
11806+
type RecursiveAlias[T] = None | list[RecursiveAlias[T]]
11807+
type RecursiveAlias2[T] = None | list[T] | list[RecursiveAlias2[T]]
1180211808
"#,
1180311809
)
1180411810
.unwrap();
@@ -11829,5 +11835,19 @@ type BivariantAlias[T] = Bivariant[T]
1182911835
.variance_of(&db, get_bound_typevar(&db, bivariant)),
1183011836
TypeVarVariance::Bivariant
1183111837
);
11838+
11839+
let recursive = get_type_alias(&db, "RecursiveAlias");
11840+
assert_eq!(
11841+
KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive))
11842+
.variance_of(&db, get_bound_typevar(&db, recursive)),
11843+
TypeVarVariance::Bivariant
11844+
);
11845+
11846+
let recursive2 = get_type_alias(&db, "RecursiveAlias2");
11847+
assert_eq!(
11848+
KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive2))
11849+
.variance_of(&db, get_bound_typevar(&db, recursive2)),
11850+
TypeVarVariance::Invariant
11851+
);
1183211852
}
1183311853
}

0 commit comments

Comments
 (0)