diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 1788d7b7558a1..00eb3841c08ac 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -8,7 +8,7 @@
Default level: error ·
Added in 0.0.13 ·
Related issues ·
-View source
+View source
@@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method`
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol):
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -157,7 +157,7 @@ def test(): -> "int":
Default level: error ·
Preview (since 0.0.16) ·
Related issues ·
-View source
+View source
@@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.7 ·
Related issues ·
-View source
+View source
@@ -261,7 +261,7 @@ def f(x: object):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -293,7 +293,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -324,7 +324,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -356,7 +356,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -388,7 +388,7 @@ class B(A): ...
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -416,7 +416,7 @@ type B = A
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -448,7 +448,7 @@ class Example:
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -504,7 +504,7 @@ false positives it can produce.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -531,7 +531,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -569,7 +569,7 @@ class A: # Crash at runtime
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -640,7 +640,7 @@ def foo() -> "intt\b": ...
Default level: error ·
Added in 0.0.20 ·
Related issues ·
-View source
+View source
@@ -672,7 +672,7 @@ def my_function() -> int:
Default level: error ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -798,7 +798,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -828,7 +828,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -854,7 +854,7 @@ t[3] # IndexError: tuple index out of range
Default level: warn ·
Added in 0.0.1-alpha.33 ·
Related issues ·
-View source
+View source
@@ -888,7 +888,7 @@ class MyClass: ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -977,7 +977,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1004,7 +1004,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1032,7 +1032,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1066,7 +1066,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
@@ -1102,7 +1102,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1126,7 +1126,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1153,7 +1153,7 @@ with 1:
Default level: error ·
Added in 0.0.12 ·
Related issues ·
-View source
+View source
@@ -1190,7 +1190,7 @@ class Foo(NamedTuple):
Default level: error ·
Added in 0.0.13 ·
Related issues ·
-View source
+View source
@@ -1222,7 +1222,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1251,7 +1251,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1295,7 +1295,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -1337,7 +1337,7 @@ class D(A):
Default level: error ·
Added in 0.0.1-alpha.35 ·
Related issues ·
-View source
+View source
@@ -1381,7 +1381,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1419,7 +1419,7 @@ class D(Generic[U, T]): ...
Default level: error ·
Added in 0.0.12 ·
Related issues ·
-View source
+View source
@@ -1498,7 +1498,7 @@ a = 20 / 0 # type: ignore
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -1537,7 +1537,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: warn ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -1598,7 +1598,7 @@ def f(x, y, /): # Python 3.8+ syntax
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1633,7 +1633,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.18 ·
Related issues ·
-View source
+View source
@@ -1661,7 +1661,7 @@ match x:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1695,7 +1695,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1802,7 +1802,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
@@ -1856,7 +1856,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: error ·
Added in 0.0.1-alpha.27 ·
Related issues ·
-View source
+View source
@@ -1886,7 +1886,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
@@ -1936,7 +1936,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1962,7 +1962,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1993,7 +1993,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
@@ -2027,7 +2027,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
@@ -2076,7 +2076,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2105,7 +2105,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2201,7 +2201,7 @@ class C: ...
Default level: error ·
Added in 0.0.10 ·
Related issues ·
-View source
+View source
@@ -2247,7 +2247,7 @@ class MyClass:
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -2274,7 +2274,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
@@ -2321,7 +2321,7 @@ Bar[int] # error: too few arguments
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2351,7 +2351,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2381,7 +2381,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
@@ -2415,7 +2415,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -2449,7 +2449,7 @@ class C:
Default level: error ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -2480,7 +2480,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2527,7 +2527,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type
Default level: error ·
Added in 0.0.16 ·
Related issues ·
-View source
+View source
@@ -2559,7 +2559,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -2594,7 +2594,7 @@ def f(x: dict):
Default level: error ·
Added in 0.0.9 ·
Related issues ·
-View source
+View source
@@ -2625,7 +2625,7 @@ class Foo(TypedDict):
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -2680,7 +2680,7 @@ def h(arg2: type):
Default level: error ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -2723,7 +2723,7 @@ def g(arg: object):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2748,7 +2748,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
@@ -2781,7 +2781,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2810,7 +2810,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2836,7 +2836,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2860,7 +2860,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -2893,7 +2893,7 @@ class B(A):
Default level: error ·
Added in 0.0.16 ·
Related issues ·
-View source
+View source
@@ -2926,7 +2926,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2953,7 +2953,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2980,7 +2980,7 @@ f(x=1) # Error raised here
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3008,7 +3008,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
@@ -3040,7 +3040,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: ignore ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3077,7 +3077,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3141,7 +3141,7 @@ def test(): -> "int":
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3168,7 +3168,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.18 ·
Related issues ·
-View source
+View source
@@ -3200,7 +3200,7 @@ class C:
Default level: error ·
Added in 0.0.20 ·
Related issues ·
-View source
+View source
@@ -3234,7 +3234,7 @@ class Outer[T]:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3264,7 +3264,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
@@ -3293,7 +3293,7 @@ class B(A): ... # Error raised here
Default level: error ·
Added in 0.0.1-alpha.30 ·
Related issues ·
-View source
+View source
@@ -3327,7 +3327,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3354,7 +3354,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3382,7 +3382,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3422,13 +3422,50 @@ class A:
- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)
+## `unbound-type-variable`
+
+
+Default level: error ·
+Added in 0.0.20 ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Checks for type variables that are used in a scope where they are not bound
+to any enclosing generic context.
+
+**Why is this bad?**
+
+Using a type variable outside of a scope that binds it has no well-defined meaning.
+
+**Examples**
+
+```python
+from typing import TypeVar, Generic
+
+T = TypeVar("T")
+S = TypeVar("S")
+
+x: T # error: unbound type variable in module scope
+
+class C(Generic[T]):
+ x: list[S] = [] # error: S is not in this class's generic context
+```
+
+**References**
+
+- [Typing spec: Scoping rules for type variables](https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables)
+
## `undefined-reveal`
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3452,7 +3489,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3479,7 +3516,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
@@ -3507,7 +3544,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -3565,7 +3602,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3590,7 +3627,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3615,7 +3652,7 @@ print(x) # NameError: name 'x' is not defined
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -3654,7 +3691,7 @@ class D(C): ... # error: [unsupported-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3691,7 +3728,7 @@ b1 < b2 < b1 # exception raised here
Default level: ignore ·
Added in 0.0.12 ·
Related issues ·
-View source
+View source
@@ -3732,7 +3769,7 @@ def factory(base: type[Base]) -> type:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3833,7 +3870,7 @@ to `false`.
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3896,7 +3933,7 @@ def foo(x: int | str) -> int | str:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
index 20a6240e4d5d2..786c614fb6a56 100644
--- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
+++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
@@ -109,6 +109,7 @@ from typing_extensions import Self, TypeAlias, TypeVar
T = TypeVar("T")
# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter"
+# error: [unbound-type-variable]
X: TypeAlias[T] = int
class Foo[T]:
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md
index 033452937f671..5a3140926d70d 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md
@@ -132,6 +132,7 @@ def _(x: ProtoInt[int]):
# TODO: TypedDict is just a function object at runtime, we should emit an error
class LegacyDict(TypedDict[T]):
+ # error: [unbound-type-variable]
x: T
type LegacyDictInt = LegacyDict[int]
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md
index 21eca20740bd0..03e007681916d 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md
@@ -17,15 +17,15 @@ from typing import TypeVar
T = TypeVar("T")
-# TODO: error
+# error: [unbound-type-variable]
x: T
class C:
- # TODO: error
+ # error: [unbound-type-variable]
x: T
def f() -> None:
- # TODO: error
+ # error: [unbound-type-variable]
x: T
```
@@ -186,11 +186,11 @@ S = TypeVar("S")
def f(x: T) -> None:
x: list[T] = []
- # TODO: invalid-assignment error
+ # error: [unbound-type-variable]
y: list[S] = []
class C(Generic[T]):
- # TODO: error: cannot use S if it's not in the current generic context
+ # error: [unbound-type-variable]
x: list[S] = []
# This is not an error, as shown in the previous test
@@ -210,11 +210,11 @@ S = TypeVar("S")
def f[T](x: T) -> None:
x: list[T] = []
- # TODO: invalid assignment error
+ # error: [unbound-type-variable]
y: list[S] = []
class C[T]:
- # TODO: error: cannot use S if it's not in the current generic context
+ # error: [unbound-type-variable]
x: list[S] = []
def m1(self, x: S) -> S:
@@ -224,6 +224,44 @@ class C[T]:
return x
```
+## Should `Callable` annotations create an implicit generic context?
+
+There is disagreement among type checkers around how to handle this case. For now, we do not emit an
+error on the following snippet, but we may change this in the future.
+
+```py
+from typing import TypeVar, Callable
+from ty_extensions import generic_context
+
+T = TypeVar("T")
+
+x: Callable[[T], T] = lambda obj: obj
+
+# TODO: if we decide that `Callable` annotations always create an implicit generic context,
+# all of these revealed types and `invalid-argument-type` diagnostics are incorrect.
+# If we decide that they do not, we should emit `unbound-type-variable` on both the
+# declaration of `x` in the global scope and the parameter annotation of `y`.
+#
+# NOTE: all the `reveal_type`s are inside a function here so that we test the behaviour
+# of the declared type (from the annotation) rather than the local inferred type
+def test(y: Callable[[T], T]):
+ # revealed: None
+ reveal_type(generic_context(x))
+ # revealed: (TypeVar, /) -> TypeVar
+ reveal_type(x)
+ # error: [invalid-argument-type]
+ # revealed: TypeVar
+ reveal_type(x(42))
+
+ # revealed: None
+ reveal_type(generic_context(y))
+ # revealed: (T@test, /) -> T@test
+ reveal_type(y)
+ # error: [invalid-argument-type]
+ # revealed: T@test
+ reveal_type(y(42))
+```
+
## Nested formal typevars must be distinct
Generic functions and classes can be nested in each other, but it is an error for the same typevar
@@ -365,7 +403,7 @@ class C[T]:
ok1: list[T] = []
class Bad:
- # TODO: error: cannot refer to T in nested scope
+ # error: [unbound-type-variable]
bad: list[T] = []
class Inner[S]: ...
diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
index 3d687c73bd74a..51eb51d0d8a54 100644
--- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
+++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
@@ -709,6 +709,7 @@ def _(doubly_specialized: ProtoInt[int]):
# TODO: TypedDict is just a function object at runtime, we should emit an error
class LegacyDict(TypedDict[T]):
+ # error: [unbound-type-variable]
x: T
# TODO: should be a `not-subscriptable` error
@@ -784,7 +785,13 @@ def _(
Similarly, if you try to specialize a union type without a binding context, we emit an error:
```py
+from typing import TypeVar
+
+T = TypeVar("T")
+
# error: [not-subscriptable] "Cannot subscript non-generic type"
+# error: [unbound-type-variable]
+# error: [unbound-type-variable]
x: (list[T] | set[T])[int]
def _():
diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md
index cee9b8eec3c79..0dd52379f8de1 100644
--- a/crates/ty_python_semantic/resources/mdtest/protocols.md
+++ b/crates/ty_python_semantic/resources/mdtest/protocols.md
@@ -3208,6 +3208,8 @@ S = TypeVar("S")
class Bar(Protocol[S]):
def x(self) -> "S | Bar[S]": ...
+# error: [unbound-type-variable]
+# error: [unbound-type-variable]
z: S | Bar[S]
```
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index debd3775a7c91..ae3ee181c4cc8 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -106,6 +106,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
registry.register_lint(&INVALID_TYPE_VARIABLE_BOUND);
registry.register_lint(&INVALID_TYPE_VARIABLE_DEFAULT);
+ registry.register_lint(&UNBOUND_TYPE_VARIABLE);
registry.register_lint(&MISSING_ARGUMENT);
registry.register_lint(&NO_MATCHING_OVERLOAD);
registry.register_lint(&NOT_SUBSCRIPTABLE);
@@ -1808,6 +1809,36 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Checks for type variables that are used in a scope where they are not bound
+ /// to any enclosing generic context.
+ ///
+ /// ## Why is this bad?
+ /// Using a type variable outside of a scope that binds it has no well-defined meaning.
+ ///
+ /// ## Examples
+ /// ```python
+ /// from typing import TypeVar, Generic
+ ///
+ /// T = TypeVar("T")
+ /// S = TypeVar("S")
+ ///
+ /// x: T # error: unbound type variable in module scope
+ ///
+ /// class C(Generic[T]):
+ /// x: list[S] = [] # error: S is not in this class's generic context
+ /// ```
+ ///
+ /// ## References
+ /// - [Typing spec: Scoping rules for type variables](https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables)
+ pub(crate) static UNBOUND_TYPE_VARIABLE = {
+ summary: "detects type variables used outside of their bound scope",
+ status: LintStatus::stable("0.0.20"),
+ 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/generics.rs b/crates/ty_python_semantic/src/types/generics.rs
index 4bde75f353e4e..fd813063e15bc 100644
--- a/crates/ty_python_semantic/src/types/generics.rs
+++ b/crates/ty_python_semantic/src/types/generics.rs
@@ -95,13 +95,25 @@ pub(crate) fn bind_typevar<'db>(
return Some(typevar.with_binding_context(db, definition));
}
}
- enclosing_generic_contexts(db, index, containing_scope)
- .find_map(|enclosing_context| enclosing_context.binds_typevar(db, typevar))
- .or_else(|| {
- typevar_binding_context.map(|typevar_binding_context| {
- typevar.with_binding_context(db, typevar_binding_context)
- })
- })
+ // Walk ancestor scopes, tracking whether we've crossed a class scope boundary.
+ // Class-scoped type variables are not visible from inner class scopes.
+ let mut crossed_class_scope = false;
+ for (_, ancestor_scope) in index.ancestor_scopes(containing_scope) {
+ let is_class_scope = ancestor_scope.kind().is_class();
+ // If we've already crossed a class boundary, skip class-scoped generic contexts.
+ // This prevents inner classes from accessing type parameters of outer classes.
+ if (!is_class_scope || !crossed_class_scope)
+ && let Some(generic_context) = ancestor_scope.node().generic_context(db, index)
+ && let Some(bound) = generic_context.binds_typevar(db, typevar)
+ {
+ return Some(bound);
+ }
+ if is_class_scope {
+ crossed_class_scope = true;
+ }
+ }
+ typevar_binding_context
+ .map(|typevar_binding_context| typevar.with_binding_context(db, typevar_binding_context))
}
/// Create a `typing.Self` type variable for a given class.
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 6754de4c9538c..3769122e88dea 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -297,6 +297,12 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> {
/// Whether we are in a context that binds unbound typevars.
typevar_binding_context: Option>,
+ /// Whether to check for unbound type variables in type expressions.
+ /// This is set to `true` when processing annotation expressions, where unbound type variables
+ /// are an error. It is `false` in other contexts (e.g., `TypeVar` defaults, explicit class
+ /// specialization) where unbound type variables are expected.
+ check_unbound_typevars: bool,
+
/// The deferred state of inferring types of certain expressions within the region.
///
/// This is different from [`InferenceRegion::Deferred`] which works on the entire definition
@@ -363,6 +369,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
bindings: VecMap::default(),
declarations: VecMap::default(),
typevar_binding_context: None,
+ check_unbound_typevars: false,
deferred: VecSet::default(),
undecorated_type: None,
cycle_recovery: None,
@@ -14603,6 +14610,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// builder only state
typevar_binding_context: _,
+ check_unbound_typevars: _,
deferred_state: _,
multi_inference_state: _,
inner_expression_inference_state: _,
@@ -14672,6 +14680,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
dataclass_field_specifiers: _,
all_definitely_bound: _,
typevar_binding_context: _,
+ check_unbound_typevars: _,
deferred_state: _,
inferring_vararg_annotation: _,
multi_inference_state: _,
@@ -14754,6 +14763,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
dataclass_field_specifiers: _,
all_definitely_bound: _,
typevar_binding_context: _,
+ check_unbound_typevars: _,
deferred_state: _,
multi_inference_state: _,
inner_expression_inference_state: _,
diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs
index 9d3a807a36b5b..148778d1272ab 100644
--- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs
@@ -71,7 +71,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
};
let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state);
+ let previous_check_unbound_typevars =
+ std::mem::replace(&mut self.check_unbound_typevars, true);
let annotation_ty = self.infer_annotation_expression_impl(annotation, pep_613_policy);
+ self.check_unbound_typevars = previous_check_unbound_typevars;
self.deferred_state = previous_deferred_state;
annotation_ty
}
@@ -134,21 +137,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
};
special_case.unwrap_or_else(|| {
- TypeAndQualifiers::declared(
- ty.default_specialize(builder.db())
- .in_type_expression(
- builder.db(),
- builder.scope(),
- builder.typevar_binding_context,
+ let result_ty = ty
+ .default_specialize(builder.db())
+ .in_type_expression(
+ builder.db(),
+ builder.scope(),
+ builder.typevar_binding_context,
+ )
+ .unwrap_or_else(|error| {
+ error.into_fallback_type(
+ &builder.context,
+ annotation,
+ builder.is_reachable(annotation),
)
- .unwrap_or_else(|error| {
- error.into_fallback_type(
- &builder.context,
- annotation,
- builder.is_reachable(annotation),
- )
- }),
- )
+ });
+ let result_ty = builder.check_for_unbound_type_variable(annotation, result_ty);
+ TypeAndQualifiers::declared(result_ty)
})
}
diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
index e593b3ea275e0..f55511381ada3 100644
--- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
@@ -5,8 +5,8 @@ use super::{DeferredExpressionState, TypeInferenceBuilder};
use crate::FxOrderSet;
use crate::semantic_index::semantic_index;
use crate::types::diagnostic::{
- self, INVALID_TYPE_FORM, NOT_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form,
- report_invalid_arguments_to_callable,
+ self, INVALID_TYPE_FORM, NOT_SUBSCRIPTABLE, UNBOUND_TYPE_VARIABLE,
+ report_invalid_argument_number_to_special_form, report_invalid_arguments_to_callable,
};
use crate::types::generics::bind_typevar;
use crate::types::infer::builder::InnerExpressionInferenceState;
@@ -82,17 +82,20 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
// https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-type_expression
match expression {
ast::Expr::Name(name) => match name.ctx {
- ast::ExprContext::Load => self
- .infer_name_expression(name)
- .default_specialize(self.db())
- .in_type_expression(self.db(), self.scope(), self.typevar_binding_context)
- .unwrap_or_else(|error| {
- error.into_fallback_type(
- &self.context,
- expression,
- self.is_reachable(expression),
- )
- }),
+ ast::ExprContext::Load => {
+ let ty = self
+ .infer_name_expression(name)
+ .default_specialize(self.db())
+ .in_type_expression(self.db(), self.scope(), self.typevar_binding_context)
+ .unwrap_or_else(|error| {
+ error.into_fallback_type(
+ &self.context,
+ expression,
+ self.is_reachable(expression),
+ )
+ });
+ self.check_for_unbound_type_variable(expression, ty)
+ }
ast::ExprContext::Invalid => Type::unknown(),
ast::ExprContext::Store | ast::ExprContext::Del => {
todo_type!("Name expression annotation in Store/Del context")
@@ -1285,81 +1288,99 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
/// Infer the type of a `Callable[...]` type expression.
pub(crate) fn infer_callable_type(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> {
- let db = self.db();
+ fn inner<'db>(
+ builder: &mut TypeInferenceBuilder<'db, '_>,
+ subscript: &ast::ExprSubscript,
+ ) -> Type<'db> {
+ let db = builder.db();
- let arguments_slice = &*subscript.slice;
+ let arguments_slice = &*subscript.slice;
- let mut arguments = match arguments_slice {
- ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()),
- _ => {
- self.infer_callable_parameter_types(arguments_slice);
- Either::Right(std::iter::empty::<&ast::Expr>())
- }
- };
+ let mut arguments = match arguments_slice {
+ ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()),
+ _ => {
+ builder.infer_callable_parameter_types(arguments_slice);
+ Either::Right(std::iter::empty::<&ast::Expr>())
+ }
+ };
- let first_argument = arguments.next();
+ let first_argument = arguments.next();
- let parameters = first_argument.and_then(|arg| self.infer_callable_parameter_types(arg));
+ let parameters =
+ first_argument.and_then(|arg| builder.infer_callable_parameter_types(arg));
- let return_type = arguments.next().map(|arg| self.infer_type_expression(arg));
+ let return_type = arguments
+ .next()
+ .map(|arg| builder.infer_type_expression(arg));
- let callable_type = if parameters.is_none()
- && let Some(first_argument) = first_argument
- && let ast::Expr::List(list) = first_argument
- && let [single_param] = &list.elts[..]
- && single_param.is_ellipsis_literal_expr()
- {
- self.store_expression_type(single_param, Type::unknown());
- if let Some(mut diagnostic) = self.report_invalid_type_expression(
- first_argument,
- "`[...]` is not a valid parameter list for `Callable`",
- ) {
- if let Some(returns) = return_type {
- diagnostic.set_primary_message(format_args!(
- "Did you mean `Callable[..., {}]`?",
- returns.display(db)
- ));
- }
- }
- Type::single_callable(
- db,
- Signature::new(
- Parameters::unknown(),
- return_type.unwrap_or_else(Type::unknown),
- ),
- )
- } else {
- let correct_argument_number = if let Some(third_argument) = arguments.next() {
- self.infer_type_expression(third_argument);
- for argument in arguments {
- self.infer_type_expression(argument);
+ let callable_type = if parameters.is_none()
+ && let Some(first_argument) = first_argument
+ && let ast::Expr::List(list) = first_argument
+ && let [single_param] = &list.elts[..]
+ && single_param.is_ellipsis_literal_expr()
+ {
+ builder.store_expression_type(single_param, Type::unknown());
+ if let Some(mut diagnostic) = builder.report_invalid_type_expression(
+ first_argument,
+ "`[...]` is not a valid parameter list for `Callable`",
+ ) {
+ if let Some(returns) = return_type {
+ diagnostic.set_primary_message(format_args!(
+ "Did you mean `Callable[..., {}]`?",
+ returns.display(db)
+ ));
+ }
}
- false
+ Type::single_callable(
+ db,
+ Signature::new(
+ Parameters::unknown(),
+ return_type.unwrap_or_else(Type::unknown),
+ ),
+ )
} else {
- return_type.is_some()
- };
+ let correct_argument_number = if let Some(third_argument) = arguments.next() {
+ builder.infer_type_expression(third_argument);
+ for argument in arguments {
+ builder.infer_type_expression(argument);
+ }
+ false
+ } else {
+ return_type.is_some()
+ };
- if !correct_argument_number {
- report_invalid_arguments_to_callable(&self.context, subscript);
- }
+ if !correct_argument_number {
+ report_invalid_arguments_to_callable(&builder.context, subscript);
+ }
- if correct_argument_number
- && let (Some(parameters), Some(return_type)) = (parameters, return_type)
- {
- Type::single_callable(db, Signature::new(parameters, return_type))
- } else {
- Type::Callable(CallableType::unknown(db))
+ if correct_argument_number
+ && let (Some(parameters), Some(return_type)) = (parameters, return_type)
+ {
+ Type::single_callable(db, Signature::new(parameters, return_type))
+ } else {
+ Type::Callable(CallableType::unknown(db))
+ }
+ };
+
+ // `Signature` / `Parameters` are not a `Type` variant, so we're storing
+ // the outer callable type on these expressions instead.
+ builder.store_expression_type(arguments_slice, callable_type);
+ if let Some(first_argument) = first_argument {
+ builder.store_expression_type(first_argument, callable_type);
}
- };
- // `Signature` / `Parameters` are not a `Type` variant, so we're storing
- // the outer callable type on these expressions instead.
- self.store_expression_type(arguments_slice, callable_type);
- if let Some(first_argument) = first_argument {
- self.store_expression_type(first_argument, callable_type);
+ callable_type
}
- callable_type
+ // There is disagreement among type checkers about whether `Callable` annotations
+ // in the global scope or similar should be considered to create an implicit generic context.
+ // For now, we do not report unbound type variables in any `Callable` contexts, but we may
+ // decide to revisit this in the future.
+ let previous_check_unbound_typevars =
+ std::mem::replace(&mut self.check_unbound_typevars, false);
+ let result = inner(self, subscript);
+ self.check_unbound_typevars = previous_check_unbound_typevars;
+ result
}
fn infer_parameterized_special_form_type_expression(
@@ -1989,4 +2010,29 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
None
}
+
+ /// Checks if the inferred type is an unbound type variable and reports a diagnostic if so.
+ ///
+ /// Returns `Unknown` as a fallback if the type variable is unbound, otherwise returns the
+ /// original type unchanged.
+ pub(super) fn check_for_unbound_type_variable(
+ &self,
+ expression: &ast::Expr,
+ ty: Type<'db>,
+ ) -> Type<'db> {
+ if !self.check_unbound_typevars {
+ return ty;
+ }
+ if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty {
+ if let Some(builder) = self.context.report_lint(&UNBOUND_TYPE_VARIABLE, expression) {
+ builder.into_diagnostic(format_args!(
+ "Type variable `{name}` is not bound to any outer generic context",
+ name = typevar.name(self.db())
+ ));
+ }
+ Type::unknown()
+ } else {
+ ty
+ }
+ }
}
diff --git a/ty.schema.json b/ty.schema.json
index 67193ebf9b9d1..f3204fc366ccc 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -1325,6 +1325,16 @@
}
]
},
+ "unbound-type-variable": {
+ "title": "detects type variables used outside of their bound scope",
+ "description": "## What it does\nChecks for type variables that are used in a scope where they are not bound\nto any enclosing generic context.\n\n## Why is this bad?\nUsing a type variable outside of a scope that binds it has no well-defined meaning.\n\n## Examples\n```python\nfrom typing import TypeVar, Generic\n\nT = TypeVar(\"T\")\nS = TypeVar(\"S\")\n\nx: T # error: unbound type variable in module scope\n\nclass C(Generic[T]):\n x: list[S] = [] # error: S is not in this class's generic context\n```\n\n## References\n- [Typing spec: Scoping rules for type variables](https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables)",
+ "default": "error",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"undefined-reveal": {
"title": "detects usages of `reveal_type` without importing it",
"description": "## What it does\nChecks for calls to `reveal_type` without importing it.\n\n## Why is this bad?\nUsing `reveal_type` without importing it will raise a `NameError` at runtime.\n\n## Examples\n```python\nreveal_type(1) # NameError: name 'reveal_type' is not defined\n```",