diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index a6b571c402..b039ba94f7 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -95,7 +95,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -126,7 +126,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -184,13 +184,41 @@ class B(A): ...
[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
+## `cyclic-type-alias-definition`
+
+
+Default level: error ·
+Preview (since 1.0.0) ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Checks for type alias definitions that (directly or mutually) refer to themselves.
+
+**Why is it bad?**
+
+Although it is permitted to define a recursive type alias, it is not meaningful
+to have a type alias whose expansion can only result in itself, and is therefore not allowed.
+
+**Examples**
+
+```python
+type Itself = Itself
+
+type A = B
+type B = A
+```
+
## `duplicate-base`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -217,7 +245,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -329,7 +357,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -359,7 +387,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -385,7 +413,7 @@ t[3] # IndexError: tuple index out of range
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -474,7 +502,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -501,7 +529,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -529,7 +557,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -563,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -599,7 +627,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -623,7 +651,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -650,7 +678,7 @@ with 1:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -679,7 +707,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -723,7 +751,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -765,7 +793,7 @@ class D(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -798,7 +826,7 @@ class C[U](Generic[T]): ...
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -837,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -872,7 +900,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -906,7 +934,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -992,7 +1020,7 @@ and `__ne__` methods accept `object` as their second argument.
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -1024,7 +1052,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -1054,7 +1082,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1104,7 +1132,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1130,7 +1158,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1161,7 +1189,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1195,7 +1223,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1244,7 +1272,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1269,7 +1297,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1327,7 +1355,7 @@ TODO #14889
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1354,7 +1382,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1384,7 +1412,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1414,7 +1442,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1448,7 +1476,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1482,7 +1510,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1517,7 +1545,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1542,7 +1570,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1575,7 +1603,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1604,7 +1632,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1628,7 +1656,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1654,7 +1682,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1681,7 +1709,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -1739,7 +1767,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1769,7 +1797,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1798,7 +1826,7 @@ class B(A): ... # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1825,7 +1853,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1853,7 +1881,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1899,7 +1927,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1926,7 +1954,7 @@ f(x=1, y=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1954,7 +1982,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1979,7 +2007,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2004,7 +2032,7 @@ print(x) # NameError: name 'x' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2041,7 +2069,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2069,7 +2097,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2094,7 +2122,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -2135,7 +2163,7 @@ class SubProto(BaseProto, Protocol):
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -2223,7 +2251,7 @@ a = 20 / 0 # type: ignore
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2251,7 +2279,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2283,7 +2311,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2315,7 +2343,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2342,7 +2370,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2366,7 +2394,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -2424,7 +2452,7 @@ def g():
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2463,7 +2491,7 @@ class D(C): ... # error: [unsupported-base]
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2526,7 +2554,7 @@ def foo(x: int | str) -> int | str:
Default level: ignore ·
Preview (since 0.0.1-alpha.1) ·
Related issues ·
-View source
+View source
@@ -2550,7 +2578,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_python_semantic/resources/corpus/classliteral_decorators_cycle.py b/crates/ty_python_semantic/resources/corpus/classliteral_decorators_cycle.py
new file mode 100644
index 0000000000..7130432c69
--- /dev/null
+++ b/crates/ty_python_semantic/resources/corpus/classliteral_decorators_cycle.py
@@ -0,0 +1,26 @@
+try:
+ type name_4 = name_1
+finally:
+ from .. import name_3
+
+try:
+ pass
+except* 0:
+ pass
+else:
+ def name_1() -> name_4:
+ pass
+
+ @name_1
+ def name_3():
+ pass
+finally:
+ try:
+ pass
+ except* 0:
+ assert name_3
+ finally:
+
+ @name_3
+ class name_1:
+ pass
diff --git a/crates/ty_python_semantic/resources/corpus/cyclic_reassignment.py b/crates/ty_python_semantic/resources/corpus/cyclic_reassignment.py
new file mode 100644
index 0000000000..5df2694654
--- /dev/null
+++ b/crates/ty_python_semantic/resources/corpus/cyclic_reassignment.py
@@ -0,0 +1,5 @@
+class foo[_: foo](object): ...
+
+[_] = (foo,) = foo
+
+def foo(): ...
diff --git a/crates/ty_python_semantic/resources/corpus/cyclic_type_alias.py b/crates/ty_python_semantic/resources/corpus/cyclic_type_alias.py
new file mode 100644
index 0000000000..38a2e3def8
--- /dev/null
+++ b/crates/ty_python_semantic/resources/corpus/cyclic_type_alias.py
@@ -0,0 +1,20 @@
+name_3: Foo = 0
+name_4 = 0
+
+if _0:
+ type name_3 = name_5
+ type name_4 = name_3
+
+_1: name_3
+
+def name_1(_2: name_4):
+ pass
+
+match 0:
+ case name_1._3:
+ pass
+ case 1:
+ type name_5 = name_4
+ case name_5:
+ pass
+name_3 = name_5
diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md
index 176865a04d..bef0451ea2 100644
--- a/crates/ty_python_semantic/resources/mdtest/attributes.md
+++ b/crates/ty_python_semantic/resources/mdtest/attributes.md
@@ -2383,6 +2383,34 @@ class B:
reveal_type(B().x) # revealed: Unknown | Literal[1]
reveal_type(A().x) # revealed: Unknown | Literal[1]
+
+class Base:
+ def flip(self) -> "Sub":
+ return Sub()
+
+class Sub(Base):
+ # error: [invalid-method-override]
+ def flip(self) -> "Base":
+ return Base()
+
+class C2:
+ def __init__(self, x: Sub):
+ self.x = x
+
+ def replace_with(self, other: "C2"):
+ self.x = other.x.flip()
+
+reveal_type(C2(Sub()).x) # revealed: Unknown | Base
+
+class C3:
+ def __init__(self, x: Sub):
+ self.x = [x]
+
+ def replace_with(self, other: "C3"):
+ self.x = [self.x[0].flip()]
+
+# TODO: should be `Unknown | list[Unknown | Sub] | list[Unknown | Base]`
+reveal_type(C3(Sub()).x) # revealed: Unknown | list[Unknown | Sub] | list[Divergent]
```
And cycles between many attributes:
@@ -2432,6 +2460,30 @@ class ManyCycles:
reveal_type(self.x5) # revealed: Unknown | int
reveal_type(self.x6) # revealed: Unknown | int
reveal_type(self.x7) # revealed: Unknown | int
+
+class ManyCycles2:
+ def __init__(self: "ManyCycles2"):
+ self.x1 = [0]
+ self.x2 = [1]
+ self.x3 = [1]
+
+ def f1(self: "ManyCycles2"):
+ # TODO: should be Unknown | list[Unknown | int] | list[Divergent]
+ reveal_type(self.x3) # revealed: Unknown | list[Unknown | int] | list[Divergent] | list[Divergent]
+
+ self.x1 = [self.x2] + [self.x3]
+ self.x2 = [self.x1] + [self.x3]
+ self.x3 = [self.x1] + [self.x2]
+
+ def f2(self: "ManyCycles2"):
+ self.x1 = self.x2 + self.x3
+ self.x2 = self.x1 + self.x3
+ self.x3 = self.x1 + self.x2
+
+ def f3(self: "ManyCycles2"):
+ self.x1 = self.x2 + self.x3
+ self.x2 = self.x1 + self.x3
+ self.x3 = self.x1 + self.x2
```
This case additionally tests our union/intersection simplification logic:
@@ -2611,12 +2663,18 @@ reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown]
## Divergent inferred implicit instance attribute types
+If an implicit attribute is defined recursively and type inference diverges, the divergent part is
+filled in with the dynamic type `Divergent`. Types containing `Divergent` can be seen as "cheap"
+recursive types: they are not true recursive types based on recursive type theory, so no unfolding
+is performed when you use them.
+
```py
class C:
def f(self, other: "C"):
self.x = (other.x, 1)
reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
+reveal_type(C().x[0]) # revealed: Unknown | Divergent
```
This also works if the tuple is not constructed directly:
@@ -2655,11 +2713,11 @@ And it also works for homogeneous tuples:
def make_homogeneous_tuple(x: T) -> tuple[T, ...]:
return (x, x)
-class E:
- def f(self, other: "E"):
+class F:
+ def f(self, other: "F"):
self.x = make_homogeneous_tuple(other.x)
-reveal_type(E().x) # revealed: Unknown | tuple[Divergent, ...]
+reveal_type(F().x) # revealed: Unknown | tuple[Divergent, ...]
```
## Attributes of standard library modules that aren't yet defined
diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md
index e52641a05c..30d8e9554a 100644
--- a/crates/ty_python_semantic/resources/mdtest/cycle.md
+++ b/crates/ty_python_semantic/resources/mdtest/cycle.md
@@ -32,6 +32,55 @@ reveal_type(p.x) # revealed: Unknown | int
reveal_type(p.y) # revealed: Unknown | int
```
+## Self-referential bare type alias
+
+```toml
+[environment]
+python-version = "3.12" # typing.TypeAliasType
+```
+
+```py
+from typing import Union, TypeAliasType, Sequence, Mapping
+
+A = list["A" | None]
+
+def f(x: A):
+ # TODO: should be `list[A | None]`?
+ reveal_type(x) # revealed: list[Divergent]
+ # TODO: should be `A | None`?
+ reveal_type(x[0]) # revealed: Divergent
+
+JSONPrimitive = Union[str, int, float, bool, None]
+JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]')
+```
+
+## Self-referential legacy type variables
+
+```py
+from typing import Generic, TypeVar
+
+B = TypeVar("B", bound="Base")
+
+class Base(Generic[B]):
+ pass
+
+T = TypeVar("T", bound="Foo[int]")
+
+class Foo(Generic[T]): ...
+```
+
+## Self-referential PEP-695 type variables
+
+```toml
+[environment]
+python-version = "3.12"
+```
+
+```py
+class Node[T: "Node[int]"]:
+ pass
+```
+
## Parameter default values
This is a regression test for . When a parameter has a
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
index 09df84b42e..9d6cd6ded7 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
@@ -732,6 +732,14 @@ class Base(Generic[T]): ...
class Sub(Base["Sub"]): ...
reveal_type(Sub) # revealed:
+
+U = TypeVar("U")
+
+class Base2(Generic[T, U]): ...
+
+# TODO: no error
+# error: [unsupported-base] "Unsupported class base with type ` | `"
+class Sub2(Base2["Sub2", U]): ...
```
#### Without string forward references
@@ -756,6 +764,8 @@ from typing_extensions import Generic, TypeVar
T = TypeVar("T")
+# TODO: no error "Unsupported class base with type ` | `"
+# error: [unsupported-base]
class Derived(list[Derived[T]], Generic[T]): ...
```
diff --git a/crates/ty_python_semantic/resources/mdtest/import/cyclic.md b/crates/ty_python_semantic/resources/mdtest/import/cyclic.md
index 4af714c7de..81c04c4620 100644
--- a/crates/ty_python_semantic/resources/mdtest/import/cyclic.md
+++ b/crates/ty_python_semantic/resources/mdtest/import/cyclic.md
@@ -38,7 +38,7 @@ See:
from pkg.sub import A
# TODO: This should be ``
-reveal_type(A) # revealed: Never
+reveal_type(A) # revealed: Divergent
```
`pkg/outer.py`:
diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md
index 13f05ddd5d..40e5d2c477 100644
--- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md
+++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md
@@ -144,17 +144,46 @@ def _(x: IntOrStr):
## Cyclic
```py
-from typing import TypeAlias
+from typing import TypeAlias, TypeVar, Union
+from types import UnionType
RecursiveTuple: TypeAlias = tuple[int | "RecursiveTuple", str]
def _(rec: RecursiveTuple):
+ # TODO should be `tuple[int | RecursiveTuple, str]`
reveal_type(rec) # revealed: tuple[Divergent, str]
RecursiveHomogeneousTuple: TypeAlias = tuple[int | "RecursiveHomogeneousTuple", ...]
def _(rec: RecursiveHomogeneousTuple):
+ # TODO should be `tuple[int | RecursiveHomogeneousTuple, ...]`
reveal_type(rec) # revealed: tuple[Divergent, ...]
+
+ClassInfo: TypeAlias = type | UnionType | tuple["ClassInfo", ...]
+reveal_type(ClassInfo) # revealed: types.UnionType
+
+def my_isinstance(obj: object, classinfo: ClassInfo) -> bool:
+ # TODO should be `type | UnionType | tuple[ClassInfo, ...]`
+ reveal_type(classinfo) # revealed: type | UnionType | tuple[Divergent, ...]
+ return isinstance(obj, classinfo)
+
+K = TypeVar("K")
+V = TypeVar("V")
+NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]]
+
+def _(nested: NestedDict[str, int]):
+ # TODO should be `dict[str, int | NestedDict[str, int]]`
+ reveal_type(nested) # revealed: @Todo(specialized generic alias in type expression)
+
+my_isinstance(1, int)
+my_isinstance(1, int | str)
+my_isinstance(1, (int, str))
+my_isinstance(1, (int, (str, float)))
+my_isinstance(1, (int, (str | float)))
+# error: [invalid-argument-type]
+my_isinstance(1, 1)
+# TODO should be an invalid-argument-type error
+my_isinstance(1, (int, (str, 1)))
```
## Conditionally imported on Python < 3.10
diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md
index f0c73fc16a..d4e4fafc73 100644
--- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md
+++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md
@@ -106,6 +106,29 @@ def _(flag: bool):
```py
type ListOrSet[T] = list[T] | set[T]
reveal_type(ListOrSet.__type_params__) # revealed: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
+type Tuple1[T] = tuple[T]
+
+def _(cond: bool):
+ Generic = ListOrSet if cond else Tuple1
+
+ def _(x: Generic[int]):
+ reveal_type(x) # revealed: list[int] | set[int] | tuple[int]
+
+try:
+ class Foo[T]:
+ x: T
+ def foo(self) -> T:
+ return self.x
+
+ ...
+except Exception:
+ class Foo[T]:
+ x: T
+ def foo(self) -> T:
+ return self.x
+
+def f(x: Foo[int]):
+ reveal_type(x.foo()) # revealed: int
```
## In unions and intersections
@@ -244,6 +267,47 @@ def f(x: IntOr, y: OrInt):
reveal_type(x) # revealed: Never
if not isinstance(y, int):
reveal_type(y) # revealed: Never
+
+# error: [cyclic-type-alias-definition] "Cyclic definition of `Itself`"
+type Itself = Itself
+
+def foo(
+ # this is a very strange thing to do, but this is a regression test to ensure it doesn't panic
+ Itself: Itself,
+):
+ x: Itself
+ reveal_type(Itself) # revealed: Divergent
+
+# A type alias defined with invalid recursion behaves as a dynamic type.
+foo(42)
+foo("hello")
+
+# error: [cyclic-type-alias-definition] "Cyclic definition of `A`"
+type A = B
+# error: [cyclic-type-alias-definition] "Cyclic definition of `B`"
+type B = A
+
+def bar(B: B):
+ x: B
+ reveal_type(B) # revealed: Divergent
+
+# error: [cyclic-type-alias-definition] "Cyclic definition of `G`"
+type G[T] = G[T]
+# error: [cyclic-type-alias-definition] "Cyclic definition of `H`"
+type H[T] = I[T]
+# error: [cyclic-type-alias-definition] "Cyclic definition of `I`"
+type I[T] = H[T]
+
+# It's not possible to create an element of this type, but it's not an error for now
+type DirectRecursiveList[T] = list[DirectRecursiveList[T]]
+
+# TODO: this should probably be a cyclic-type-alias-definition error
+type Foo[T] = list[T] | Bar[T]
+type Bar[T] = int | Foo[T]
+
+def _(x: Bar[int]):
+ # TODO: should be `int | list[int]`
+ reveal_type(x) # revealed: int | list[int] | Any
```
### With legacy generic
@@ -327,7 +391,7 @@ class C(P[T]):
pass
reveal_type(C[int]()) # revealed: C[int]
-reveal_type(C()) # revealed: C[Divergent]
+reveal_type(C()) # revealed: C[C[Divergent]]
```
### Union inside generic
diff --git a/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md b/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md
index 600e408331..309a41422c 100644
--- a/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md
+++ b/crates/ty_python_semantic/resources/mdtest/regression/1377_iteration_count_mismatch.md
@@ -2,10 +2,7 @@
Regression test for .
-The code is an excerpt from that is minimal enough to
-trigger the iteration count mismatch bug in Salsa.
-
-
+The code is an excerpt from .
```toml
[environment]
diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs
index 4957a53914..589a4720f4 100644
--- a/crates/ty_python_semantic/src/place.rs
+++ b/crates/ty_python_semantic/src/place.rs
@@ -87,7 +87,7 @@ impl TypeOrigin {
/// bound_or_declared: Place::Defined(Literal[1], TypeOrigin::Inferred, Definedness::PossiblyUndefined),
/// non_existent: Place::Undefined,
/// ```
-#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) enum Place<'db> {
Defined(Type<'db>, TypeOrigin, Definedness),
Undefined,
@@ -532,7 +532,7 @@ impl<'db> PlaceFromDeclarationsResult<'db> {
/// that this comes with a [`CLASS_VAR`] type qualifier.
///
/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR
-#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) struct PlaceAndQualifiers<'db> {
pub(crate) place: Place<'db>,
pub(crate) qualifiers: TypeQualifiers,
@@ -689,6 +689,51 @@ impl<'db> PlaceAndQualifiers<'db> {
.or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn()))
.into()
}
+
+ pub(crate) fn cycle_normalized(
+ self,
+ db: &'db dyn Db,
+ previous_place: Self,
+ cycle: &salsa::Cycle,
+ ) -> Self {
+ let place = match (previous_place.place, self.place) {
+ // In fixed-point iteration of type inference, the member type must be monotonically widened and not "oscillate".
+ // Here, monotonicity is guaranteed by pre-unioning the type of the previous iteration into the current result.
+ (Place::Defined(prev_ty, _, _), Place::Defined(ty, origin, definedness)) => {
+ Place::Defined(ty.cycle_normalized(db, prev_ty, cycle), origin, definedness)
+ }
+ // If a `Place` in the current cycle is `Defined` but `Undefined` in the previous cycle,
+ // that means that its definedness depends on the truthiness of the previous cycle value.
+ // In this case, the definedness of the current cycle `Place` is set to `PossiblyUndefined`.
+ // Actually, this branch is unreachable. We evaluate the truthiness of non-definitely-bound places as Ambiguous (see #19579),
+ // so convergence is guaranteed without resorting to this handling.
+ // However, the handling described above may reduce the exactness of reachability analysis,
+ // so it may be better to remove it. In that case, this branch is necessary.
+ (Place::Undefined, Place::Defined(ty, origin, _definedness)) => Place::Defined(
+ ty.recursive_type_normalized(db, cycle),
+ origin,
+ Definedness::PossiblyUndefined,
+ ),
+ // If a `Place` that was `Defined(Divergent)` in the previous cycle is actually found to be unreachable in the current cycle,
+ // it is set to `Undefined` (because the cycle initial value does not include meaningful reachability information).
+ (Place::Defined(ty, origin, _definedness), Place::Undefined) => {
+ if cycle.head_ids().any(|id| ty == Type::divergent(id)) {
+ Place::Undefined
+ } else {
+ Place::Defined(
+ ty.recursive_type_normalized(db, cycle),
+ origin,
+ Definedness::PossiblyUndefined,
+ )
+ }
+ }
+ (Place::Undefined, Place::Undefined) => Place::Undefined,
+ };
+ PlaceAndQualifiers {
+ place,
+ qualifiers: self.qualifiers,
+ }
+ }
}
impl<'db> From> for PlaceAndQualifiers<'db> {
@@ -699,16 +744,30 @@ impl<'db> From> for PlaceAndQualifiers<'db> {
fn place_cycle_initial<'db>(
_db: &'db dyn Db,
- _id: salsa::Id,
+ id: salsa::Id,
_scope: ScopeId<'db>,
_place_id: ScopedPlaceId,
_requires_explicit_reexport: RequiresExplicitReExport,
_considered_definitions: ConsideredDefinitions,
) -> PlaceAndQualifiers<'db> {
- Place::bound(Type::Never).into()
+ Place::bound(Type::divergent(id)).into()
}
-#[salsa::tracked(cycle_initial=place_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
+#[allow(clippy::too_many_arguments)]
+fn place_cycle_recover<'db>(
+ db: &'db dyn Db,
+ cycle: &salsa::Cycle,
+ previous_place: &PlaceAndQualifiers<'db>,
+ place: PlaceAndQualifiers<'db>,
+ _scope: ScopeId<'db>,
+ _place_id: ScopedPlaceId,
+ _requires_explicit_reexport: RequiresExplicitReExport,
+ _considered_definitions: ConsideredDefinitions,
+) -> PlaceAndQualifiers<'db> {
+ place.cycle_normalized(db, *previous_place, cycle)
+}
+
+#[salsa::tracked(cycle_fn=place_cycle_recover, cycle_initial=place_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn place_by_id<'db>(
db: &'db dyn Db,
scope: ScopeId<'db>,
diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs
index 24c3f09ecb..3f2a377c20 100644
--- a/crates/ty_python_semantic/src/semantic_model.rs
+++ b/crates/ty_python_semantic/src/semantic_model.rs
@@ -12,8 +12,7 @@ use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::FileScopeId;
use crate::semantic_index::semantic_index;
-use crate::types::ide_support::all_declarations_and_bindings;
-use crate::types::ide_support::{Member, all_members};
+use crate::types::ide_support::{Member, all_declarations_and_bindings, all_members};
use crate::types::{Type, binding_type, infer_scope_types};
/// The primary interface the LSP should use for querying semantic information about a [`File`].
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index 6194965659..c6b6c540f5 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -1,7 +1,6 @@
use compact_str::{CompactString, ToCompactString};
use infer::nearest_enclosing_class;
use itertools::{Either, Itertools};
-use ruff_db::parsed::parsed_module;
use std::borrow::Cow;
use std::time::Duration;
@@ -13,6 +12,7 @@ use diagnostic::{INVALID_CONTEXT_MANAGER, NOT_ITERABLE, POSSIBLY_MISSING_IMPLICI
use ruff_db::Instant;
use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity};
use ruff_db::files::File;
+use ruff_db::parsed::parsed_module;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_text_size::{Ranged, TextRange};
@@ -29,7 +29,7 @@ pub(crate) use self::infer::{
TypeContext, infer_deferred_types, infer_definition_types, infer_expression_type,
infer_expression_types, infer_scope_types, static_expression_truthiness,
};
-pub(crate) use self::signatures::{CallableSignature, Parameter, Parameters, Signature};
+pub(crate) use self::signatures::{CallableSignature, Signature};
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
pub use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic;
use crate::module_name::ModuleName;
@@ -61,16 +61,16 @@ use crate::types::generics::{
InferableTypeVars, PartialSpecialization, Specialization, bind_typevar, typing_self,
walk_generic_context,
};
-use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
use crate::types::newtype::NewType;
+pub(crate) use crate::types::signatures::{Parameter, Parameters};
use crate::types::signatures::{ParameterForm, walk_signature};
use crate::types::tuple::{TupleSpec, TupleSpecBuilder};
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
pub use crate::types::variance::TypeVarVariance;
use crate::types::variance::VarianceInferable;
-use crate::types::visitor::{any_over_type, exceeds_max_specialization_depth};
+use crate::types::visitor::any_over_type;
use crate::unpack::EvaluationMode;
use crate::{Db, FxOrderSet, Module, Program};
pub use class::KnownClass;
@@ -387,22 +387,46 @@ impl Default for MemberLookupPolicy {
fn member_lookup_cycle_initial<'db>(
_db: &'db dyn Db,
- _id: salsa::Id,
+ id: salsa::Id,
_self: Type<'db>,
_name: Name,
_policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
- Place::bound(Type::Never).into()
+ Place::bound(Type::divergent(id)).into()
+}
+
+fn member_lookup_cycle_recover<'db>(
+ db: &'db dyn Db,
+ cycle: &salsa::Cycle,
+ previous_member: &PlaceAndQualifiers<'db>,
+ member: PlaceAndQualifiers<'db>,
+ _self_type: Type<'db>,
+ _name: Name,
+ _policy: MemberLookupPolicy,
+) -> PlaceAndQualifiers<'db> {
+ member.cycle_normalized(db, *previous_member, cycle)
}
fn class_lookup_cycle_initial<'db>(
_db: &'db dyn Db,
- _id: salsa::Id,
+ id: salsa::Id,
_self: Type<'db>,
_name: Name,
_policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
- Place::bound(Type::Never).into()
+ Place::bound(Type::divergent(id)).into()
+}
+
+fn class_lookup_cycle_recover<'db>(
+ db: &'db dyn Db,
+ cycle: &salsa::Cycle,
+ previous_member: &PlaceAndQualifiers<'db>,
+ member: PlaceAndQualifiers<'db>,
+ _self_type: Type<'db>,
+ _name: Name,
+ _policy: MemberLookupPolicy,
+) -> PlaceAndQualifiers<'db> {
+ member.cycle_normalized(db, *previous_member, cycle)
}
fn variance_cycle_initial<'db, T>(
@@ -543,6 +567,32 @@ impl<'db> PropertyInstanceType<'db> {
)
}
+ fn recursive_type_normalized_impl(
+ self,
+ db: &'db dyn Db,
+ div: Type<'db>,
+ nested: bool,
+ visitor: &NormalizedVisitor<'db>,
+ ) -> Option {
+ let getter = match self.getter(db) {
+ Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true, visitor)?),
+ Some(ty) => Some(
+ ty.recursive_type_normalized_impl(db, div, true, visitor)
+ .unwrap_or(div),
+ ),
+ None => None,
+ };
+ let setter = match self.setter(db) {
+ Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true, visitor)?),
+ Some(ty) => Some(
+ ty.recursive_type_normalized_impl(db, div, true, visitor)
+ .unwrap_or(div),
+ ),
+ None => None,
+ };
+ Some(Self::new(db, getter, setter))
+ }
+
fn find_legacy_typevars_impl(
self,
db: &'db dyn Db,
@@ -712,7 +762,7 @@ impl<'db> DataclassParams<'db> {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
pub enum Type<'db> {
/// The dynamic type: a statically unknown set of values
- Dynamic(DynamicType<'db>),
+ Dynamic(DynamicType),
/// The empty set of values
Never,
/// A specific function object
@@ -829,8 +879,8 @@ impl<'db> Type<'db> {
Self::Dynamic(DynamicType::Unknown)
}
- pub(crate) fn divergent(scope: Option>) -> Self {
- Self::Dynamic(DynamicType::Divergent(DivergentType { scope }))
+ pub(crate) fn divergent(id: salsa::Id) -> Self {
+ Self::Dynamic(DynamicType::Divergent(DivergentType { id }))
}
pub(crate) const fn is_divergent(&self) -> bool {
@@ -850,6 +900,16 @@ impl<'db> Type<'db> {
matches!(self, Type::Callable(..))
}
+ pub(crate) fn cycle_normalized(
+ self,
+ db: &'db dyn Db,
+ previous: Self,
+ cycle: &salsa::Cycle,
+ ) -> Self {
+ UnionType::from_elements_cycle_recovery(db, [self, previous])
+ .recursive_type_normalized(db, cycle)
+ }
+
fn is_none(&self, db: &'db dyn Db) -> bool {
self.is_instance_of(db, KnownClass::NoneType)
}
@@ -1080,7 +1140,7 @@ impl<'db> Type<'db> {
}
}
- pub(crate) const fn as_dynamic(self) -> Option> {
+ pub(crate) const fn as_dynamic(self) -> Option {
match self {
Type::Dynamic(dynamic_type) => Some(dynamic_type),
_ => None,
@@ -1094,7 +1154,7 @@ impl<'db> Type<'db> {
}
}
- pub(crate) const fn expect_dynamic(self) -> DynamicType<'db> {
+ pub(crate) const fn expect_dynamic(self) -> DynamicType {
self.as_dynamic().expect("Expected a Type::Dynamic variant")
}
@@ -1475,6 +1535,164 @@ impl<'db> Type<'db> {
}
}
+ /// Performs nest reduction for recursive types (types that contain `Divergent` types).
+ /// For example, consider the following implicit attribute inference:
+ /// ```python
+ /// class C:
+ /// def f(self, other: "C"):
+ /// self.x = (other.x, 1)
+ ///
+ /// reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
+ /// ```
+ ///
+ /// A query that performs implicit attribute type inference enters a cycle because the attribute is recursively defined, and the cycle initial value is set to `Divergent`.
+ /// In the next (1st) cycle it is inferred to be `tuple[Divergent, Literal[1]]`, and in the 2nd cycle it becomes `tuple[tuple[Divergent, Literal[1]], Literal[1]]`.
+ /// If this continues, the query will not converge, so this method is called in the cycle recovery function.
+ /// Then `tuple[tuple[Divergent, Literal[1]], Literal[1]]` is replaced with `tuple[Divergent, Literal[1]]` and the query converges.
+ #[must_use]
+ pub(crate) fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self {
+ cycle.head_ids().fold(self, |ty, id| {
+ let visitor = NormalizedVisitor::new(Type::divergent(id));
+ ty.recursive_type_normalized_impl(db, Type::divergent(id), false, &visitor)
+ .unwrap_or(Type::divergent(id))
+ })
+ }
+
+ /// Normalizes types including divergent types (recursive types), which is necessary for convergence of fixed-point iteration.
+ /// When nested is true, propagate `None`. That is, if the type contains a `Divergent` type, the return value of this method is `None`.
+ /// When nested is false, create a type containing `Divergent` types instead of propagating `None`.
+ /// This is to preserve the structure of the non-divergent parts of the type instead of completely collapsing the type containing a `Divergent` type into a `Divergent` type.
+ /// ```python
+ /// tuple[tuple[Divergent, Literal[1]], Literal[1]].recursive_type_normalized(nested: false)
+ /// => tuple[
+ /// tuple[Divergent, Literal[1]].recursive_type_normalized_impl(nested: true).unwrap_or(Divergent),
+ /// Literal[1].recursive_type_normalized_impl(nested: true).unwrap_or(Divergent)
+ /// ]
+ /// => tuple[Divergent, Literal[1]]
+ /// ```
+ /// Generic nominal types such as `list[T]` and `tuple[T]` should send `nested=true` for `T`. This is necessary for normalization.
+ /// Structural types such as union and intersection do not need to send `nested=true` for element types; that is, types that are "flat" from the perspective of recursive types. `T | U` should send `nested` as is for `T`, `U`.
+ /// For other types, the decision depends on whether they are interpreted as nominal or structural.
+ /// For example, `KnownInstanceType::UnionType` should simply send `nested` as is.
+ fn recursive_type_normalized_impl(
+ self,
+ db: &'db dyn Db,
+ div: Type<'db>,
+ nested: bool,
+ visitor: &NormalizedVisitor<'db>,
+ ) -> Option {
+ if nested && self == div {
+ return None;
+ }
+ match self {
+ Type::Union(union) => visitor.try_visit(self, || {
+ union.recursive_type_normalized_impl(db, div, nested, visitor)
+ }),
+ Type::Intersection(intersection) => visitor.try_visit(self, || {
+ intersection
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::Intersection)
+ }),
+ Type::Callable(callable) => visitor.try_visit(self, || {
+ callable
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::Callable)
+ }),
+ Type::ProtocolInstance(protocol) => visitor.try_visit(self, || {
+ protocol
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::ProtocolInstance)
+ }),
+ Type::NominalInstance(instance) => visitor.try_visit(self, || {
+ instance
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::NominalInstance)
+ }),
+ Type::FunctionLiteral(function) => visitor.try_visit(self, || {
+ function
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::FunctionLiteral)
+ }),
+ Type::PropertyInstance(property) => visitor.try_visit(self, || {
+ property
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::PropertyInstance)
+ }),
+ Type::KnownBoundMethod(method_kind) => visitor.try_visit(self, || {
+ method_kind
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::KnownBoundMethod)
+ }),
+ Type::BoundMethod(method) => visitor.try_visit(self, || {
+ method
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::BoundMethod)
+ }),
+ Type::BoundSuper(bound_super) => visitor.try_visit(self, || {
+ bound_super
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::BoundSuper)
+ }),
+ Type::GenericAlias(generic) => visitor.try_visit(self, || {
+ generic
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::GenericAlias)
+ }),
+ Type::SubclassOf(subclass_of) => visitor.try_visit(self, || {
+ subclass_of
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::SubclassOf)
+ }),
+ Type::TypeVar(_) => Some(self),
+ Type::KnownInstance(known_instance) => visitor.try_visit(self, || {
+ known_instance
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Type::KnownInstance)
+ }),
+ Type::TypeIs(type_is) => visitor.try_visit(self, || {
+ let ty = if nested {
+ type_is
+ .return_type(db)
+ .recursive_type_normalized_impl(db, div, true, visitor)?
+ } else {
+ type_is
+ .return_type(db)
+ .recursive_type_normalized_impl(db, div, true, visitor)
+ .unwrap_or(div)
+ };
+ Some(type_is.with_type(db, ty))
+ }),
+ Type::Dynamic(dynamic) => Some(Type::Dynamic(dynamic.recursive_type_normalized())),
+ Type::TypedDict(_) => {
+ // TODO: Normalize TypedDicts
+ Some(self)
+ }
+ Type::TypeAlias(_) => Some(self),
+ Type::NewTypeInstance(newtype) => visitor.try_visit(self, || {
+ newtype
+ .try_map_base_class_type(db, |class_type| {
+ class_type.recursive_type_normalized_impl(db, div, nested, visitor)
+ })
+ .map(Type::NewTypeInstance)
+ }),
+ Type::LiteralString
+ | Type::AlwaysFalsy
+ | Type::AlwaysTruthy
+ | Type::BooleanLiteral(_)
+ | Type::BytesLiteral(_)
+ | Type::EnumLiteral(_)
+ | Type::StringLiteral(_)
+ | Type::Never
+ | Type::WrapperDescriptor(_)
+ | Type::DataclassDecorator(_)
+ | Type::DataclassTransformer(_)
+ | Type::ModuleLiteral(_)
+ | Type::ClassLiteral(_)
+ | Type::SpecialForm(_)
+ | Type::IntLiteral(_) => Some(self),
+ }
+ }
+
/// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for
/// any `T` of this type.
///
@@ -3912,7 +4130,7 @@ impl<'db> Type<'db> {
self.class_member_with_policy(db, name, MemberLookupPolicy::default())
}
- #[salsa::tracked(cycle_initial=class_lookup_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
+ #[salsa::tracked(cycle_fn=class_lookup_cycle_recover, cycle_initial=class_lookup_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
fn class_member_with_policy(
self,
db: &'db dyn Db,
@@ -4380,7 +4598,7 @@ impl<'db> Type<'db> {
/// Similar to [`Type::member`], but allows the caller to specify what policy should be used
/// when looking up attributes. See [`MemberLookupPolicy`] for more information.
- #[salsa::tracked(cycle_initial=member_lookup_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
+ #[salsa::tracked(cycle_fn=member_lookup_cycle_recover, cycle_initial=member_lookup_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn member_lookup_with_policy(
self,
db: &'db dyn Db,
@@ -4786,7 +5004,7 @@ impl<'db> Type<'db> {
let owner_attr = bound_super.find_name_in_mro_after_pivot(db, name_str, policy);
bound_super
- .try_call_dunder_get_on_attribute(db, owner_attr.clone())
+ .try_call_dunder_get_on_attribute(db, owner_attr)
.unwrap_or(owner_attr)
}
}
@@ -7056,7 +7274,6 @@ impl<'db> Type<'db> {
.unwrap_or(SubclassOfInner::unknown()),
),
},
-
Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class_literal(db),
Type::Dynamic(dynamic) => SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)),
// TODO intersections
@@ -7113,7 +7330,7 @@ impl<'db> Type<'db> {
/// Note that this does not specialize generic classes, functions, or type aliases! That is a
/// different operation that is performed explicitly (via a subscript operation), or implicitly
/// via a call to the generic object.
- #[salsa::tracked(heap_size=ruff_memory_usage::heap_size, cycle_initial=apply_specialization_cycle_initial)]
+ #[salsa::tracked(heap_size=ruff_memory_usage::heap_size, cycle_fn=apply_specialization_cycle_recover, cycle_initial=apply_specialization_cycle_initial)]
pub(crate) fn apply_specialization(
self,
db: &'db dyn Db,
@@ -7162,7 +7379,7 @@ impl<'db> Type<'db> {
match self {
Type::TypeVar(bound_typevar) => match type_mapping {
TypeMapping::Specialization(specialization) => {
- specialization.get(db, bound_typevar).unwrap_or(self).fallback_to_divergent(db)
+ specialization.get(db, bound_typevar).unwrap_or(self)
}
TypeMapping::PartialSpecialization(partial) => {
partial.get(db, bound_typevar).unwrap_or(self)
@@ -7189,7 +7406,8 @@ impl<'db> Type<'db> {
}
TypeMapping::PromoteLiterals(_)
| TypeMapping::ReplaceParameterDefaults
- | TypeMapping::BindLegacyTypevars(_) => self,
+ | TypeMapping::BindLegacyTypevars(_)
+ | TypeMapping::EagerExpansion => self,
TypeMapping::Materialize(materialization_kind) => {
Type::TypeVar(bound_typevar.materialize_impl(db, *materialization_kind, visitor))
}
@@ -7205,7 +7423,8 @@ impl<'db> Type<'db> {
TypeMapping::BindSelf(_) |
TypeMapping::ReplaceSelf { .. } |
TypeMapping::Materialize(_) |
- TypeMapping::ReplaceParameterDefaults => self,
+ TypeMapping::ReplaceParameterDefaults |
+ TypeMapping::EagerExpansion => self,
}
Type::FunctionLiteral(function) => {
@@ -7306,6 +7525,9 @@ impl<'db> Type<'db> {
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping, tcx)),
Type::TypeAlias(alias) => {
+ if TypeMapping::EagerExpansion == *type_mapping {
+ return alias.raw_value_type(db).expand_eagerly(db);
+ }
// Do not call `value_type` here. `value_type` does the specialization internally, so `apply_type_mapping` is performed without `visitor` inheritance.
// In the case of recursive type aliases, this leads to infinite recursion.
// Instead, call `raw_value_type` and perform the specialization after the `visitor` cache has been created.
@@ -7327,6 +7549,7 @@ impl<'db> Type<'db> {
TypeMapping::ReplaceSelf { .. } |
TypeMapping::Materialize(_) |
TypeMapping::ReplaceParameterDefaults |
+ TypeMapping::EagerExpansion |
TypeMapping::PromoteLiterals(PromoteLiteralsMode::Off) => self,
TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => self.promote_literals_impl(db, tcx)
}
@@ -7338,7 +7561,8 @@ impl<'db> Type<'db> {
TypeMapping::BindSelf(_) |
TypeMapping::ReplaceSelf { .. } |
TypeMapping::PromoteLiterals(_) |
- TypeMapping::ReplaceParameterDefaults => self,
+ TypeMapping::ReplaceParameterDefaults |
+ TypeMapping::EagerExpansion => self,
TypeMapping::Materialize(materialization_kind) => match materialization_kind {
MaterializationKind::Top => Type::object(),
MaterializationKind::Bottom => Type::Never,
@@ -7545,6 +7769,18 @@ impl<'db> Type<'db> {
)
}
+ /// Returns the eagerly expanded type.
+ /// In the case of recursive type aliases, this will diverge, so that part will be replaced with `Divergent`.
+ fn expand_eagerly(self, db: &'db dyn Db) -> Type<'db> {
+ self.expand_eagerly_(db, ())
+ }
+
+ #[allow(clippy::used_underscore_binding)]
+ #[salsa::tracked(heap_size=ruff_memory_usage::heap_size, cycle_fn=expand_eagerly_cycle_recover, cycle_initial=expand_eagerly_cycle_initial)]
+ fn expand_eagerly_(self, db: &'db dyn Db, _unit: ()) -> Type<'db> {
+ self.apply_type_mapping(db, &TypeMapping::EagerExpansion, TypeContext::default())
+ }
+
/// Return the string representation of this type when converted to string as it would be
/// provided by the `__str__` method.
///
@@ -7757,20 +7993,6 @@ impl<'db> Type<'db> {
_ => None,
}
}
-
- pub(super) fn has_divergent_type(self, db: &'db dyn Db, div: Type<'db>) -> bool {
- any_over_type(db, self, &|ty| ty == div, false)
- }
-
- /// If the specialization depth of `self` exceeds the maximum limit allowed,
- /// return `Divergent`. Otherwise, return `self`.
- pub(super) fn fallback_to_divergent(self, db: &'db dyn Db) -> Type<'db> {
- if exceeds_max_specialization_depth(db, self) {
- Type::divergent(None)
- } else {
- self
- }
- }
}
impl<'db> From<&Type<'db>> for Type<'db> {
@@ -7889,13 +8111,44 @@ fn is_redundant_with_cycle_initial<'db>(
true
}
-fn apply_specialization_cycle_initial<'db>(
- _db: &'db dyn Db,
- _id: salsa::Id,
+fn apply_specialization_cycle_recover<'db>(
+ db: &'db dyn Db,
+ cycle: &salsa::Cycle,
+ previous_value: &Type<'db>,
+ value: Type<'db>,
_self: Type<'db>,
_specialization: Specialization<'db>,
) -> Type<'db> {
- Type::Never
+ value.cycle_normalized(db, *previous_value, cycle)
+}
+
+fn apply_specialization_cycle_initial<'db>(
+ _db: &'db dyn Db,
+ id: salsa::Id,
+ _self: Type<'db>,
+ _specialization: Specialization<'db>,
+) -> Type<'db> {
+ Type::divergent(id)
+}
+
+fn expand_eagerly_cycle_initial<'db>(
+ _db: &'db dyn Db,
+ id: salsa::Id,
+ _self: Type<'db>,
+ _unit: (),
+) -> Type<'db> {
+ Type::divergent(id)
+}
+
+fn expand_eagerly_cycle_recover<'db>(
+ db: &'db dyn Db,
+ cycle: &salsa::Cycle,
+ previous_value: &Type<'db>,
+ value: Type<'db>,
+ _self: Type<'db>,
+ _unit: (),
+) -> Type<'db> {
+ value.cycle_normalized(db, *previous_value, cycle)
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)]
@@ -7940,6 +8193,9 @@ pub enum TypeMapping<'a, 'db> {
/// Replace default types in parameters of callables with `Unknown`. This is used to avoid infinite
/// recursion when the type of the default value of a parameter depends on the callable itself.
ReplaceParameterDefaults,
+ /// Apply eager expansion to the type.
+ /// In the case of recursive type aliases, this will diverge, so that part will be replaced with `Divergent`.
+ EagerExpansion,
}
impl<'db> TypeMapping<'_, 'db> {
@@ -7955,7 +8211,8 @@ impl<'db> TypeMapping<'_, 'db> {
| TypeMapping::PromoteLiterals(_)
| TypeMapping::BindLegacyTypevars(_)
| TypeMapping::Materialize(_)
- | TypeMapping::ReplaceParameterDefaults => context,
+ | TypeMapping::ReplaceParameterDefaults
+ | TypeMapping::EagerExpansion => context,
TypeMapping::BindSelf(_) => GenericContext::from_typevar_instances(
db,
context
@@ -7991,7 +8248,8 @@ impl<'db> TypeMapping<'_, 'db> {
| TypeMapping::BindLegacyTypevars(_)
| TypeMapping::BindSelf(_)
| TypeMapping::ReplaceSelf { .. }
- | TypeMapping::ReplaceParameterDefaults => self.clone(),
+ | TypeMapping::ReplaceParameterDefaults
+ | TypeMapping::EagerExpansion => self.clone(),
}
}
}
@@ -8184,6 +8442,56 @@ impl<'db> KnownInstanceType<'db> {
}
}
+ fn recursive_type_normalized_impl(
+ self,
+ db: &'db dyn Db,
+ div: Type<'db>,
+ nested: bool,
+ visitor: &NormalizedVisitor<'db>,
+ ) -> Option {
+ match self {
+ // Nothing to normalize
+ Self::SubscriptedProtocol(context) => Some(Self::SubscriptedProtocol(context)),
+ Self::SubscriptedGeneric(context) => Some(Self::SubscriptedGeneric(context)),
+ Self::Deprecated(deprecated) => Some(Self::Deprecated(deprecated)),
+ Self::ConstraintSet(set) => Some(Self::ConstraintSet(set)),
+ Self::TypeVar(typevar) => Some(Self::TypeVar(typevar)),
+ Self::TypeAliasType(type_alias) => type_alias
+ .recursive_type_normalized_impl(db, div, visitor)
+ .map(Self::TypeAliasType),
+ Self::Field(field) => field
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Self::Field),
+ Self::UnionType(union_type) => union_type
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Self::UnionType),
+ Self::Literal(ty) => ty
+ .recursive_type_normalized_impl(db, div, true, visitor)
+ .map(Self::Literal),
+ Self::Annotated(ty) => ty
+ .recursive_type_normalized_impl(db, div, true, visitor)
+ .map(Self::Annotated),
+ Self::TypeGenericAlias(ty) => ty
+ .recursive_type_normalized_impl(db, div, true, visitor)
+ .map(Self::TypeGenericAlias),
+ Self::LiteralStringAlias(ty) => ty
+ .recursive_type_normalized_impl(db, div, true, visitor)
+ .map(Self::LiteralStringAlias),
+ Self::Callable(callable) => callable
+ .recursive_type_normalized_impl(db, div, nested, visitor)
+ .map(Self::Callable),
+ Self::NewType(newtype) => newtype
+ .try_map_base_class_type(db, |class_type| {
+ class_type.recursive_type_normalized_impl(db, div, true, visitor)
+ })
+ .map(Self::NewType),
+ Self::GenericContext(generic) => Some(Self::GenericContext(generic)),
+ Self::Specialization(specialization) => specialization
+ .recursive_type_normalized_impl(db, div, true, visitor)
+ .map(Self::Specialization),
+ }
+ }
+
fn class(self, db: &'db dyn Db) -> KnownClass {
match self {
Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm,
@@ -8239,14 +8547,17 @@ impl<'db> KnownInstanceType<'db> {
/// (e.g. `Divergent` is assignable to `@Todo`, but `@Todo | Divergent` must not be reducted to `@Todo`).
/// Otherwise, type inference cannot converge properly.
/// For detailed properties of this type, see the unit test at the end of the file.
-#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)]
-pub struct DivergentType<'db> {
- /// The scope where this divergence was detected.
- scope: Option>,
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, salsa::Update)]
+pub struct DivergentType {
+ /// The query ID that caused the cycle.
+ id: salsa::Id,
}
+// The Salsa heap is tracked separately.
+impl get_size2::GetSize for DivergentType {}
+
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, get_size2::GetSize)]
-pub enum DynamicType<'db> {
+pub enum DynamicType {
/// An explicitly annotated `typing.Any`
Any,
/// An unannotated value, or a dynamic type resulting from an error
@@ -8263,11 +8574,11 @@ pub enum DynamicType<'db> {
Todo(TodoType),
/// A special Todo-variant for `Unpack[Ts]`, so that we can treat it specially in `Generic[Unpack[Ts]]`
TodoUnpack,
- /// A type that is determined to be divergent during type inference for a recursive function.
- Divergent(DivergentType<'db>),
+ /// A type that is determined to be divergent during recursive type inference.
+ Divergent(DivergentType),
}
-impl DynamicType<'_> {
+impl DynamicType {
fn normalized(self) -> Self {
if matches!(self, Self::Divergent(_)) {
self
@@ -8276,12 +8587,16 @@ impl DynamicType<'_> {
}
}
+ fn recursive_type_normalized(self) -> Self {
+ self
+ }
+
pub(crate) fn is_todo(&self) -> bool {
matches!(self, Self::Todo(_) | Self::TodoUnpack)
}
}
-impl std::fmt::Display for DynamicType<'_> {
+impl std::fmt::Display for DynamicType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DynamicType::Any => f.write_str("Any"),
@@ -8398,6 +8713,17 @@ impl<'db> TypeAndQualifiers<'db> {
pub(crate) fn qualifiers(&self) -> TypeQualifiers {
self.qualifiers
}
+
+ pub(crate) fn map_type(
+ &self,
+ f: impl FnOnce(Type<'db>) -> Type<'db>,
+ ) -> TypeAndQualifiers<'db> {
+ TypeAndQualifiers {
+ inner: f(self.inner),
+ origin: self.origin,
+ qualifiers: self.qualifiers,
+ }
+ }
}
/// Error struct providing information on type(s) that were deemed to be invalid
@@ -8633,6 +8959,33 @@ impl<'db> FieldInstance<'db> {
self.alias(db),
)
}
+
+ fn recursive_type_normalized_impl(
+ self,
+ db: &'db dyn Db,
+ div: Type<'db>,
+ nested: bool,
+ visitor: &NormalizedVisitor<'db>,
+ ) -> Option {
+ let default_type = match self.default_type(db) {
+ Some(default) if nested => {
+ Some(default.recursive_type_normalized_impl(db, div, true, visitor)?)
+ }
+ Some(default) => Some(
+ default
+ .recursive_type_normalized_impl(db, div, true, visitor)
+ .unwrap_or(div),
+ ),
+ None => None,
+ };
+ Some(FieldInstance::new(
+ db,
+ default_type,
+ self.init(db),
+ self.kw_only(db),
+ self.alias(db),
+ ))
+ }
}
/// Whether this typevar was created via the legacy `TypeVar` constructor, using PEP 695 syntax,
@@ -8991,7 +9344,7 @@ impl<'db> TypeVarInstance<'db> {
Some(TypeVarBoundOrConstraints::Constraints(ty))
}
- #[salsa::tracked(cycle_initial=lazy_default_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
+ #[salsa::tracked(cycle_fn=lazy_default_cycle_recover, cycle_initial=lazy_default_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
fn lazy_default(self, db: &'db dyn Db) -> Option> {
let definition = self.definition(db)?;
let module = parsed_module(db, definition.file(db)).load(db);
@@ -9048,12 +9401,28 @@ fn lazy_bound_or_constraints_cycle_initial<'db>(
None
}
+#[allow(clippy::ref_option)]
+fn lazy_default_cycle_recover<'db>(
+ db: &'db dyn Db,
+ cycle: &salsa::Cycle,
+ previous_default: &Option>,
+ default: Option>,
+ _typevar: TypeVarInstance<'db>,
+) -> Option> {
+ match (previous_default, default) {
+ (Some(prev), Some(default)) => Some(default.cycle_normalized(db, *prev, cycle)),
+ (None, Some(default)) => Some(default.recursive_type_normalized(db, cycle)),
+ (_, None) => None,
+ }
+}
+
+#[allow(clippy::unnecessary_wraps)]
fn lazy_default_cycle_initial<'db>(
_db: &'db dyn Db,
- _id: salsa::Id,
+ id: salsa::Id,
_self: TypeVarInstance<'db>,
) -> Option> {
- None
+ Some(Type::divergent(id))
}
/// Where a type variable is bound and usable.
@@ -9184,13 +9553,9 @@ impl<'db> BoundTypeVarInstance<'db> {
match self.typevar(db).explicit_variance(db) {
Some(explicit_variance) => explicit_variance.compose(polarity),
None => match self.binding_context(db) {
- BindingContext::Definition(definition) => {
- let type_inference = infer_definition_types(db, definition);
- type_inference
- .binding_type(definition)
- .with_polarity(polarity)
- .variance_of(db, self)
- }
+ BindingContext::Definition(definition) => binding_type(db, definition)
+ .with_polarity(polarity)
+ .variance_of(db, self),
BindingContext::Synthetic => TypeVarVariance::Invariant,
},
}
@@ -9488,6 +9853,44 @@ impl<'db> UnionTypeInstance<'db> {
Self::new(db, value_expr_types, union_type)
}
+
+ fn recursive_type_normalized_impl(
+ self,
+ db: &'db dyn Db,
+ div: Type<'db>,
+ nested: bool,
+ visitor: &NormalizedVisitor<'db>,
+ ) -> Option {
+ // The `Divergent` elimination rules are different within union types.
+ // See `UnionType::recursive_type_normalized_impl` for details.
+ let value_expr_types = match self._value_expr_types(db).as_ref() {
+ Some(types) if nested => Some(
+ types
+ .iter()
+ .map(|ty| ty.recursive_type_normalized_impl(db, div, nested, visitor))
+ .collect::