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```",