diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 86e1d127a8..8b35be52a6 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
@@ -848,16 +848,21 @@ Checks for the creation of invalid generic classes
**Why is this bad?**
There are several requirements that you must follow when defining a generic class.
+Many of these result in `TypeError` being raised at runtime if they are violated.
**Examples**
```python
-from typing import Generic, TypeVar
+from typing_extensions import Generic, TypeVar
-T = TypeVar("T") # okay
+T = TypeVar("T")
+U = TypeVar("U", default=int)
# error: class uses both PEP-695 syntax and legacy syntax
class C[U](Generic[T]): ...
+
+# error: type parameter with default comes before type parameter without default
+class D(Generic[U, T]): ...
```
**References**
@@ -870,7 +875,7 @@ class C[U](Generic[T]): ...
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -909,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
@@ -944,7 +949,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -978,7 +983,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1085,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
@@ -1139,7 +1144,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -1169,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
@@ -1219,7 +1224,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1245,7 +1250,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1276,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
@@ -1310,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
@@ -1359,7 +1364,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1384,7 +1389,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1442,7 +1447,7 @@ TODO #14889
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1469,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
@@ -1516,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
@@ -1546,7 +1551,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1576,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
@@ -1610,7 +1615,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1638,47 +1643,13 @@ class C:
def f(self) -> TypeIs[int]: ... # Error, only positional argument expected is `self`
```
-## `invalid-type-param-order`
-
-
-Default level: error ·
-Added in 0.0.1-alpha.1 ·
-Related issues ·
-View source
-
-
-
-**What it does**
-
-Checks for type parameters without defaults that come after type parameters with defaults.
-
-**Why is this bad?**
-
-Type parameters without defaults must come before type parameters with defaults.
-
-**Example**
-
-
-```python
-from typing import Generic, TypeVar
-
-T = TypeVar("T")
-U = TypeVar("U")
-# Error: T has no default but comes after U which has a default
-class Foo(Generic[U = int, T]): ...
-```
-
-**References**
-
-- [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/)
-
## `invalid-type-variable-constraints`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1713,7 +1684,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
@@ -1738,7 +1709,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
@@ -1771,7 +1742,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1800,7 +1771,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1824,7 +1795,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
@@ -1850,7 +1821,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
@@ -1883,7 +1854,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1910,7 +1881,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -1968,7 +1939,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1998,7 +1969,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
@@ -2027,7 +1998,7 @@ class B(A): ... # Error raised here
Default level: error ·
Preview (since 0.0.1-alpha.30) ·
Related issues ·
-View source
+View source
@@ -2061,7 +2032,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2088,7 +2059,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2116,7 +2087,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2162,7 +2133,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2189,7 +2160,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
@@ -2217,7 +2188,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
@@ -2242,7 +2213,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2267,7 +2238,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
@@ -2304,7 +2275,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2332,7 +2303,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
@@ -2357,7 +2328,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
@@ -2398,7 +2369,7 @@ class SubProto(BaseProto, Protocol):
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -2486,7 +2457,7 @@ a = 20 / 0 # type: ignore
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2514,7 +2485,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
@@ -2546,7 +2517,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
@@ -2578,7 +2549,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
@@ -2605,7 +2576,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2629,7 +2600,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
@@ -2687,7 +2658,7 @@ def g():
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2726,7 +2697,7 @@ class D(C): ... # error: [unsupported-base]
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2789,7 +2760,7 @@ def foo(x: int | str) -> int | str:
Default level: ignore ·
Preview (since 0.0.1-alpha.1) ·
Related issues ·
-View source
+View source
@@ -2813,7 +2784,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/diagnostics/invalid_type_param_order.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md
deleted file mode 100644
index 2b5d956f9b..0000000000
--- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Invalid Type Param Order
-
-
-
-```toml
-[environment]
-python-version = "3.13"
-```
-
-```py
-from typing import TypeVar, Generic
-
-T1 = TypeVar("T1", default=int)
-T2 = TypeVar("T2")
-T3 = TypeVar("T3")
-DefaultStrT = TypeVar("DefaultStrT", default=str)
-
-class SubclassMe(Generic[T1, DefaultStrT]):
- x: DefaultStrT
-
-class Baz(SubclassMe[int, DefaultStrT]):
- pass
-
-class Foo(Generic[T1, T2]): # error: [invalid-type-param-order]
- pass
-
-class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order]
- pass
-```
diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md
new file mode 100644
index 0000000000..705ceae6b6
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md
@@ -0,0 +1,43 @@
+# Invalid Order of Legacy Type Parameters
+
+
+
+```toml
+[environment]
+python-version = "3.13"
+```
+
+```py
+from typing import TypeVar, Generic, Protocol
+
+T1 = TypeVar("T1", default=int)
+
+T2 = TypeVar("T2")
+T3 = TypeVar("T3")
+
+DefaultStrT = TypeVar("DefaultStrT", default=str)
+
+class SubclassMe(Generic[T1, DefaultStrT]):
+ x: DefaultStrT
+
+class Baz(SubclassMe[int, DefaultStrT]):
+ pass
+
+# error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
+class Foo(Generic[T1, T2]):
+ pass
+
+class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
+ pass
+
+class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+ pass
+
+class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+ pass
+
+class VeryBad(
+ Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
+ Generic[T1, T2, DefaultStrT, T3],
+): ...
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md
index 00f50240e4..5e3cbe3888 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md
@@ -425,7 +425,7 @@ reveal_type(p3.attr1) # revealed: (int, /) -> None
reveal_type(p3.attr2) # revealed: (str, /) -> None
# Un-ordered type variables as the default of `PAnother` is `P`
-class ParamSpecWithDefault5(Generic[PAnother, P]): # error: [invalid-type-param-order]
+class ParamSpecWithDefault5(Generic[PAnother, P]): # error: [invalid-generic-class]
attr: Callable[PAnother, None]
# TODO: error
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o…_-_Invalid_Type_Param_O…_(8ff6f101710809cb).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o…_-_Invalid_Type_Param_O…_(8ff6f101710809cb).snap
deleted file mode 100644
index 74cc600ed1..0000000000
--- a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_param_o…_-_Invalid_Type_Param_O…_(8ff6f101710809cb).snap
+++ /dev/null
@@ -1,63 +0,0 @@
----
-source: crates/ty_test/src/lib.rs
-expression: snapshot
----
----
-mdtest name: invalid_type_param_order.md - Invalid Type Param Order
-mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_param_order.md
----
-
-# Python source files
-
-## mdtest_snippet.py
-
-```
- 1 | from typing import TypeVar, Generic
- 2 |
- 3 | T1 = TypeVar("T1", default=int)
- 4 | T2 = TypeVar("T2")
- 5 | T3 = TypeVar("T3")
- 6 | DefaultStrT = TypeVar("DefaultStrT", default=str)
- 7 |
- 8 | class SubclassMe(Generic[T1, DefaultStrT]):
- 9 | x: DefaultStrT
-10 |
-11 | class Baz(SubclassMe[int, DefaultStrT]):
-12 | pass
-13 |
-14 | class Foo(Generic[T1, T2]): # error: [invalid-type-param-order]
-15 | pass
-16 |
-17 | class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order]
-18 | pass
-```
-
-# Diagnostics
-
-```
-error[invalid-type-param-order]: Type parameter T2 without a default follows type parameter with a default
- --> src/mdtest_snippet.py:14:7
- |
-12 | pass
-13 |
-14 | class Foo(Generic[T1, T2]): # error: [invalid-type-param-order]
- | ^^^^^^^^^^^^^^^^^^^^
-15 | pass
- |
-info: rule `invalid-type-param-order` is enabled by default
-
-```
-
-```
-error[invalid-type-param-order]: Type parameter T3 without a default follows type parameter with a default
- --> src/mdtest_snippet.py:17:7
- |
-15 | pass
-16 |
-17 | class Bar(Generic[T2, T1, T3]): # error: [invalid-type-param-order]
- | ^^^^^^^^^^^^^^^^^^^^^^^^
-18 | pass
- |
-info: rule `invalid-type-param-order` is enabled by default
-
-```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet…_-_Invalid_Order_of_Leg…_(eaa359e8d6b3031d).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet…_-_Invalid_Order_of_Leg…_(eaa359e8d6b3031d).snap
new file mode 100644
index 0000000000..290fbb0ae0
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet…_-_Invalid_Order_of_Leg…_(eaa359e8d6b3031d).snap
@@ -0,0 +1,190 @@
+---
+source: crates/ty_test/src/lib.rs
+expression: snapshot
+---
+---
+mdtest name: invalid_type_parameter_order.md - Invalid Order of Legacy Type Parameters
+mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md
+---
+
+# Python source files
+
+## mdtest_snippet.py
+
+```
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ 6 | T3 = TypeVar("T3")
+ 7 |
+ 8 | DefaultStrT = TypeVar("DefaultStrT", default=str)
+ 9 |
+10 | class SubclassMe(Generic[T1, DefaultStrT]):
+11 | x: DefaultStrT
+12 |
+13 | class Baz(SubclassMe[int, DefaultStrT]):
+14 | pass
+15 |
+16 | # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
+17 | class Foo(Generic[T1, T2]):
+18 | pass
+19 |
+20 | class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
+21 | pass
+22 |
+23 | class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+24 | pass
+25 |
+26 | class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+27 | pass
+28 |
+29 | class VeryBad(
+30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
+31 | Generic[T1, T2, DefaultStrT, T3],
+32 | ): ...
+```
+
+# Diagnostics
+
+```
+error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
+ --> src/mdtest_snippet.py:17:19
+ |
+16 | # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
+17 | class Foo(Generic[T1, T2]):
+ | ^^^^^^
+ | |
+ | Type variable `T2` does not have a default
+ | Earlier TypeVar `T1` does
+18 | pass
+ |
+ ::: src/mdtest_snippet.py:3:1
+ |
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ | ------------------------------- `T1` defined here
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ | ------------------ `T2` defined here
+ 6 | T3 = TypeVar("T3")
+ |
+info: rule `invalid-generic-class` is enabled by default
+
+```
+
+```
+error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
+ --> src/mdtest_snippet.py:20:19
+ |
+18 | pass
+19 |
+20 | class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
+ | ^^^^^^^^^^
+ | |
+ | Type variable `T3` does not have a default
+ | Earlier TypeVar `T1` does
+21 | pass
+ |
+ ::: src/mdtest_snippet.py:3:1
+ |
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ | ------------------------------- `T1` defined here
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ 6 | T3 = TypeVar("T3")
+ | ------------------ `T3` defined here
+ 7 |
+ 8 | DefaultStrT = TypeVar("DefaultStrT", default=str)
+ |
+info: rule `invalid-generic-class` is enabled by default
+
+```
+
+```
+error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
+ --> src/mdtest_snippet.py:23:20
+ |
+21 | pass
+22 |
+23 | class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+ | ^^^^^^^^^^^^^^^^^^^^^^^
+ | |
+ | Type variables `T2` and `T3` do not have defaults
+ | Earlier TypeVar `T1` does
+24 | pass
+ |
+ ::: src/mdtest_snippet.py:3:1
+ |
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ | ------------------------------- `T1` defined here
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ | ------------------ `T2` defined here
+ 6 | T3 = TypeVar("T3")
+ |
+info: rule `invalid-generic-class` is enabled by default
+
+```
+
+```
+error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
+ --> src/mdtest_snippet.py:26:20
+ |
+24 | pass
+25 |
+26 | class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+ | ^^^^^^^^^^^^^^^^^^^^^^^
+ | |
+ | Type variables `T2` and `T3` do not have defaults
+ | Earlier TypeVar `T1` does
+27 | pass
+ |
+ ::: src/mdtest_snippet.py:3:1
+ |
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ | ------------------------------- `T1` defined here
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ | ------------------ `T2` defined here
+ 6 | T3 = TypeVar("T3")
+ |
+info: rule `invalid-generic-class` is enabled by default
+
+```
+
+```
+error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
+ --> src/mdtest_snippet.py:30:14
+ |
+29 | class VeryBad(
+30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
+ | ^^^^^^^^^^^^^^^^^^^^^^^
+ | |
+ | Type variables `T2` and `T3` do not have defaults
+ | Earlier TypeVar `T1` does
+31 | Generic[T1, T2, DefaultStrT, T3],
+32 | ): ...
+ |
+ ::: src/mdtest_snippet.py:3:1
+ |
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ | ------------------------------- `T1` defined here
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ | ------------------ `T2` defined here
+ 6 | T3 = TypeVar("T3")
+ |
+info: rule `invalid-generic-class` 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 9fc7141057..e136057d45 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -30,7 +30,7 @@ use crate::types::{
ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type,
protocol_class::ProtocolClass,
};
-use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy};
+use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy, TypeVarInstance};
use crate::{Db, DisplaySettings, FxIndexMap, Module, ModuleName, Program, declare_lint};
use itertools::Itertools;
use ruff_db::{
@@ -89,7 +89,6 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_TYPE_FORM);
registry.register_lint(&INVALID_TYPE_GUARD_DEFINITION);
registry.register_lint(&INVALID_TYPE_GUARD_CALL);
- registry.register_lint(&INVALID_TYPE_PARAM_ORDER);
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
registry.register_lint(&MISSING_ARGUMENT);
registry.register_lint(&NO_MATCHING_OVERLOAD);
@@ -895,15 +894,20 @@ declare_lint! {
///
/// ## Why is this bad?
/// There are several requirements that you must follow when defining a generic class.
+ /// Many of these result in `TypeError` being raised at runtime if they are violated.
///
/// ## Examples
/// ```python
- /// from typing import Generic, TypeVar
+ /// from typing_extensions import Generic, TypeVar
///
- /// T = TypeVar("T") # okay
+ /// T = TypeVar("T")
+ /// U = TypeVar("U", default=int)
///
/// # error: class uses both PEP-695 syntax and legacy syntax
/// class C[U](Generic[T]): ...
+ ///
+ /// # error: type parameter with default comes before type parameter without default
+ /// class D(Generic[U, T]): ...
/// ```
///
/// ## References
@@ -988,33 +992,6 @@ declare_lint! {
}
}
-declare_lint! {
- /// ## What it does
- /// Checks for type parameters without defaults that come after type parameters with defaults.
- ///
- /// ## Why is this bad?
- /// Type parameters without defaults must come before type parameters with defaults.
- ///
- /// ## Example
- ///
- /// ```python
- /// from typing import Generic, TypeVar
- ///
- /// T = TypeVar("T")
- /// U = TypeVar("U")
- /// # Error: T has no default but comes after U which has a default
- /// class Foo(Generic[U = int, T]): ...
- /// ```
- ///
- /// ## References
- /// - [PEP 696: Type defaults for type parameters](https://peps.python.org/pep-0696/)
- pub(crate) static INVALID_TYPE_PARAM_ORDER = {
- summary: "detects invalid type parameter order",
- status: LintStatus::stable("0.0.1-alpha.1"),
- default_level: Level::Error,
- }
-}
-
declare_lint! {
/// ## What it does
/// Checks for the creation of invalid `NewType`s
@@ -3726,14 +3703,84 @@ pub(crate) fn report_cannot_pop_required_field_on_typed_dict<'db>(
pub(crate) fn report_invalid_type_param_order<'db>(
context: &InferContext<'db, '_>,
class: ClassLiteral<'db>,
- name: &str,
+ node: &ast::StmtClassDef,
+ typevar_with_default: TypeVarInstance<'db>,
+ invalid_later_typevars: &[TypeVarInstance<'db>],
) {
- if let Some(builder) =
- context.report_lint(&INVALID_TYPE_PARAM_ORDER, class.header_range(context.db()))
- {
- builder.into_diagnostic(format_args!(
- "Type parameter {name} without a default follows type parameter with a default",
+ let db = context.db();
+
+ let base_index = class
+ .explicit_bases(db)
+ .iter()
+ .position(|base| {
+ matches!(
+ base,
+ Type::KnownInstance(
+ KnownInstanceType::SubscriptedProtocol(_)
+ | KnownInstanceType::SubscriptedGeneric(_)
+ )
+ )
+ })
+ .expect(
+ "It should not be possible for a class to have a legacy generic context \
+ if it does not inherit from `Protocol[]` or `Generic[]`",
+ );
+
+ let base_node = &node.bases()[base_index];
+
+ let primary_diagnostic_range = base_node
+ .as_subscript_expr()
+ .map(|subscript| &*subscript.slice)
+ .unwrap_or(base_node)
+ .range();
+
+ let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, primary_diagnostic_range)
+ else {
+ return;
+ };
+
+ let mut diagnostic = builder.into_diagnostic(
+ "Type parameters without defaults cannot follow type parameters with defaults",
+ );
+
+ diagnostic.set_concise_message(format_args!(
+ "Type parameter `{}` without a default cannot follow earlier parameter `{}` with a default",
+ invalid_later_typevars[0].name(db),
+ typevar_with_default.name(db),
+ ));
+
+ if let [single_typevar] = invalid_later_typevars {
+ diagnostic.set_primary_message(format_args!(
+ "Type variable `{}` does not have a default",
+ single_typevar.name(db),
));
+ } else {
+ let later_typevars =
+ format_enumeration(invalid_later_typevars.iter().map(|tv| tv.name(db)));
+ diagnostic.set_primary_message(format_args!(
+ "Type variables {later_typevars} do not have defaults",
+ ));
+ }
+
+ diagnostic.annotate(
+ Annotation::primary(Span::from(context.file()).with_range(primary_diagnostic_range))
+ .message(format_args!(
+ "Earlier TypeVar `{}` does",
+ typevar_with_default.name(db)
+ )),
+ );
+
+ for tvar in [typevar_with_default, invalid_later_typevars[0]] {
+ let Some(definition) = tvar.definition(db) else {
+ continue;
+ };
+ let file = definition.file(db);
+ diagnostic.annotate(
+ Annotation::secondary(Span::from(
+ definition.full_range(db, &parsed_module(db, file).load(db)),
+ ))
+ .message(format_args!("`{}` defined here", tvar.name(db))),
+ );
}
}
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index b502f2f474..723e89519c 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -62,7 +62,7 @@ use crate::types::diagnostic::{
INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY,
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_PARAM_ORDER,
+ 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,
@@ -949,44 +949,62 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- if self.context.is_lint_enabled(&INVALID_TYPE_PARAM_ORDER) {
- if let Some(generic_context) = class.generic_context(self.db()) {
- let mut seen_default = false;
+ if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS) {
+ if !class.has_pep_695_type_params(self.db())
+ && let Some(generic_context) = class.legacy_generic_context(self.db())
+ {
+ struct State<'db> {
+ typevar_with_default: TypeVarInstance<'db>,
+ invalid_later_tvars: Vec>,
+ }
+
+ let mut state: Option> = None;
for bound_typevar in generic_context.variables(self.db()) {
let typevar = bound_typevar.typevar(self.db());
let has_default = typevar.default_type(self.db()).is_some();
- if seen_default && !has_default {
- report_invalid_type_param_order(
- &self.context,
- class,
- typevar.name(self.db()).as_str(),
- );
- }
- if has_default {
- seen_default = true;
+ if let Some(state) = state.as_mut() {
+ if !has_default {
+ state.invalid_later_tvars.push(typevar);
+ }
+ } else if has_default {
+ state = Some(State {
+ typevar_with_default: typevar,
+ invalid_later_tvars: vec![],
+ });
}
}
+
+ if let Some(state) = state
+ && !state.invalid_later_tvars.is_empty()
+ {
+ report_invalid_type_param_order(
+ &self.context,
+ class,
+ class_node,
+ state.typevar_with_default,
+ &state.invalid_later_tvars,
+ );
+ }
}
- }
- let scope = class.body_scope(self.db()).scope(self.db());
- if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS)
- && let Some(parent) = scope.parent()
- {
- for self_typevar in class.typevars_referenced_in_definition(self.db()) {
- let self_typevar_name = self_typevar.typevar(self.db()).name(self.db());
- for enclosing in enclosing_generic_contexts(self.db(), self.index, parent) {
- if let Some(other_typevar) =
- enclosing.binds_named_typevar(self.db(), self_typevar_name)
- {
- report_rebound_typevar(
- &self.context,
- self_typevar_name,
- class,
- class_node,
- other_typevar,
- );
+
+ let scope = class.body_scope(self.db()).scope(self.db());
+ if let Some(parent) = scope.parent() {
+ for self_typevar in class.typevars_referenced_in_definition(self.db()) {
+ let self_typevar_name = self_typevar.typevar(self.db()).name(self.db());
+ for enclosing in enclosing_generic_contexts(self.db(), self.index, parent) {
+ if let Some(other_typevar) =
+ enclosing.binds_named_typevar(self.db(), self_typevar_name)
+ {
+ report_rebound_typevar(
+ &self.context,
+ self_typevar_name,
+ class,
+ class_node,
+ other_typevar,
+ );
+ }
}
}
}