Skip to content

Commit 459ae8f

Browse files
committed
Fix various overflows with recursive type aliases
1 parent a8d3850 commit 459ae8f

23 files changed

Lines changed: 1233 additions & 372 deletions

crates/ty_python_semantic/resources/mdtest/call/constructor.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,52 @@ a: int | MaybeInt = MaybeInt("42") # OK
291291
b: int = MaybeInt("42") # error: [invalid-assignment]
292292
```
293293

294+
### `__new__` returning a recursive alias union
295+
296+
```toml
297+
[environment]
298+
python-version = "3.12"
299+
```
300+
301+
```py
302+
from __future__ import annotations
303+
304+
type RecursiveNewReturn = RecursiveNew | RecursiveNewReturn
305+
306+
class RecursiveNew:
307+
def __new__(cls, value: int) -> RecursiveNewReturn:
308+
raise NotImplementedError
309+
310+
def __init__(self, value: str) -> None: ...
311+
312+
# error: [invalid-argument-type] "Argument to `RecursiveNew.__init__` is incorrect: Expected `str`, found `Literal[1]`"
313+
reveal_type(RecursiveNew(1)) # revealed: RecursiveNew
314+
```
315+
316+
### Metaclass `__call__` returning a recursive alias union
317+
318+
```toml
319+
[environment]
320+
python-version = "3.12"
321+
```
322+
323+
```py
324+
from __future__ import annotations
325+
326+
type RecursiveMetaCallReturn = RecursiveMetaCall | RecursiveMetaCallReturn
327+
328+
class RecursiveMeta(type):
329+
def __call__(cls, value: int) -> RecursiveMetaCallReturn:
330+
raise NotImplementedError
331+
332+
class RecursiveMetaCall(metaclass=RecursiveMeta):
333+
def __new__(cls, value: str) -> RecursiveMetaCall:
334+
raise NotImplementedError
335+
336+
# error: [invalid-argument-type] "Argument to constructor `RecursiveMetaCall.__new__` is incorrect: Expected `str`, found `Literal[1]`"
337+
reveal_type(RecursiveMetaCall(1)) # revealed: RecursiveMetaCall
338+
```
339+
294340
### `__new__` returning an intersection type
295341

296342
```py

crates/ty_python_semantic/resources/mdtest/call/dunder.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,30 @@ class_with_callable_dunder = ClassWithNonMethodDunder()
171171
reveal_type(class_with_callable_dunder[0]) # revealed: str
172172
```
173173

174+
Recursive aliases in the dunder annotation should still terminate when we look up and call the
175+
dunder via `type(obj)`:
176+
177+
```toml
178+
[environment]
179+
python-version = "3.12"
180+
```
181+
182+
```py
183+
from typing import reveal_type
184+
185+
class G:
186+
def __call__(self, key: int) -> "RecursiveGetItem":
187+
return self
188+
189+
type RecursiveGetItem = G | RecursiveGetItem
190+
191+
class C:
192+
__getitem__: RecursiveGetItem = G()
193+
194+
def f(x: C):
195+
reveal_type(x[0]) # revealed: G | Unknown
196+
```
197+
174198
## Dunders are looked up using the descriptor protocol
175199

176200
Here, we demonstrate that the descriptor protocol is invoked when looking up a dunder method. Note

crates/ty_python_semantic/resources/mdtest/cycle.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,33 @@ class Cyclic:
156156
reveal_type(Cyclic("").data)
157157
```
158158

159+
## Meta-type lookups through recursive aliases
160+
161+
Recursive aliases can show up in class-body annotations and in operations that need the meta type,
162+
such as `__class__` lookup. These should not recurse indefinitely.
163+
164+
```toml
165+
[environment]
166+
python-version = "3.12"
167+
```
168+
169+
```py
170+
from typing import reveal_type
171+
172+
class G:
173+
pass
174+
175+
type RecursiveAlias = G | RecursiveAlias
176+
177+
class C:
178+
x: RecursiveAlias
179+
180+
reveal_type(C.x) # revealed: G
181+
182+
def f(x: RecursiveAlias):
183+
reveal_type(x.__class__) # revealed: type[G]
184+
```
185+
159186
## Lazy cached property behind `hasattr`
160187

161188
This pattern used to panic with "too many cycle iterations".

crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,157 @@ def _(x: Bar[int]):
439439
reveal_type(x) # revealed: int | list[int]
440440
```
441441

442+
### Recursive aliases in operations
443+
444+
```py
445+
from typing import Callable, Iterator, Literal, TypedDict, overload
446+
from ty_extensions import all_members, has_member
447+
448+
class RecursiveItem: ...
449+
450+
type RecursiveUnion = RecursiveItem | RecursiveUnion
451+
452+
def subscript_recursive_alias(x: RecursiveUnion):
453+
# error: [not-subscriptable] "Cannot subscript object of type `RecursiveItem` with no `__getitem__` method"
454+
reveal_type(x[0]) # revealed: Unknown
455+
456+
class RecursiveIterableItem:
457+
def __iter__(self) -> Iterator["RecursiveIterable"]:
458+
return iter([self])
459+
460+
type RecursiveIterable = RecursiveIterableItem | RecursiveIterable
461+
462+
def iterate_recursive_alias(x: RecursiveIterable):
463+
for y in x:
464+
reveal_type(y) # revealed: RecursiveIterableItem | Unknown
465+
466+
def star_recursive_alias(x: RecursiveIterable):
467+
def g(*args: object): ...
468+
469+
g(*x)
470+
471+
class RecursiveCallable:
472+
def __call__(self) -> "DunderRecursive":
473+
return self
474+
475+
type DunderRecursive = RecursiveCallable | DunderRecursive
476+
477+
class HasRecursiveLen:
478+
__len__: DunderRecursive = RecursiveCallable()
479+
480+
def len_recursive_alias(x: HasRecursiveLen):
481+
# error: [invalid-argument-type] "Argument to function `len` is incorrect: Expected `Sized`, found `HasRecursiveLen`"
482+
reveal_type(len(x)) # revealed: int
483+
484+
class RecursiveMembers:
485+
attr: int
486+
487+
type RecursiveMembersAlias = RecursiveMembers | RecursiveMembersAlias
488+
489+
def list_recursive_alias_members(x: RecursiveMembersAlias):
490+
all_members(x)
491+
reveal_type(has_member(x, "attr")) # revealed: Literal[False]
492+
493+
class RecursiveAttribute:
494+
attr: int
495+
496+
type RecursiveAttributeAlias = RecursiveAttribute | RecursiveAttributeAlias
497+
498+
def assign_recursive_alias_attribute(x: RecursiveAttributeAlias):
499+
x.attr = 1
500+
501+
def delete_recursive_alias_attribute(x: RecursiveAttributeAlias):
502+
del x.attr
503+
504+
class RecursiveKwargs(TypedDict):
505+
kind: Literal["a"]
506+
a: int
507+
508+
type RecursiveKwargsAlias = RecursiveKwargs | RecursiveKwargsAlias
509+
510+
def call_with_recursive_kwargs_alias(x: RecursiveKwargsAlias):
511+
def g(a: int): ...
512+
513+
g(**x)
514+
515+
def narrow_recursive_typed_dict_alias(x: RecursiveKwargsAlias):
516+
if x["kind"] == "a":
517+
reveal_type(x) # revealed: RecursiveKwargs
518+
519+
type RecursiveDecoratorReturn = Callable[[int], int] | RecursiveDecoratorReturn
520+
521+
def recursive_decorator_return(fn: Callable[[int], int]) -> RecursiveDecoratorReturn: ...
522+
@recursive_decorator_return
523+
def decorated(x: int) -> int:
524+
return x
525+
526+
reveal_type(decorated) # revealed: ((int, /) -> int) | Unknown
527+
528+
class BaseWithMethod:
529+
def method(self) -> None: ...
530+
531+
class RecursiveSuperOwner(BaseWithMethod): ...
532+
533+
type RecursiveSuperOwnerAlias = RecursiveSuperOwner | RecursiveSuperOwnerAlias
534+
535+
def recursive_super_owner(x: RecursiveSuperOwnerAlias):
536+
super(RecursiveSuperOwner, x).method()
537+
538+
type RecursiveTuple = tuple[RecursiveTuple]
539+
540+
@overload
541+
def overloaded_recursive_tuple(x: int) -> int: ...
542+
@overload
543+
def overloaded_recursive_tuple(x: str) -> str: ...
544+
def overloaded_recursive_tuple(x: object) -> object:
545+
return x
546+
547+
def expand_recursive_tuple_argument(x: RecursiveTuple):
548+
# error: [no-matching-overload] "No overload of function `overloaded_recursive_tuple` matches arguments"
549+
reveal_type(overloaded_recursive_tuple(x)) # revealed: Unknown
550+
551+
@overload
552+
def overloaded_recursive_tuple_variadic(x: int) -> int: ...
553+
@overload
554+
def overloaded_recursive_tuple_variadic(x: int, y: int) -> str: ...
555+
def overloaded_recursive_tuple_variadic(*args: object) -> object:
556+
return args
557+
558+
def expand_recursive_variadic_tuple_argument(x: RecursiveTuple):
559+
# error: [invalid-argument-type] "Argument to function `overloaded_recursive_tuple_variadic` is incorrect: Expected `int`, found `RecursiveTuple`"
560+
reveal_type(overloaded_recursive_tuple_variadic(*x)) # revealed: Unknown
561+
562+
type RecursiveClassInfo = type[int] | RecursiveClassInfo
563+
564+
def narrow_recursive_classinfo(x: object, y: RecursiveClassInfo):
565+
if isinstance(x, y):
566+
reveal_type(x) # revealed: object
567+
568+
def narrow_recursive_subclassinfo(x: type[object], y: RecursiveClassInfo):
569+
if issubclass(x, y):
570+
reveal_type(x) # revealed: type
571+
572+
type RecursiveSingleValuedTuple = tuple[None, RecursiveSingleValuedTuple]
573+
574+
def narrow_recursive_single_valued_tuple(x: object, y: RecursiveSingleValuedTuple):
575+
if x == y:
576+
reveal_type(x) # revealed: object
577+
```
578+
579+
### Parser-recovery operation regressions
580+
581+
```py
582+
# error: [invalid-syntax] "Expected an expression"
583+
type CrashyOperation = | CrashyOperation
584+
585+
def subscript_recovered_alias(x: CrashyOperation):
586+
reveal_type(x[0]) # revealed: Unknown
587+
588+
def iterate_recovered_alias(x: CrashyOperation):
589+
for y in x:
590+
reveal_type(y) # revealed: Unknown
591+
```
592+
442593
### With legacy generic
443594

444595
```py

crates/ty_python_semantic/resources/mdtest/promotion.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,19 @@ def _(a: A | None):
735735
return {}
736736
```
737737

738+
Callable promotion visits parameters with promotion disabled and returns with promotion enabled, so
739+
those two traversals should not share cached intersection results:
740+
741+
```py
742+
from typing import Literal
743+
from ty_extensions import Intersection, Not
744+
745+
def f(x: Intersection[int, Not[Literal[2]]]) -> Intersection[int, Not[Literal[2]]]:
746+
return x
747+
748+
reveal_type([f]) # revealed: list[(x: int & ~Literal[2]) -> int]
749+
```
750+
738751
## Module-literal types are not promoted
739752

740753
Since module-literal types are "literal" types in a certain sense (each type is a singleton type),

0 commit comments

Comments
 (0)