diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index f50105fe48..c42f0d0d1a 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -308,26 +308,8 @@ mod tests { "#, ); - assert_snapshot!(test.goto_type_definition(), @r#" - info[goto-type-definition]: Type definition - --> main.py:4:1 - | - 2 | from typing_extensions import TypeAliasType - 3 | - 4 | Alias = TypeAliasType("Alias", tuple[int, int]) - | ^^^^^ - 5 | - 6 | Alias - | - info: Source - --> main.py:6:1 - | - 4 | Alias = TypeAliasType("Alias", tuple[int, int]) - 5 | - 6 | Alias - | ^^^^^ - | - "#); + // TODO: This should jump to the definition of `Alias` above. + assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); } #[test] diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md new file mode 100644 index 0000000000..02661d5cc0 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md @@ -0,0 +1,139 @@ +# Legacy typevar creation diagnostics + +The full tests for these features are in `generics/legacy/variables.md`. + + + +## Must have a name + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar() +``` + +## Name can't be given more than once + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", name="T") +``` + +## Must be directly assigned to a variable + +> A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as +> part of a larger expression). + +```py +from typing import TypeVar + +T = TypeVar("T") +# error: [invalid-legacy-type-variable] +U: TypeVar = TypeVar("U") + +# error: [invalid-legacy-type-variable] +tuple_with_typevar = ("foo", TypeVar("W")) +``` + +## `TypeVar` parameter must match variable name + +> The argument to `TypeVar()` must be a string equal to the variable name to which it is assigned. + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("Q") +``` + +## No variadic arguments + +```py +from typing import TypeVar + +types = (int, str) + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", *types) + +# error: [invalid-legacy-type-variable] +S = TypeVar("S", **{"bound": int}) +``` + +## Cannot have only one constraint + +> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should +> be at least two constraints, if any; specifying a single constraint is disallowed. + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", int) +``` + +## Cannot have both bound and constraint + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", int, str, bound=bytes) +``` + +## Cannot be both covariant and contravariant + +> To facilitate the declaration of container types where covariant or contravariant type checking is +> acceptable, type variables accept keyword arguments `covariant=True` or `contravariant=True`. At +> most one of these may be passed. + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", covariant=True, contravariant=True) +``` + +## Boolean parameters must be unambiguous + +```py +from typing_extensions import TypeVar + +def cond() -> bool: + return True + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", covariant=cond()) + +# error: [invalid-legacy-type-variable] +U = TypeVar("U", contravariant=cond()) + +# error: [invalid-legacy-type-variable] +V = TypeVar("V", infer_variance=cond()) +``` + +## Invalid keyword arguments + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", invalid_keyword=True) +``` + +## Invalid feature for this Python version + +```toml +[environment] +python-version = "3.10" +``` + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", default=int) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 9ca51b060b..84f89e1811 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -108,7 +108,7 @@ reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTyp The type parameter can be specified explicitly: ```py -from typing import Generic, Literal, TypeVar +from typing_extensions import Generic, Literal, TypeVar T = TypeVar("T") @@ -195,7 +195,7 @@ reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int] We can infer the type parameter from a type context: ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") @@ -240,7 +240,7 @@ consistent with each other. ### `__new__` only ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") @@ -257,7 +257,7 @@ wrong_innards: C[int] = C("five") ### `__init__` only ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") @@ -273,7 +273,7 @@ wrong_innards: C[int] = C("five") ### Identical `__new__` and `__init__` signatures ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") @@ -292,7 +292,7 @@ wrong_innards: C[int] = C("five") ### Compatible `__new__` and `__init__` signatures ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") @@ -325,7 +325,7 @@ If either method comes from a generic base class, we don't currently use its inf to specialize the class. ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") U = TypeVar("U") @@ -344,7 +344,7 @@ reveal_type(D(1)) # revealed: D[int] ### Generic class inherits `__init__` from generic base class ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") U = TypeVar("U") @@ -364,7 +364,7 @@ reveal_type(D(1, "str")) # revealed: D[int, str] This is a specific example of the above, since it was reported specifically by a user. ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") U = TypeVar("U") @@ -382,7 +382,7 @@ for `tuple`, so we use a different mechanism to make sure it has the right inher context. But from the user's point of view, this is another example of the above.) ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") U = TypeVar("U") @@ -403,7 +403,7 @@ python-version = "3.11" ``` ```py -from typing import TypeVar, Sequence, Never +from typing_extensions import TypeVar, Sequence, Never T = TypeVar("T") @@ -421,7 +421,7 @@ def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: t ### `__init__` is itself generic ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar S = TypeVar("S") T = TypeVar("T") @@ -440,7 +440,7 @@ wrong_innards: C[int] = C("five", 1) ### Some `__init__` overloads only apply to certain specializations ```py -from typing import overload, Generic, TypeVar +from typing_extensions import overload, Generic, TypeVar T = TypeVar("T") @@ -480,7 +480,7 @@ C[None](12) ```py from dataclasses import dataclass -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") @@ -494,7 +494,7 @@ reveal_type(A(x=1)) # revealed: A[int] ### Class typevar has another typevar as a default ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") U = TypeVar("U", default=T) @@ -515,7 +515,7 @@ When a generic subclass fills its superclass's type parameter with one of its ow propagate through: ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") U = TypeVar("U") @@ -549,7 +549,7 @@ scope for the method. ```py from ty_extensions import generic_context -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") U = TypeVar("U") @@ -581,7 +581,7 @@ In a specialized generic alias, the specialization is applied to the attributes class. ```py -from typing import Generic, TypeVar, Protocol +from typing_extensions import Generic, TypeVar, Protocol T = TypeVar("T") U = TypeVar("U") @@ -639,7 +639,7 @@ reveal_type(d.method3().x) # revealed: int When a method is overloaded, the specialization is applied to all overloads. ```py -from typing import overload, Generic, TypeVar +from typing_extensions import overload, Generic, TypeVar S = TypeVar("S") @@ -667,7 +667,7 @@ A class can use itself as the type parameter of one of its superclasses. (This i Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). ```pyi -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") @@ -682,7 +682,7 @@ reveal_type(Sub) # revealed: A similar case can work in a non-stub file, if forward references are stringified: ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") @@ -697,7 +697,7 @@ reveal_type(Sub) # revealed: In a non-stub file, without stringified forward references, this raises a `NameError`: ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") @@ -710,7 +710,7 @@ class Sub(Base[Sub]): ... ### Cyclic inheritance as a generic parameter ```pyi -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") @@ -722,7 +722,7 @@ class Derived(list[Derived[T]], Generic[T]): ... Inheritance that would result in a cyclic MRO is detected as an error. ```py -from typing import Generic, TypeVar +from typing_extensions import Generic, TypeVar T = TypeVar("T") 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 d34c642bb3..f79cf6f826 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -6,6 +6,8 @@ for both type variable syntaxes. Unless otherwise specified, all quotations come from the [Generics] section of the typing spec. +Diagnostics for invalid type variables are snapshotted in `diagnostics/legacy_typevars.md`. + ## Type variables ### Defining legacy type variables @@ -24,7 +26,16 @@ reveal_type(T) # revealed: typing.TypeVar reveal_type(T.__name__) # revealed: Literal["T"] ``` -### Directly assigned to a variable +The typevar name can also be provided as a keyword argument: + +```py +from typing import TypeVar + +T = TypeVar(name="T") +reveal_type(T.__name__) # revealed: Literal["T"] +``` + +### Must be directly assigned to a variable > A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as > part of a larger expression). @@ -33,13 +44,24 @@ reveal_type(T.__name__) # revealed: Literal["T"] from typing import TypeVar T = TypeVar("T") -# TODO: no error # error: [invalid-legacy-type-variable] U: TypeVar = TypeVar("U") -# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable" -# error: [invalid-type-form] "Function calls are not allowed in type expressions" -TestList = list[TypeVar("W")] +# error: [invalid-legacy-type-variable] +tuple_with_typevar = ("foo", TypeVar("W")) +reveal_type(tuple_with_typevar[1]) # revealed: TypeVar +``` + +```py +from typing_extensions import TypeVar + +T = TypeVar("T") +# error: [invalid-legacy-type-variable] +U: TypeVar = TypeVar("U") + +# error: [invalid-legacy-type-variable] +tuple_with_typevar = ("foo", TypeVar("W")) +reveal_type(tuple_with_typevar[1]) # revealed: TypeVar ``` ### `TypeVar` parameter must match variable name @@ -49,7 +71,7 @@ TestList = list[TypeVar("W")] ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)" +# error: [invalid-legacy-type-variable] T = TypeVar("Q") ``` @@ -66,6 +88,22 @@ T = TypeVar("T") T = TypeVar("T") ``` +### No variadic arguments + +```py +from typing import TypeVar + +types = (int, str) + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", *types) +reveal_type(T) # revealed: TypeVar + +# error: [invalid-legacy-type-variable] +S = TypeVar("S", **{"bound": int}) +reveal_type(S) # revealed: TypeVar +``` + ### Type variables with a default Note that the `__default__` property is only available in Python ≥3.13. @@ -91,6 +129,11 @@ reveal_type(S.__default__) # revealed: NoDefault ### Using other typevars as a default +```toml +[environment] +python-version = "3.13" +``` + ```py from typing import Generic, TypeVar, Union @@ -124,6 +167,15 @@ S = TypeVar("S") reveal_type(S.__bound__) # revealed: None ``` +The upper bound must be a valid type expression: + +```py +from typing import TypedDict + +# error: [invalid-type-form] +T = TypeVar("T", bound=TypedDict) +``` + ### Type variables with constraints ```py @@ -138,6 +190,16 @@ S = TypeVar("S") reveal_type(S.__constraints__) # revealed: tuple[()] ``` +Constraints are not simplified relative to each other, even if one is a subtype of the other: + +```py +T = TypeVar("T", int, bool) +reveal_type(T.__constraints__) # revealed: tuple[int, bool] + +S = TypeVar("S", float, str) +reveal_type(S.__constraints__) # revealed: tuple[int | float, str] +``` + ### Cannot have only one constraint > `TypeVar` supports constraining parametric types to a fixed set of possible types...There should @@ -146,10 +208,19 @@ reveal_type(S.__constraints__) # revealed: tuple[()] ```py from typing import TypeVar -# TODO: error: [invalid-type-variable-constraints] +# error: [invalid-legacy-type-variable] T = TypeVar("T", int) ``` +### Cannot have both bound and constraint + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", int, str, bound=bytes) +``` + ### Cannot be both covariant and contravariant > To facilitate the declaration of container types where covariant or contravariant type checking is @@ -163,10 +234,10 @@ from typing import TypeVar T = TypeVar("T", covariant=True, contravariant=True) ``` -### Variance parameters must be unambiguous +### Boolean parameters must be unambiguous ```py -from typing import TypeVar +from typing_extensions import TypeVar def cond() -> bool: return True @@ -176,6 +247,73 @@ T = TypeVar("T", covariant=cond()) # error: [invalid-legacy-type-variable] U = TypeVar("U", contravariant=cond()) + +# error: [invalid-legacy-type-variable] +V = TypeVar("V", infer_variance=cond()) +``` + +### Invalid keyword arguments + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", invalid_keyword=True) +``` + +```pyi +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", invalid_keyword=True) + +``` + +### Constructor signature versioning + +#### For `typing.TypeVar` + +```toml +[environment] +python-version = "3.10" +``` + +In a stub file, features from the latest supported Python version can be used on any version. +There's no need to require use of `typing_extensions.TypeVar` in a stub file, when the type checker +can understand the typevar definition perfectly well either way, and there can be no runtime error. +(Perhaps it's arguable whether this special case is worth it, but other type checkers do it, so we +maintain compatibility.) + +```pyi +from typing import TypeVar +T = TypeVar("T", default=int) +``` + +But this raises an error in a non-stub file: + +```py +from typing import TypeVar + +# error: [invalid-legacy-type-variable] +T = TypeVar("T", default=int) +``` + +#### For `typing_extensions.TypeVar` + +`typing_extensions.TypeVar` always supports the latest features, on any Python version. + +```toml +[environment] +python-version = "3.10" +``` + +```py +from typing_extensions import TypeVar + +T = TypeVar("T", default=int) +# TODO: should not error, should reveal `int` +# error: [unresolved-attribute] +reveal_type(T.__default__) # revealed: Unknown ``` ## Callability @@ -231,4 +369,96 @@ def constrained(x: T_constrained): reveal_type(type(x)) # revealed: type[int] | type[str] ``` +## Cycles + +### Bounds and constraints + +A typevar's bounds and constraints cannot be generic, cyclic or otherwise: + +```py +from typing import Any, TypeVar + +S = TypeVar("S") + +# TODO: error +T = TypeVar("T", bound=list[S]) + +# TODO: error +U = TypeVar("U", list["T"], str) + +# TODO: error +V = TypeVar("V", list["V"], str) +``` + +However, they are lazily evaluated and can cyclically refer to their own type: + +```py +from typing import TypeVar, Generic + +T = TypeVar("T", bound=list["G"]) + +class G(Generic[T]): + x: T + +reveal_type(G[list[G]]().x) # revealed: list[G[Unknown]] +``` + +### Defaults + +```toml +[environment] +python-version = "3.13" +``` + +Defaults can be generic, but can only refer to typevars from the same scope if they were defined +earlier in that scope: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U", default=T) + +class C(Generic[T, U]): + x: T + y: U + +reveal_type(C[int, str]().x) # revealed: int +reveal_type(C[int, str]().y) # revealed: str +reveal_type(C[int]().x) # revealed: int +reveal_type(C[int]().y) # revealed: int + +# TODO: error +V = TypeVar("V", default="V") + +class D(Generic[V]): + x: V + +# TODO: we shouldn't leak a typevar like this in type inference +reveal_type(D().x) # revealed: V@D +``` + +## Regression + +### Use of typevar with default inside a function body that binds it + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import Generic, TypeVar + +_DataT = TypeVar("_DataT", bound=int, default=int) + +class Event(Generic[_DataT]): + def __init__(self, data: _DataT) -> None: + self.data = data + +def async_fire_internal(event_data: _DataT): + event: Event[_DataT] | None = None + event = Event(event_data) +``` + [generics]: https://typing.python.org/en/latest/spec/generics.html diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 9821987281..b6c712f94b 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -197,9 +197,9 @@ from typing_extensions import TypeAliasType, TypeVar T = TypeVar("T") -IntAnd = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,)) +IntAndT = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,)) -def f(x: IntAnd[str]) -> None: +def f(x: IntAndT[str]) -> None: reveal_type(x) # revealed: @Todo(Generic manual PEP-695 type alias) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Boolean_parameters_m…_(3edf97b20f58fa11).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Boolean_parameters_m…_(3edf97b20f58fa11).snap new file mode 100644 index 0000000000..ca39b8044b --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Boolean_parameters_m…_(3edf97b20f58fa11).snap @@ -0,0 +1,70 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Boolean parameters must be unambiguous +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing_extensions import TypeVar + 2 | + 3 | def cond() -> bool: + 4 | return True + 5 | + 6 | # error: [invalid-legacy-type-variable] + 7 | T = TypeVar("T", covariant=cond()) + 8 | + 9 | # error: [invalid-legacy-type-variable] +10 | U = TypeVar("U", contravariant=cond()) +11 | +12 | # error: [invalid-legacy-type-variable] +13 | V = TypeVar("V", infer_variance=cond()) +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: The `covariant` parameter of `TypeVar` cannot have an ambiguous truthiness + --> src/mdtest_snippet.py:7:28 + | +6 | # error: [invalid-legacy-type-variable] +7 | T = TypeVar("T", covariant=cond()) + | ^^^^^^ +8 | +9 | # error: [invalid-legacy-type-variable] + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` + +``` +error[invalid-legacy-type-variable]: The `contravariant` parameter of `TypeVar` cannot have an ambiguous truthiness + --> src/mdtest_snippet.py:10:32 + | + 9 | # error: [invalid-legacy-type-variable] +10 | U = TypeVar("U", contravariant=cond()) + | ^^^^^^ +11 | +12 | # error: [invalid-legacy-type-variable] + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` + +``` +error[invalid-legacy-type-variable]: The `infer_variance` parameter of `TypeVar` cannot have an ambiguous truthiness + --> src/mdtest_snippet.py:13:33 + | +12 | # error: [invalid-legacy-type-variable] +13 | V = TypeVar("V", infer_variance=cond()) + | ^^^^^^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Cannot_be_both_covar…_(b7b0976739681470).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Cannot_be_both_covar…_(b7b0976739681470).snap new file mode 100644 index 0000000000..4f2e6633ee --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Cannot_be_both_covar…_(b7b0976739681470).snap @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot be both covariant and contravariant +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar +2 | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", covariant=True, contravariant=True) +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: A `TypeVar` cannot be both covariant and contravariant + --> src/mdtest_snippet.py:4:5 + | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", covariant=True, contravariant=True) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Cannot_have_both_bou…_(4ca5f13621915554).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Cannot_have_both_bou…_(4ca5f13621915554).snap new file mode 100644 index 0000000000..34ade6dbcf --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Cannot_have_both_bou…_(4ca5f13621915554).snap @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot have both bound and constraint +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar +2 | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", int, str, bound=bytes) +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: A `TypeVar` cannot have both a bound and constraints + --> src/mdtest_snippet.py:4:5 + | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", int, str, bound=bytes) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Cannot_have_only_one…_(8b0258f5188209c6).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Cannot_have_only_one…_(8b0258f5188209c6).snap new file mode 100644 index 0000000000..f65fedf52f --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Cannot_have_only_one…_(8b0258f5188209c6).snap @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot have only one constraint +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar +2 | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", int) +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: A `TypeVar` cannot have exactly one constraint + --> src/mdtest_snippet.py:4:18 + | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", int) + | ^^^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Invalid_feature_for_…_(72827c64b5c73d05).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Invalid_feature_for_…_(72827c64b5c73d05).snap new file mode 100644 index 0000000000..f821820e2d --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Invalid_feature_for_…_(72827c64b5c73d05).snap @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Invalid feature for this Python version +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar +2 | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", default=int) +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: The `default` parameter of `typing.TypeVar` was added in Python 3.13 + --> src/mdtest_snippet.py:4:18 + | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", default=int) + | ^^^^^^^^^^^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Invalid_keyword_argu…_(39164266ada3dc2f).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Invalid_keyword_argu…_(39164266ada3dc2f).snap new file mode 100644 index 0000000000..1f528a53a9 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Invalid_keyword_argu…_(39164266ada3dc2f).snap @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Invalid keyword arguments +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar +2 | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", invalid_keyword=True) +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: Unknown keyword argument `invalid_keyword` in `TypeVar` creation + --> src/mdtest_snippet.py:4:18 + | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", invalid_keyword=True) + | ^^^^^^^^^^^^^^^^^^^^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Must_be_directly_ass…_(c2e3e46852bb268f).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Must_be_directly_ass…_(c2e3e46852bb268f).snap new file mode 100644 index 0000000000..61f017d813 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Must_be_directly_ass…_(c2e3e46852bb268f).snap @@ -0,0 +1,52 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Must be directly assigned to a variable +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar +2 | +3 | T = TypeVar("T") +4 | # error: [invalid-legacy-type-variable] +5 | U: TypeVar = TypeVar("U") +6 | +7 | # error: [invalid-legacy-type-variable] +8 | tuple_with_typevar = ("foo", TypeVar("W")) +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment + --> src/mdtest_snippet.py:5:14 + | +3 | T = TypeVar("T") +4 | # error: [invalid-legacy-type-variable] +5 | U: TypeVar = TypeVar("U") + | ^^^^^^^^^^^^ +6 | +7 | # error: [invalid-legacy-type-variable] + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` + +``` +error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment + --> src/mdtest_snippet.py:8:30 + | +7 | # error: [invalid-legacy-type-variable] +8 | tuple_with_typevar = ("foo", TypeVar("W")) + | ^^^^^^^^^^^^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Must_have_a_name_(79a4ce09338e666b).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Must_have_a_name_(79a4ce09338e666b).snap new file mode 100644 index 0000000000..4b4dc20db3 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Must_have_a_name_(79a4ce09338e666b).snap @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Must have a name +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar +2 | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar() +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` is required. + --> src/mdtest_snippet.py:4:5 + | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar() + | ^^^^^^^^^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Name_can't_be_given_…_(8f6aed0dba79e995).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Name_can't_be_given_…_(8f6aed0dba79e995).snap new file mode 100644 index 0000000000..6938f2c2a8 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_Name_can't_be_given_…_(8f6aed0dba79e995).snap @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Name can't be given more than once +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar +2 | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", name="T") +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` can only be provided once. + --> src/mdtest_snippet.py:4:18 + | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("T", name="T") + | ^^^^^^^^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_No_variadic_argument…_(9d57505425233fd8).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_No_variadic_argument…_(9d57505425233fd8).snap new file mode 100644 index 0000000000..f2c8a4162c --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_No_variadic_argument…_(9d57505425233fd8).snap @@ -0,0 +1,52 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - No variadic arguments +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar +2 | +3 | types = (int, str) +4 | +5 | # error: [invalid-legacy-type-variable] +6 | T = TypeVar("T", *types) +7 | +8 | # error: [invalid-legacy-type-variable] +9 | S = TypeVar("S", **{"bound": int}) +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation + --> src/mdtest_snippet.py:6:18 + | +5 | # error: [invalid-legacy-type-variable] +6 | T = TypeVar("T", *types) + | ^^^^^^ +7 | +8 | # error: [invalid-legacy-type-variable] + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` + +``` +error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation + --> src/mdtest_snippet.py:9:18 + | +8 | # error: [invalid-legacy-type-variable] +9 | S = TypeVar("S", **{"bound": int}) + | ^^^^^^^^^^^^^^^^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_`TypeVar`_parameter_…_(8424f2b8bc4351f9).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_`TypeVar`_parameter_…_(8424f2b8bc4351f9).snap new file mode 100644 index 0000000000..f464f2861f --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat…_-_`TypeVar`_parameter_…_(8424f2b8bc4351f9).snap @@ -0,0 +1,33 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - `TypeVar` parameter must match variable name +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import TypeVar +2 | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("Q") +``` + +# Diagnostics + +``` +error[invalid-legacy-type-variable]: The name of a `TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`) + --> src/mdtest_snippet.py:4:1 + | +3 | # error: [invalid-legacy-type-variable] +4 | T = TypeVar("Q") + | ^ + | +info: rule `invalid-legacy-type-variable` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 2cbdea0261..b7d19d075b 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -1165,10 +1165,6 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { target: &'ast ast::Expr, value: Expression<'db>, ) { - // We only handle assignments to names and unpackings here, other targets like - // attribute and subscript are handled separately as they don't create a new - // definition. - let current_assignment = match target { ast::Expr::List(_) | ast::Expr::Tuple(_) => { if matches!(unpackable, Unpackable::Comprehension { .. }) { @@ -1628,10 +1624,22 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { debug_assert_eq!(&self.current_assignments, &[]); self.visit_expr(&node.value); - let value = self.add_standalone_assigned_expression(&node.value, node); - for target in &node.targets { - self.add_unpackable_assignment(&Unpackable::Assign(node), target, value); + // Optimization for the common case: if there's just one target, and it's not an + // unpacking, and the target is a simple name, we don't need the RHS to be a + // standalone expression at all. + if let [target] = &node.targets[..] + && target.is_name_expr() + { + self.push_assignment(CurrentAssignment::Assign { node, unpack: None }); + self.visit_expr(target); + self.pop_assignment(); + } else { + let value = self.add_standalone_assigned_expression(&node.value, node); + + for target in &node.targets { + self.add_unpackable_assignment(&Unpackable::Assign(node), target, value); + } } } ast::Stmt::AnnAssign(node) => { diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index f43b999d5b..368994fd34 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -706,13 +706,6 @@ impl DefinitionKind<'_> { matches!(self, DefinitionKind::Assignment(_)) } - pub(crate) fn as_typevar(&self) -> Option<&AstNodeRef> { - match self { - DefinitionKind::TypeVar(type_var) => Some(type_var), - _ => None, - } - } - /// Returns the [`TextRange`] of the definition target. /// /// A definition target would mainly be the node representing the place being defined i.e., diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d8f99738ca..a8d05b4b3a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -33,7 +33,7 @@ pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; use crate::module_name::ModuleName; use crate::module_resolver::{KnownModule, resolve_module}; use crate::place::{Boundness, Place, PlaceAndQualifiers, imported_symbol}; -use crate::semantic_index::definition::Definition; +use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::place::ScopedPlaceId; use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{imported_modules, place_table, semantic_index}; @@ -1642,7 +1642,9 @@ impl<'db> Type<'db> { ( Type::NonInferableTypeVar(lhs_bound_typevar), Type::NonInferableTypeVar(rhs_bound_typevar), - ) if lhs_bound_typevar == rhs_bound_typevar => ConstraintSet::from(true), + ) if lhs_bound_typevar.is_identical_to(db, rhs_bound_typevar) => { + ConstraintSet::from(true) + } // A fully static typevar is a subtype of its upper bound, and to something similar to // the union of its constraints. An unbound, unconstrained, fully static typevar has an @@ -4841,56 +4843,6 @@ impl<'db> Type<'db> { .into() } - Some(KnownClass::TypeVar) => { - // ```py - // class TypeVar: - // def __new__( - // cls, - // name: str, - // *constraints: Any, - // bound: Any | None = None, - // contravariant: bool = False, - // covariant: bool = False, - // infer_variance: bool = False, - // default: Any = ..., - // ) -> Self: ... - // ``` - Binding::single( - self, - Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("name")) - .with_annotated_type(Type::LiteralString), - Parameter::variadic(Name::new_static("constraints")) - .type_form() - .with_annotated_type(Type::any()), - Parameter::keyword_only(Name::new_static("bound")) - .type_form() - .with_annotated_type(UnionType::from_elements( - db, - [Type::any(), Type::none(db)], - )) - .with_default_type(Type::none(db)), - Parameter::keyword_only(Name::new_static("default")) - .type_form() - .with_annotated_type(Type::any()) - .with_default_type(KnownClass::NoneType.to_instance(db)), - Parameter::keyword_only(Name::new_static("contravariant")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("covariant")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("infer_variance")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - ]), - Some(KnownClass::TypeVar.to_instance(db)), - ), - ) - .into() - } - Some(KnownClass::Deprecated) => { // ```py // class deprecated: @@ -7832,6 +7784,12 @@ pub struct TypeVarInstance<'db> { _default: Option>, pub kind: TypeVarKind, + + /// If this typevar was transformed from another typevar via `mark_typevars_inferable`, this + /// records the identity of the "original" typevar, so we can recognize them as the same + /// typevar in `bind_typevar`. TODO: this (and the `is_identical_to` methods) should be + /// removable once we remove `mark_typevars_inferable`. + pub(crate) original: Option>, } // The Salsa heap is tracked separately. @@ -7942,6 +7900,7 @@ impl<'db> TypeVarInstance<'db> { .map(|ty| ty.normalized_impl(db, visitor).into()), }), self.kind(db), + self.original(db), ) } @@ -7987,6 +7946,7 @@ impl<'db> TypeVarInstance<'db> { .map(|ty| ty.materialize(db, materialization_kind, visitor).into()), }), self.kind(db), + self.original(db), ) } @@ -8000,10 +7960,7 @@ impl<'db> TypeVarInstance<'db> { // inferable, so we set the parameter to `None` here. let type_mapping = &TypeMapping::MarkTypeVarsInferable(None); - Self::new( - db, - self.name(db), - self.definition(db), + let new_bound_or_constraints = self._bound_or_constraints(db) .map(|bound_or_constraints| match bound_or_constraints { TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { @@ -8013,22 +7970,46 @@ impl<'db> TypeVarInstance<'db> { } TypeVarBoundOrConstraintsEvaluation::LazyUpperBound | TypeVarBoundOrConstraintsEvaluation::LazyConstraints => bound_or_constraints, - }), - self.explicit_variance(db), - self._default(db).and_then(|default| match default { - TypeVarDefaultEvaluation::Eager(ty) => Some( - ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor) - .into(), - ), - TypeVarDefaultEvaluation::Lazy => self.lazy_default(db).map(|ty| { - ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor) - .into() - }), + }); + + let new_default = self._default(db).and_then(|default| match default { + TypeVarDefaultEvaluation::Eager(ty) => Some( + ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor) + .into(), + ), + TypeVarDefaultEvaluation::Lazy => self.lazy_default(db).map(|ty| { + ty.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor) + .into() }), + }); + + // Ensure that we only modify the `original` field if we are going to modify one or both of + // `_bound_or_constraints` and `_default`; don't trigger creation of a new + // `TypeVarInstance` unnecessarily. + let new_original = if new_bound_or_constraints == self._bound_or_constraints(db) + && new_default == self._default(db) + { + self.original(db) + } else { + Some(self) + }; + + Self::new( + db, + self.name(db), + self.definition(db), + new_bound_or_constraints, + self.explicit_variance(db), + new_default, self.kind(db), + new_original, ) } + fn is_identical_to(self, db: &'db dyn Db, other: Self) -> bool { + self == other || (self.original(db) == Some(other) || other.original(db) == Some(self)) + } + fn to_instance(self, db: &'db dyn Db) -> Option { let bound_or_constraints = match self.bound_or_constraints(db)? { TypeVarBoundOrConstraints::UpperBound(upper_bound) => { @@ -8046,38 +8027,88 @@ impl<'db> TypeVarInstance<'db> { self.explicit_variance(db), None, self.kind(db), + self.original(db), )) } - #[salsa::tracked(cycle_fn=lazy_bound_cycle_recover, cycle_initial=lazy_bound_cycle_initial)] + #[salsa::tracked(cycle_fn=lazy_bound_cycle_recover, cycle_initial=lazy_bound_cycle_initial, heap_size=ruff_memory_usage::heap_size)] fn lazy_bound(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; let module = parsed_module(db, definition.file(db)).load(db); - let typevar_node = definition.kind(db).as_typevar()?.node(&module); - let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?); + let ty = match definition.kind(db) { + // PEP 695 typevar + DefinitionKind::TypeVar(typevar) => { + let typevar_node = typevar.node(&module); + definition_expression_type(db, definition, typevar_node.bound.as_ref()?) + } + // legacy typevar + DefinitionKind::Assignment(assignment) => { + let call_expr = assignment.value(&module).as_call_expr()?; + let expr = &call_expr.arguments.find_keyword("bound")?.value; + definition_expression_type(db, definition, expr) + } + _ => return None, + }; Some(TypeVarBoundOrConstraints::UpperBound(ty)) } - #[salsa::tracked] + #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] fn lazy_constraints(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; let module = parsed_module(db, definition.file(db)).load(db); - let typevar_node = definition.kind(db).as_typevar()?.node(&module); - let ty = definition_expression_type(db, definition, typevar_node.bound.as_ref()?) - .into_union()?; + let ty = match definition.kind(db) { + // PEP 695 typevar + DefinitionKind::TypeVar(typevar) => { + let typevar_node = typevar.node(&module); + definition_expression_type(db, definition, typevar_node.bound.as_ref()?) + .into_union()? + } + // legacy typevar + DefinitionKind::Assignment(assignment) => { + let call_expr = assignment.value(&module).as_call_expr()?; + // We don't use `UnionType::from_elements` or `UnionBuilder` here, + // because we don't want to simplify the list of constraints as we would with + // an actual union type. + // TODO: We probably shouldn't use `UnionType` to store these at all? TypeVar + // constraints are not a union. + UnionType::new( + db, + call_expr + .arguments + .args + .iter() + .skip(1) + .map(|arg| definition_expression_type(db, definition, arg)) + .collect::>(), + ) + } + _ => return None, + }; Some(TypeVarBoundOrConstraints::Constraints(ty)) } - #[salsa::tracked] + #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)] fn lazy_default(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; let module = parsed_module(db, definition.file(db)).load(db); - let typevar_node = definition.kind(db).as_typevar()?.node(&module); - Some(definition_expression_type( - db, - definition, - typevar_node.default.as_ref()?, - )) + match definition.kind(db) { + // PEP 695 typevar + DefinitionKind::TypeVar(typevar) => { + let typevar_node = typevar.node(&module); + Some(definition_expression_type( + db, + definition, + typevar_node.default.as_ref()?, + )) + } + // legacy typevar + DefinitionKind::Assignment(assignment) => { + let call_expr = assignment.value(&module).as_call_expr()?; + let expr = &call_expr.arguments.find_keyword("default")?.value; + Some(definition_expression_type(db, definition, expr)) + } + _ => None, + } } } @@ -8153,6 +8184,7 @@ impl<'db> BoundTypeVarInstance<'db> { Some(variance), None, // _default TypeVarKind::Pep695, + None, ), BindingContext::Synthetic, ) @@ -8174,11 +8206,24 @@ impl<'db> BoundTypeVarInstance<'db> { Some(TypeVarVariance::Invariant), None, TypeVarKind::TypingSelf, + None, ), binding_context, ) } + pub(crate) fn is_identical_to(self, db: &'db dyn Db, other: Self) -> bool { + if self == other { + return true; + } + + if self.binding_context(db) != other.binding_context(db) { + return false; + } + + self.typevar(db).is_identical_to(db, other.typevar(db)) + } + pub(crate) fn variance_with_polarity( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 2b580e9c29..8136f93184 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -16,7 +16,7 @@ use crate::semantic_index::{ }; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::context::InferContext; -use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE}; +use crate::types::diagnostic::INVALID_TYPE_ALIAS_TYPE; use crate::types::enums::enum_metadata; use crate::types::function::{DataclassTransformerParams, KnownFunction}; use crate::types::generics::{GenericContext, Specialization, walk_specialization}; @@ -29,9 +29,8 @@ use crate::types::{ DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, - TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, - TypeVarKind, TypedDictParams, UnionBuilder, VarianceInferable, declaration_type, - determine_upper_bound, infer_definition_types, + TypeContext, TypeMapping, TypeRelation, TypedDictParams, UnionBuilder, VarianceInferable, + declaration_type, determine_upper_bound, infer_definition_types, }; use crate::{ Db, FxIndexMap, FxOrderSet, Program, @@ -3761,6 +3760,8 @@ pub enum KnownClass { SupportsIndex, Iterable, Iterator, + // typing_extensions + ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features // Collections ChainMap, Counter, @@ -3815,6 +3816,7 @@ impl KnownClass { | Self::VersionInfo | Self::TypeAliasType | Self::TypeVar + | Self::ExtensionsTypeVar | Self::ParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs @@ -3943,6 +3945,7 @@ impl KnownClass { | KnownClass::StdlibAlias | KnownClass::SpecialForm | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar | KnownClass::ParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs @@ -4025,6 +4028,7 @@ impl KnownClass { | KnownClass::StdlibAlias | KnownClass::SpecialForm | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar | KnownClass::ParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs @@ -4107,6 +4111,7 @@ impl KnownClass { | KnownClass::StdlibAlias | KnownClass::SpecialForm | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar | KnownClass::ParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs @@ -4194,6 +4199,7 @@ impl KnownClass { | Self::NoneType | Self::SpecialForm | Self::TypeVar + | Self::ExtensionsTypeVar | Self::ParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs @@ -4289,6 +4295,7 @@ impl KnownClass { | KnownClass::StdlibAlias | KnownClass::SpecialForm | KnownClass::TypeVar + | KnownClass::ExtensionsTypeVar | KnownClass::ParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs @@ -4358,6 +4365,7 @@ impl KnownClass { Self::NoneType => "NoneType", Self::SpecialForm => "_SpecialForm", Self::TypeVar => "TypeVar", + Self::ExtensionsTypeVar => "TypeVar", Self::ParamSpec => "ParamSpec", Self::ParamSpecArgs => "ParamSpecArgs", Self::ParamSpecKwargs => "ParamSpecKwargs", @@ -4656,6 +4664,7 @@ impl KnownClass { | Self::ProtocolMeta | Self::SupportsIndex => KnownModule::Typing, Self::TypeAliasType + | Self::ExtensionsTypeVar | Self::TypeVarTuple | Self::ParamSpec | Self::ParamSpecArgs @@ -4752,6 +4761,7 @@ impl KnownClass { | Self::SupportsIndex | Self::StdlibAlias | Self::TypeVar + | Self::ExtensionsTypeVar | Self::ParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs @@ -4838,6 +4848,7 @@ impl KnownClass { | Self::Generator | Self::Deprecated | Self::TypeVar + | Self::ExtensionsTypeVar | Self::ParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs @@ -4875,99 +4886,102 @@ impl KnownClass { ) -> Option { // We assert that this match is exhaustive over the right-hand side in the unit test // `known_class_roundtrip_from_str()` - let candidate = match class_name { - "bool" => Self::Bool, - "object" => Self::Object, - "bytes" => Self::Bytes, - "bytearray" => Self::Bytearray, - "tuple" => Self::Tuple, - "type" => Self::Type, - "int" => Self::Int, - "float" => Self::Float, - "complex" => Self::Complex, - "str" => Self::Str, - "set" => Self::Set, - "frozenset" => Self::FrozenSet, - "dict" => Self::Dict, - "list" => Self::List, - "slice" => Self::Slice, - "property" => Self::Property, - "BaseException" => Self::BaseException, - "BaseExceptionGroup" => Self::BaseExceptionGroup, - "Exception" => Self::Exception, - "ExceptionGroup" => Self::ExceptionGroup, - "staticmethod" => Self::Staticmethod, - "classmethod" => Self::Classmethod, - "Awaitable" => Self::Awaitable, - "Generator" => Self::Generator, - "deprecated" => Self::Deprecated, - "GenericAlias" => Self::GenericAlias, - "NoneType" => Self::NoneType, - "ModuleType" => Self::ModuleType, - "GeneratorType" => Self::GeneratorType, - "AsyncGeneratorType" => Self::AsyncGeneratorType, - "CoroutineType" => Self::CoroutineType, - "FunctionType" => Self::FunctionType, - "MethodType" => Self::MethodType, - "UnionType" => Self::UnionType, - "MethodWrapperType" => Self::MethodWrapperType, - "WrapperDescriptorType" => Self::WrapperDescriptorType, - "BuiltinFunctionType" => Self::BuiltinFunctionType, - "NewType" => Self::NewType, - "TypeAliasType" => Self::TypeAliasType, - "TypeVar" => Self::TypeVar, - "Iterable" => Self::Iterable, - "Iterator" => Self::Iterator, - "ParamSpec" => Self::ParamSpec, - "ParamSpecArgs" => Self::ParamSpecArgs, - "ParamSpecKwargs" => Self::ParamSpecKwargs, - "TypeVarTuple" => Self::TypeVarTuple, - "ChainMap" => Self::ChainMap, - "Counter" => Self::Counter, - "defaultdict" => Self::DefaultDict, - "deque" => Self::Deque, - "OrderedDict" => Self::OrderedDict, - "_Alias" => Self::StdlibAlias, - "_SpecialForm" => Self::SpecialForm, - "_NoDefaultType" => Self::NoDefaultType, - "SupportsIndex" => Self::SupportsIndex, - "Enum" => Self::Enum, - "EnumMeta" => Self::EnumType, + let candidates: &[Self] = match class_name { + "bool" => &[Self::Bool], + "object" => &[Self::Object], + "bytes" => &[Self::Bytes], + "bytearray" => &[Self::Bytearray], + "tuple" => &[Self::Tuple], + "type" => &[Self::Type], + "int" => &[Self::Int], + "float" => &[Self::Float], + "complex" => &[Self::Complex], + "str" => &[Self::Str], + "set" => &[Self::Set], + "frozenset" => &[Self::FrozenSet], + "dict" => &[Self::Dict], + "list" => &[Self::List], + "slice" => &[Self::Slice], + "property" => &[Self::Property], + "BaseException" => &[Self::BaseException], + "BaseExceptionGroup" => &[Self::BaseExceptionGroup], + "Exception" => &[Self::Exception], + "ExceptionGroup" => &[Self::ExceptionGroup], + "staticmethod" => &[Self::Staticmethod], + "classmethod" => &[Self::Classmethod], + "Awaitable" => &[Self::Awaitable], + "Generator" => &[Self::Generator], + "deprecated" => &[Self::Deprecated], + "GenericAlias" => &[Self::GenericAlias], + "NoneType" => &[Self::NoneType], + "ModuleType" => &[Self::ModuleType], + "GeneratorType" => &[Self::GeneratorType], + "AsyncGeneratorType" => &[Self::AsyncGeneratorType], + "CoroutineType" => &[Self::CoroutineType], + "FunctionType" => &[Self::FunctionType], + "MethodType" => &[Self::MethodType], + "UnionType" => &[Self::UnionType], + "MethodWrapperType" => &[Self::MethodWrapperType], + "WrapperDescriptorType" => &[Self::WrapperDescriptorType], + "BuiltinFunctionType" => &[Self::BuiltinFunctionType], + "NewType" => &[Self::NewType], + "TypeAliasType" => &[Self::TypeAliasType], + "TypeVar" => &[Self::TypeVar, Self::ExtensionsTypeVar], + "Iterable" => &[Self::Iterable], + "Iterator" => &[Self::Iterator], + "ParamSpec" => &[Self::ParamSpec], + "ParamSpecArgs" => &[Self::ParamSpecArgs], + "ParamSpecKwargs" => &[Self::ParamSpecKwargs], + "TypeVarTuple" => &[Self::TypeVarTuple], + "ChainMap" => &[Self::ChainMap], + "Counter" => &[Self::Counter], + "defaultdict" => &[Self::DefaultDict], + "deque" => &[Self::Deque], + "OrderedDict" => &[Self::OrderedDict], + "_Alias" => &[Self::StdlibAlias], + "_SpecialForm" => &[Self::SpecialForm], + "_NoDefaultType" => &[Self::NoDefaultType], + "SupportsIndex" => &[Self::SupportsIndex], + "Enum" => &[Self::Enum], + "EnumMeta" => &[Self::EnumType], "EnumType" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { - Self::EnumType + &[Self::EnumType] } "StrEnum" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { - Self::StrEnum + &[Self::StrEnum] } - "auto" => Self::Auto, - "member" => Self::Member, - "nonmember" => Self::Nonmember, - "ABCMeta" => Self::ABCMeta, - "super" => Self::Super, - "_version_info" => Self::VersionInfo, + "auto" => &[Self::Auto], + "member" => &[Self::Member], + "nonmember" => &[Self::Nonmember], + "ABCMeta" => &[Self::ABCMeta], + "super" => &[Self::Super], + "_version_info" => &[Self::VersionInfo], "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { - Self::EllipsisType + &[Self::EllipsisType] } "EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { - Self::EllipsisType + &[Self::EllipsisType] } - "_NotImplementedType" => Self::NotImplementedType, - "Field" => Self::Field, - "KW_ONLY" => Self::KwOnly, - "InitVar" => Self::InitVar, - "NamedTupleFallback" => Self::NamedTupleFallback, - "NamedTupleLike" => Self::NamedTupleLike, - "ConstraintSet" => Self::ConstraintSet, - "TypedDictFallback" => Self::TypedDictFallback, - "Template" => Self::Template, - "Path" => Self::Path, - "_ProtocolMeta" => Self::ProtocolMeta, + "_NotImplementedType" => &[Self::NotImplementedType], + "Field" => &[Self::Field], + "KW_ONLY" => &[Self::KwOnly], + "InitVar" => &[Self::InitVar], + "NamedTupleFallback" => &[Self::NamedTupleFallback], + "NamedTupleLike" => &[Self::NamedTupleLike], + "ConstraintSet" => &[Self::ConstraintSet], + "TypedDictFallback" => &[Self::TypedDictFallback], + "Template" => &[Self::Template], + "Path" => &[Self::Path], + "_ProtocolMeta" => &[Self::ProtocolMeta], _ => return None, }; - candidate - .check_module(db, file_to_module(db, file)?.known(db)?) - .then_some(candidate) + let module = file_to_module(db, file)?.known(db)?; + + candidates + .iter() + .copied() + .find(|&candidate| candidate.check_module(db, module)) } /// Return `true` if the module of `self` matches `module` @@ -5028,6 +5042,8 @@ impl KnownClass { | Self::InitVar | Self::NamedTupleFallback | Self::TypedDictFallback + | Self::TypeVar + | Self::ExtensionsTypeVar | Self::NamedTupleLike | Self::ConstraintSet | Self::Awaitable @@ -5036,7 +5052,6 @@ impl KnownClass { | Self::Path => module == self.canonical_module(db), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), Self::SpecialForm - | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType | Self::SupportsIndex @@ -5059,7 +5074,6 @@ impl KnownClass { context: &InferContext<'db, '_>, index: &SemanticIndex<'db>, overload: &mut Binding<'db>, - call_arguments: &CallArguments<'_, 'db>, call_expression: &ast::ExprCall, ) { let db = context.db(); @@ -5132,6 +5146,7 @@ impl KnownClass { _ => {} } } + KnownClass::Deprecated => { // Parsing something of the form: // @@ -5158,153 +5173,6 @@ impl KnownClass { DeprecatedInstance::new(db, message.into_string_literal()), ))); } - KnownClass::TypeVar => { - let assigned_to = index - .try_expression(ast::ExprRef::from(call_expression)) - .and_then(|expr| expr.assigned_to(db)); - - let Some(target) = assigned_to.as_ref().and_then(|assigned_to| { - match assigned_to.node(module).targets.as_slice() { - [ast::Expr::Name(target)] => Some(target), - _ => None, - } - }) else { - if let Some(builder) = - context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression) - { - builder.into_diagnostic( - "A legacy `typing.TypeVar` must be immediately assigned to a variable", - ); - } - return; - }; - - let [ - Some(name_param), - constraints, - bound, - default, - contravariant, - covariant, - _infer_variance, - ] = overload.parameter_types() - else { - return; - }; - - let covariant = covariant - .map(|ty| ty.bool(db)) - .unwrap_or(Truthiness::AlwaysFalse); - - let contravariant = contravariant - .map(|ty| ty.bool(db)) - .unwrap_or(Truthiness::AlwaysFalse); - - let variance = match (contravariant, covariant) { - (Truthiness::Ambiguous, _) => { - if let Some(builder) = - context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression) - { - builder.into_diagnostic( - "The `contravariant` parameter of a legacy `typing.TypeVar` \ - cannot have an ambiguous value", - ); - } - return; - } - (_, Truthiness::Ambiguous) => { - if let Some(builder) = - context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression) - { - builder.into_diagnostic( - "The `covariant` parameter of a legacy `typing.TypeVar` \ - cannot have an ambiguous value", - ); - } - return; - } - (Truthiness::AlwaysTrue, Truthiness::AlwaysTrue) => { - if let Some(builder) = - context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression) - { - builder.into_diagnostic( - "A legacy `typing.TypeVar` cannot be both \ - covariant and contravariant", - ); - } - return; - } - (Truthiness::AlwaysTrue, Truthiness::AlwaysFalse) => { - TypeVarVariance::Contravariant - } - (Truthiness::AlwaysFalse, Truthiness::AlwaysTrue) => TypeVarVariance::Covariant, - (Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => { - TypeVarVariance::Invariant - } - }; - - let name_param = name_param.into_string_literal().map(|name| name.value(db)); - - if name_param.is_none_or(|name_param| name_param != target.id) { - if let Some(builder) = - context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression) - { - builder.into_diagnostic(format_args!( - "The name of a legacy `typing.TypeVar`{} must match \ - the name of the variable it is assigned to (`{}`)", - if let Some(name_param) = name_param { - format!(" (`{name_param}`)") - } else { - String::new() - }, - target.id, - )); - } - return; - } - - let bound_or_constraint = match (bound, constraints) { - (Some(bound), None) => { - Some(TypeVarBoundOrConstraints::UpperBound(*bound).into()) - } - - (None, Some(_constraints)) => { - // We don't use UnionType::from_elements or UnionBuilder here, - // because we don't want to simplify the list of constraints like - // we do with the elements of an actual union type. - // TODO: Consider using a new `OneOfType` connective here instead, - // since that more accurately represents the actual semantics of - // typevar constraints. - let elements = UnionType::new( - db, - overload - .arguments_for_parameter(call_arguments, 1) - .map(|(_, ty)| ty) - .collect::>(), - ); - Some(TypeVarBoundOrConstraints::Constraints(elements).into()) - } - - // TODO: Emit a diagnostic that TypeVar cannot be both bounded and - // constrained - (Some(_), Some(_)) => return, - - (None, None) => None, - }; - - let containing_assignment = index.expect_single_definition(target); - overload.set_return_type(Type::KnownInstance(KnownInstanceType::TypeVar( - TypeVarInstance::new( - db, - &target.id, - Some(containing_assignment), - bound_or_constraint, - Some(variance), - default.map(Into::into), - TypeVarKind::Legacy, - ), - ))); - } KnownClass::TypeAliasType => { let assigned_to = index diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index f84cf8b4b1..eaf1c73f43 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -126,6 +126,7 @@ pub(crate) fn typing_self<'db>( Some(TypeVarVariance::Invariant), None, TypeVarKind::TypingSelf, + None, ); bind_typevar( @@ -396,7 +397,7 @@ impl<'db> GenericContext<'db> { typevar: TypeVarInstance<'db>, ) -> Option> { self.variables(db) - .find(|self_bound_typevar| self_bound_typevar.typevar(db) == typevar) + .find(|self_bound_typevar| self_bound_typevar.typevar(db).is_identical_to(db, typevar)) } /// Creates a specialization of this generic context. Panics if the length of `types` does not diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 9c0d38a601..705731980b 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -631,7 +631,7 @@ struct DefinitionInferenceExtra<'db> { /// Is this a cycle-recovery inference result, and if so, what kind? cycle_recovery: Option>, - /// The definitions that are deferred. + /// The definitions that have some deferred parts. deferred: Box<[Definition<'db>]>, /// The diagnostics for this region. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 5f4ae38a81..2ab009025e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -53,11 +53,12 @@ use crate::types::diagnostic::{ CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, - INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_NAMED_TUPLE, INVALID_PARAMETER_DEFAULT, - INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, - IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, - UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, - UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, report_bad_dunder_set_call, + INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_NAMED_TUPLE, + INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, + INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, + POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, UNDEFINED_REVEAL, + UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, + UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, report_implicit_return_type, report_instance_layout_conflict, report_invalid_assignment, report_invalid_attribute_assignment, report_invalid_generator_function_return_type, @@ -95,7 +96,7 @@ use crate::types::{ Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind, - TypedDictType, UnionBuilder, UnionType, binding_type, todo_type, + TypeVarVariance, TypedDictType, UnionBuilder, UnionType, binding_type, todo_type, }; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::{EvaluationMode, UnpackPosition}; @@ -212,9 +213,9 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> { /// The list should only contain one entry per declaration at most. declarations: VecMap, TypeAndQualifiers<'db>>, - /// The definitions that are deferred. + /// The definitions with deferred sub-parts. /// - /// The list should only contain one entry per deferred. + /// The list should only contain one entry per definition. deferred: VecSet>, /// The returned types and their corresponding ranges of the region, if it is a function body. @@ -497,8 +498,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // Infer the deferred types for the definitions here to consider the end-of-scope - // semantics. + // Infer deferred types for all definitions. for definition in std::mem::take(&mut self.deferred) { self.extend_definition(infer_deferred_types(self.db(), definition)); } @@ -1245,6 +1245,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { DefinitionKind::TypeVar(typevar) => { self.infer_typevar_deferred(typevar.node(self.module())); } + DefinitionKind::Assignment(assignment) => { + self.infer_assignment_deferred(assignment.value(self.module())); + } _ => {} } } @@ -2961,6 +2964,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { None, default.as_deref().map(|_| TypeVarDefaultEvaluation::Lazy), TypeVarKind::Pep695, + None, ))); self.add_declaration_with_binding( node.into(), @@ -3993,7 +3997,36 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { unpacked.expression_type(target) } TargetKind::Single => { - let value_ty = self.infer_standalone_expression(value, TypeContext::default()); + let tcx = TypeContext::default(); + let value_ty = if let Some(standalone_expression) = self.index.try_expression(value) + { + self.infer_standalone_expression_impl(value, standalone_expression, tcx) + } else if let ast::Expr::Call(call_expr) = value { + // If the RHS is not a standalone expression, this is a simple assignment + // (single target, no unpackings). That means it's a valid syntactic form + // for a legacy TypeVar creation; check for that. + let callable_type = self.infer_maybe_standalone_expression( + call_expr.func.as_ref(), + TypeContext::default(), + ); + + let typevar_class = callable_type + .into_class_literal() + .and_then(|cls| cls.known(self.db())) + .filter(|cls| { + matches!(cls, KnownClass::TypeVar | KnownClass::ExtensionsTypeVar) + }); + + let ty = if let Some(typevar_class) = typevar_class { + self.infer_legacy_typevar(target, call_expr, definition, typevar_class) + } else { + self.infer_call_expression_impl(call_expr, callable_type, tcx) + }; + self.store_expression_type(value, ty); + ty + } else { + self.infer_expression(value, tcx) + }; // `TYPE_CHECKING` is a special variable that should only be assigned `False` // at runtime, but is always considered `True` in type checking. @@ -4024,6 +4057,272 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.add_binding(target.into(), definition, target_ty); } + fn infer_legacy_typevar( + &mut self, + target: &ast::Expr, + call_expr: &ast::ExprCall, + definition: Definition<'db>, + known_class: KnownClass, + ) -> Type<'db> { + fn error<'db>( + context: &InferContext<'db, '_>, + message: impl std::fmt::Display, + node: impl Ranged, + ) -> Type<'db> { + if let Some(builder) = context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, node) { + builder.into_diagnostic(message); + } + // If the call doesn't create a valid typevar, we'll emit diagnostics and fall back to + // just creating a regular instance of `typing.TypeVar`. + KnownClass::TypeVar.to_instance(context.db()) + } + + let db = self.db(); + let arguments = &call_expr.arguments; + let is_typing_extensions = known_class == KnownClass::ExtensionsTypeVar; + let assume_all_features = self.in_stub() || is_typing_extensions; + let python_version = Program::get(db).python_version(db); + let have_features_from = + |version: PythonVersion| assume_all_features || python_version >= version; + + let mut has_bound = false; + let mut default = None; + let mut covariant = false; + let mut contravariant = false; + let mut name_param_ty = None; + + if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) { + return error( + &self.context, + "Starred arguments are not supported in `TypeVar` creation", + starred, + ); + } + + for kwarg in &arguments.keywords { + let Some(identifier) = kwarg.arg.as_ref() else { + return error( + &self.context, + "Starred arguments are not supported in `TypeVar` creation", + kwarg, + ); + }; + match identifier.id().as_str() { + "name" => { + // Duplicate keyword argument is a syntax error, so we don't have to check if + // `name_param_ty.is_some()` here. + if !arguments.args.is_empty() { + return error( + &self.context, + "The `name` parameter of `TypeVar` can only be provided once.", + kwarg, + ); + } + name_param_ty = + Some(self.infer_expression(&kwarg.value, TypeContext::default())); + } + "bound" => has_bound = true, + "covariant" => { + match self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + { + Truthiness::AlwaysTrue => covariant = true, + Truthiness::AlwaysFalse => {} + Truthiness::Ambiguous => { + return error( + &self.context, + "The `covariant` parameter of `TypeVar` \ + cannot have an ambiguous truthiness", + &kwarg.value, + ); + } + } + } + "contravariant" => { + match self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + { + Truthiness::AlwaysTrue => contravariant = true, + Truthiness::AlwaysFalse => {} + Truthiness::Ambiguous => { + return error( + &self.context, + "The `contravariant` parameter of `TypeVar` \ + cannot have an ambiguous truthiness", + &kwarg.value, + ); + } + } + } + "default" => { + if !have_features_from(PythonVersion::PY313) { + // We don't return here; this error is informational since this will error + // at runtime, but the user's intent is plain, we may as well respect it. + error( + &self.context, + "The `default` parameter of `typing.TypeVar` was added in Python 3.13", + kwarg, + ); + } + + default = Some(TypeVarDefaultEvaluation::Lazy); + } + "infer_variance" => { + if !have_features_from(PythonVersion::PY312) { + // We don't return here; this error is informational since this will error + // at runtime, but the user's intent is plain, we may as well respect it. + error( + &self.context, + "The `infer_variance` parameter of `typing.TypeVar` was added in Python 3.12", + kwarg, + ); + } + // TODO support `infer_variance` in legacy TypeVars + if self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + .is_ambiguous() + { + return error( + &self.context, + "The `infer_variance` parameter of `TypeVar` \ + cannot have an ambiguous truthiness", + &kwarg.value, + ); + } + } + name => { + // We don't return here; this error is informational since this will error + // at runtime, but it will likely cause fewer cascading errors if we just + // ignore the unknown keyword and still understand as much of the typevar as we + // can. + error( + &self.context, + format_args!("Unknown keyword argument `{name}` in `TypeVar` creation",), + kwarg, + ); + self.infer_expression(&kwarg.value, TypeContext::default()); + } + } + } + + let variance = match (covariant, contravariant) { + (true, true) => { + return error( + &self.context, + "A `TypeVar` cannot be both covariant and contravariant", + call_expr, + ); + } + (true, false) => TypeVarVariance::Covariant, + (false, true) => TypeVarVariance::Contravariant, + (false, false) => TypeVarVariance::Invariant, + }; + + let Some(name_param_ty) = name_param_ty.or_else(|| { + arguments + .find_positional(0) + .map(|arg| self.infer_expression(arg, TypeContext::default())) + }) else { + return error( + &self.context, + "The `name` parameter of `TypeVar` is required.", + call_expr, + ); + }; + + let Some(name_param) = name_param_ty + .into_string_literal() + .map(|name| name.value(db)) + else { + return error( + &self.context, + "The first argument to `TypeVar` must be a string literal.", + call_expr, + ); + }; + + let ast::Expr::Name(ast::ExprName { + id: target_name, .. + }) = target + else { + return error( + &self.context, + "A `TypeVar` definition must be a simple variable assignment", + target, + ); + }; + + if name_param != target_name { + return error( + &self.context, + format_args!( + "The name of a `TypeVar` (`{name_param}`) must match \ + the name of the variable it is assigned to (`{target_name}`)" + ), + target, + ); + } + + // Inference of bounds, constraints, and defaults must be deferred, to avoid cycles. So we + // only check presence/absence/number here. + + let num_constraints = arguments.args.len().saturating_sub(1); + + let bound_or_constraints = match (has_bound, num_constraints) { + (false, 0) => None, + (true, 0) => Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), + (true, _) => { + return error( + &self.context, + "A `TypeVar` cannot have both a bound and constraints", + call_expr, + ); + } + (_, 1) => { + return error( + &self.context, + "A `TypeVar` cannot have exactly one constraint", + &arguments.args[1], + ); + } + (false, _) => Some(TypeVarBoundOrConstraintsEvaluation::LazyConstraints), + }; + + if bound_or_constraints.is_some() || default.is_some() { + self.deferred.insert(definition); + } + + Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( + db, + target_name, + Some(definition), + bound_or_constraints, + Some(variance), + default, + TypeVarKind::Legacy, + None, + ))) + } + + fn infer_assignment_deferred(&mut self, value: &ast::Expr) { + // Infer deferred bounds/constraints/defaults of a legacy TypeVar. + let ast::Expr::Call(ast::ExprCall { arguments, .. }) = value else { + return; + }; + for arg in arguments.args.iter().skip(1) { + self.infer_type_expression(arg); + } + if let Some(bound) = arguments.find_keyword("bound") { + self.infer_type_expression(&bound.value); + } + if let Some(default) = arguments.find_keyword("default") { + self.infer_type_expression(&default.value); + } + } + fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) { if assignment.target.is_name_expr() { self.infer_definition(assignment); @@ -6045,6 +6344,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, call_expression: &ast::ExprCall, tcx: TypeContext<'db>, + ) -> Type<'db> { + // TODO: Use the type context for more precise inference. + let callable_type = + self.infer_maybe_standalone_expression(&call_expression.func, TypeContext::default()); + + self.infer_call_expression_impl(call_expression, callable_type, tcx) + } + + fn infer_call_expression_impl( + &mut self, + call_expression: &ast::ExprCall, + callable_type: Type<'db>, + tcx: TypeContext<'db>, ) -> Type<'db> { let ast::ExprCall { range: _, @@ -6065,9 +6377,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ty }); - // TODO: Use the type context for more precise inference. - let callable_type = self.infer_maybe_standalone_expression(func, TypeContext::default()); - // Special handling for `TypedDict` method calls if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() { let value_type = self.expression_type(value); @@ -6171,7 +6480,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | KnownClass::Object | KnownClass::Property | KnownClass::Super - | KnownClass::TypeVar | KnownClass::TypeAliasType | KnownClass::Deprecated ) @@ -6194,6 +6502,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); + if matches!( + class.known(self.db()), + Some(KnownClass::TypeVar | KnownClass::ExtensionsTypeVar) + ) { + // Inference of correctly-placed `TypeVar` definitions is done in + // `TypeInferenceBuilder::infer_legacy_typevar`, and doesn't use the full + // call-binding machinery. If we reach here, it means that someone is trying to + // instantiate a `typing.TypeVar` in an invalid context. + if let Some(builder) = self + .context + .report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression) + { + builder.into_diagnostic( + "A `TypeVar` definition must be a simple variable assignment", + ); + } + } + return callable_type .try_call_constructor(self.db(), call_arguments, tcx) .unwrap_or_else(|err| { @@ -6253,7 +6579,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &self.context, self.index, overload, - &call_arguments, call_expression, ); } @@ -9353,7 +9678,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); assert!( deferred.is_empty(), - "Expression region can't have deferred types" + "Expression region can't have deferred definitions" ); let extra = diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index 0c3a7967ad..9f6ae996be 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -418,7 +418,8 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { "/src/main.py", r#" from mod import C - x = C().attr + # multiple targets ensures RHS is a standalone expression, relied on by this test + x = y = C().attr "#, )?; @@ -508,7 +509,8 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { "/src/main.py", r#" from mod import C - x = C().attr + # multiple targets ensures RHS is a standalone expression, relied on by this test + x = y = C().attr "#, )?; @@ -603,7 +605,8 @@ fn dependency_implicit_class_member() -> anyhow::Result<()> { r#" from mod import C C.method() - x = C().class_attr + # multiple targets ensures RHS is a standalone expression, relied on by this test + x = y = C().class_attr "#, )?; @@ -688,7 +691,8 @@ fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> { r#" from foo import foo - a = foo() + # multiple targets ensures RHS is a standalone expression, relied on by this test + a = b = foo() "#, )?;