diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 8b35be52a6..9a691a0cb9 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
@@ -190,7 +190,7 @@ class B(A): ...
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -218,7 +218,7 @@ type B = A
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -245,7 +245,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -357,7 +357,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -387,7 +387,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -413,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
@@ -502,7 +502,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -529,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
@@ -557,7 +557,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -591,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
@@ -627,7 +627,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -651,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
@@ -678,7 +678,7 @@ with 1:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -707,7 +707,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -751,7 +751,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -793,7 +793,7 @@ class D(A):
Default level: error ·
Added in 0.0.1-alpha.35 ·
Related issues ·
-View source
+View source
@@ -837,7 +837,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -875,7 +875,7 @@ class D(Generic[U, T]): ...
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -914,7 +914,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -949,7 +949,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -983,7 +983,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1090,7 +1090,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -1144,7 +1144,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -1174,7 +1174,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
@@ -1224,7 +1224,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1250,7 +1250,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1281,7 +1281,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
@@ -1315,7 +1315,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
@@ -1364,7 +1364,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1389,7 +1389,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1447,7 +1447,7 @@ TODO #14889
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1474,7 +1474,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -1521,7 +1521,7 @@ Bar[int] # error: too few arguments
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1551,7 +1551,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1581,7 +1581,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
@@ -1615,7 +1615,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1643,23 +1643,56 @@ class C:
def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self`
```
+## `invalid-type-variable-bound`
+
+
+Default level: error ·
+Added in 1.0.0 ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Checks for [type variables] whose bounds reference type variables.
+
+**Why is this bad?**
+
+The bound of a type variable must be a concrete type.
+
+**Examples**
+
+```python
+T = TypeVar('T', bound=list['T']) # error: [invalid-type-variable-bound]
+U = TypeVar('U')
+T = TypeVar('T', bound=U) # error: [invalid-type-variable-bound]
+
+def f[T: list[T]](): ... # error: [invalid-type-variable-bound]
+def g[U, T: U](): ... # error: [invalid-type-variable-bound]
+```
+
+[type variable]: https://docs.python.org/3/library/typing.html#typing.TypeVar
+
## `invalid-type-variable-constraints`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
**What it does**
-Checks for constrained [type variables] with only one constraint.
+Checks for constrained [type variables] with only one constraint,
+or that those constraints reference type variables.
**Why is this bad?**
-A constrained type variable must have at least two constraints.
+A constrained type variable must have at least two constraints,
+and the constraints must be concrete types.
**Examples**
@@ -1667,6 +1700,9 @@ A constrained type variable must have at least two constraints.
from typing import TypeVar
T = TypeVar('T', str) # invalid constrained TypeVar
+
+I = TypeVar('I', bound=int)
+U = TypeVar('U', list[I], int) # invalid constrained TypeVar
```
Use instead:
@@ -1674,6 +1710,8 @@ Use instead:
T = TypeVar('T', str, int) # valid constrained TypeVar
# or
T = TypeVar('T', bound=str) # valid bound TypeVar
+
+U = TypeVar('U', list[int], int) # valid constrained Type
```
[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar
@@ -1684,7 +1722,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
@@ -1709,7 +1747,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
@@ -1742,7 +1780,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1771,7 +1809,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1795,7 +1833,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
@@ -1821,7 +1859,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -1854,7 +1892,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1881,7 +1919,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -1939,7 +1977,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1969,7 +2007,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
@@ -1998,7 +2036,7 @@ class B(A): ... # Error raised here
Default level: error ·
Preview (since 0.0.1-alpha.30) ·
Related issues ·
-View source
+View source
@@ -2032,7 +2070,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2059,7 +2097,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2087,7 +2125,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2133,7 +2171,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2160,7 +2198,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
@@ -2188,7 +2226,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
@@ -2213,7 +2251,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2238,7 +2276,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
@@ -2275,7 +2313,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2303,7 +2341,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
@@ -2328,7 +2366,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
@@ -2369,7 +2407,7 @@ class SubProto(BaseProto, Protocol):
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -2457,7 +2495,7 @@ a = 20 / 0 # type: ignore
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2485,7 +2523,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
@@ -2517,7 +2555,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
@@ -2549,7 +2587,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
@@ -2576,7 +2614,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2600,7 +2638,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
@@ -2658,7 +2696,7 @@ def g():
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2697,7 +2735,7 @@ class D(C): ... # error: [unsupported-base]
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2760,7 +2798,7 @@ def foo(x: int | str) -> int | str:
Default level: ignore ·
Preview (since 0.0.1-alpha.1) ·
Related issues ·
-View source
+View source
@@ -2784,7 +2822,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/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md
index b419b61e71..06fdd7b2d8 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md
@@ -441,6 +441,8 @@ def constrained(x: T_constrained):
### Bounds and constraints
+
+
A typevar's bounds and constraints cannot be generic, cyclic or otherwise:
```py
@@ -448,13 +450,13 @@ from typing import Any, TypeVar
S = TypeVar("S")
-# TODO: error
+# error: [invalid-type-variable-bound]
T = TypeVar("T", bound=list[S])
-# TODO: error
+# error: [invalid-type-variable-constraints]
U = TypeVar("U", list["T"], str)
-# TODO: error
+# error: [invalid-type-variable-constraints]
V = TypeVar("V", list["V"], str)
```
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
index 71e05171e8..d74cf7c677 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
@@ -710,10 +710,10 @@ reveal_type(WithOverloadedMethod[int].method)
### No back-references
-Typevar bounds/constraints/defaults are lazy, but cannot refer to later typevars:
+Typevar bounds/constraints/defaults are lazy, but cannot refer to other typevars:
```py
-# TODO error
+# error: [invalid-type-variable-bound]
class C[S: T, T]:
pass
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md
index d70c130649..f635ee08d1 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md
@@ -794,16 +794,18 @@ def constrained[T: (int, str)](x: T):
### Bounds and constraints
+
+
A typevar's bounds and constraints cannot be generic, cyclic or otherwise:
```py
from typing import Any
-# TODO: error
+# error: [invalid-type-variable-bound]
def f[S, T: list[S]](x: S, y: T) -> S | T:
return x or y
-# TODO: error
+# error: [invalid-type-variable-bound]
class C[S, T: list[S]]:
x: S
y: T
@@ -811,21 +813,21 @@ class C[S, T: list[S]]:
reveal_type(C[int, list[Any]]().x) # revealed: int
reveal_type(C[int, list[Any]]().y) # revealed: list[Any]
-# TODO: error
+# error: [invalid-type-variable-bound]
def g[T: list[T]](x: T) -> T:
return x
-# TODO: error
+# error: [invalid-type-variable-bound]
class D[T: list[T]]:
x: T
reveal_type(D[list[Any]]().x) # revealed: list[Any]
-# TODO: error
+# error: [invalid-type-variable-constraints]
def h[S, T: (list[S], str)](x: S, y: T) -> S | T:
return x or y
-# TODO: error
+# error: [invalid-type-variable-constraints]
class E[S, T: (list[S], str)]:
x: S
y: T
@@ -833,11 +835,11 @@ class E[S, T: (list[S], str)]:
reveal_type(E[int, str]().x) # revealed: int
reveal_type(E[int, str]().y) # revealed: str
-# TODO: error
+# error: [invalid-type-variable-constraints]
def i[T: (list[T], str)](x: T) -> T:
return x
-# TODO: error
+# error: [invalid-type-variable-constraints]
class F[T: (list[T], str)]:
x: T
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable…_-_Cycles_-_Bounds_and_constrain…_(1c0a5d9209a531cd).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable…_-_Cycles_-_Bounds_and_constrain…_(1c0a5d9209a531cd).snap
new file mode 100644
index 0000000000..8f07a39708
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable…_-_Cycles_-_Bounds_and_constrain…_(1c0a5d9209a531cd).snap
@@ -0,0 +1,124 @@
+---
+source: crates/ty_test/src/lib.rs
+expression: snapshot
+---
+---
+mdtest name: variables.md - Legacy type variables - Cycles - Bounds and constraints
+mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md
+---
+
+# Python source files
+
+## mdtest_snippet.py
+
+```
+ 1 | from typing import Any, TypeVar
+ 2 |
+ 3 | S = TypeVar("S")
+ 4 |
+ 5 | # error: [invalid-type-variable-bound]
+ 6 | T = TypeVar("T", bound=list[S])
+ 7 |
+ 8 | # error: [invalid-type-variable-constraints]
+ 9 | U = TypeVar("U", list["T"], str)
+10 |
+11 | # error: [invalid-type-variable-constraints]
+12 | V = TypeVar("V", list["V"], str)
+13 | from typing import TypeVar, Generic
+14 |
+15 | T = TypeVar("T", bound=list["G"])
+16 |
+17 | class G(Generic[T]):
+18 | x: T
+19 |
+20 | reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]]
+21 | from typing import TypeVar, Generic
+22 |
+23 | # error: [invalid-type-arguments]
+24 | T = TypeVar("T", bound="Node[int]")
+25 |
+26 | class Node(Generic[T]):
+27 | pass
+28 |
+29 | # error: [invalid-type-arguments]
+30 | def _(n: Node[str]):
+31 | reveal_type(n) # revealed: Node[Unknown]
+```
+
+# Diagnostics
+
+```
+error[invalid-type-variable-bound]: Type variable bound type cannot be generic
+ --> src/mdtest_snippet.py:6:18
+ |
+5 | # error: [invalid-type-variable-bound]
+6 | T = TypeVar("T", bound=list[S])
+ | ^^^^^^^^^^^^^
+7 |
+8 | # error: [invalid-type-variable-constraints]
+ |
+info: rule `invalid-type-variable-bound` is enabled by default
+
+```
+
+```
+error[invalid-type-variable-constraints]: Type variable constraint types cannot be generic
+ --> src/mdtest_snippet.py:9:18
+ |
+ 8 | # error: [invalid-type-variable-constraints]
+ 9 | U = TypeVar("U", list["T"], str)
+ | ^^^^^^^^^
+10 |
+11 | # error: [invalid-type-variable-constraints]
+ |
+info: rule `invalid-type-variable-constraints` is enabled by default
+
+```
+
+```
+error[invalid-type-variable-constraints]: Type variable constraint types cannot be generic
+ --> src/mdtest_snippet.py:12:18
+ |
+11 | # error: [invalid-type-variable-constraints]
+12 | V = TypeVar("V", list["V"], str)
+ | ^^^^^^^^^
+13 | from typing import TypeVar, Generic
+ |
+info: rule `invalid-type-variable-constraints` is enabled by default
+
+```
+
+```
+error[invalid-type-arguments]: Type `int` is not assignable to upper bound `Node[Unknown] | Node[int]` of type variable `T@Node`
+ --> src/mdtest_snippet.py:24:1
+ |
+23 | # error: [invalid-type-arguments]
+24 | T = TypeVar("T", bound="Node[int]")
+ | - Type variable defined here ^^^
+25 |
+26 | class Node(Generic[T]):
+ |
+info: rule `invalid-type-arguments` is enabled by default
+
+```
+
+```
+error[invalid-type-arguments]: Type `str` is not assignable to upper bound `Node[Unknown] | Node[int]` of type variable `T@Node`
+ --> src/mdtest_snippet.py:30:15
+ |
+29 | # error: [invalid-type-arguments]
+30 | def _(n: Node[str]):
+ | ^^^
+31 | reveal_type(n) # revealed: Node[Unknown]
+ |
+ ::: src/mdtest_snippet.py:24:1
+ |
+23 | # error: [invalid-type-arguments]
+24 | T = TypeVar("T", bound="Node[int]")
+ | - Type variable defined here
+25 |
+26 | class Node(Generic[T]):
+ |
+info: rule `invalid-type-arguments` is enabled by default
+
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_PEP_695_Generics_-_Cycles_-_Bounds_and_constrain…_(c7a923bb701940f3).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_PEP_695_Generics_-_Cycles_-_Bounds_and_constrain…_(c7a923bb701940f3).snap
new file mode 100644
index 0000000000..15b66c8a64
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_PEP_695_Generics_-_Cycles_-_Bounds_and_constrain…_(c7a923bb701940f3).snap
@@ -0,0 +1,216 @@
+---
+source: crates/ty_test/src/lib.rs
+expression: snapshot
+---
+---
+mdtest name: variables.md - PEP 695 Generics - Cycles - Bounds and constraints
+mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md
+---
+
+# Python source files
+
+## mdtest_snippet.py
+
+```
+ 1 | from typing import Any
+ 2 |
+ 3 | # error: [invalid-type-variable-bound]
+ 4 | def f[S, T: list[S]](x: S, y: T) -> S | T:
+ 5 | return x or y
+ 6 |
+ 7 | # error: [invalid-type-variable-bound]
+ 8 | class C[S, T: list[S]]:
+ 9 | x: S
+10 | y: T
+11 |
+12 | reveal_type(C[int, list[Any]]().x) # revealed: int
+13 | reveal_type(C[int, list[Any]]().y) # revealed: list[Any]
+14 |
+15 | # error: [invalid-type-variable-bound]
+16 | def g[T: list[T]](x: T) -> T:
+17 | return x
+18 |
+19 | # error: [invalid-type-variable-bound]
+20 | class D[T: list[T]]:
+21 | x: T
+22 |
+23 | reveal_type(D[list[Any]]().x) # revealed: list[Any]
+24 |
+25 | # error: [invalid-type-variable-constraints]
+26 | def h[S, T: (list[S], str)](x: S, y: T) -> S | T:
+27 | return x or y
+28 |
+29 | # error: [invalid-type-variable-constraints]
+30 | class E[S, T: (list[S], str)]:
+31 | x: S
+32 | y: T
+33 |
+34 | reveal_type(E[int, str]().x) # revealed: int
+35 | reveal_type(E[int, str]().y) # revealed: str
+36 |
+37 | # error: [invalid-type-variable-constraints]
+38 | def i[T: (list[T], str)](x: T) -> T:
+39 | return x
+40 |
+41 | # error: [invalid-type-variable-constraints]
+42 | class F[T: (list[T], str)]:
+43 | x: T
+44 |
+45 | reveal_type(F[list[Any]]().x) # revealed: list[Any]
+46 | class G[T: list[G]]:
+47 | x: T
+48 |
+49 | reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]]
+50 | # error: [invalid-type-arguments]
+51 | class Node[T: "Node[int]"]:
+52 | pass
+53 |
+54 | # error: [invalid-type-arguments]
+55 | def _(n: Node[str]):
+56 | reveal_type(n) # revealed: Node[Unknown]
+```
+
+# Diagnostics
+
+```
+error[invalid-type-variable-bound]: Type variable bound type cannot be generic
+ --> src/mdtest_snippet.py:4:13
+ |
+3 | # error: [invalid-type-variable-bound]
+4 | def f[S, T: list[S]](x: S, y: T) -> S | T:
+ | ^^^^^^^
+5 | return x or y
+ |
+info: rule `invalid-type-variable-bound` is enabled by default
+
+```
+
+```
+error[invalid-type-variable-bound]: Type variable bound type cannot be generic
+ --> src/mdtest_snippet.py:8:15
+ |
+ 7 | # error: [invalid-type-variable-bound]
+ 8 | class C[S, T: list[S]]:
+ | ^^^^^^^
+ 9 | x: S
+10 | y: T
+ |
+info: rule `invalid-type-variable-bound` is enabled by default
+
+```
+
+```
+error[invalid-type-variable-bound]: Type variable bound type cannot be generic
+ --> src/mdtest_snippet.py:16:10
+ |
+15 | # error: [invalid-type-variable-bound]
+16 | def g[T: list[T]](x: T) -> T:
+ | ^^^^^^^
+17 | return x
+ |
+info: rule `invalid-type-variable-bound` is enabled by default
+
+```
+
+```
+error[invalid-type-variable-bound]: Type variable bound type cannot be generic
+ --> src/mdtest_snippet.py:20:12
+ |
+19 | # error: [invalid-type-variable-bound]
+20 | class D[T: list[T]]:
+ | ^^^^^^^
+21 | x: T
+ |
+info: rule `invalid-type-variable-bound` is enabled by default
+
+```
+
+```
+error[invalid-type-variable-constraints]: Type variable constraint types cannot be generic
+ --> src/mdtest_snippet.py:26:14
+ |
+25 | # error: [invalid-type-variable-constraints]
+26 | def h[S, T: (list[S], str)](x: S, y: T) -> S | T:
+ | ^^^^^^^
+27 | return x or y
+ |
+info: rule `invalid-type-variable-constraints` is enabled by default
+
+```
+
+```
+error[invalid-type-variable-constraints]: Type variable constraint types cannot be generic
+ --> src/mdtest_snippet.py:30:16
+ |
+29 | # error: [invalid-type-variable-constraints]
+30 | class E[S, T: (list[S], str)]:
+ | ^^^^^^^
+31 | x: S
+32 | y: T
+ |
+info: rule `invalid-type-variable-constraints` is enabled by default
+
+```
+
+```
+error[invalid-type-variable-constraints]: Type variable constraint types cannot be generic
+ --> src/mdtest_snippet.py:38:11
+ |
+37 | # error: [invalid-type-variable-constraints]
+38 | def i[T: (list[T], str)](x: T) -> T:
+ | ^^^^^^^
+39 | return x
+ |
+info: rule `invalid-type-variable-constraints` is enabled by default
+
+```
+
+```
+error[invalid-type-variable-constraints]: Type variable constraint types cannot be generic
+ --> src/mdtest_snippet.py:42:13
+ |
+41 | # error: [invalid-type-variable-constraints]
+42 | class F[T: (list[T], str)]:
+ | ^^^^^^^
+43 | x: T
+ |
+info: rule `invalid-type-variable-constraints` is enabled by default
+
+```
+
+```
+error[invalid-type-arguments]: Type `int` is not assignable to upper bound `Node[Unknown] | Node[int]` of type variable `T@Node`
+ --> src/mdtest_snippet.py:51:12
+ |
+49 | reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]]
+50 | # error: [invalid-type-arguments]
+51 | class Node[T: "Node[int]"]:
+ | - ^^^
+ | |
+ | Type variable defined here
+52 | pass
+ |
+info: rule `invalid-type-arguments` is enabled by default
+
+```
+
+```
+error[invalid-type-arguments]: Type `str` is not assignable to upper bound `Node[Unknown] | Node[int]` of type variable `T@Node`
+ --> src/mdtest_snippet.py:55:15
+ |
+54 | # error: [invalid-type-arguments]
+55 | def _(n: Node[str]):
+ | ^^^
+56 | reveal_type(n) # revealed: Node[Unknown]
+ |
+ ::: src/mdtest_snippet.py:51:12
+ |
+49 | reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]]
+50 | # error: [invalid-type-arguments]
+51 | class Node[T: "Node[int]"]:
+ | - Type variable defined here
+52 | pass
+ |
+info: rule `invalid-type-arguments` is enabled by default
+
+```
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index e136057d45..7a33cec187 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -90,6 +90,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_TYPE_GUARD_DEFINITION);
registry.register_lint(&INVALID_TYPE_GUARD_CALL);
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
+ registry.register_lint(&INVALID_TYPE_VARIABLE_BOUND);
registry.register_lint(&MISSING_ARGUMENT);
registry.register_lint(&NO_MATCHING_OVERLOAD);
registry.register_lint(&NON_SUBSCRIPTABLE);
@@ -1350,16 +1351,21 @@ declare_lint! {
declare_lint! {
/// ## What it does
- /// Checks for constrained [type variables] with only one constraint.
+ /// Checks for constrained [type variables] with only one constraint,
+ /// or that those constraints reference type variables.
///
/// ## Why is this bad?
- /// A constrained type variable must have at least two constraints.
+ /// A constrained type variable must have at least two constraints,
+ /// and the constraints must be concrete types.
///
/// ## Examples
/// ```python
/// from typing import TypeVar
///
/// T = TypeVar('T', str) # invalid constrained TypeVar
+ ///
+ /// I = TypeVar('I', bound=int)
+ /// U = TypeVar('U', list[I], int) # invalid constrained TypeVar
/// ```
///
/// Use instead:
@@ -1367,6 +1373,8 @@ declare_lint! {
/// T = TypeVar('T', str, int) # valid constrained TypeVar
/// # or
/// T = TypeVar('T', bound=str) # valid bound TypeVar
+ ///
+ /// U = TypeVar('U', list[int], int) # valid constrained Type
/// ```
///
/// [type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar
@@ -1377,6 +1385,31 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Checks for [type variables] whose bounds reference type variables.
+ ///
+ /// ## Why is this bad?
+ /// The bound of a type variable must be a concrete type.
+ ///
+ /// ## Examples
+ /// ```python
+ /// T = TypeVar('T', bound=list['T']) # error: [invalid-type-variable-bound]
+ /// U = TypeVar('U')
+ /// T = TypeVar('T', bound=U) # error: [invalid-type-variable-bound]
+ ///
+ /// def f[T: list[T]](): ... # error: [invalid-type-variable-bound]
+ /// def g[U, T: U](): ... # error: [invalid-type-variable-bound]
+ /// ```
+ ///
+ /// [type variable]: https://docs.python.org/3/library/typing.html#typing.TypeVar
+ pub(crate) static INVALID_TYPE_VARIABLE_BOUND = {
+ summary: "detects invalid type variable bounds",
+ status: LintStatus::stable("1.0.0"),
+ default_level: Level::Error,
+ }
+}
+
declare_lint! {
/// ## What it does
/// Checks for missing required arguments in a call.
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 723e89519c..e278e3163f 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -63,11 +63,11 @@ use crate::types::diagnostic::{
INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE,
INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL,
INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
- INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE,
- POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
- SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
- UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
- hint_if_stdlib_attribute_exists_on_other_versions,
+ INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
+ NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL,
+ POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
+ UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR,
+ USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
report_cannot_pop_required_field_on_typed_dict, report_duplicate_bases,
@@ -564,6 +564,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
+ if self.db().should_check_file(self.file()) {
+ self.check_legacy_typevars();
+ }
// Infer deferred types for all definitions.
for definition in std::mem::take(&mut self.deferred) {
self.extend_definition(infer_deferred_types(self.db(), definition));
@@ -577,6 +580,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if self.db().should_check_file(self.file()) {
self.check_class_definitions();
self.check_overloaded_functions(node);
+ self.check_pep695_typevars();
}
}
@@ -1291,6 +1295,131 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
+ fn check_legacy_typevars(&mut self) {
+ let typevars = self.deferred.iter().filter_map(|definition| {
+ if let DefinitionKind::Assignment(assignment) = definition.kind(self.db()) {
+ let ast::Expr::Call(call) = assignment.value(self.module()) else {
+ return None;
+ };
+ let inference = infer_definition_types(self.db(), *definition);
+ Some((call, inference.binding_type(*definition)))
+ } else {
+ None
+ }
+ });
+
+ // The current Python type specification is that the bounds and constraints of type variables must be concrete types,
+ // and an error must occur if type variables are included.
+ for (call, ty) in typevars {
+ let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty else {
+ continue;
+ };
+ let has_typevar = |ty| {
+ any_over_type(
+ self.db(),
+ ty,
+ &|ty| matches!(ty, Type::KnownInstance(KnownInstanceType::TypeVar(_))),
+ false,
+ )
+ };
+ match typevar.bound_or_constraints(self.db()) {
+ Some(TypeVarBoundOrConstraints::UpperBound(bound_ty)) => {
+ let Some(bound) = call.arguments.find_keyword("bound") else {
+ continue;
+ };
+ if has_typevar(bound_ty)
+ && let Some(builder) = self
+ .context
+ .report_lint(&INVALID_TYPE_VARIABLE_BOUND, bound)
+ {
+ builder.into_diagnostic("Type variable bound type cannot be generic");
+ }
+ }
+ Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
+ for constraint in constraints
+ .elements(self.db())
+ .iter()
+ .enumerate()
+ .filter_map(|(i, ty)| {
+ has_typevar(*ty).then_some(call.arguments.args.get(i + 1)?)
+ })
+ {
+ if let Some(builder) = self
+ .context
+ .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, constraint)
+ {
+ builder.into_diagnostic(
+ "Type variable constraint types cannot be generic",
+ );
+ }
+ }
+ }
+ None => {}
+ }
+ }
+ }
+
+ fn check_pep695_typevars(&mut self) {
+ let typevars = self.declarations.iter().filter_map(|(definition, ty)| {
+ if let DefinitionKind::TypeVar(typevar) = definition.kind(self.db()) {
+ Some((typevar.node(self.module()), ty.inner_type()))
+ } else {
+ None
+ }
+ });
+
+ // The current Python type specification is that the bounds and constraints of type variables must be concrete types,
+ // and an error must occur if type variables are included.
+ for (node, ty) in typevars {
+ let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty else {
+ continue;
+ };
+ let Some(bound) = node.bound.as_deref() else {
+ continue;
+ };
+ let has_typevar = |ty| {
+ any_over_type(
+ self.db(),
+ ty,
+ &|ty| matches!(ty, Type::KnownInstance(KnownInstanceType::TypeVar(_))),
+ false,
+ )
+ };
+ match typevar.bound_or_constraints(self.db()) {
+ Some(TypeVarBoundOrConstraints::UpperBound(bound_ty)) => {
+ if has_typevar(bound_ty)
+ && let Some(builder) = self
+ .context
+ .report_lint(&INVALID_TYPE_VARIABLE_BOUND, bound)
+ {
+ builder.into_diagnostic("Type variable bound type cannot be generic");
+ }
+ }
+ Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
+ let ast::Expr::Tuple(tuple) = bound else {
+ continue;
+ };
+ for constraint in constraints
+ .elements(self.db())
+ .iter()
+ .enumerate()
+ .filter_map(|(i, ty)| has_typevar(*ty).then_some(tuple.elts.get(i)?))
+ {
+ if let Some(builder) = self
+ .context
+ .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, constraint)
+ {
+ builder.into_diagnostic(
+ "Type variable constraint types cannot be generic",
+ );
+ }
+ }
+ }
+ None => {}
+ }
+ }
+ }
+
fn infer_region_definition(&mut self, definition: Definition<'db>) {
match definition.kind(self.db()) {
DefinitionKind::Function(function) => {
@@ -13075,6 +13204,11 @@ impl VecSet {
self.0.is_empty()
}
+ #[inline]
+ fn iter(&self) -> std::slice::Iter<'_, V> {
+ self.0.iter()
+ }
+
fn into_boxed_slice(self) -> Box<[V]> {
self.0.into_boxed_slice()
}
diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
index cf1b0afc64..22f1df57c6 100644
--- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
+++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
@@ -78,6 +78,7 @@ Settings: Settings {
"invalid-type-form": Error (Default),
"invalid-type-guard-call": Error (Default),
"invalid-type-guard-definition": Error (Default),
+ "invalid-type-variable-bound": Error (Default),
"invalid-type-variable-constraints": Error (Default),
"missing-argument": Error (Default),
"missing-typed-dict-key": Error (Default),
diff --git a/ty.schema.json b/ty.schema.json
index 74c142ec4c..814c76b251 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -813,9 +813,19 @@
}
]
},
+ "invalid-type-variable-bound": {
+ "title": "detects invalid type variable bounds",
+ "description": "## What it does\nChecks for [type variables] whose bounds reference type variables.\n\n## Why is this bad?\nThe bound of a type variable must be a concrete type.\n\n## Examples\n```python\nT = TypeVar('T', bound=list['T']) # error: [invalid-type-variable-bound]\nU = TypeVar('U')\nT = TypeVar('T', bound=U) # error: [invalid-type-variable-bound]\n\ndef f[T: list[T]](): ... # error: [invalid-type-variable-bound]\ndef g[U, T: U](): ... # error: [invalid-type-variable-bound]\n```\n\n[type variable]: https://docs.python.org/3/library/typing.html#typing.TypeVar",
+ "default": "error",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"invalid-type-variable-constraints": {
"title": "detects invalid type variable constraints",
- "description": "## What it does\nChecks for constrained [type variables] with only one constraint.\n\n## Why is this bad?\nA constrained type variable must have at least two constraints.\n\n## Examples\n```python\nfrom typing import TypeVar\n\nT = TypeVar('T', str) # invalid constrained TypeVar\n```\n\nUse instead:\n```python\nT = TypeVar('T', str, int) # valid constrained TypeVar\n# or\nT = TypeVar('T', bound=str) # valid bound TypeVar\n```\n\n[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar",
+ "description": "## What it does\nChecks for constrained [type variables] with only one constraint,\nor that those constraints reference type variables.\n\n## Why is this bad?\nA constrained type variable must have at least two constraints,\nand the constraints must be concrete types.\n\n## Examples\n```python\nfrom typing import TypeVar\n\nT = TypeVar('T', str) # invalid constrained TypeVar\n\nI = TypeVar('I', bound=int)\nU = TypeVar('U', list[I], int) # invalid constrained TypeVar\n```\n\nUse instead:\n```python\nT = TypeVar('T', str, int) # valid constrained TypeVar\n# or\nT = TypeVar('T', bound=str) # valid bound TypeVar\n\nU = TypeVar('U', list[int], int) # valid constrained Type\n```\n\n[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar",
"default": "error",
"oneOf": [
{