diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index 83c58e5e82..fe282249a5 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -59,7 +59,29 @@ type KeyDiagnosticFields = ( Severity, ); -static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[]; +static EXPECTED_TOMLLIB_DIAGNOSTICS: &[KeyDiagnosticFields] = &[ + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(8224..8254), + "Argument to this function is incorrect", + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(16914..16948), + "Argument to this function is incorrect", + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(17319..17363), + "Argument to this function is incorrect", + Severity::Error, + ), +]; fn tomllib_path(file: &TestFile) -> SystemPathBuf { SystemPathBuf::from("src").join(file.name()) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 46f3343701..87c73f5717 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -33,7 +33,9 @@ class Shape: reveal_type(self) # revealed: Unknown return self -reveal_type(Shape().nested_type()) # revealed: @Todo(specialized non-generic class) +# TODO: should be `list[Shape]` +reveal_type(Shape().nested_type()) # revealed: list[Self] + reveal_type(Shape().nested_func()) # revealed: Shape class Circle(Shape): diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md index 61887e7f29..e102a79a2d 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md @@ -14,7 +14,7 @@ Ts = TypeVarTuple("Ts") def append_int(*args: *Ts) -> tuple[*Ts, int]: # TODO: tuple[*Ts] - reveal_type(args) # revealed: tuple + reveal_type(args) # revealed: tuple[Unknown, ...] return (*args, 1) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md index 82057136f8..94d6a9ec7f 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -30,24 +30,22 @@ def f( ordered_dict_bare: typing.OrderedDict, ordered_dict_parametrized: typing.OrderedDict[int, str], ): - # TODO: revealed: list[Unknown] - reveal_type(list_bare) # revealed: list + reveal_type(list_bare) # revealed: list[Unknown] # TODO: revealed: list[int] - reveal_type(list_parametrized) # revealed: list + reveal_type(list_parametrized) # revealed: list[Unknown] reveal_type(dict_bare) # revealed: dict[Unknown, Unknown] # TODO: revealed: dict[int, str] reveal_type(dict_parametrized) # revealed: dict[Unknown, Unknown] - # TODO: revealed: set[Unknown] - reveal_type(set_bare) # revealed: set + reveal_type(set_bare) # revealed: set[Unknown] # TODO: revealed: set[int] - reveal_type(set_parametrized) # revealed: set + reveal_type(set_parametrized) # revealed: set[Unknown] # TODO: revealed: frozenset[Unknown] - reveal_type(frozen_set_bare) # revealed: frozenset + reveal_type(frozen_set_bare) # revealed: frozenset[Unknown] # TODO: revealed: frozenset[str] - reveal_type(frozen_set_parametrized) # revealed: frozenset + reveal_type(frozen_set_parametrized) # revealed: frozenset[Unknown] reveal_type(chain_map_bare) # revealed: ChainMap[Unknown, Unknown] # TODO: revealed: ChainMap[str, int] @@ -61,10 +59,9 @@ def f( # TODO: revealed: defaultdict[str, int] reveal_type(default_dict_parametrized) # revealed: defaultdict[Unknown, Unknown] - # TODO: revealed: deque[Unknown] - reveal_type(deque_bare) # revealed: deque + reveal_type(deque_bare) # revealed: deque[Unknown] # TODO: revealed: deque[str] - reveal_type(deque_parametrized) # revealed: deque + reveal_type(deque_parametrized) # revealed: deque[Unknown] reveal_type(ordered_dict_bare) # revealed: OrderedDict[Unknown, Unknown] # TODO: revealed: OrderedDict[int, str] @@ -84,26 +81,23 @@ import typing class ListSubclass(typing.List): ... -# TODO: generic protocols -# revealed: tuple[, , , , , , , , @Todo(`Protocol[]` subscript), typing.Generic, ] +# revealed: tuple[, , , , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], ] reveal_type(ListSubclass.__mro__) class DictSubclass(typing.Dict): ... -# TODO: generic protocols -# revealed: tuple[, , , , , , , @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], ] +# TODO: should not have multiple `Generic[]` elements +# revealed: tuple[, , , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], ] reveal_type(DictSubclass.__mro__) class SetSubclass(typing.Set): ... -# TODO: generic protocols -# revealed: tuple[, , , , , , , @Todo(`Protocol[]` subscript), typing.Generic, ] +# revealed: tuple[, , , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], ] reveal_type(SetSubclass.__mro__) class FrozenSetSubclass(typing.FrozenSet): ... -# TODO: generic protocols -# revealed: tuple[, , , , , , @Todo(`Protocol[]` subscript), typing.Generic, ] +# revealed: tuple[, , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], ] reveal_type(FrozenSetSubclass.__mro__) #################### @@ -112,31 +106,30 @@ reveal_type(FrozenSetSubclass.__mro__) class ChainMapSubclass(typing.ChainMap): ... -# TODO: generic protocols -# revealed: tuple[, , , , , , , @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], ] +# TODO: should not have multiple `Generic[]` elements +# revealed: tuple[, , , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], ] reveal_type(ChainMapSubclass.__mro__) class CounterSubclass(typing.Counter): ... -# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[, , , , , , , , @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], typing.Generic[_T], ] +# TODO: Should have one `Generic[]` element, not three(!) +# revealed: tuple[, , , , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], typing.Generic[_T], ] reveal_type(CounterSubclass.__mro__) class DefaultDictSubclass(typing.DefaultDict): ... -# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[, , , , , , , , @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], ] +# TODO: Should not have multiple `Generic[]` elements +# revealed: tuple[, , , , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], ] reveal_type(DefaultDictSubclass.__mro__) class DequeSubclass(typing.Deque): ... -# TODO: generic protocols -# revealed: tuple[, , , , , , , , @Todo(`Protocol[]` subscript), typing.Generic, ] +# revealed: tuple[, , , , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], ] reveal_type(DequeSubclass.__mro__) class OrderedDictSubclass(typing.OrderedDict): ... -# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[, , , , , , , , @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], ] +# TODO: Should not have multiple `Generic[]` elements +# revealed: tuple[, , , , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], ] reveal_type(OrderedDictSubclass.__mro__) ``` 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 e1df6e97ce..c225220e01 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 @@ -16,7 +16,7 @@ Alias: TypeAlias = int def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: # TODO: should understand the annotation - reveal_type(args) # revealed: tuple + reveal_type(args) # revealed: tuple[Unknown, ...] reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`) @@ -24,7 +24,7 @@ def g() -> TypeGuard[int]: ... def h() -> TypeIs[int]: ... def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co: # TODO: should understand the annotation - reveal_type(args) # revealed: tuple + reveal_type(args) # revealed: tuple[Unknown, ...] reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)] return callback(42, *args, **kwargs) diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index 52c51ea490..3045460ec4 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -61,7 +61,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]] reveal_type(e) # revealed: @Todo(full tuple[...] support) reveal_type(f) # revealed: @Todo(full tuple[...] support) reveal_type(g) # revealed: @Todo(full tuple[...] support) -reveal_type(h) # revealed: tuple[@Todo(specialized non-generic class), @Todo(specialized non-generic class)] +reveal_type(h) # revealed: tuple[list[int], list[int]] reveal_type(i) # revealed: tuple[str | int, str | int] reveal_type(j) # revealed: tuple[str | int] @@ -76,7 +76,7 @@ a: tuple[()] = (1, 2) # error: [invalid-assignment] "Object of type `tuple[Literal["foo"]]` is not assignable to `tuple[int]`" b: tuple[int] = ("foo",) -# error: [invalid-assignment] "Object of type `tuple[list, Literal["foo"]]` is not assignable to `tuple[str | int, str]`" +# error: [invalid-assignment] "Object of type `tuple[list[Unknown], Literal["foo"]]` is not assignable to `tuple[str | int, str]`" c: tuple[str | int, str] = ([], "foo") ``` diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 75af3ec2d8..3c655db8e3 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -1728,7 +1728,7 @@ reveal_type(False.real) # revealed: Literal[0] All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`: ```py -# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(specialized non-generic class), /) -> bytes +# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes reveal_type(b"foo".join) # revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool reveal_type(b"foo".endswith) diff --git a/crates/ty_python_semantic/resources/mdtest/expression/lambda.md b/crates/ty_python_semantic/resources/mdtest/expression/lambda.md index d7d01ed8f5..dbafc217ac 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/lambda.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/lambda.md @@ -79,8 +79,7 @@ lambda x=1: reveal_type(x) # revealed: Unknown | Literal[1] Using a variadic parameter: ```py -# TODO: should be `tuple[Unknown, ...]` (needs generics) -lambda *args: reveal_type(args) # revealed: tuple +lambda *args: reveal_type(args) # revealed: tuple[Unknown, ...] ``` Using a keyword-variadic parameter: diff --git a/crates/ty_python_semantic/resources/mdtest/function/parameters.md b/crates/ty_python_semantic/resources/mdtest/function/parameters.md index b4d7b8cd14..d61a320c62 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/parameters.md +++ b/crates/ty_python_semantic/resources/mdtest/function/parameters.md @@ -25,8 +25,8 @@ def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, reveal_type(f) # revealed: Literal[4] reveal_type(g) # revealed: Unknown | Literal[5] reveal_type(h) # revealed: Literal[6] - # TODO: should be `tuple[object, ...]` (needs generics) - reveal_type(args) # revealed: tuple + # TODO: should be `tuple[object, ...]` + reveal_type(args) # revealed: tuple[Unknown, ...] reveal_type(kwargs) # revealed: dict[str, str] ``` @@ -36,8 +36,7 @@ def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, ```py def g(*args, **kwargs): - # TODO: should be `tuple[Unknown, ...]` (needs generics) - reveal_type(args) # revealed: tuple + reveal_type(args) # revealed: tuple[Unknown, ...] reveal_type(kwargs) # revealed: dict[str, Unknown] ``` 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 4d2b3d5fe9..2ddf11d0d1 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -399,7 +399,7 @@ In a specialized generic alias, the specialization is applied to the attributes class. ```py -from typing import Generic, TypeVar +from typing import Generic, TypeVar, Protocol T = TypeVar("T") U = TypeVar("U") @@ -425,6 +425,33 @@ reveal_type(c.y) # revealed: str reveal_type(c.method1()) # revealed: int reveal_type(c.method2()) # revealed: str reveal_type(c.method3()) # revealed: LinkedList[int] + +class SomeProtocol(Protocol[T]): + x: T + +class Foo: + x: int + +class D(Generic[T, U]): + x: T + y: U + + def method1(self) -> T: + return self.x + + def method2(self) -> U: + return self.y + + def method3(self) -> SomeProtocol[T]: + return Foo() + +d = D[int, str]() +reveal_type(d.x) # revealed: int +reveal_type(d.y) # revealed: str +reveal_type(d.method1()) # revealed: int +reveal_type(d.method2()) # revealed: str +reveal_type(d.method3()) # revealed: SomeProtocol[int] +reveal_type(d.method3().x) # revealed: int ``` ## Cyclic class definitions 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 651c9bee47..35cadfd399 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -38,6 +38,7 @@ T = TypeVar("T") 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")] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 6162470aba..b85d0f8960 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -65,7 +65,7 @@ from typing import Generic, TypeVar T = TypeVar("T") -# error: [invalid-generic-class] "Cannot both inherit from `Generic` and use PEP 695 type variables" +# error: [invalid-generic-class] "Cannot both inherit from `typing.Generic` and use PEP 695 type variables" class BothGenericSyntaxes[U](Generic[T]): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md b/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md index f10b33d40b..d77973265b 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md +++ b/crates/ty_python_semantic/resources/mdtest/import/dunder_all.md @@ -785,7 +785,7 @@ from subexporter import * # TODO: Should be `list[str]` # TODO: Should we avoid including `Unknown` for this case? -reveal_type(__all__) # revealed: Unknown | list +reveal_type(__all__) # revealed: Unknown | list[Unknown] __all__.append("B") diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md index 70c98aa3a6..53915f27c2 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/list.md @@ -3,5 +3,5 @@ ## Empty list ```py -reveal_type([]) # revealed: list +reveal_type([]) # revealed: list[Unknown] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md index 452fc719db..85acd78e3e 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/set.md @@ -3,5 +3,5 @@ ## Basic set ```py -reveal_type({1, 2}) # revealed: set +reveal_type({1, 2}) # revealed: set[Unknown] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index db3aab7bfd..845ef71cdf 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -48,8 +48,8 @@ alice2 = Person2(1, "Alice") # TODO: should be an error Person2(1) -reveal_type(alice2.id) # revealed: @Todo(GenericAlias instance) -reveal_type(alice2.name) # revealed: @Todo(GenericAlias instance) +reveal_type(alice2.id) # revealed: @Todo(functional `NamedTuple` syntax) +reveal_type(alice2.name) # revealed: @Todo(functional `NamedTuple` syntax) ``` ### Multiple Inheritance diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 8fd2f7cfdd..a1cd8842f3 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -133,11 +133,11 @@ match x: case "foo" | 42 | None: reveal_type(x) # revealed: Literal["foo", 42] | None case "foo" | tuple(): - reveal_type(x) # revealed: tuple + reveal_type(x) # revealed: tuple[Unknown, ...] case True | False: reveal_type(x) # revealed: bool case 3.14 | 2.718 | 1.414: - reveal_type(x) # revealed: float & ~tuple + reveal_type(x) # revealed: float & ~tuple[Unknown, ...] reveal_type(x) # revealed: object ``` @@ -155,7 +155,7 @@ reveal_type(x) # revealed: object match x: case "foo" | 42 | None if reveal_type(x): # revealed: Literal["foo", 42] | None pass - case "foo" | tuple() if reveal_type(x): # revealed: Literal["foo"] | tuple + case "foo" | tuple() if reveal_type(x): # revealed: Literal["foo"] | tuple[Unknown, ...] pass case True | False if reveal_type(x): # revealed: bool pass diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 47cd3632ef..28b12d8d85 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -58,6 +58,7 @@ class Bar1(Protocol[T], Generic[T]): class Bar2[T](Protocol): x: T +# error: [invalid-generic-class] "Cannot both inherit from subscripted `typing.Protocol` and use PEP 695 type variables" class Bar3[T](Protocol[T]): x: T ``` @@ -70,8 +71,8 @@ simultaneously: class DuplicateBases(Protocol, Protocol[T]): x: T -# TODO: should not have `Protocol` multiple times -# revealed: tuple[, typing.Protocol, @Todo(`Protocol[]` subscript), typing.Generic, ] +# TODO: should not have `Protocol` or `Generic` multiple times +# revealed: tuple[, typing.Protocol, typing.Generic, typing.Protocol[T], typing.Generic[T], ] reveal_type(DuplicateBases.__mro__) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md index 4448d70428..71c298b8d9 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -13,8 +13,7 @@ reveal_type(__loader__) # revealed: LoaderProtocol | None reveal_type(__package__) # revealed: str | None reveal_type(__doc__) # revealed: str | None reveal_type(__spec__) # revealed: ModuleSpec | None - -reveal_type(__path__) # revealed: @Todo(specialized non-generic class) +reveal_type(__path__) # revealed: MutableSequence[str] class X: reveal_type(__name__) # revealed: str diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/lists.md b/crates/ty_python_semantic/resources/mdtest/subscript/lists.md index d074d1b826..dff1905d0d 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/lists.md @@ -9,13 +9,13 @@ A list can be indexed into with: ```py x = [1, 2, 3] -reveal_type(x) # revealed: list +reveal_type(x) # revealed: list[Unknown] # TODO reveal int reveal_type(x[0]) # revealed: Unknown -# TODO reveal list -reveal_type(x[0:1]) # revealed: @Todo(specialized non-generic class) +# TODO reveal list[int] +reveal_type(x[0:1]) # revealed: list[Unknown] # error: [call-non-callable] reveal_type(x["a"]) # revealed: Unknown diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md index 2e733a8921..20d01d4fb8 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md @@ -83,9 +83,8 @@ python-version = "3.9" ```py class A(tuple[int, str]): ... -# Runtime value: `(A, tuple, object)` -# TODO: Generics -reveal_type(A.__mro__) # revealed: tuple[, @Todo(GenericAlias instance), ] +# revealed: tuple[, , , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], ] +reveal_type(A.__mro__) ``` ## `typing.Tuple` @@ -100,7 +99,7 @@ from typing import Any, Tuple class A: ... def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]): - reveal_type(c) # revealed: tuple + reveal_type(c) # revealed: tuple[Unknown, ...] reveal_type(d) # revealed: tuple[int, A] reveal_type(e) # revealed: @Todo(full tuple[...] support) ``` @@ -115,7 +114,6 @@ from typing import Tuple class C(Tuple): ... -# TODO: generic protocols -# revealed: tuple[, , , , , , , @Todo(`Protocol[]` subscript), typing.Generic, ] +# revealed: tuple[, , , , , , , typing.Protocol[_T_co], typing.Generic[_T_co], ] reveal_type(C.__mro__) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 3132989bda..22fba74fe5 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -151,7 +151,8 @@ static_assert(not is_subtype_of(tuple[B1, B2], tuple[()])) static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1])) static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1, A2, Unrelated])) -static_assert(is_subtype_of(tuple[int], tuple)) +# TODO: should pass +static_assert(is_subtype_of(tuple[int], tuple[object, ...])) # error: [static-assert-error] ``` ## Union types diff --git a/crates/ty_python_semantic/resources/primer/bad.txt b/crates/ty_python_semantic/resources/primer/bad.txt index 23e7e6e43e..d54f0407e8 100644 --- a/crates/ty_python_semantic/resources/primer/bad.txt +++ b/crates/ty_python_semantic/resources/primer/bad.txt @@ -6,14 +6,20 @@ cpython # access to field whilst being initialized, too many cycle iterations discord.py # some kind of hang, only when multi-threaded? freqtrade # hangs hydpy # too many iterations +ibis # too many iterations +jax # too many iterations +packaging # too many iterations pandas # slow pandas-stubs # hangs/slow, or else https://github.com/salsa-rs/salsa/issues/831 pandera # stack overflow +pip # vendors packaging, see above prefect # slow pylint # cycle panics (self-recursive type alias) +pyodide # too many cycle iterations pywin32 # bad use-def map (binding with definitely-visible unbound) schemathesis # https://github.com/salsa-rs/salsa/issues/831 scikit-learn # success, but mypy-primer hangs processing the output +setuptools # vendors packaging, see above spack # success, but mypy-primer hangs processing the output spark # too many iterations steam.py # hangs diff --git a/crates/ty_python_semantic/resources/primer/good.txt b/crates/ty_python_semantic/resources/primer/good.txt index a437e24ade..abaebc99b1 100644 --- a/crates/ty_python_semantic/resources/primer/good.txt +++ b/crates/ty_python_semantic/resources/primer/good.txt @@ -42,13 +42,11 @@ git-revise graphql-core httpx-caching hydra-zen -ibis ignite imagehash isort itsdangerous janus -jax jinja koda-validate kopf @@ -70,11 +68,9 @@ openlibrary operator optuna paasta -packaging paroxython parso pegen -pip poetry porcupine ppb-vector @@ -86,7 +82,6 @@ pydantic pyinstrument pyjwt pylox -pyodide pyp pyppeteer pytest @@ -100,7 +95,6 @@ rotki schema_salad scipy scrapy -setuptools sockeye speedrun.com_global_scoreboard_webapp sphinx diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index ed2f79231e..dcc0932e9a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -662,7 +662,7 @@ impl<'db> Type<'db> { pub fn contains_todo(&self, db: &'db dyn Db) -> bool { match self { - Self::Dynamic(DynamicType::Todo(_) | DynamicType::SubscriptedProtocol) => true, + Self::Dynamic(DynamicType::Todo(_)) => true, Self::AlwaysFalsy | Self::AlwaysTruthy @@ -703,9 +703,7 @@ impl<'db> Type<'db> { } Self::SubclassOf(subclass_of) => match subclass_of.subclass_of() { - SubclassOfInner::Dynamic( - DynamicType::Todo(_) | DynamicType::SubscriptedProtocol, - ) => true, + SubclassOfInner::Dynamic(DynamicType::Todo(_)) => true, SubclassOfInner::Dynamic(DynamicType::Unknown | DynamicType::Any) => false, SubclassOfInner::Class(_) => false, }, @@ -722,12 +720,10 @@ impl<'db> Type<'db> { Self::BoundSuper(bound_super) => { matches!( bound_super.pivot_class(db), - ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::SubscriptedProtocol) + ClassBase::Dynamic(DynamicType::Todo(_)) ) || matches!( bound_super.owner(db), - SuperOwnerKind::Dynamic( - DynamicType::Todo(_) | DynamicType::SubscriptedProtocol - ) + SuperOwnerKind::Dynamic(DynamicType::Todo(_)) ) } @@ -3939,6 +3935,9 @@ impl<'db> Type<'db> { ); Signatures::single(signature) } + Some(KnownClass::NamedTuple) => { + Signatures::single(CallableSignature::todo("functional `NamedTuple` syntax")) + } Some(KnownClass::Object) => { // ```py // class object: @@ -4328,6 +4327,12 @@ impl<'db> Type<'db> { return Ok(UnionType::from_elements(db, tuple_type.elements(db))); } + if let Type::GenericAlias(alias) = self { + if alias.origin(db).is_known(db, KnownClass::Tuple) { + return Ok(todo_type!("*tuple[] annotations")); + } + } + let try_call_dunder_getitem = || { self.try_call_dunder( db, @@ -4826,7 +4831,7 @@ impl<'db> Type<'db> { KnownInstanceType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")), KnownInstanceType::TypedDict => Ok(todo_type!("Support for `typing.TypedDict`")), - KnownInstanceType::Protocol => Err(InvalidTypeExpressionError { + KnownInstanceType::Protocol(_) => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol], fallback_type: Type::unknown(), }), @@ -4931,9 +4936,6 @@ impl<'db> Type<'db> { Some(KnownClass::UnionType) => Ok(todo_type!( "Support for `types.UnionType` instances in type expressions" )), - Some(KnownClass::NamedTuple) => Ok(todo_type!( - "Support for functional `typing.NamedTuple` syntax" - )), _ => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType( *self @@ -5093,6 +5095,10 @@ impl<'db> Type<'db> { instance.apply_type_mapping(db, type_mapping), ), + Type::ProtocolInstance(instance) => { + Type::ProtocolInstance(instance.apply_specialization(db, type_mapping)) + } + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet( function.apply_type_mapping(db, type_mapping), @@ -5176,8 +5182,6 @@ impl<'db> Type<'db> { | Type::StringLiteral(_) | Type::BytesLiteral(_) | Type::BoundSuper(_) - // Same for `ProtocolInstance` - | Type::ProtocolInstance(_) | Type::KnownInstance(_) => self, } } @@ -5498,9 +5502,6 @@ pub enum DynamicType { /// /// This variant should be created with the `todo_type!` macro. Todo(TodoType), - /// Temporary type until we support generic protocols. - /// We use a separate variant (instead of `Todo(…)`) in order to be able to match on them explicitly. - SubscriptedProtocol, } impl std::fmt::Display for DynamicType { @@ -5511,11 +5512,6 @@ impl std::fmt::Display for DynamicType { // `DynamicType::Todo`'s display should be explicit that is not a valid display of // any other type DynamicType::Todo(todo) => write!(f, "@Todo{todo}"), - DynamicType::SubscriptedProtocol => f.write_str(if cfg!(debug_assertions) { - "@Todo(`Protocol[]` subscript)" - } else { - "@Todo" - }), } } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index e06fc9e890..422eea1ea9 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -312,7 +312,7 @@ impl<'db> ClassType<'db> { ClassBase::Dynamic(_) => false, // Protocol and Generic are not represented by a ClassType. - ClassBase::Protocol | ClassBase::Generic(_) => false, + ClassBase::Protocol(_) | ClassBase::Generic(_) => false, ClassBase::Class(base) => match (base, other) { (ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other, @@ -350,7 +350,7 @@ impl<'db> ClassType<'db> { ClassBase::Dynamic(_) => false, // Protocol and Generic are not represented by a ClassType. - ClassBase::Protocol | ClassBase::Generic(_) => false, + ClassBase::Protocol(_) | ClassBase::Generic(_) => false, ClassBase::Class(base) => match (base, other) { (ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other, @@ -536,7 +536,10 @@ impl<'db> ClassLiteral<'db> { pub(crate) fn legacy_generic_context(self, db: &'db dyn Db) -> Option> { self.explicit_bases(db).iter().find_map(|base| match base { - Type::KnownInstance(KnownInstanceType::Generic(generic_context)) => *generic_context, + Type::KnownInstance( + KnownInstanceType::Generic(generic_context) + | KnownInstanceType::Protocol(generic_context), + ) => *generic_context, _ => None, }) } @@ -608,6 +611,17 @@ impl<'db> ClassLiteral<'db> { } } + /// Returns a specialization of this class with a `@Todo`-type + pub(crate) fn todo_specialization(self, db: &'db dyn Db, todo: &'static str) -> ClassType<'db> { + match self.generic_context(db) { + None => ClassType::NonGeneric(self), + Some(generic_context) => { + let specialization = generic_context.todo_specialization(db, todo); + ClassType::Generic(GenericAlias::new(db, self, specialization)) + } + } + } + /// Returns the unknown specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// maps each of the class's typevars to `Unknown`. @@ -678,13 +692,11 @@ impl<'db> ClassLiteral<'db> { // - OR be the last-but-one base (with the final base being `Generic[]` or `object`) // - OR be the last-but-two base (with the penultimate base being `Generic[]` // and the final base being `object`) - self.explicit_bases(db).iter().rev().take(3).any(|base| { - matches!( - base, - Type::KnownInstance(KnownInstanceType::Protocol) - | Type::Dynamic(DynamicType::SubscriptedProtocol) - ) - }) + self.explicit_bases(db) + .iter() + .rev() + .take(3) + .any(|base| matches!(base, Type::KnownInstance(KnownInstanceType::Protocol(_)))) }) } @@ -1011,12 +1023,8 @@ impl<'db> ClassLiteral<'db> { for superclass in mro_iter { match superclass { - ClassBase::Dynamic(DynamicType::SubscriptedProtocol) - | ClassBase::Generic(_) - | ClassBase::Protocol => { - // TODO: We currently skip `Protocol` when looking up class members, in order to - // avoid creating many dynamic types in our test suite that would otherwise - // result from looking up attributes on builtin types like `str`, `list`, `tuple` + ClassBase::Generic(_) | ClassBase::Protocol(_) => { + // Skip over these very special class bases that aren't really classes. } ClassBase::Dynamic(_) => { // Note: calling `Type::from(superclass).member()` would be incorrect here. @@ -1354,12 +1362,8 @@ impl<'db> ClassLiteral<'db> { for superclass in self.iter_mro(db, specialization) { match superclass { - ClassBase::Dynamic(DynamicType::SubscriptedProtocol) - | ClassBase::Generic(_) - | ClassBase::Protocol => { - // TODO: We currently skip these when looking up instance members, in order to - // avoid creating many dynamic types in our test suite that would otherwise - // result from looking up attributes on builtin types like `str`, `list`, `tuple` + ClassBase::Generic(_) | ClassBase::Protocol(_) => { + // Skip over these very special class bases that aren't really classes. } ClassBase::Dynamic(_) => { return SymbolAndQualifiers::todo( diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index be27959c50..551f47dafb 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -18,7 +18,7 @@ pub enum ClassBase<'db> { Class(ClassType<'db>), /// Although `Protocol` is not a class in typeshed's stubs, it is at runtime, /// and can appear in the MRO of a class. - Protocol, + Protocol(Option>), /// Bare `Generic` cannot be subclassed directly in user code, /// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`, /// `Protocol[T]`, or bare `Protocol`. @@ -50,11 +50,17 @@ impl<'db> ClassBase<'db> { ClassBase::Class(ClassType::Generic(alias)) => { write!(f, "", alias.display(self.db)) } - ClassBase::Protocol => f.write_str("typing.Protocol"), + ClassBase::Protocol(generic_context) => { + f.write_str("typing.Protocol")?; + if let Some(generic_context) = generic_context { + generic_context.display(self.db).fmt(f)?; + } + Ok(()) + } ClassBase::Generic(generic_context) => { f.write_str("typing.Generic")?; if let Some(generic_context) = generic_context { - write!(f, "{}", generic_context.display(self.db))?; + generic_context.display(self.db).fmt(f)?; } Ok(()) } @@ -71,9 +77,7 @@ impl<'db> ClassBase<'db> { ClassBase::Dynamic(DynamicType::Any) => "Any", ClassBase::Dynamic(DynamicType::Unknown) => "Unknown", ClassBase::Dynamic(DynamicType::Todo(_)) => "@Todo", - ClassBase::Protocol | ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => { - "Protocol" - } + ClassBase::Protocol(_) => "Protocol", ClassBase::Generic(_) => "Generic", } } @@ -199,7 +203,9 @@ impl<'db> ClassBase<'db> { KnownInstanceType::Callable => { Self::try_from_type(db, todo_type!("Support for Callable as a base class")) } - KnownInstanceType::Protocol => Some(ClassBase::Protocol), + KnownInstanceType::Protocol(generic_context) => { + Some(ClassBase::Protocol(generic_context)) + } KnownInstanceType::Generic(generic_context) => { Some(ClassBase::Generic(generic_context)) } @@ -210,14 +216,14 @@ impl<'db> ClassBase<'db> { pub(super) fn into_class(self) -> Option> { match self { Self::Class(class) => Some(class), - Self::Dynamic(_) | Self::Generic(_) | Self::Protocol => None, + Self::Dynamic(_) | Self::Generic(_) | Self::Protocol(_) => None, } } fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self { match self { Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)), - Self::Dynamic(_) | Self::Generic(_) | Self::Protocol => self, + Self::Dynamic(_) | Self::Generic(_) | Self::Protocol(_) => self, } } @@ -241,7 +247,7 @@ impl<'db> ClassBase<'db> { .try_mro(db, specialization) .is_err_and(MroError::is_cycle) } - ClassBase::Dynamic(_) | ClassBase::Generic(_) | ClassBase::Protocol => false, + ClassBase::Dynamic(_) | ClassBase::Generic(_) | ClassBase::Protocol(_) => false, } } @@ -252,11 +258,8 @@ impl<'db> ClassBase<'db> { additional_specialization: Option>, ) -> impl Iterator> { match self { - ClassBase::Protocol => { - ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(None)) - } - ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => { - ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(None)) + ClassBase::Protocol(context) => { + ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(context)) } ClassBase::Dynamic(_) | ClassBase::Generic(_) => { ClassBaseMroIterator::length_2(db, self) @@ -279,7 +282,9 @@ impl<'db> From> for Type<'db> { match value { ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic), ClassBase::Class(class) => class.into(), - ClassBase::Protocol => Type::KnownInstance(KnownInstanceType::Protocol), + ClassBase::Protocol(generic_context) => { + Type::KnownInstance(KnownInstanceType::Protocol(generic_context)) + } ClassBase::Generic(generic_context) => { Type::KnownInstance(KnownInstanceType::Generic(generic_context)) } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index ea7cb78618..b5bc35e0e2 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -174,7 +174,9 @@ impl Display for DisplayRepresentation<'_> { function = function.name(self.db), specialization = if let Some(specialization) = function.specialization(self.db) { - specialization.display_short(self.db).to_string() + specialization + .display_short(self.db, TupleSpecialization::No) + .to_string() } else { String::new() }, @@ -187,7 +189,9 @@ impl Display for DisplayRepresentation<'_> { function = function.name(self.db), specialization = if let Some(specialization) = function.specialization(self.db) { - specialization.display_short(self.db).to_string() + specialization + .display_short(self.db, TupleSpecialization::No) + .to_string() } else { String::new() }, @@ -274,7 +278,10 @@ impl Display for DisplayGenericAlias<'_> { f, "{origin}{specialization}", origin = self.origin.name(self.db), - specialization = self.specialization.display_short(self.db), + specialization = self.specialization.display_short( + self.db, + TupleSpecialization::from_class(self.db, self.origin) + ), ) } } @@ -327,22 +334,32 @@ impl Display for DisplayGenericContext<'_> { impl<'db> Specialization<'db> { /// Renders the specialization in full, e.g. `{T = int, U = str}`. - pub fn display(&'db self, db: &'db dyn Db) -> DisplaySpecialization<'db> { + pub fn display( + &'db self, + db: &'db dyn Db, + tuple_specialization: TupleSpecialization, + ) -> DisplaySpecialization<'db> { DisplaySpecialization { typevars: self.generic_context(db).variables(db), types: self.types(db), db, full: true, + tuple_specialization, } } /// Renders the specialization as it would appear in a subscript expression, e.g. `[int, str]`. - pub fn display_short(&'db self, db: &'db dyn Db) -> DisplaySpecialization<'db> { + pub fn display_short( + &'db self, + db: &'db dyn Db, + tuple_specialization: TupleSpecialization, + ) -> DisplaySpecialization<'db> { DisplaySpecialization { typevars: self.generic_context(db).variables(db), types: self.types(db), db, full: false, + tuple_specialization, } } } @@ -352,6 +369,7 @@ pub struct DisplaySpecialization<'db> { types: &'db [Type<'db>], db: &'db dyn Db, full: bool, + tuple_specialization: TupleSpecialization, } impl Display for DisplaySpecialization<'_> { @@ -373,11 +391,34 @@ impl Display for DisplaySpecialization<'_> { } ty.display(self.db).fmt(f)?; } + if self.tuple_specialization.is_yes() { + f.write_str(", ...")?; + } f.write_char(']') } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TupleSpecialization { + Yes, + No, +} + +impl TupleSpecialization { + const fn is_yes(self) -> bool { + matches!(self, Self::Yes) + } + + fn from_class(db: &dyn Db, class: ClassLiteral) -> Self { + if class.is_known(db, KnownClass::Tuple) { + Self::Yes + } else { + Self::No + } + } +} + impl<'db> CallableType<'db> { pub(crate) fn display(&'db self, db: &'db dyn Db) -> DisplayCallableType<'db> { DisplayCallableType { diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 5c5b4de549..cacad480c8 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -4,8 +4,8 @@ use rustc_hash::FxHashMap; use crate::semantic_index::SemanticIndex; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ - declaration_type, KnownInstanceType, Type, TypeVarBoundOrConstraints, TypeVarInstance, - TypeVarVariance, UnionType, + declaration_type, todo_type, KnownInstanceType, Type, TypeVarBoundOrConstraints, + TypeVarInstance, TypeVarVariance, UnionType, }; use crate::{Db, FxOrderSet}; @@ -17,6 +17,7 @@ use crate::{Db, FxOrderSet}; pub struct GenericContext<'db> { #[returns(ref)] pub(crate) variables: FxOrderSet>, + pub(crate) origin: GenericContextOrigin, } impl<'db> GenericContext<'db> { @@ -30,7 +31,7 @@ impl<'db> GenericContext<'db> { .iter() .filter_map(|type_param| Self::variable_from_type_param(db, index, type_param)) .collect(); - Self::new(db, variables) + Self::new(db, variables, GenericContextOrigin::TypeParameterList) } fn variable_from_type_param( @@ -76,7 +77,11 @@ impl<'db> GenericContext<'db> { if variables.is_empty() { return None; } - Some(Self::new(db, variables)) + Some(Self::new( + db, + variables, + GenericContextOrigin::LegacyGenericFunction, + )) } /// Creates a generic context from the legacy `TypeVar`s that appear in class's base class @@ -92,7 +97,7 @@ impl<'db> GenericContext<'db> { if variables.is_empty() { return None; } - Some(Self::new(db, variables)) + Some(Self::new(db, variables, GenericContextOrigin::Inherited)) } pub(crate) fn len(self, db: &'db dyn Db) -> usize { @@ -133,6 +138,20 @@ impl<'db> GenericContext<'db> { self.specialize_partial(db, &vec![None; self.variables(db).len()]) } + #[allow(unused_variables)] // Only unused in release builds + pub(crate) fn todo_specialization( + self, + db: &'db dyn Db, + todo: &'static str, + ) -> Specialization<'db> { + let types = self + .variables(db) + .iter() + .map(|typevar| typevar.default_ty(db).unwrap_or(todo_type!(todo))) + .collect(); + self.specialize(db, types) + } + pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> { let types = self .variables(db) @@ -209,6 +228,58 @@ impl<'db> GenericContext<'db> { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub enum GenericContextOrigin { + LegacyBase(LegacyGenericBase), + Inherited, + LegacyGenericFunction, + TypeParameterList, +} + +impl GenericContextOrigin { + pub(crate) const fn as_str(self) -> &'static str { + match self { + Self::LegacyBase(base) => base.as_str(), + Self::Inherited => "inherited", + Self::LegacyGenericFunction => "legacy generic function", + Self::TypeParameterList => "type parameter list", + } + } +} + +impl std::fmt::Display for GenericContextOrigin { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub enum LegacyGenericBase { + Generic, + Protocol, +} + +impl LegacyGenericBase { + pub(crate) const fn as_str(self) -> &'static str { + match self { + Self::Generic => "`typing.Generic`", + Self::Protocol => "subscripted `typing.Protocol`", + } + } +} + +impl std::fmt::Display for LegacyGenericBase { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl From for GenericContextOrigin { + fn from(base: LegacyGenericBase) -> Self { + Self::LegacyBase(base) + } +} + /// An assignment of a specific type to each type variable in a generic scope. /// /// TODO: Handle nested specializations better, with actual parent links to the specialization of diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 7d8530f0ec..e071c60306 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -107,6 +107,7 @@ use super::diagnostic::{ report_unresolved_reference, INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, REDUNDANT_CAST, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, }; +use super::generics::{GenericContextOrigin, LegacyGenericBase}; use super::slots::check_class_slots; use super::string_annotation::{ parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, @@ -1020,14 +1021,16 @@ impl<'db> TypeInferenceBuilder<'db> { // (5) Check that a generic class does not have invalid or conflicting generic // contexts. - if class.pep695_generic_context(self.db()).is_some() - && class.legacy_generic_context(self.db()).is_some() - { - if let Some(builder) = self.context.report_lint(&INVALID_GENERIC_CLASS, class_node) - { - builder.into_diagnostic( - "Cannot both inherit from `Generic` and use PEP 695 type variables", - ); + if class.pep695_generic_context(self.db()).is_some() { + if let Some(legacy_context) = class.legacy_generic_context(self.db()) { + if let Some(builder) = + self.context.report_lint(&INVALID_GENERIC_CLASS, class_node) + { + builder.into_diagnostic(format_args!( + "Cannot both inherit from {} and use PEP 695 type variables", + legacy_context.origin(self.db()) + )); + } } } @@ -4734,6 +4737,7 @@ impl<'db> TypeInferenceBuilder<'db> { | KnownClass::Property | KnownClass::Super | KnownClass::TypeVar + | KnownClass::NamedTuple ) ) // temporary special-casing for all subclasses of `enum.Enum` @@ -5795,8 +5799,6 @@ impl<'db> TypeInferenceBuilder<'db> { | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), (todo @ Type::Dynamic(DynamicType::Todo(_)), _, _) | (_, todo @ Type::Dynamic(DynamicType::Todo(_)), _) => Some(todo), - (todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _, _) - | (_, todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _) => Some(todo), (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( @@ -6884,6 +6886,13 @@ impl<'db> TypeInferenceBuilder<'db> { // special cases, too. let value_ty = self.infer_expression(value); if let Type::ClassLiteral(class) = value_ty { + if class.is_known(self.db(), KnownClass::Tuple) { + self.infer_expression(slice); + // TODO heterogeneous and homogeneous tuples in value expressions + return Type::from( + class.todo_specialization(self.db(), "Generic tuple specializations"), + ); + } if let Some(generic_context) = class.generic_context(self.db()) { return self.infer_explicit_class_specialization( subscript, @@ -7072,14 +7081,44 @@ impl<'db> TypeInferenceBuilder<'db> { value_ty, Type::IntLiteral(i64::from(bool)), ), - (Type::KnownInstance(KnownInstanceType::Protocol), _, _) => { - Type::Dynamic(DynamicType::SubscriptedProtocol) + (Type::KnownInstance(KnownInstanceType::Protocol(None)), Type::Tuple(typevars), _) => { + self.legacy_generic_class_context( + value_node, + typevars.elements(self.db()), + LegacyGenericBase::Protocol, + ) + .map(|context| Type::KnownInstance(KnownInstanceType::Protocol(Some(context)))) + .unwrap_or_else(Type::unknown) + } + (Type::KnownInstance(KnownInstanceType::Protocol(None)), typevar, _) => self + .legacy_generic_class_context( + value_node, + std::slice::from_ref(&typevar), + LegacyGenericBase::Protocol, + ) + .map(|context| Type::KnownInstance(KnownInstanceType::Protocol(Some(context)))) + .unwrap_or_else(Type::unknown), + (Type::KnownInstance(KnownInstanceType::Protocol(Some(_))), _, _) => { + // TODO: emit a diagnostic + todo_type!("doubly-specialized typing.Protocol") } (Type::KnownInstance(KnownInstanceType::Generic(None)), Type::Tuple(typevars), _) => { - self.infer_subscript_legacy_generic_class(value_node, typevars.elements(self.db())) + self.legacy_generic_class_context( + value_node, + typevars.elements(self.db()), + LegacyGenericBase::Generic, + ) + .map(|context| Type::KnownInstance(KnownInstanceType::Generic(Some(context)))) + .unwrap_or_else(Type::unknown) } (Type::KnownInstance(KnownInstanceType::Generic(None)), typevar, _) => self - .infer_subscript_legacy_generic_class(value_node, std::slice::from_ref(&typevar)), + .legacy_generic_class_context( + value_node, + std::slice::from_ref(&typevar), + LegacyGenericBase::Generic, + ) + .map(|context| Type::KnownInstance(KnownInstanceType::Generic(Some(context)))) + .unwrap_or_else(Type::unknown), (Type::KnownInstance(KnownInstanceType::Generic(Some(_))), _, _) => { // TODO: emit a diagnostic todo_type!("doubly-specialized typing.Generic") @@ -7238,11 +7277,12 @@ impl<'db> TypeInferenceBuilder<'db> { } } - fn infer_subscript_legacy_generic_class( + fn legacy_generic_class_context( &mut self, value_node: &ast::Expr, typevars: &[Type<'db>], - ) -> Type<'db> { + origin: LegacyGenericBase, + ) -> Option> { let typevars: Option> = typevars .iter() .map(|typevar| match typevar { @@ -7252,7 +7292,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node) { builder.into_diagnostic(format_args!( - "`{}` is not a valid argument to `typing.Generic`", + "`{}` is not a valid argument to {origin}", typevar.display(self.db()), )); } @@ -7260,11 +7300,9 @@ impl<'db> TypeInferenceBuilder<'db> { } }) .collect(); - let Some(typevars) = typevars else { - return Type::unknown(); - }; - let generic_context = GenericContext::new(self.db(), typevars); - Type::KnownInstance(KnownInstanceType::Generic(Some(generic_context))) + typevars.map(|typevars| { + GenericContext::new(self.db(), typevars, GenericContextOrigin::from(origin)) + }) } fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { @@ -8420,9 +8458,14 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_type_expression(arguments_slice); todo_type!("`Unpack[]` special form") } - KnownInstanceType::Protocol => { + KnownInstanceType::Protocol(_) => { self.infer_type_expression(arguments_slice); - Type::Dynamic(DynamicType::SubscriptedProtocol) + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "`typing.Protocol` is not allowed in type expressions", + )); + } + Type::unknown() } KnownInstanceType::Generic(_) => { self.infer_type_expression(arguments_slice); diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index c834927d6b..59de727467 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -260,6 +260,21 @@ impl<'db> ProtocolInstanceType<'db> { .unwrap_or_else(|| KnownClass::Object.to_instance(db).instance_member(db, name)), } } + + pub(super) fn apply_specialization<'a>( + self, + db: &'db dyn Db, + type_mapping: TypeMapping<'a, 'db>, + ) -> Self { + match self.0 { + Protocol::FromClass(class) => Self(Protocol::FromClass( + class.apply_type_mapping(db, type_mapping), + )), + Protocol::Synthesized(synthesized) => Self(Protocol::Synthesized( + synthesized.apply_type_mapping(db, type_mapping), + )), + } + } } /// An enumeration of the two kinds of protocol types: those that originate from a class @@ -287,6 +302,7 @@ impl<'db> Protocol<'db> { mod synthesized_protocol { use crate::db::Db; + use crate::types::generics::TypeMapping; use crate::types::protocol_class::ProtocolInterface; /// A "synthesized" protocol type that is dissociated from a class definition in source code. @@ -306,6 +322,14 @@ mod synthesized_protocol { Self(interface.normalized(db)) } + pub(super) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: TypeMapping<'a, 'db>, + ) -> Self { + Self(self.0.specialized_and_normalized(db, type_mapping)) + } + pub(in crate::types) fn interface(self) -> ProtocolInterface<'db> { self.0 } diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index 41849ddc65..a9966fc2a4 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -60,7 +60,7 @@ pub enum KnownInstanceType<'db> { /// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`) OrderedDict, /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) - Protocol, + Protocol(Option>), /// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`) Generic(Option>), /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) @@ -146,7 +146,7 @@ impl<'db> KnownInstanceType<'db> { | Self::Deque | Self::ChainMap | Self::OrderedDict - | Self::Protocol + | Self::Protocol(_) | Self::Generic(_) | Self::ReadOnly | Self::TypeAliasType(_) @@ -203,7 +203,7 @@ impl<'db> KnownInstanceType<'db> { Self::Deque => KnownClass::StdlibAlias, Self::ChainMap => KnownClass::StdlibAlias, Self::OrderedDict => KnownClass::StdlibAlias, - Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says + Self::Protocol(_) => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says Self::Generic(_) => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says Self::TypeVar(_) => KnownClass::TypeVar, Self::TypeAliasType(_) => KnownClass::TypeAliasType, @@ -249,7 +249,7 @@ impl<'db> KnownInstanceType<'db> { "ChainMap" => Self::ChainMap, "OrderedDict" => Self::OrderedDict, "Generic" => Self::Generic(None), - "Protocol" => Self::Protocol, + "Protocol" => Self::Protocol(None), "Optional" => Self::Optional, "Union" => Self::Union, "NoReturn" => Self::NoReturn, @@ -311,7 +311,7 @@ impl<'db> KnownInstanceType<'db> { | Self::Generic(_) | Self::Callable => module.is_typing(), Self::Annotated - | Self::Protocol + | Self::Protocol(_) | Self::Literal | Self::LiteralString | Self::Never @@ -384,11 +384,17 @@ impl Display for KnownInstanceRepr<'_> { KnownInstanceType::Deque => f.write_str("typing.Deque"), KnownInstanceType::ChainMap => f.write_str("typing.ChainMap"), KnownInstanceType::OrderedDict => f.write_str("typing.OrderedDict"), - KnownInstanceType::Protocol => f.write_str("typing.Protocol"), + KnownInstanceType::Protocol(generic_context) => { + f.write_str("typing.Protocol")?; + if let Some(generic_context) = generic_context { + generic_context.display(self.db).fmt(f)?; + } + Ok(()) + } KnownInstanceType::Generic(generic_context) => { f.write_str("typing.Generic")?; if let Some(generic_context) = generic_context { - write!(f, "{}", generic_context.display(self.db))?; + generic_context.display(self.db).fmt(f)?; } Ok(()) } diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 8167337669..6919623464 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -174,7 +174,9 @@ impl<'db> Mro<'db> { continue; } match base { - ClassBase::Class(_) | ClassBase::Generic(_) | ClassBase::Protocol => { + ClassBase::Class(_) + | ClassBase::Generic(_) + | ClassBase::Protocol(_) => { errors.push(DuplicateBaseError { duplicate_base: base, first_index: *first_index, diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index da43422d14..871b6d8351 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -8,7 +8,7 @@ use crate::{ db::Db, semantic_index::{symbol_table, use_def_map}, symbol::{symbol_from_bindings, symbol_from_declarations}, - types::{ClassBase, ClassLiteral, KnownFunction, Type, TypeQualifiers}, + types::{ClassBase, ClassLiteral, KnownFunction, Type, TypeMapping, TypeQualifiers}, }; impl<'db> ClassLiteral<'db> { @@ -146,6 +146,29 @@ impl<'db> ProtocolInterface<'db> { Self::SelfReference => Self::SelfReference, } } + + pub(super) fn specialized_and_normalized<'a>( + self, + db: &'db dyn Db, + type_mapping: TypeMapping<'a, 'db>, + ) -> Self { + match self { + Self::Members(members) => Self::Members(ProtocolInterfaceMembers::new( + db, + members + .inner(db) + .iter() + .map(|(name, data)| { + ( + name.clone(), + data.apply_type_mapping(db, type_mapping).normalized(db), + ) + }) + .collect::>(), + )), + Self::SelfReference => Self::SelfReference, + } + } } #[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)] @@ -161,6 +184,13 @@ impl<'db> ProtocolMemberData<'db> { qualifiers: self.qualifiers, } } + + fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self { + Self { + ty: self.ty.apply_type_mapping(db, type_mapping), + qualifiers: self.qualifiers, + } + } } /// A single member of a protocol interface. diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index c61ca001fd..2099badb0d 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -153,11 +153,15 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(&right), (ClassBase::Class(_), _) => Ordering::Less, (_, ClassBase::Class(_)) => Ordering::Greater, - (ClassBase::Protocol, _) => Ordering::Less, - (_, ClassBase::Protocol) => Ordering::Greater, + + (ClassBase::Protocol(left), ClassBase::Protocol(right)) => left.cmp(&right), + (ClassBase::Protocol(_), _) => Ordering::Less, + (_, ClassBase::Protocol(_)) => Ordering::Greater, + (ClassBase::Generic(left), ClassBase::Generic(right)) => left.cmp(&right), (ClassBase::Generic(_), _) => Ordering::Less, (_, ClassBase::Generic(_)) => Ordering::Greater, + (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { dynamic_elements_ordering(left, right) } @@ -253,8 +257,11 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (KnownInstanceType::Generic(_), _) => Ordering::Less, (_, KnownInstanceType::Generic(_)) => Ordering::Greater, - (KnownInstanceType::Protocol, _) => Ordering::Less, - (_, KnownInstanceType::Protocol) => Ordering::Greater, + (KnownInstanceType::Protocol(left), KnownInstanceType::Protocol(right)) => { + left.cmp(right) + } + (KnownInstanceType::Protocol(_), _) => Ordering::Less, + (_, KnownInstanceType::Protocol(_)) => Ordering::Greater, (KnownInstanceType::NoReturn, _) => Ordering::Less, (_, KnownInstanceType::NoReturn) => Ordering::Greater, @@ -379,8 +386,5 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering #[cfg(not(debug_assertions))] (DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal, - - (DynamicType::SubscriptedProtocol, _) => Ordering::Less, - (_, DynamicType::SubscriptedProtocol) => Ordering::Greater, } }