From e59740eca35cf85c78e3480d7f0fa9136ef51b62 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama Date: Sat, 13 Dec 2025 16:52:19 +0900 Subject: [PATCH] [ty] disallow type variables within type variable bounds/constraints --- crates/ty/docs/rules.md | 192 +++++++++------- .../mdtest/generics/legacy/variables.md | 8 +- .../mdtest/generics/pep695/classes.md | 4 +- .../mdtest/generics/pep695/variables.md | 18 +- ...ounds_and_constrain…_(1c0a5d9209a531cd).snap | 124 ++++++++++ ...nds_and_constrain…_(c7a923bb701940f3).snap | 216 ++++++++++++++++++ .../src/types/diagnostic.rs | 37 ++- .../src/types/infer/builder.rs | 144 +++++++++++- .../e2e__commands__debug_command.snap | 1 + ty.schema.json | 12 +- 10 files changed, 658 insertions(+), 98 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable…_-_Cycles_-_Bounds_and_constrain…_(1c0a5d9209a531cd).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_PEP_695_Generics_-_Cycles_-_Bounds_and_constrain…_(c7a923bb701940f3).snap 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": [ {