diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 741f9b89b2..6be6a8d9ec 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -183,33 +183,27 @@ def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... class SingleElementTuple(tuple[int]): ... def _(args: SingleElementTuple) -> None: - # TODO: we should emit `[too-many-positional-arguments]` here - takes_zero(*args) + takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) - # TODO: we should emit `[missing-argument]` on both of these - takes_two(*args) - takes_two_positional_only(*args) + takes_two(*args) # error: [missing-argument] + takes_two_positional_only(*args) # error: [missing-argument] - # TODO: these should both be `[missing-argument]`, not `[invalid-argument-type]` - takes_two_different(*args) # error: [invalid-argument-type] - takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) # error: [missing-argument] + takes_two_different_positional_only(*args) # error: [missing-argument] takes_at_least_zero(*args) takes_at_least_one(*args) - # TODO: we should emit `[missing-argument]` on both of these - takes_at_least_two(*args) - takes_at_least_two_positional_only(*args) + takes_at_least_two(*args) # error: [missing-argument] + takes_at_least_two_positional_only(*args) # error: [missing-argument] class TwoElementIntTuple(tuple[int, int]): ... def _(args: TwoElementIntTuple) -> None: - # TODO: we should emit `[too-many-positional-arguments]` on both of these - takes_zero(*args) - takes_one(*args) - + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) takes_two_positional_only(*args) takes_two_different(*args) # error: [invalid-argument-type] @@ -222,40 +216,23 @@ def _(args: TwoElementIntTuple) -> None: class IntStrTuple(tuple[int, str]): ... def _(args: IntStrTuple) -> None: - # TODO: we should emit `[too-many-positional-arguments]` here - takes_zero(*args) + takes_zero(*args) # error: [too-many-positional-arguments] - # TODO: this should be `[too-many-positional-arguments]`, not `[invalid-argument-type]` - takes_one(*args) # error: [invalid-argument-type] + takes_one(*args) # error: [too-many-positional-arguments] - # TODO: we should have one diagnostic for each of these, not two - # error: [invalid-argument-type] # error: [invalid-argument-type] takes_two(*args) # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_positional_only(*args) - # TODO: these are all false positives - # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_different(*args) - # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_different_positional_only(*args) - takes_at_least_zero(*args) - - # TODO: false positive - # error: [invalid-argument-type] takes_at_least_one(*args) - # TODO: we should only emit one diagnostic for each of these, not two - # error: [invalid-argument-type] # error: [invalid-argument-type] takes_at_least_two(*args) # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_at_least_two_positional_only(*args) ``` @@ -377,9 +354,7 @@ def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... class IntStarInt(tuple[int, *tuple[int, ...]]): ... def _(args: IntStarInt) -> None: - # TODO: we should emit `[too-many-positional-arguments]` here - takes_zero(*args) - + takes_zero(*args) # error: [too-many-positional-arguments] takes_one(*args) takes_two(*args) takes_two_positional_only(*args) @@ -393,50 +368,32 @@ def _(args: IntStarInt) -> None: class IntStarStr(tuple[int, *tuple[str, ...]]): ... def _(args: IntStarStr) -> None: - # TODO: we should emit `[too-many-positional-arguments]` here - takes_zero(*args) + takes_zero(*args) # error: [too-many-positional-arguments] - # TODO: false positive - # error: [invalid-argument-type] takes_one(*args) - # TODO: we should only emit one diagnostic for each of these, not two - # error: [invalid-argument-type] # error: [invalid-argument-type] takes_two(*args) # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_positional_only(*args) - # TODO: false positives - # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_different(*args) - # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_different_positional_only(*args) takes_at_least_zero(*args) - # TODO: false positive - # error: [invalid-argument-type] takes_at_least_one(*args) - # TODO: we should only have one diagnostic for each of these, not two - # error: [invalid-argument-type] # error: [invalid-argument-type] takes_at_least_two(*args) # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_at_least_two_positional_only(*args) class IntIntStarInt(tuple[int, int, *tuple[int, ...]]): ... def _(args: IntIntStarInt) -> None: - # TODO: we should emit `[too-many-positional-arguments]` on both of these - takes_zero(*args) - takes_one(*args) - + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) takes_two_positional_only(*args) takes_two_different(*args) # error: [invalid-argument-type] @@ -449,51 +406,31 @@ def _(args: IntIntStarInt) -> None: class IntIntStarStr(tuple[int, int, *tuple[str, ...]]): ... def _(args: IntIntStarStr) -> None: - # TODO: we should emit `[too-many-positional-arguments]` here - takes_zero(*args) + takes_zero(*args) # error: [too-many-positional-arguments] - # TODO: this should be `[too-many-positional-arguments]`, not `invalid-argument-type` - takes_one(*args) # error: [invalid-argument-type] + takes_one(*args) # error: [too-many-positional-arguments] - # TODO: these are all false positives - # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two(*args) - # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_positional_only(*args) - # TODO: each of these should only have one diagnostic, not two - # error: [invalid-argument-type] # error: [invalid-argument-type] takes_two_different(*args) # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_different_positional_only(*args) takes_at_least_zero(*args) - # TODO: false positive - # error: [invalid-argument-type] takes_at_least_one(*args) - # TODO: these are both false positives - # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_at_least_two(*args) - # TODO: these are both false positives - # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_at_least_two_positional_only(*args) class IntStarIntInt(tuple[int, *tuple[int, ...], int]): ... def _(args: IntStarIntInt) -> None: - # TODO: we should emit `[too-many-positional-arguments]` on both of these - takes_zero(*args) - takes_one(*args) - + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] takes_two(*args) takes_two_positional_only(*args) takes_two_different(*args) # error: [invalid-argument-type] @@ -506,40 +443,25 @@ def _(args: IntStarIntInt) -> None: class IntStarStrInt(tuple[int, *tuple[str, ...], int]): ... def _(args: IntStarStrInt) -> None: - # TODO: we should emit `too-many-positional-arguments` here - takes_zero(*args) + takes_zero(*args) # error: [too-many-positional-arguments] - # TODO: this should be `too-many-positional-arguments`, not `invalid-argument-type` - takes_one(*args) # error: [invalid-argument-type] + takes_one(*args) # error: [too-many-positional-arguments] - # TODO: we should only emit one diagnostic for each of these - # error: [invalid-argument-type] # error: [invalid-argument-type] takes_two(*args) # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_positional_only(*args) - # TODO: we should not emit diagnostics for these - # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_different(*args) - # error: [invalid-argument-type] - # error: [invalid-argument-type] takes_two_different_positional_only(*args) takes_at_least_zero(*args) - # TODO: false positive - takes_at_least_one(*args) # error: [invalid-argument-type] + takes_at_least_one(*args) - # TODO: should only have one diagnostic here - # error: [invalid-argument-type] # error: [invalid-argument-type] takes_at_least_two(*args) - # TODO: should only have one diagnostic here - # error: [invalid-argument-type] # error: [invalid-argument-type] takes_at_least_two_positional_only(*args) ``` 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 2435f66730..0a60e08d85 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -411,10 +411,9 @@ def test_seq(x: Sequence[T]) -> Sequence[T]: return x def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: tuple[()]): - # TODO: should be `Sequence[int | float | complex | list[int]]` - reveal_type(test_seq(t1)) # revealed: Sequence[Unknown] - # TODO: should be `Sequence[int | str]` - reveal_type(test_seq(t2)) # revealed: Sequence[Unknown] + reveal_type(test_seq(t1)) # revealed: Sequence[int | float | complex | list[int]] + reveal_type(test_seq(t2)) # revealed: Sequence[int | str] + # TODO: this should be `Sequence[Never]` reveal_type(test_seq(t3)) # revealed: Sequence[Unknown] ``` 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 bef2ca2ecc..9b82e4ff9a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -367,10 +367,9 @@ def test_seq[T](x: Sequence[T]) -> Sequence[T]: return x def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: tuple[()]): - # TODO: should be `Sequence[int | float | complex | list[int]]` - reveal_type(test_seq(t1)) # revealed: Sequence[Unknown] - # TODO: should be `Sequence[int | str]` - reveal_type(test_seq(t2)) # revealed: Sequence[Unknown] + reveal_type(test_seq(t1)) # revealed: Sequence[int | float | complex | list[int]] + reveal_type(test_seq(t2)) # revealed: Sequence[int | str] + # TODO: this should be `Sequence[Never]` reveal_type(test_seq(t3)) # revealed: Sequence[Unknown] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md b/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md index e436f401fb..6de0978fc9 100644 --- a/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md +++ b/crates/ty_python_semantic/resources/mdtest/mdtest_custom_typeshed.md @@ -23,6 +23,7 @@ We can then place custom stub files in `/typeshed/stdlib`, for example: ```pyi class object: ... +class tuple: ... class BuiltinClass: ... builtin_symbol: BuiltinClass @@ -97,6 +98,12 @@ simple untyped definition is enough to make `reveal_type` work in tests: typeshed = "/typeshed" ``` +`/typeshed/stdlib/builtins.pyi`: + +```pyi +class tuple: ... +``` + `/typeshed/stdlib/typing_extensions.pyi`: ```pyi @@ -104,5 +111,5 @@ def reveal_type(obj, /): ... ``` ```py -reveal_type(()) # revealed: tuple[()] +reveal_type(()) # revealed: tuple ``` 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 7a438b9985..ddc5527bf8 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -193,6 +193,7 @@ typeshed = "/typeshed" ```pyi class object: ... +class tuple: ... class int: ... class bytes: ... diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md index 0dae5486d4..45a9b61b3c 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md @@ -39,9 +39,13 @@ class HeterogeneousSubclass0(tuple[()]): ... reveal_type(HeterogeneousSubclass0.__getitem__) def f0(h0: HeterogeneousSubclass0, i: int): - reveal_type(h0[0]) # revealed: Never - reveal_type(h0[1]) # revealed: Never - reveal_type(h0[-1]) # revealed: Never + # error: [index-out-of-bounds] + reveal_type(h0[0]) # revealed: Unknown + # error: [index-out-of-bounds] + reveal_type(h0[1]) # revealed: Unknown + # error: [index-out-of-bounds] + reveal_type(h0[-1]) # revealed: Unknown + reveal_type(h0[i]) # revealed: Never class HeterogeneousSubclass1(tuple[I0]): ... @@ -51,7 +55,8 @@ reveal_type(HeterogeneousSubclass1.__getitem__) def f0(h1: HeterogeneousSubclass1, i: int): reveal_type(h1[0]) # revealed: I0 - reveal_type(h1[1]) # revealed: I0 + # error: [index-out-of-bounds] + reveal_type(h1[1]) # revealed: Unknown reveal_type(h1[-1]) # revealed: I0 reveal_type(h1[i]) # revealed: I0 @@ -84,25 +89,19 @@ def g(m: MixedSubclass, i: int): reveal_type(m[2]) # revealed: I1 | I2 | I3 reveal_type(m[3]) # revealed: I1 | I2 | I3 reveal_type(m[4]) # revealed: I1 | I2 | I3 | I5 + reveal_type(m[5]) # revealed: I1 | I2 | I3 | I5 + reveal_type(m[10]) # revealed: I1 | I2 | I3 | I5 reveal_type(m[-1]) # revealed: I5 reveal_type(m[-2]) # revealed: I2 reveal_type(m[-3]) # revealed: I3 reveal_type(m[-4]) # revealed: I2 - reveal_type(m[-5]) # revealed: I1 | I0 + reveal_type(m[-5]) # revealed: I0 | I1 + reveal_type(m[-6]) # revealed: I0 | I1 + reveal_type(m[-10]) # revealed: I0 | I1 reveal_type(m[i]) # revealed: I0 | I1 | I2 | I3 | I5 - # Ideally we would not include `I0` in the unions for these, - # but it's not possible to do this using only synthesized overloads. - reveal_type(m[5]) # revealed: I0 | I1 | I2 | I3 | I5 - reveal_type(m[10]) # revealed: I0 | I1 | I2 | I3 | I5 - - # Similarly, ideally these would just be `I0` | I1`, - # but achieving that with only synthesized overloads wouldn't be possible - reveal_type(m[-6]) # revealed: I0 | I1 | I2 | I3 | I5 - reveal_type(m[-10]) # revealed: I0 | I1 | I2 | I3 | I5 - class MixedSubclass2(tuple[I0, I1, *tuple[I2, ...], I3]): ... # revealed: Overload[(self, index: Literal[0], /) -> I0, (self, index: Literal[-2], /) -> I2 | I1, (self, index: Literal[1], /) -> I1, (self, index: Literal[-3], /) -> I2 | I1 | I0, (self, index: Literal[-1], /) -> I3, (self, index: Literal[2], /) -> I2 | I3, (self, index: SupportsIndex, /) -> I0 | I1 | I2 | I3, (self, index: slice[Any, Any, Any], /) -> tuple[I0 | I1 | I2 | I3, ...]] @@ -112,18 +111,12 @@ def g(m: MixedSubclass2, i: int): reveal_type(m[0]) # revealed: I0 reveal_type(m[1]) # revealed: I1 reveal_type(m[2]) # revealed: I2 | I3 - - # Ideally this would just be `I2 | I3`, - # but that's not possible to achieve with synthesized overloads - reveal_type(m[3]) # revealed: I0 | I1 | I2 | I3 + reveal_type(m[3]) # revealed: I2 | I3 reveal_type(m[-1]) # revealed: I3 - reveal_type(m[-2]) # revealed: I2 | I1 - reveal_type(m[-3]) # revealed: I2 | I1 | I0 - - # Ideally this would just be `I2 | I1 | I0`, - # but that's not possible to achieve with synthesized overloads - reveal_type(m[-4]) # revealed: I0 | I1 | I2 | I3 + reveal_type(m[-2]) # revealed: I1 | I2 + reveal_type(m[-3]) # revealed: I0 | I1 | I2 + reveal_type(m[-4]) # revealed: I0 | I1 | I2 ``` The stdlib API `os.stat` is a commonly used API that returns an instance of a tuple subclass @@ -225,75 +218,47 @@ class I3: ... class HeterogeneousTupleSubclass(tuple[I0, I1, I2, I3]): ... def __(t: HeterogeneousTupleSubclass, m: int, n: int): - # TODO: should be `tuple[()]` - reveal_type(t[0:0]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I0]` - reveal_type(t[0:1]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I0, I1]` - reveal_type(t[0:2]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be tuple[I0, I1, I2, I3]` - reveal_type(t[0:4]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be tuple[I0, I1, I2, I3]` - reveal_type(t[0:5]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I0, I1]` - reveal_type(t[1:3]) # revealed: tuple[I0 | I1 | I2 | I3, ...] + reveal_type(t[0:0]) # revealed: tuple[()] + reveal_type(t[0:1]) # revealed: tuple[I0] + reveal_type(t[0:2]) # revealed: tuple[I0, I1] + reveal_type(t[0:4]) # revealed: tuple[I0, I1, I2, I3] + reveal_type(t[0:5]) # revealed: tuple[I0, I1, I2, I3] + reveal_type(t[1:3]) # revealed: tuple[I1, I2] - # TODO: should be `tuple[I2, I3]` - reveal_type(t[-2:4]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I1, I2]` - reveal_type(t[-3:-1]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I0, I1, I2, I3]` - reveal_type(t[-10:10]) # revealed: tuple[I0 | I1 | I2 | I3, ...] + reveal_type(t[-2:4]) # revealed: tuple[I2, I3] + reveal_type(t[-3:-1]) # revealed: tuple[I1, I2] + reveal_type(t[-10:10]) # revealed: tuple[I0, I1, I2, I3] - # TODO: should be `tuple[I0, I1, I2, I3]` - reveal_type(t[0:]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I2, I3]` - reveal_type(t[2:]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[()]` - reveal_type(t[4:]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[()]` - reveal_type(t[:0]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I0, I1]` - reveal_type(t[:2]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I0, I1, I2, I3]` - reveal_type(t[:10]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I0, I1, I2, I3]` - reveal_type(t[:]) # revealed: tuple[I0 | I1 | I2 | I3, ...] + reveal_type(t[0:]) # revealed: tuple[I0, I1, I2, I3] + reveal_type(t[2:]) # revealed: tuple[I2, I3] + reveal_type(t[4:]) # revealed: tuple[()] + reveal_type(t[:0]) # revealed: tuple[()] + reveal_type(t[:2]) # revealed: tuple[I0, I1] + reveal_type(t[:10]) # revealed: tuple[I0, I1, I2, I3] + reveal_type(t[:]) # revealed: tuple[I0, I1, I2, I3] - # TODO: should be `tuple[I3, I2, I1, I0]` - reveal_type(t[::-1]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I0, I2]` - reveal_type(t[::2]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I2, I1, I0]` - reveal_type(t[-2:-5:-1]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I3, I1]` - reveal_type(t[::-2]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I3, I0]` - reveal_type(t[-1::-3]) # revealed: tuple[I0 | I1 | I2 | I3, ...] + reveal_type(t[::-1]) # revealed: tuple[I3, I2, I1, I0] + reveal_type(t[::2]) # revealed: tuple[I0, I2] + reveal_type(t[-2:-5:-1]) # revealed: tuple[I2, I1, I0] + reveal_type(t[::-2]) # revealed: tuple[I3, I1] + reveal_type(t[-1::-3]) # revealed: tuple[I3, I0] - # TODO: should be `tuple[I0, I1]` - reveal_type(t[None:2:None]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I1, I2, I3]` - reveal_type(t[1:None:1]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I0, I1, I2, I3]` - reveal_type(t[None:None:None]) # revealed: tuple[I0 | I1 | I2 | I3, ...] + reveal_type(t[None:2:None]) # revealed: tuple[I0, I1] + reveal_type(t[1:None:1]) # revealed: tuple[I1, I2, I3] + reveal_type(t[None:None:None]) # revealed: tuple[I0, I1, I2, I3] start = 1 stop = None step = 2 - # TODO: should be `tuple[I1, I3]` - reveal_type(t[start:stop:step]) # revealed: tuple[I0 | I1 | I2 | I3, ...] + reveal_type(t[start:stop:step]) # revealed: tuple[I1, I3] - # TODO: should be `tuple[I0]` - reveal_type(t[False:True]) # revealed: tuple[I0 | I1 | I2 | I3, ...] - # TODO: should be `tuple[I1, I2]` - reveal_type(t[True:3]) # revealed: tuple[I0 | I1 | I2 | I3, ...] + reveal_type(t[False:True]) # revealed: tuple[I0] + reveal_type(t[True:3]) # revealed: tuple[I1, I2] - # TODO: we should emit `zero-stepsize-in-slice` on all of these: - t[0:4:0] - t[:4:0] - t[0::0] - t[::0] + t[0:4:0] # error: [zero-stepsize-in-slice] + t[:4:0] # error: [zero-stepsize-in-slice] + t[0::0] # error: [zero-stepsize-in-slice] + t[::0] # error: [zero-stepsize-in-slice] tuple_slice = t[m:n] reveal_type(tuple_slice) # revealed: tuple[I0 | I1 | I2 | I3, ...] diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md index 475420431a..71f177ed6a 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -502,10 +502,10 @@ For covariant types, such as `frozenset`, the ideal behaviour would be to not pr types to their instance supertypes: doing so causes more false positives than it fixes: ```py -# TODO: should be `frozenset[Literal[1, 2, 3]]` -reveal_type(frozenset((1, 2, 3))) # revealed: frozenset[Unknown] -# TODO: should be `frozenset[tuple[Literal[1], Literal[2], Literal[3]]]` -reveal_type(frozenset(((1, 2, 3),))) # revealed: frozenset[Unknown] +# TODO: better here would be `frozenset[Literal[1, 2, 3]]` +reveal_type(frozenset((1, 2, 3))) # revealed: frozenset[int] +# TODO: better here would be `frozenset[tuple[Literal[1], Literal[2], Literal[3]]]` +reveal_type(frozenset(((1, 2, 3),))) # revealed: frozenset[tuple[int, int, int]] ``` Literals are always promoted for invariant containers such as `list`, however, even though this can @@ -514,15 +514,15 @@ in some cases cause false positives: ```py from typing import Literal -# TODO: should be `list[int]` -reveal_type(list((1, 2, 3))) # revealed: list[Unknown] -# TODO: should be `list[tuple[int, int, int]]` -reveal_type(list(((1, 2, 3),))) # revealed: list[Unknown] +reveal_type(list((1, 2, 3))) # revealed: list[int] +reveal_type(list(((1, 2, 3),))) # revealed: list[tuple[int, int, int]] +# TODO: we could bidirectionally infer that the user does not want literals to be promoted here, +# and avoid this diagnostic +# +# error: [invalid-assignment] "`list[int]` is not assignable to `list[Literal[1, 2, 3]]`" x: list[Literal[1, 2, 3]] = list((1, 2, 3)) - -# TODO: should be `list[Literal[1, 2, 3]]` -reveal_type(x) # revealed: list[Unknown] +reveal_type(x) # revealed: list[Literal[1, 2, 3]] ``` [not a singleton type]: https://discuss.python.org/t/should-we-specify-in-the-language-reference-that-the-empty-tuple-is-a-singleton/67957 diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md index 6f668a7f88..0692250475 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md @@ -26,10 +26,8 @@ class HeterogeneousTupleSubclass(tuple[Literal[True], Literal[1]]): ... # from being overridden on a tuple subclass. This is something we plan to do as part of # our implementation of the Liskov Substitution Principle # (https://github.com/astral-sh/ty/issues/166) -# -# TODO: these should pass -static_assert(is_single_valued(EmptyTupleSubclass)) # error: [static-assert-error] -static_assert(is_single_valued(HeterogeneousTupleSubclass)) # error: [static-assert-error] +static_assert(is_single_valued(EmptyTupleSubclass)) +static_assert(is_single_valued(HeterogeneousTupleSubclass)) static_assert(not is_single_valued(str)) static_assert(not is_single_valued(Never)) diff --git a/crates/ty_python_semantic/resources/mdtest/unpacking.md b/crates/ty_python_semantic/resources/mdtest/unpacking.md index fc71ba3449..ef8cf63519 100644 --- a/crates/ty_python_semantic/resources/mdtest/unpacking.md +++ b/crates/ty_python_semantic/resources/mdtest/unpacking.md @@ -424,142 +424,97 @@ class HeterogeneousTupleSubclass(tuple[I0, I1, I2]): ... def f(x: HeterogeneousTupleSubclass): a, b, c = x + reveal_type(a) # revealed: I0 + reveal_type(b) # revealed: I1 + reveal_type(c) # revealed: I2 - # TODO: should be `I0` - reveal_type(a) # revealed: I0 | I1 | I2 - # TODO: should be `I1` - reveal_type(b) # revealed: I0 | I1 | I2 - # TODO: should be `I2` - reveal_type(c) # revealed: I0 | I1 | I2 + d, e = x # error: [invalid-assignment] "Too many values to unpack: Expected 2" - # TODO: should emit a diagnostic ([invalid-assignment] "Too many values to unpack: Expected 2") - d, e = x + reveal_type(d) # revealed: Unknown + reveal_type(e) # revealed: Unknown - reveal_type(d) # revealed: I0 | I1 | I2 - reveal_type(e) # revealed: I0 | I1 | I2 + f, g, h, i = x # error: [invalid-assignment] "Not enough values to unpack: Expected 4" - # TODO: should emit a diagnostic ([invalid-assignment] "Not enough values to unpack: Expected 4") - f, g, h, i = x - - reveal_type(f) # revealed: I0 | I1 | I2 - reveal_type(g) # revealed: I0 | I1 | I2 - reveal_type(h) # revealed: I0 | I1 | I2 - reveal_type(i) # revealed: I0 | I1 | I2 + reveal_type(f) # revealed: Unknown + reveal_type(g) # revealed: Unknown + reveal_type(h) # revealed: Unknown + reveal_type(i) # revealed: Unknown [j, *k] = x - - # TODO: should be `I0` - reveal_type(j) # revealed: I0 | I1 | I2 - # TODO: should be `list[I1 | I2]` - reveal_type(k) # revealed: list[I0 | I1 | I2] + reveal_type(j) # revealed: I0 + reveal_type(k) # revealed: list[I1 | I2] [l, m, *n] = x - - # TODO: should be `I0` - reveal_type(l) # revealed: I0 | I1 | I2 - # TODO: should be `I1` - reveal_type(m) # revealed: I0 | I1 | I2 - # TODO: should be `list[I2]` - reveal_type(n) # revealed: list[I0 | I1 | I2] + reveal_type(l) # revealed: I0 + reveal_type(m) # revealed: I1 + reveal_type(n) # revealed: list[I2] [o, p, q, *r] = x + reveal_type(o) # revealed: I0 + reveal_type(p) # revealed: I1 + reveal_type(q) # revealed: I2 + reveal_type(r) # revealed: list[Never] - # TODO: should be `I0` - reveal_type(o) # revealed: I0 | I1 | I2 - # TODO: should be `I1` - reveal_type(p) # revealed: I0 | I1 | I2 - # TODO: should be `I2` - reveal_type(q) # revealed: I0 | I1 | I2 - # TODO: should be `list[Never]` - reveal_type(r) # revealed: list[I0 | I1 | I2] - - # TODO: should emit a diagnostic ([invalid-assignment] "Not enough values to unpack: Expected at least 4") + # error: [invalid-assignment] "Not enough values to unpack: Expected at least 4" [s, t, u, v, *w] = x - reveal_type(s) # revealed: I0 | I1 | I2 - reveal_type(t) # revealed: I0 | I1 | I2 - reveal_type(u) # revealed: I0 | I1 | I2 - reveal_type(v) # revealed: I0 | I1 | I2 - reveal_type(w) # revealed: list[I0 | I1 | I2] + reveal_type(s) # revealed: Unknown + reveal_type(t) # revealed: Unknown + reveal_type(u) # revealed: Unknown + reveal_type(v) # revealed: Unknown + reveal_type(w) # revealed: list[Unknown] class MixedTupleSubclass(tuple[I0, *tuple[I1, ...], I2]): ... def f(x: MixedTupleSubclass): - # TODO: should emit a diagnostic: ([invalid-assignment] "Too many values to unpack: Expected 1"`) - (a,) = x - reveal_type(a) # revealed: I0 | I1 | I2 + (a,) = x # error: [invalid-assignment] "Too many values to unpack: Expected 1" + reveal_type(a) # revealed: Unknown c, d = x - # TODO: should be `I0` - reveal_type(c) # revealed: I0 | I1 | I2 - # TODO: should be `I2` - reveal_type(d) # revealed: I0 | I1 | I2 + reveal_type(c) # revealed: I0 + reveal_type(d) # revealed: I2 e, f, g = x - - # TODO: should be `I0` - reveal_type(e) # revealed: I0 | I1 | I2 - # TODO: should be `I1` - reveal_type(f) # revealed: I0 | I1 | I2 - # TODO: should be `I2` - reveal_type(g) # revealed: I0 | I1 | I2 + reveal_type(e) # revealed: I0 + reveal_type(f) # revealed: I1 + reveal_type(g) # revealed: I2 h, i, j, k = x - - # TODO: should be `I0` - reveal_type(h) # revealed: I0 | I1 | I2 - # TODO: should be `I1` - reveal_type(i) # revealed: I0 | I1 | I2 - # TODO: should be `I1` - reveal_type(j) # revealed: I0 | I1 | I2 - # TODO: should be `I2` - reveal_type(k) # revealed: I0 | I1 | I2 + reveal_type(h) # revealed: I0 + reveal_type(i) # revealed: I1 + reveal_type(j) # revealed: I1 + reveal_type(k) # revealed: I2 [l, *m] = x - - # TODO: should be `I0` - reveal_type(l) # revealed: I0 | I1 | I2 - # TODO: should be `list[I1 | I2]` - reveal_type(m) # revealed: list[I0 | I1 | I2] + reveal_type(l) # revealed: I0 + reveal_type(m) # revealed: list[I1 | I2] [n, o, *p] = x + reveal_type(n) # revealed: I0 - # TODO: should be `I0` - reveal_type(n) # revealed: I0 | I1 | I2 - # TODO: should be `I1 | I2` - reveal_type(o) # revealed: I0 | I1 | I2 - # TODO: should be `list[I1 | I2]` - reveal_type(p) # revealed: list[I0 | I1 | I2] + # TODO: `I1 | I2` might be better here? (https://github.com/astral-sh/ty/issues/947) + reveal_type(o) # revealed: I1 + + reveal_type(p) # revealed: list[I1 | I2] [o, p, q, *r] = x + reveal_type(o) # revealed: I0 - # TODO: should be `I0` - reveal_type(o) # revealed: I0 | I1 | I2 - # TODO: should be `I1 | I2` - reveal_type(p) # revealed: I0 | I1 | I2 - # TODO: should be `I1 | I2 - reveal_type(q) # revealed: I0 | I1 | I2 - # TODO: should be `list[I1 | I2] - reveal_type(r) # revealed: list[I0 | I1 | I2] + # TODO: `I1 | I2` might be better for both of these? (https://github.com/astral-sh/ty/issues/947) + reveal_type(p) # revealed: I1 + reveal_type(q) # revealed: I1 + + reveal_type(r) # revealed: list[I1 | I2] s, *t, u = x - - # TODO: should be `I0` - reveal_type(s) # revealed: I0 | I1 | I2 - # TODO: should be `list[I1]` - reveal_type(t) # revealed: list[I0 | I1 | I2] - # TODO: should be `I2` - reveal_type(u) # revealed: I0 | I1 | I2 + reveal_type(s) # revealed: I0 + reveal_type(t) # revealed: list[I1] + reveal_type(u) # revealed: I2 aa, bb, *cc, dd = x - - # TODO: should be `I0` - reveal_type(aa) # revealed: I0 | I1 | I2 - # TODO: should be `I1` - reveal_type(bb) # revealed: I0 | I1 | I2 - # TODO: should be `list[I1]` - reveal_type(cc) # revealed: list[I0 | I1 | I2] - # TODO: should be I2 - reveal_type(dd) # revealed: I0 | I1 | I2 + reveal_type(aa) # revealed: I0 + reveal_type(bb) # revealed: I1 + reveal_type(cc) # revealed: list[I1] + reveal_type(dd) # revealed: I2 ``` ## String diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 2ea44b1f21..220e808c30 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -222,7 +222,6 @@ impl<'db> Completion<'db> { // "struct" here as a more general "object." ---AG Type::NominalInstance(_) | Type::PropertyInstance(_) - | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypedDict(_) => CompletionKind::Struct, Type::IntLiteral(_) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 45b75f869a..805e41be7a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -59,7 +59,7 @@ use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature}; -use crate::types::tuple::{TupleSpec, TupleType}; +use crate::types::tuple::TupleSpec; use crate::unpack::EvaluationMode; pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic; use crate::{Db, FxOrderMap, FxOrderSet, Module, Program}; @@ -594,11 +594,6 @@ pub enum Type<'db> { LiteralString, /// A bytes literal BytesLiteral(BytesLiteralType<'db>), - /// An instance of the builtin `tuple` class. - /// TODO: Consider removing this in favor of `NominalInstance`. This is currently stored as a - /// separate variant partly for historical reasons, and partly to allow us to easily - /// distinguish tuples since they occur so often. - Tuple(TupleType<'db>), /// An instance of a typevar in a generic class or function. When the generic class or function /// is specialized, we will replace this typevar with its specialization. TypeVar(BoundTypeVarInstance<'db>), @@ -641,22 +636,25 @@ impl<'db> Type<'db> { fn is_none(&self, db: &'db dyn Db) -> bool { self.into_nominal_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType)) + .is_some_and(|instance| instance.class(db).is_known(db, KnownClass::NoneType)) } fn is_bool(&self, db: &'db dyn Db) -> bool { self.into_nominal_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::Bool)) + .is_some_and(|instance| instance.class(db).is_known(db, KnownClass::Bool)) } pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { - self.into_nominal_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::NotImplementedType)) + self.into_nominal_instance().is_some_and(|instance| { + instance + .class(db) + .is_known(db, KnownClass::NotImplementedType) + }) } pub fn is_object(&self, db: &'db dyn Db) -> bool { self.into_nominal_instance() - .is_some_and(|instance| instance.class.is_object(db)) + .is_some_and(|instance| instance.is_object(db)) } pub const fn is_todo(&self) -> bool { @@ -685,6 +683,30 @@ impl<'db> Type<'db> { self.materialize(db, TypeVarVariance::Contravariant) } + /// If this type is an instance type where the class has a tuple spec, returns the tuple spec. + /// + /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. + /// For a subclass of `tuple[int, str]`, it will return the same tuple spec. + fn tuple_instance_spec(&self, db: &'db dyn Db) -> Option>> { + self.into_nominal_instance() + .and_then(|instance| instance.tuple_spec(db)) + } + + /// If this type is an *exact* tuple type (*not* a subclass of `tuple`), returns the + /// tuple spec. + /// + /// You usually don't want to use this method, as you usually want to consider a subclass + /// of a tuple type in the same way as the `tuple` type itself. Only use this method if you + /// are certain that a *literal tuple* is required, and that a subclass of tuple will not + /// do. + /// + /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. + /// But for a subclass of `tuple[int, str]`, it will return `None`. + fn exact_tuple_instance_spec(&self, db: &'db dyn Db) -> Option>> { + self.into_nominal_instance() + .and_then(|instance| instance.own_tuple_spec(db)) + } + /// Returns the materialization of this type depending on the given `variance`. /// /// More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of @@ -747,9 +769,7 @@ impl<'db> Type<'db> { *self } - Type::NominalInstance(nominal_instance_type) => { - Type::NominalInstance(nominal_instance_type.materialize(db, variance)) - } + Type::NominalInstance(instance) => instance.materialize(db, variance), Type::GenericAlias(generic_alias) => { Type::GenericAlias(generic_alias.materialize(db, variance)) } @@ -782,7 +802,6 @@ impl<'db> Type<'db> { .map(|ty| ty.materialize(db, variance.flip())), ) .build(), - Type::Tuple(tuple_type) => Type::tuple(tuple_type.materialize(db, variance)), Type::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar.materialize(db, variance)), Type::TypeIs(type_is) => { type_is.with_type(db, type_is.return_type(db).materialize(db, variance)) @@ -1053,18 +1072,15 @@ impl<'db> Type<'db> { Type::Intersection(intersection) => visitor.visit(self, |v| { Type::Intersection(intersection.normalized_impl(db, v)) }), - Type::Tuple(tuple) => { - visitor.visit(self, |v| Type::tuple(tuple.normalized_impl(db, v))) - } Type::Callable(callable) => { visitor.visit(self, |v| Type::Callable(callable.normalized_impl(db, v))) } Type::ProtocolInstance(protocol) => { visitor.visit(self, |v| protocol.normalized_impl(db, v)) } - Type::NominalInstance(instance) => visitor.visit(self, |v| { - Type::NominalInstance(instance.normalized_impl(db, v)) - }), + Type::NominalInstance(instance) => { + visitor.visit(self, |v| instance.normalized_impl(db, v)) + } Type::FunctionLiteral(function) => visitor.visit(self, |v| { Type::FunctionLiteral(function.normalized_impl(db, v)) }), @@ -1166,7 +1182,6 @@ impl<'db> Type<'db> { | Type::Union(_) | Type::Intersection(_) | Type::Callable(_) - | Type::Tuple(_) | Type::TypeVar(_) | Type::BoundSuper(_) | Type::TypeIs(_) @@ -1232,7 +1247,6 @@ impl<'db> Type<'db> { | Type::StringLiteral(_) | Type::LiteralString | Type::BytesLiteral(_) - | Type::Tuple(_) | Type::TypeIs(_) | Type::TypedDict(_) => None, @@ -1309,7 +1323,7 @@ impl<'db> Type<'db> { match (self, target) { // Everything is a subtype of `object`. - (_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true, + (_, Type::NominalInstance(instance)) if instance.is_object(db) => true, // `Never` is the bottom type, the empty set. // It is a subtype of all other types. @@ -1577,24 +1591,6 @@ impl<'db> Type<'db> { (Type::Callable(_), _) => false, - (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { - self_tuple.has_relation_to(db, target_tuple, relation) - } - - (Type::Tuple(self_tuple), Type::NominalInstance(target_instance)) => { - self_tuple.to_class_type(db).is_some_and(|self_class| { - self_class.has_relation_to(db, target_instance.class, relation) - }) - } - (Type::NominalInstance(self_instance), Type::Tuple(target_tuple)) => { - target_tuple.to_class_type(db).is_some_and(|target_class| { - self_instance - .class - .has_relation_to(db, target_class, relation) - }) - } - (Type::Tuple(_), _) => false, - (Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target), (Type::BoundSuper(_), _) => KnownClass::Super .to_instance(db) @@ -1724,8 +1720,6 @@ impl<'db> Type<'db> { first.is_equivalent_to(db, second) } - (Type::Tuple(first), Type::Tuple(second)) => first.is_equivalent_to(db, second), - (Type::Union(first), Type::Union(second)) => first.is_equivalent_to(db, second), (Type::Intersection(first), Type::Intersection(second)) => { @@ -1748,7 +1742,7 @@ impl<'db> Type<'db> { } (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { - n.class.is_object(db) && protocol.normalized(db) == nominal + n.is_object(db) && protocol.normalized(db) == nominal } // An instance of an enum class is equivalent to an enum literal of that class, // if that enum has only has one member. @@ -1758,7 +1752,7 @@ impl<'db> Type<'db> { return false; } - let class_literal = instance.class.class_literal(db).0; + let class_literal = instance.class(db).class_literal(db).0; is_single_member_enum(db, class_literal) } _ => false, @@ -1906,45 +1900,6 @@ impl<'db> Type<'db> { | Type::KnownInstance(..)), ) => left != right, - // One tuple type can be a subtype of another tuple type, - // but we know for sure that any given tuple type is disjoint from all single-valued types - ( - Type::Tuple(..), - Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::ModuleLiteral(..) - | Type::BooleanLiteral(..) - | Type::BytesLiteral(..) - | Type::FunctionLiteral(..) - | Type::BoundMethod(..) - | Type::MethodWrapper(..) - | Type::WrapperDescriptor(..) - | Type::DataclassDecorator(..) - | Type::DataclassTransformer(..) - | Type::IntLiteral(..) - | Type::EnumLiteral(..) - | Type::StringLiteral(..) - | Type::LiteralString, - ) - | ( - Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::ModuleLiteral(..) - | Type::BooleanLiteral(..) - | Type::BytesLiteral(..) - | Type::FunctionLiteral(..) - | Type::BoundMethod(..) - | Type::MethodWrapper(..) - | Type::WrapperDescriptor(..) - | Type::DataclassDecorator(..) - | Type::DataclassTransformer(..) - | Type::IntLiteral(..) - | Type::EnumLiteral(..) - | Type::StringLiteral(..) - | Type::LiteralString, - Type::Tuple(..), - ) => true, - ( Type::SubclassOf(_), Type::BooleanLiteral(..) @@ -2058,7 +2013,7 @@ impl<'db> Type<'db> { // () (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) - if n.class.is_final(db) => + if n.class(db).is_final(db) => { any_protocol_members_absent_or_disjoint(db, protocol, nominal, visitor) } @@ -2107,29 +2062,19 @@ impl<'db> Type<'db> { (Type::SpecialForm(special_form), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => { - !special_form.is_instance_of(db, instance.class) + !special_form.is_instance_of(db, instance.class(db)) } (Type::KnownInstance(known_instance), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => { - !known_instance.is_instance_of(db, instance.class) + !known_instance.is_instance_of(db, instance.class(db)) } - (Type::SpecialForm(special_form), Type::Tuple(tuple)) - | (Type::Tuple(tuple), Type::SpecialForm(special_form)) => tuple - .to_class_type(db) - .is_some_and(|tuple_class| !special_form.is_instance_of(db, tuple_class)), - - (Type::KnownInstance(known_instance), Type::Tuple(tuple)) - | (Type::Tuple(tuple), Type::KnownInstance(known_instance)) => tuple - .to_class_type(db) - .is_some_and(|tuple_class| !known_instance.is_instance_of(db, tuple_class)), - (Type::BooleanLiteral(..) | Type::TypeIs(_), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::BooleanLiteral(..) | Type::TypeIs(_)) => { // A `Type::BooleanLiteral()` must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) - !KnownClass::Bool.is_subclass_of(db, instance.class) + !KnownClass::Bool.is_subclass_of(db, instance.class(db)) } (Type::BooleanLiteral(..) | Type::TypeIs(_), _) @@ -2139,7 +2084,7 @@ impl<'db> Type<'db> { | (Type::NominalInstance(instance), Type::IntLiteral(..)) => { // A `Type::IntLiteral()` must be an instance of exactly `int` // (it cannot be an instance of an `int` subclass) - !KnownClass::Int.is_subclass_of(db, instance.class) + !KnownClass::Int.is_subclass_of(db, instance.class(db)) } (Type::IntLiteral(..), _) | (_, Type::IntLiteral(..)) => true, @@ -2151,7 +2096,7 @@ impl<'db> Type<'db> { | (Type::NominalInstance(instance), Type::StringLiteral(..) | Type::LiteralString) => { // A `Type::StringLiteral()` or a `Type::LiteralString` must be an instance of exactly `str` // (it cannot be an instance of a `str` subclass) - !KnownClass::Str.is_subclass_of(db, instance.class) + !KnownClass::Str.is_subclass_of(db, instance.class(db)) } (Type::LiteralString, Type::LiteralString) => false, @@ -2161,7 +2106,7 @@ impl<'db> Type<'db> { | (Type::NominalInstance(instance), Type::BytesLiteral(..)) => { // A `Type::BytesLiteral()` must be an instance of exactly `bytes` // (it cannot be an instance of a `bytes` subclass) - !KnownClass::Bytes.is_subclass_of(db, instance.class) + !KnownClass::Bytes.is_subclass_of(db, instance.class(db)) } (Type::EnumLiteral(enum_literal), instance@Type::NominalInstance(_)) @@ -2188,7 +2133,7 @@ impl<'db> Type<'db> { | (Type::NominalInstance(instance), Type::FunctionLiteral(..)) => { // A `Type::FunctionLiteral()` must be an instance of exactly `types.FunctionType` // (it cannot be an instance of a `types.FunctionType` subclass) - !KnownClass::FunctionType.is_subclass_of(db, instance.class) + !KnownClass::FunctionType.is_subclass_of(db, instance.class(db)) } (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => KnownClass::MethodType @@ -2234,12 +2179,12 @@ impl<'db> Type<'db> { ( Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), - instance @ Type::NominalInstance(NominalInstanceType { class, .. }), + instance @ Type::NominalInstance(nominal), ) | ( - instance @ Type::NominalInstance(NominalInstanceType { class, .. }), + instance @ Type::NominalInstance(nominal), Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), - ) if class.is_final(db) => instance + ) if nominal.class(db).is_final(db) => instance .member_lookup_with_policy( db, Name::new_static("__call__"), @@ -2270,18 +2215,7 @@ impl<'db> Type<'db> { } (Type::NominalInstance(left), Type::NominalInstance(right)) => { - left.is_disjoint_from_impl(db, right) - } - - (Type::Tuple(tuple), Type::Tuple(other_tuple)) => { - tuple.is_disjoint_from_impl(db, other_tuple, visitor) - } - - (Type::Tuple(tuple), Type::NominalInstance(instance)) - | (Type::NominalInstance(instance), Type::Tuple(tuple)) => { - tuple.to_class_type(db).is_some_and(|tuple_class| { - !instance.class.could_coexist_in_mro_with(db, tuple_class) - }) + left.is_disjoint_from_impl(db, right, visitor) } (Type::PropertyInstance(_), other) | (other, Type::PropertyInstance(_)) => { @@ -2396,14 +2330,6 @@ impl<'db> Type<'db> { Type::DataclassDecorator(_) | Type::DataclassTransformer(_) => false, Type::NominalInstance(instance) => instance.is_singleton(db), Type::PropertyInstance(_) => false, - Type::Tuple(..) => { - // The empty tuple is a singleton on CPython and PyPy, but not on other Python - // implementations such as GraalPy. Its *use* as a singleton is discouraged and - // should not be relied on for type narrowing, so we do not treat it as one. - // See: - // https://docs.python.org/3/reference/expressions.html#parenthesized-forms - false - } Type::Union(..) => { // A single-element union, where the sole element was a singleton, would itself // be a singleton type. However, unions with length < 2 should never appear in @@ -2493,7 +2419,6 @@ impl<'db> Type<'db> { false } - Type::Tuple(tuple) => tuple.is_single_valued(db), Type::NominalInstance(instance) => instance.is_single_valued(db), Type::BoundSuper(_) => { @@ -2609,7 +2534,9 @@ impl<'db> Type<'db> { // i.e. Type::NominalInstance(type). So looking up a name in the MRO of // `Type::NominalInstance(type)` is equivalent to looking up the name in the // MRO of the class `object`. - Type::NominalInstance(instance) if instance.class.is_known(db, KnownClass::Type) => { + Type::NominalInstance(instance) + if instance.class(db).is_known(db, KnownClass::Type) => + { if policy.mro_no_object_fallback() { Some(Place::Unbound.into()) } else { @@ -2637,7 +2564,6 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(_) | Type::EnumLiteral(_) - | Type::Tuple(_) | Type::TypeVar(_) | Type::NominalInstance(_) | Type::ProtocolInstance(_) @@ -2724,7 +2650,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Never => Place::bound(self).into(), - Type::NominalInstance(instance) => instance.class.instance_member(db, name), + Type::NominalInstance(instance) => instance.class(db).instance_member(db, name), Type::ProtocolInstance(protocol) => protocol.instance_member(db, name), @@ -2745,12 +2671,12 @@ impl<'db> Type<'db> { .to_instance(db) .instance_member(db, name), Type::Callable(_) | Type::DataclassTransformer(_) => { - KnownClass::Object.to_instance(db).instance_member(db, name) + Type::object(db).instance_member(db, name) } Type::TypeVar(bound_typevar) => { match bound_typevar.typevar(db).bound_or_constraints(db) { - None => KnownClass::Object.to_instance(db).instance_member(db, name), + None => Type::object(db).instance_member(db, name), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { bound.instance_member(db, name) } @@ -2772,10 +2698,6 @@ impl<'db> Type<'db> { Type::EnumLiteral(enum_literal) => enum_literal .enum_class_instance(db) .instance_member(db, name), - Type::Tuple(tuple) => tuple - .to_class_type(db) - .map(|class| class.instance_member(db, name)) - .unwrap_or(Place::Unbound.into()), Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object(db).instance_member(db, name), Type::ModuleLiteral(_) => KnownClass::ModuleType @@ -3235,13 +3157,13 @@ impl<'db> Type<'db> { .to_instance(db) .member_lookup_with_policy(db, name, policy), - Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Object - .to_instance(db) - .member_lookup_with_policy(db, name, policy), + Type::Callable(_) | Type::DataclassTransformer(_) => { + Type::object(db).member_lookup_with_policy(db, name, policy) + } Type::NominalInstance(instance) if matches!(name.as_str(), "major" | "minor") - && instance.class.is_known(db, KnownClass::VersionInfo) => + && instance.class(db).is_known(db, KnownClass::VersionInfo) => { let python_version = Program::get(db).python_version(db); let segment = if name == "major" { @@ -3285,7 +3207,6 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::EnumLiteral(..) | Type::LiteralString - | Type::Tuple(..) | Type::TypeVar(..) | Type::SpecialForm(..) | Type::KnownInstance(..) @@ -3315,7 +3236,7 @@ impl<'db> Type<'db> { // resolve the attribute. if matches!( self.into_nominal_instance() - .and_then(|instance| instance.class.known(db)), + .and_then(|instance| instance.class(db).known(db)), Some(KnownClass::ModuleType | KnownClass::GenericAlias) ) { return Place::Unbound.into(); @@ -3633,10 +3554,12 @@ impl<'db> Type<'db> { } } - Type::NominalInstance(instance) => match instance.class.known(db) { - Some(known_class) => known_class.bool(), - None => try_dunder_bool()?, - }, + Type::NominalInstance(instance) => instance + .class(db) + .known(db) + .and_then(KnownClass::bool) + .map(Ok) + .unwrap_or_else(try_dunder_bool)?, Type::ProtocolInstance(_) => try_dunder_bool()?, @@ -3657,7 +3580,6 @@ impl<'db> Type<'db> { Type::BooleanLiteral(bool) => Truthiness::from(*bool), Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()), Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()), - Type::Tuple(tuple) => tuple.truthiness(db), }; Ok(truthiness) @@ -3684,13 +3606,6 @@ impl<'db> Type<'db> { let usize_len = match self { Type::BytesLiteral(bytes) => Some(bytes.python_len(db)), Type::StringLiteral(string) => Some(string.python_len(db)), - - // N.B. This is strictly-speaking redundant, since the `__len__` method on tuples - // is special-cased in `ClassType::own_class_member`. However, it's probably more - // efficient to short-circuit here and check against the tuple spec directly, - // rather than going through the `__len__` method. - Type::Tuple(tuple) => tuple.tuple(db).len().into_fixed_length(), - _ => None, }; @@ -4236,10 +4151,7 @@ impl<'db> Type<'db> { // ``` Binding::single( self, - Signature::new( - Parameters::empty(), - Some(KnownClass::Object.to_instance(db)), - ), + Signature::new(Parameters::empty(), Some(Type::object(db))), ) .into() } @@ -4633,7 +4545,6 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::BooleanLiteral(_) | Type::LiteralString - | Type::Tuple(_) | Type::BoundSuper(_) | Type::ModuleLiteral(_) | Type::TypeIs(_) @@ -4791,7 +4702,11 @@ impl<'db> Type<'db> { } match self { - Type::Tuple(tuple_type) => return Ok(Cow::Borrowed(tuple_type.tuple(db))), + Type::NominalInstance(nominal) => { + if let Some(spec) = nominal.tuple_spec(db) { + return Ok(spec); + } + } Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => { return Ok(Cow::Owned(TupleSpec::homogeneous(todo_type!( "*tuple[] annotations" @@ -5017,7 +4932,7 @@ impl<'db> Type<'db> { match self { Type::NominalInstance(instance) => { - instance.class.iter_mro(db).find_map(from_class_base) + instance.class(db).iter_mro(db).find_map(from_class_base) } Type::ProtocolInstance(instance) => { if let Protocol::FromClass(class) = instance.inner { @@ -5276,7 +5191,6 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(_) | Type::IntLiteral(_) | Type::StringLiteral(_) - | Type::Tuple(_) | Type::LiteralString | Type::BoundSuper(_) | Type::AlwaysTruthy @@ -5344,7 +5258,6 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::ModuleLiteral(_) | Type::StringLiteral(_) - | Type::Tuple(_) | Type::TypeVar(_) | Type::Callable(_) | Type::BoundMethod(_) @@ -5557,7 +5470,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) => Ok(*self), - Type::NominalInstance(instance) => match instance.class.known(db) { + Type::NominalInstance(instance) => match instance.class(db).known(db) { Some(KnownClass::TypeVar) => Ok(todo_type!( "Support for `typing.TypeVar` instances in type expressions" )), @@ -5593,40 +5506,6 @@ impl<'db> Type<'db> { KnownClass::NoneType.to_instance(db) } - /// Return the type of `tuple(sys.version_info)`. - /// - /// This is not exactly the type that `sys.version_info` has at runtime, - /// but it's a useful fallback for us in order to infer `Literal` types from `sys.version_info` comparisons. - fn version_info_tuple(db: &'db dyn Db) -> Self { - let python_version = Program::get(db).python_version(db); - let int_instance_ty = KnownClass::Int.to_instance(db); - - // TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there) - let release_level_ty = { - let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"] - .iter() - .map(|level| Type::string_literal(db, level)) - .collect(); - - // For most unions, it's better to go via `UnionType::from_elements` or use `UnionBuilder`; - // those techniques ensure that union elements are deduplicated and unions are eagerly simplified - // into other types where necessary. Here, however, we know that there are no duplicates - // in this union, so it's probably more efficient to use `UnionType::new()` directly. - Type::Union(UnionType::new(db, elements)) - }; - - Type::heterogeneous_tuple( - db, - [ - Type::IntLiteral(python_version.major.into()), - Type::IntLiteral(python_version.minor.into()), - int_instance_ty, - release_level_ty, - int_instance_ty, - ], - ) - } - /// Given a type that is assumed to represent an instance of a class, /// return a type that represents that class itself. /// @@ -5655,9 +5534,6 @@ impl<'db> Type<'db> { } Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db), Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), - Type::Tuple(tuple) => tuple - .to_subclass_of(db) - .unwrap_or_else(SubclassOfType::subclass_of_unknown), Type::TypeVar(bound_typevar) => { match bound_typevar.typevar(db).bound_or_constraints(db) { None => KnownClass::Type.to_instance(db), @@ -5777,9 +5653,8 @@ impl<'db> Type<'db> { method.self_instance(db).apply_type_mapping(db, type_mapping), )), - Type::NominalInstance(instance) => Type::NominalInstance( + Type::NominalInstance(instance) => instance.apply_type_mapping(db, type_mapping), - ), Type::ProtocolInstance(instance) => { Type::ProtocolInstance(instance.apply_type_mapping(db, type_mapping)) @@ -5844,7 +5719,6 @@ impl<'db> Type<'db> { } builder.build() } - Type::Tuple(tuple) => Type::tuple(tuple.apply_type_mapping(db, type_mapping)), Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping)), @@ -5950,10 +5824,6 @@ impl<'db> Type<'db> { } } - Type::Tuple(tuple) => { - tuple.find_legacy_typevars(db, binding_context, typevars); - } - Type::GenericAlias(alias) => { alias.find_legacy_typevars(db, binding_context, typevars); } @@ -6071,7 +5941,7 @@ impl<'db> Type<'db> { } Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))), Self::NominalInstance(instance) => { - Some(TypeDefinition::Class(instance.class.definition(db))) + Some(TypeDefinition::Class(instance.class(db).definition(db))) } Self::KnownInstance(instance) => match instance { KnownInstanceType::TypeVar(var) => { @@ -6100,8 +5970,7 @@ impl<'db> Type<'db> { | Self::DataclassDecorator(_) | Self::DataclassTransformer(_) | Self::PropertyInstance(_) - | Self::BoundSuper(_) - | Self::Tuple(_) => self.to_meta_type(db).definition(db), + | Self::BoundSuper(_) => self.to_meta_type(db).definition(db), Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)), @@ -6193,7 +6062,7 @@ impl<'db> Type<'db> { match self { Type::GenericAlias(generic) => Some(generic.origin(db)), Type::NominalInstance(instance) => { - if let ClassType::Generic(generic) = instance.class { + if let ClassType::Generic(generic) = instance.class(db) { Some(generic.origin(db)) } else { None @@ -9280,9 +9149,11 @@ impl<'db> SuperOwnerKind<'db> { SuperOwnerKind::Class(class) => { SuperOwnerKind::Class(class.normalized_impl(db, visitor)) } - SuperOwnerKind::Instance(instance) => { - SuperOwnerKind::Instance(instance.normalized_impl(db, visitor)) - } + SuperOwnerKind::Instance(instance) => instance + .normalized_impl(db, visitor) + .into_nominal_instance() + .map(Self::Instance) + .unwrap_or(Self::Dynamic(DynamicType::Any)), } } @@ -9292,7 +9163,7 @@ impl<'db> SuperOwnerKind<'db> { Either::Left(ClassBase::Dynamic(dynamic).mro(db, None)) } SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)), - SuperOwnerKind::Instance(instance) => Either::Right(instance.class.iter_mro(db)), + SuperOwnerKind::Instance(instance) => Either::Right(instance.class(db).iter_mro(db)), } } @@ -9304,11 +9175,11 @@ impl<'db> SuperOwnerKind<'db> { } } - fn into_class(self) -> Option> { + fn into_class(self, db: &'db dyn Db) -> Option> { match self { SuperOwnerKind::Dynamic(_) => None, SuperOwnerKind::Class(class) => Some(class), - SuperOwnerKind::Instance(instance) => Some(instance.class), + SuperOwnerKind::Instance(instance) => Some(instance.class(db)), } } @@ -9406,7 +9277,7 @@ impl<'db> BoundSuperType<'db> { let Some(pivot_class) = pivot_class.into_class() else { return Some(owner); }; - let Some(owner_class) = owner.into_class() else { + let Some(owner_class) = owner.into_class(db) else { return Some(owner); }; if owner_class.is_subclass_of(db, pivot_class) { @@ -9507,7 +9378,7 @@ impl<'db> BoundSuperType<'db> { .expect("Calling `find_name_in_mro` on dynamic type should return `Some`"); } SuperOwnerKind::Class(class) => class, - SuperOwnerKind::Instance(instance) => instance.class, + SuperOwnerKind::Instance(instance) => instance.class(db), }; let (class_literal, _) = class.class_literal(db); diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 8db4a3cccf..7ad2215c96 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -572,7 +572,8 @@ impl<'db> IntersectionBuilder<'db> { self } Type::NominalInstance(instance) - if enum_metadata(self.db, instance.class.class_literal(self.db).0).is_some() => + if enum_metadata(self.db, instance.class(self.db).class_literal(self.db).0) + .is_some() => { let mut contains_enum_literal_as_negative_element = false; for intersection in &self.intersections { @@ -596,7 +597,7 @@ impl<'db> IntersectionBuilder<'db> { let db = self.db; self.add_positive(Type::Union(UnionType::new( db, - enum_member_literals(db, instance.class.class_literal(db).0, None) + enum_member_literals(db, instance.class(db).class_literal(db).0, None) .expect("Calling `enum_member_literals` on an enum class") .collect::>(), ))) @@ -762,7 +763,7 @@ impl<'db> InnerIntersectionBuilder<'db> { _ => { let known_instance = new_positive .into_nominal_instance() - .and_then(|instance| instance.class.known(db)); + .and_then(|instance| instance.class(db).known(db)); if known_instance == Some(KnownClass::Object) { // `object & T` -> `T`; it is always redundant to add `object` to an intersection @@ -782,7 +783,7 @@ impl<'db> InnerIntersectionBuilder<'db> { new_positive = Type::BooleanLiteral(false); } Type::NominalInstance(instance) - if instance.class.is_known(db, KnownClass::Bool) => + if instance.class(db).is_known(db, KnownClass::Bool) => { match new_positive { // `bool & AlwaysTruthy` -> `Literal[True]` @@ -876,7 +877,7 @@ impl<'db> InnerIntersectionBuilder<'db> { self.positive .iter() .filter_map(|ty| ty.into_nominal_instance()) - .filter_map(|instance| instance.class.known(db)) + .filter_map(|instance| instance.class(db).known(db)) .any(KnownClass::is_bool) }; @@ -892,7 +893,7 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::Never => { // Adding ~Never to an intersection is a no-op. } - Type::NominalInstance(instance) if instance.class.is_object(db) => { + Type::NominalInstance(instance) if instance.is_object(db) => { // Adding ~object to an intersection results in Never. *self = Self::default(); self.positive.insert(Type::Never); diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 5a2575b7a7..7c943a986d 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -6,7 +6,7 @@ use ruff_python_ast as ast; use crate::Db; use crate::types::KnownClass; use crate::types::enums::enum_member_literals; -use crate::types::tuple::{TupleLength, TupleSpec}; +use crate::types::tuple::{Tuple, TupleLength, TupleType}; use super::Type; @@ -214,47 +214,49 @@ impl<'a, 'db> FromIterator<(Argument<'a>, Option>)> for CallArguments< fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { match ty { Type::NominalInstance(instance) => { - if instance.class.is_known(db, KnownClass::Bool) { + let class = instance.class(db); + + if class.is_known(db, KnownClass::Bool) { return Some(vec![ Type::BooleanLiteral(true), Type::BooleanLiteral(false), ]); } - let class_literal = instance.class.class_literal(db).0; + // If the class is a fixed-length tuple subtype, we expand it to its elements. + if let Some(spec) = instance.tuple_spec(db) { + return match &*spec { + Tuple::Fixed(fixed_length_tuple) => { + let expanded = fixed_length_tuple + .all_elements() + .map(|element| { + if let Some(expanded) = expand_type(db, *element) { + Either::Left(expanded.into_iter()) + } else { + Either::Right(std::iter::once(*element)) + } + }) + .multi_cartesian_product() + .map(|types| Type::tuple(TupleType::from_elements(db, types))) + .collect::>(); - if let Some(enum_members) = enum_member_literals(db, class_literal, None) { + if expanded.len() == 1 { + // There are no elements in the tuple type that can be expanded. + None + } else { + Some(expanded) + } + } + Tuple::Variable(_) => None, + }; + } + + if let Some(enum_members) = enum_member_literals(db, class.class_literal(db).0, None) { return Some(enum_members.collect()); } None } - Type::Tuple(tuple_type) => { - // Note: This should only account for tuples of known length, i.e., `tuple[bool, ...]` - // should not be expanded here. - let tuple = tuple_type.tuple(db); - if !matches!(tuple, TupleSpec::Fixed(_)) { - return None; - } - let expanded = tuple - .all_elements() - .map(|element| { - if let Some(expanded) = expand_type(db, *element) { - Either::Left(expanded.into_iter()) - } else { - Either::Right(std::iter::once(*element)) - } - }) - .multi_cartesian_product() - .map(|types| Type::heterogeneous_tuple(db, types)) - .collect::>(); - if expanded.len() == 1 { - // There are no elements in the tuple type that can be expanded. - None - } else { - Some(expanded) - } - } Type::Union(union) => Some(union.iter(db).copied().collect()), // We don't handle `type[A | B]` here because it's already stored in the expanded form // i.e., `type[A] | type[B]` which is handled by the `Type::Union` case. diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 719dc604ee..f04e78da56 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -273,8 +273,6 @@ impl<'db> GenericAlias<'db> { binding_context: Option>, typevars: &mut FxOrderSet>, ) { - // A tuple's specialization will include all of its element types, so we don't need to also - // look in `self.tuple`. self.specialization(db) .find_legacy_typevars(db, binding_context, typevars); } @@ -316,6 +314,13 @@ impl<'db> ClassType<'db> { matches!(self, Self::NonGeneric(_)) } + pub(super) const fn into_generic_alias(self) -> Option> { + match self { + Self::NonGeneric(_) => None, + Self::Generic(generic) => Some(generic), + } + } + pub(super) fn normalized_impl( self, db: &'db dyn Db, @@ -663,7 +668,8 @@ impl<'db> ClassType<'db> { match name { "__len__" if class_literal.is_tuple(db) => { let return_type = specialization - .and_then(|spec| spec.tuple(db).len().into_fixed_length()) + .and_then(|spec| spec.tuple(db)) + .and_then(|tuple| tuple.len().into_fixed_length()) .and_then(|len| i64::try_from(len).ok()) .map(Type::IntLiteral) .unwrap_or_else(|| KnownClass::Int.to_instance(db)); @@ -673,7 +679,8 @@ impl<'db> ClassType<'db> { "__bool__" if class_literal.is_tuple(db) => { let return_type = specialization - .map(|spec| spec.tuple(db).truthiness().into_type(db)) + .and_then(|spec| spec.tuple(db)) + .map(|tuple| tuple.truthiness().into_type(db)) .unwrap_or_else(|| KnownClass::Bool.to_instance(db)); synthesize_simple_tuple_method(return_type) @@ -681,9 +688,8 @@ impl<'db> ClassType<'db> { "__getitem__" if class_literal.is_tuple(db) => { specialization - .map(|spec| { - let tuple = spec.tuple(db); - + .and_then(|spec| spec.tuple(db)) + .map(|tuple| { let mut element_type_to_indices: FxIndexMap, Vec> = FxIndexMap::default(); @@ -846,11 +852,12 @@ impl<'db> ClassType<'db> { let mut iterable_parameter = Parameter::positional_only(Some(Name::new_static("iterable"))); - match specialization { - Some(spec) => { + let tuple = specialization.and_then(|spec| spec.tuple(db)); + + match tuple { + Some(tuple) => { // TODO: Once we support PEP 646 annotations for `*args` parameters, we can // use the tuple itself as the argument type. - let tuple = spec.tuple(db); let tuple_len = tuple.len(); if tuple_len.minimum() == 0 && tuple_len.maximum().is_none() { @@ -885,7 +892,7 @@ impl<'db> ClassType<'db> { // - a zero-length tuple // - an unspecialized tuple // - a tuple with no minimum length - if specialization.is_none_or(|spec| spec.tuple(db).len().minimum() == 0) { + if tuple.is_none_or(|tuple| tuple.len().minimum() == 0) { iterable_parameter = iterable_parameter.with_default_type(Type::empty_tuple(db)); } @@ -1227,7 +1234,7 @@ impl<'db> ClassLiteral<'db> { let parsed = parsed_module(db, file).load(db); let class_def_node = scope.node(db).expect_class(&parsed); class_def_node.type_params.as_ref().map(|type_params| { - let index = semantic_index(db, file); + let index = semantic_index(db, scope.file(db)); let definition = index.expect_single_definition(class_def_node); GenericContext::from_type_params(db, index, definition, type_params) }) @@ -1297,7 +1304,8 @@ impl<'db> ClassLiteral<'db> { specialization: Option>, ) -> ClassType<'db> { self.apply_specialization(db, |generic_context| { - specialization.unwrap_or_else(|| generic_context.default_specialization(db)) + specialization + .unwrap_or_else(|| generic_context.default_specialization(db, self.known(db))) }) } @@ -1306,7 +1314,7 @@ impl<'db> ClassLiteral<'db> { /// applies the default specialization to the class's typevars. pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { self.apply_specialization(db, |generic_context| { - generic_context.default_specialization(db) + generic_context.default_specialization(db, self.known(db)) }) } @@ -1887,7 +1895,7 @@ impl<'db> ClassLiteral<'db> { if field_ty .into_nominal_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::KwOnly)) + .is_some_and(|instance| instance.class(db).is_known(db, KnownClass::KwOnly)) { // Attributes annotated with `dataclass.KW_ONLY` are not present in the synthesized // `__init__` method; they are used to indicate that the following parameters are @@ -3100,7 +3108,10 @@ impl KnownClass { /// Determine whether instances of this class are always truthy, always falsy, /// or have an ambiguous truthiness. - pub(crate) const fn bool(self) -> Truthiness { + /// + /// Returns `None` for `KnownClass::Tuple`, since the truthiness of a tuple + /// depends on its spec. + pub(crate) const fn bool(self) -> Option { match self { // N.B. It's only generally safe to infer `Truthiness::AlwaysTrue` for a `KnownClass` // variant if the class's `__bool__` method always returns the same thing *and* the @@ -3126,9 +3137,9 @@ impl KnownClass { | Self::GeneratorType | Self::AsyncGeneratorType | Self::MethodWrapperType - | Self::CoroutineType => Truthiness::AlwaysTrue, + | Self::CoroutineType => Some(Truthiness::AlwaysTrue), - Self::NoneType => Truthiness::AlwaysFalse, + Self::NoneType => Some(Truthiness::AlwaysFalse), Self::Any | Self::BaseException @@ -3145,7 +3156,6 @@ impl KnownClass { | Self::StdlibAlias | Self::SupportsIndex | Self::Set - | Self::Tuple | Self::Int | Self::Type | Self::Bytes @@ -3184,7 +3194,9 @@ impl KnownClass { | Self::KwOnly | Self::InitVar | Self::NamedTupleFallback - | Self::TypedDictFallback => Truthiness::Ambiguous, + | Self::TypedDictFallback => Some(Truthiness::Ambiguous), + + Self::Tuple => None, } } @@ -3432,6 +3444,82 @@ impl KnownClass { } } + pub(crate) const fn is_tuple_subclass(self) -> bool { + match self { + KnownClass::Tuple | KnownClass::VersionInfo => true, + + KnownClass::Bool + | KnownClass::Object + | KnownClass::Bytes + | KnownClass::Bytearray + | KnownClass::Type + | KnownClass::Int + | KnownClass::Float + | KnownClass::Complex + | KnownClass::Str + | KnownClass::List + | KnownClass::Set + | KnownClass::FrozenSet + | KnownClass::Dict + | KnownClass::Slice + | KnownClass::Property + | KnownClass::BaseException + | KnownClass::Exception + | KnownClass::BaseExceptionGroup + | KnownClass::ExceptionGroup + | KnownClass::Staticmethod + | KnownClass::Classmethod + | KnownClass::Awaitable + | KnownClass::Generator + | KnownClass::Deprecated + | KnownClass::Super + | KnownClass::Enum + | KnownClass::EnumType + | KnownClass::Auto + | KnownClass::Member + | KnownClass::Nonmember + | KnownClass::ABCMeta + | KnownClass::GenericAlias + | KnownClass::ModuleType + | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType + | KnownClass::UnionType + | KnownClass::GeneratorType + | KnownClass::AsyncGeneratorType + | KnownClass::CoroutineType + | KnownClass::NoneType + | KnownClass::Any + | KnownClass::StdlibAlias + | KnownClass::SpecialForm + | KnownClass::TypeVar + | KnownClass::ParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs + | KnownClass::TypeVarTuple + | KnownClass::TypeAliasType + | KnownClass::NoDefaultType + | KnownClass::NamedTuple + | KnownClass::NewType + | KnownClass::SupportsIndex + | KnownClass::Iterable + | KnownClass::Iterator + | KnownClass::ChainMap + | KnownClass::Counter + | KnownClass::DefaultDict + | KnownClass::Deque + | KnownClass::OrderedDict + | KnownClass::EllipsisType + | KnownClass::NotImplementedType + | KnownClass::Field + | KnownClass::KwOnly + | KnownClass::InitVar + | KnownClass::TypedDictFallback + | KnownClass::NamedTupleFallback => false, + } + } + /// Return `true` if this class is a protocol class. /// /// In an ideal world, perhaps we wouldn't hardcode this knowledge here; @@ -3874,8 +3962,10 @@ impl KnownClass { } } - /// Return true if all instances of this `KnownClass` compare equal. - pub(super) const fn is_single_valued(self) -> bool { + /// Returns `Some(true)` if all instances of this `KnownClass` compare equal. + /// Returns `None` for `KnownClass::Tuple`, since whether or not a tuple type + /// is single-valued depends on the tuple spec. + pub(super) const fn is_single_valued(self) -> Option { match self { Self::NoneType | Self::NoDefaultType @@ -3883,7 +3973,7 @@ impl KnownClass { | Self::EllipsisType | Self::TypeAliasType | Self::UnionType - | Self::NotImplementedType => true, + | Self::NotImplementedType => Some(true), Self::Any | Self::Bool @@ -3896,7 +3986,6 @@ impl KnownClass { | Self::Complex | Self::Str | Self::List - | Self::Tuple | Self::Set | Self::FrozenSet | Self::Dict @@ -3948,7 +4037,9 @@ impl KnownClass { | Self::Iterable | Self::Iterator | Self::NamedTupleFallback - | Self::TypedDictFallback => false, + | Self::TypedDictFallback => Some(false), + + Self::Tuple => None, } } @@ -4555,47 +4646,6 @@ impl<'db> KnownClassLookupError<'db> { } } -pub(crate) struct SliceLiteral { - pub(crate) start: Option, - pub(crate) stop: Option, - pub(crate) step: Option, -} - -impl<'db> ClassType<'db> { - /// If this class is a specialization of `slice`, returns a [`SliceLiteral`] describing it. - /// Otherwise returns `None`. - /// - /// The specialization must be one in which the typevars are solved as being statically known - /// integers or `None`. - pub(crate) fn slice_literal(self, db: &'db dyn Db) -> Option { - let ClassType::Generic(alias) = self else { - return None; - }; - if !alias.origin(db).is_known(db, KnownClass::Slice) { - return None; - } - let [start, stop, step] = alias.specialization(db).types(db) else { - return None; - }; - - let to_u32 = |ty: &Type<'db>| match ty { - Type::IntLiteral(n) => i32::try_from(*n).map(Some).ok(), - Type::BooleanLiteral(b) => Some(Some(i32::from(*b))), - Type::NominalInstance(instance) - if instance.class.is_known(db, KnownClass::NoneType) => - { - Some(None) - } - _ => None, - }; - Some(SliceLiteral { - start: to_u32(start)?, - stop: to_u32(stop)?, - step: to_u32(step)?, - }) - } -} - #[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(super) struct MetaclassError<'db> { kind: MetaclassErrorKind<'db>, @@ -4663,16 +4713,14 @@ impl SlotsKind { match slots_ty { // __slots__ = ("a", "b") - Type::Tuple(tuple) => { - let tuple = tuple.tuple(db); - if tuple.is_variadic() { - Self::Dynamic - } else if tuple.is_empty() { - Self::Empty - } else { - Self::NotEmpty - } - } + Type::NominalInstance(nominal) => match nominal + .tuple_spec(db) + .and_then(|spec| spec.len().into_fixed_length()) + { + Some(0) => Self::Empty, + Some(_) => Self::NotEmpty, + None => Self::Dynamic, + }, // __slots__ = "abc" # Same as `("abc",)` Type::StringLiteral(_) => Self::NotEmpty, diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 4c5363971b..dfc90a1867 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -87,7 +87,7 @@ impl<'db> ClassBase<'db> { } Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), Type::NominalInstance(instance) - if instance.class.is_known(db, KnownClass::GenericAlias) => + if instance.class(db).is_known(db, KnownClass::GenericAlias) => { Self::try_from_type(db, todo_type!("GenericAlias instance")) } @@ -153,7 +153,6 @@ impl<'db> ClassBase<'db> { | Type::EnumLiteral(_) | Type::StringLiteral(_) | Type::LiteralString - | Type::Tuple(_) | Type::ModuleLiteral(_) | Type::TypeVar(_) | Type::BoundSuper(_) diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 2004c6a26f..544e2c066d 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -73,9 +73,17 @@ impl Display for DisplayRepresentation<'_> { Type::Dynamic(dynamic) => dynamic.fmt(f), Type::Never => f.write_str("Never"), Type::NominalInstance(instance) => { - match (instance.class, instance.class.known(self.db)) { + let class = instance.class(self.db); + + match (class, class.known(self.db)) { (_, Some(KnownClass::NoneType)) => f.write_str("None"), (_, Some(KnownClass::NoDefaultType)) => f.write_str("NoDefault"), + (ClassType::Generic(alias), Some(KnownClass::Tuple)) => alias + .specialization(self.db) + .tuple(self.db) + .expect("Specialization::tuple() should always return `Some()` for `KnownClass::Tuple`") + .display(self.db) + .fmt(f), (ClassType::NonGeneric(class), _) => f.write_str(class.name(self.db)), (ClassType::Generic(alias), _) => alias.display(self.db).fmt(f), } @@ -207,7 +215,6 @@ impl Display for DisplayRepresentation<'_> { name = enum_literal.name(self.db), ) } - Type::Tuple(specialization) => specialization.tuple(self.db).display(self.db).fmt(f), Type::TypeVar(bound_typevar) => { f.write_str(bound_typevar.typevar(self.db).name(self.db))?; if let Some(binding_context) = bound_typevar.binding_context(self.db).name(self.db) @@ -395,8 +402,8 @@ pub(crate) struct DisplayGenericAlias<'db> { impl Display for DisplayGenericAlias<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if self.origin.is_known(self.db, KnownClass::Tuple) { - self.specialization.tuple(self.db).display(self.db).fmt(f) + if let Some(tuple) = self.specialization.tuple(self.db) { + tuple.display(self.db).fmt(f) } else { write!( f, diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 5eb183dade..31c2e1fddf 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -119,54 +119,58 @@ pub(crate) fn enum_metadata<'db>( } let inferred = place_from_bindings(db, bindings); + let value_ty = match inferred { Place::Unbound => { return None; } Place::Type(ty, _) => { - match ty { + let special_case = match ty { Type::Callable(_) | Type::FunctionLiteral(_) => { // Some types are specifically disallowed for enum members. return None; } - // enum.nonmember - Type::NominalInstance(instance) - if instance.class.is_known(db, KnownClass::Nonmember) => - { - return None; - } - // enum.member - Type::NominalInstance(instance) - if instance.class.is_known(db, KnownClass::Member) => - { - ty.member(db, "value") - .place - .ignore_possibly_unbound() - .unwrap_or(Type::unknown()) - } - // enum.auto - Type::NominalInstance(instance) - if instance.class.is_known(db, KnownClass::Auto) => - { - auto_counter += 1; - Type::IntLiteral(auto_counter) - } - _ => { - let dunder_get = ty - .member_lookup_with_policy( - db, - "__get__".into(), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place; + Type::NominalInstance(instance) => match instance.class(db).known(db) { + // enum.nonmember + Some(KnownClass::Nonmember) => return None, - match dunder_get { - Place::Unbound | Place::Type(Type::Dynamic(_), _) => ty, + // enum.member + Some(KnownClass::Member) => Some( + ty.member(db, "value") + .place + .ignore_possibly_unbound() + .unwrap_or(Type::unknown()), + ), - Place::Type(_, _) => { - // Descriptors are not considered members. - return None; - } + // enum.auto + Some(KnownClass::Auto) => { + auto_counter += 1; + Some(Type::IntLiteral(auto_counter)) + } + + _ => None, + }, + + _ => None, + }; + + if let Some(special_case) = special_case { + special_case + } else { + let dunder_get = ty + .member_lookup_with_policy( + db, + "__get__".into(), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place; + + match dunder_get { + Place::Unbound | Place::Type(Type::Dynamic(_), _) => ty, + + Place::Type(_, _) => { + // Descriptors are not considered members. + return None; } } } @@ -203,7 +207,7 @@ pub(crate) fn enum_metadata<'db>( Ok(PlaceAndQualifiers { place: Place::Type(Type::NominalInstance(instance), _), .. - }) if instance.class.is_known(db, KnownClass::Member) => { + }) if instance.class(db).is_known(db, KnownClass::Member) => { // If the attribute is specifically declared with `enum.member`, it is considered a member } _ => { diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 98d584c1d9..c1434f6cb0 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -945,7 +945,7 @@ fn is_instance_truthiness<'db>( let is_instance = |ty: &Type<'_>| { if let Type::NominalInstance(instance) = ty { if instance - .class + .class(db) .iter_mro(db) .filter_map(ClassBase::into_class) .any(|c| match c { @@ -994,8 +994,6 @@ fn is_instance_truthiness<'db>( .is_some_and(is_instance), ), - Type::Tuple(..) => always_true_if(class.is_known(db, KnownClass::Tuple)), - Type::FunctionLiteral(..) => { always_true_if(is_instance(&KnownClass::FunctionType.to_instance(db))) } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 3fb478c34e..20851a9f69 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -10,13 +10,13 @@ use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKind}; use crate::types::class::ClassType; use crate::types::class_base::ClassBase; use crate::types::infer::infer_definition_types; -use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType}; +use crate::types::instance::{Protocol, ProtocolInstanceType}; use crate::types::signatures::{Parameter, Parameters, Signature}; -use crate::types::tuple::{TupleSpec, TupleType}; +use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::{ - BoundTypeVarInstance, KnownInstanceType, Type, TypeMapping, TypeRelation, TypeTransformer, - TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType, binding_type, - declaration_type, + BoundTypeVarInstance, KnownClass, KnownInstanceType, Type, TypeMapping, TypeRelation, + TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarVariance, UnionType, + binding_type, declaration_type, }; use crate::{Db, FxOrderSet}; @@ -230,8 +230,22 @@ impl<'db> GenericContext<'db> { parameter } - pub(crate) fn default_specialization(self, db: &'db dyn Db) -> Specialization<'db> { - self.specialize_partial(db, &vec![None; self.variables(db).len()]) + pub(crate) fn default_specialization( + self, + db: &'db dyn Db, + known_class: Option, + ) -> Specialization<'db> { + let partial = self.specialize_partial(db, &vec![None; self.variables(db).len()]); + if known_class == Some(KnownClass::Tuple) { + Specialization::new( + db, + self, + partial.types(db), + TupleType::homogeneous(db, Type::unknown()), + ) + } else { + partial + } } pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> Specialization<'db> { @@ -401,24 +415,14 @@ pub(super) fn walk_specialization<'db, V: super::visitor::TypeVisitor<'db> + ?Si visitor.visit_type(db, *ty); } if let Some(tuple) = specialization.tuple_inner(db) { - visitor.visit_tuple_type(db, tuple); + walk_tuple_type(db, tuple, visitor); } } impl<'db> Specialization<'db> { /// Returns the tuple spec for a specialization of the `tuple` class. - pub(crate) fn tuple(self, db: &'db dyn Db) -> &'db TupleSpec<'db> { - if let Some(tuple) = self.tuple_inner(db).map(|tuple_type| tuple_type.tuple(db)) { - return tuple; - } - if let [element_type] = self.types(db) { - if let Some(tuple) = TupleType::new(db, TupleSpec::homogeneous(*element_type)) { - return tuple.tuple(db); - } - } - TupleType::new(db, TupleSpec::homogeneous(Type::unknown())) - .expect("tuple[Unknown, ...] should never contain Never") - .tuple(db) + pub(crate) fn tuple(self, db: &'db dyn Db) -> Option<&'db TupleSpec<'db>> { + self.tuple_inner(db).map(|tuple_type| tuple_type.tuple(db)) } /// Returns the type that a typevar is mapped to, or None if the typevar isn't part of this @@ -628,6 +632,16 @@ impl<'db> Specialization<'db> { } } + match (self.tuple_inner(db), other.tuple_inner(db)) { + (Some(_), None) | (None, Some(_)) => return false, + (None, None) => {} + (Some(self_tuple), Some(other_tuple)) => { + if !self_tuple.is_equivalent_to(db, other_tuple) { + return false; + } + } + } + true } @@ -640,6 +654,8 @@ impl<'db> Specialization<'db> { for ty in self.types(db) { ty.find_legacy_typevars(db, binding_context, typevars); } + // A tuple's specialization will include all of its element types, so we don't need to also + // look in `self.tuple`. } } @@ -834,60 +850,67 @@ impl<'db> SpecializationBuilder<'db> { } } - (Type::Tuple(formal_tuple), Type::Tuple(actual_tuple)) => { - let formal_tuple = formal_tuple.tuple(self.db); - let actual_tuple = actual_tuple.tuple(self.db); - let Some(most_precise_length) = formal_tuple.len().most_precise(actual_tuple.len()) else { - return Ok(()); - }; - let Ok(formal_tuple) = formal_tuple.resize(self.db, most_precise_length) else { - return Ok(()); - }; - let Ok(actual_tuple) = actual_tuple.resize(self.db, most_precise_length) else { - return Ok(()); - }; - for (formal_element, actual_element) in - formal_tuple.all_elements().zip(actual_tuple.all_elements()) - { - self.infer(*formal_element, *actual_element)?; - } - } - - ( - Type::NominalInstance(NominalInstanceType { - class: ClassType::Generic(formal_alias), - .. - }) - // TODO: This will only handle classes that explicit implement a generic protocol - // by listing it as a base class. To handle classes that implicitly implement a - // generic protocol, we will need to check the types of the protocol members to be - // able to infer the specialization of the protocol that the class implements. - | Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::FromClass(ClassType::Generic(formal_alias)), - .. - }), - Type::NominalInstance(NominalInstanceType { - class: actual_class, - .. - }), - ) => { - let formal_origin = formal_alias.origin(self.db); - for base in actual_class.iter_mro(self.db) { - let ClassBase::Class(ClassType::Generic(base_alias)) = base else { - continue; + (formal, Type::NominalInstance(actual_nominal)) => { + // Special case: `formal` and `actual` are both tuples. + if let (Some(formal_tuple), Some(actual_tuple)) = ( + formal.tuple_instance_spec(self.db), + actual_nominal.tuple_spec(self.db), + ) { + let Some(most_precise_length) = + formal_tuple.len().most_precise(actual_tuple.len()) + else { + return Ok(()); }; - if formal_origin != base_alias.origin(self.db) { - continue; - } - let formal_specialization = formal_alias.specialization(self.db).types(self.db); - let base_specialization = base_alias.specialization(self.db).types(self.db); - for (formal_ty, base_ty) in - formal_specialization.iter().zip(base_specialization) + let Ok(formal_tuple) = formal_tuple.resize(self.db, most_precise_length) else { + return Ok(()); + }; + let Ok(actual_tuple) = actual_tuple.resize(self.db, most_precise_length) else { + return Ok(()); + }; + for (formal_element, actual_element) in + formal_tuple.all_elements().zip(actual_tuple.all_elements()) { - self.infer(*formal_ty, *base_ty)?; + self.infer(*formal_element, *actual_element)?; } return Ok(()); } + + // Extract formal_alias if this is a generic class + let formal_alias = match formal { + Type::NominalInstance(formal_nominal) => { + formal_nominal.class(self.db).into_generic_alias() + } + // TODO: This will only handle classes that explicit implement a generic protocol + // by listing it as a base class. To handle classes that implicitly implement a + // generic protocol, we will need to check the types of the protocol members to be + // able to infer the specialization of the protocol that the class implements. + Type::ProtocolInstance(ProtocolInstanceType { + inner: Protocol::FromClass(ClassType::Generic(alias)), + .. + }) => Some(alias), + _ => None, + }; + + if let Some(formal_alias) = formal_alias { + let formal_origin = formal_alias.origin(self.db); + for base in actual_nominal.class(self.db).iter_mro(self.db) { + let ClassBase::Class(ClassType::Generic(base_alias)) = base else { + continue; + }; + if formal_origin != base_alias.origin(self.db) { + continue; + } + let formal_specialization = + formal_alias.specialization(self.db).types(self.db); + let base_specialization = base_alias.specialization(self.db).types(self.db); + for (formal_ty, base_ty) in + formal_specialization.iter().zip(base_specialization) + { + self.infer(*formal_ty, *base_ty)?; + } + return Ok(()); + } + } } // TODO: Add more forms that we can structurally induct into: type[C], callables diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index b88270a43a..da53963671 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -94,7 +94,7 @@ impl<'db> AllMembers<'db> { ), Type::NominalInstance(instance) => { - let (class_literal, _specialization) = instance.class.class_literal(db); + let (class_literal, _specialization) = instance.class(db).class_literal(db); self.extend_with_instance_members(db, ty, class_literal); } @@ -137,7 +137,6 @@ impl<'db> AllMembers<'db> { | Type::BytesLiteral(_) | Type::EnumLiteral(_) | Type::LiteralString - | Type::Tuple(_) | Type::PropertyInstance(_) | Type::FunctionLiteral(_) | Type::BoundMethod(_) @@ -208,7 +207,7 @@ impl<'db> AllMembers<'db> { match ty { Type::NominalInstance(instance) if matches!( - instance.class.known(db), + instance.class(db).known(db), Some( KnownClass::TypeVar | KnownClass::TypeVarTuple @@ -868,7 +867,7 @@ mod resolve_definition { pub enum ImportAliasResolution { /// Resolve import aliases to their original definitions ResolveAliases, - /// Keep import aliases as-is, don't resolve to original definitions + /// Keep import aliases as-is, don't resolve to original definitions PreserveAliases, } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 698c590474..509555ab4c 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -62,7 +62,7 @@ use super::string_annotation::{ BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation, }; use super::subclass_of::SubclassOfInner; -use super::{ClassBase, NominalInstanceType, add_inferred_python_version_hint_to_diagnostic}; +use super::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::module_name::{ModuleName, ModuleNameResolutionError}; use crate::module_resolver::resolve_module; use crate::node_key::NodeKey; @@ -90,7 +90,7 @@ use crate::semantic_index::{ ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, semantic_index, }; use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind}; -use crate::types::class::{CodeGeneratorKind, Field, MetaclassErrorKind, SliceLiteral}; +use crate::types::class::{CodeGeneratorKind, Field, MetaclassErrorKind}; use crate::types::diagnostic::{ self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, @@ -111,9 +111,10 @@ use crate::types::function::{ FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral, }; use crate::types::generics::{GenericContext, bind_typevar}; +use crate::types::instance::SliceLiteral; use crate::types::mro::MroErrorKind; use crate::types::signatures::{CallableSignature, Signature}; -use crate::types::tuple::{TupleSpec, TupleType}; +use crate::types::tuple::{Tuple, TupleSpec, TupleType}; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType, @@ -1375,7 +1376,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; }; - if !instance.class.is_known(self.db(), KnownClass::KwOnly) { + if !instance + .class(self.db()) + .is_known(self.db(), KnownClass::KwOnly) + { continue; } @@ -1772,7 +1776,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::BooleanLiteral(_) | Type::IntLiteral(_) => {} Type::NominalInstance(instance) if matches!( - instance.class.known(self.db()), + instance.class(self.db()).known(self.db()), Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool) ) => {} _ => return false, @@ -3286,22 +3290,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_exception(&mut self, node: Option<&ast::Expr>, is_star: bool) -> Type<'db> { - fn extract_tuple_specialization<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { - let class = ty.into_nominal_instance()?.class; - if !class.is_known(db, KnownClass::Tuple) { - return None; - } - let ClassType::Generic(class) = class else { - return None; - }; - let specialization = class.specialization(db).types(db)[0]; - let specialization_instance = specialization.to_instance(db)?; - - specialization_instance - .is_assignable_to(db, KnownClass::BaseException.to_instance(db)) - .then_some(specialization_instance) - } - // If there is no handled exception, it's invalid syntax; // a diagnostic will have already been emitted let node_ty = node.map_or(Type::unknown(), |ty| self.infer_expression(ty)); @@ -3309,9 +3297,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If it's an `except*` handler, this won't actually be the type of the bound symbol; // it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`. - let symbol_ty = if let Type::Tuple(tuple) = node_ty { + let symbol_ty = if let Some(tuple_spec) = node_ty.tuple_instance_spec(self.db()) { let mut builder = UnionBuilder::new(self.db()); - for element in tuple.tuple(self.db()).all_elements().copied() { + for element in tuple_spec.all_elements().copied() { builder = builder.add( if element.is_assignable_to(self.db(), type_base_exception) { element.to_instance(self.db()).expect( @@ -3336,7 +3324,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.db(), Type::homogeneous_tuple(self.db(), type_base_exception), ) { - extract_tuple_specialization(self.db(), node_ty) + node_ty + .tuple_instance_spec(self.db()) + .and_then(|spec| { + let specialization = spec + .homogeneous_element_type(self.db()) + .to_instance(self.db()); + + debug_assert!(specialization.is_some_and(|specialization_type| { + specialization_type.is_assignable_to( + self.db(), + KnownClass::BaseException.to_instance(self.db()), + ) + })); + + specialization + }) .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db())) } else if node_ty.is_assignable_to( self.db(), @@ -3954,7 +3957,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // Super instances do not allow attribute assignment - Type::NominalInstance(instance) if instance.class.is_known(db, KnownClass::Super) => { + Type::NominalInstance(instance) + if instance.class(db).is_known(db, KnownClass::Super) => + { if emit_diagnostics { if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) { builder.into_diagnostic(format_args!( @@ -3987,7 +3992,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::BytesLiteral(..) | Type::EnumLiteral(..) | Type::LiteralString - | Type::Tuple(..) | Type::SpecialForm(..) | Type::KnownInstance(..) | Type::PropertyInstance(..) @@ -4403,15 +4407,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::Expr::Name(name) => self.infer_definition(name), ast::Expr::List(ast::ExprList { elts, .. }) | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { - let mut assigned_tys = match assigned_ty { - Some(Type::Tuple(tuple)) => { - Either::Left(tuple.tuple(self.db()).all_elements().copied()) + if let Some(tuple_spec) = + assigned_ty.and_then(|ty| ty.tuple_instance_spec(self.db())) + { + let mut assigned_tys = tuple_spec.all_elements(); + for element in elts { + self.infer_target_impl(element, value, assigned_tys.next().copied()); + } + } else { + for element in elts { + self.infer_target_impl(element, value, None); } - Some(_) | None => Either::Right(std::iter::empty()), - }; - - for element in elts { - self.infer_target_impl(element, value, assigned_tys.next()); } } ast::Expr::Attribute( @@ -4613,7 +4619,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Handle various singletons. if let Type::NominalInstance(instance) = declared.inner_type() { - if instance.class.is_known(self.db(), KnownClass::SpecialForm) { + if instance + .class(self.db()) + .is_known(self.db(), KnownClass::SpecialForm) + { if let Some(name_expr) = target.as_name_expr() { if let Some(special_form) = SpecialFormType::try_from_file_and_name( self.db(), @@ -7182,7 +7191,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::LiteralString | Type::BytesLiteral(_) | Type::EnumLiteral(_) - | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypeVar(_) | Type::TypeIs(_) @@ -7508,7 +7516,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::LiteralString | Type::BytesLiteral(_) | Type::EnumLiteral(_) - | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypeVar(_) | Type::TypeIs(_) @@ -7538,7 +7545,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::LiteralString | Type::BytesLiteral(_) | Type::EnumLiteral(_) - | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypeVar(_) | Type::TypeIs(_) @@ -7938,14 +7944,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // language spec. // - `[ast::CompOp::Is]`: return `false` if unequal, `bool` if equal // - `[ast::CompOp::IsNot]`: return `true` if unequal, `bool` if equal - match (left, right) { + let comparison_result = match (left, right) { (Type::Union(union), other) => { let mut builder = UnionBuilder::new(self.db()); for element in union.elements(self.db()) { builder = builder.add(self.infer_binary_type_comparison(*element, op, other, range)?); } - Ok(builder.build()) + Some(Ok(builder.build())) } (other, Type::Union(union)) => { let mut builder = UnionBuilder::new(self.db()); @@ -7953,27 +7959,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder = builder.add(self.infer_binary_type_comparison(other, op, *element, range)?); } - Ok(builder.build()) + Some(Ok(builder.build())) } - (Type::Intersection(intersection), right) => self - .infer_binary_intersection_type_comparison( + (Type::Intersection(intersection), right) => { + Some(self.infer_binary_intersection_type_comparison( intersection, op, right, IntersectionOn::Left, range, - ), - (left, Type::Intersection(intersection)) => self - .infer_binary_intersection_type_comparison( + )) + } + (left, Type::Intersection(intersection)) => { + Some(self.infer_binary_intersection_type_comparison( intersection, op, left, IntersectionOn::Right, range, - ), + )) + } - (Type::IntLiteral(n), Type::IntLiteral(m)) => match op { + (Type::IntLiteral(n), Type::IntLiteral(m)) => Some(match op { ast::CmpOp::Eq => Ok(Type::BooleanLiteral(n == m)), ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(n != m)), ast::CmpOp::Lt => Ok(Type::BooleanLiteral(n < m)), @@ -8002,45 +8010,54 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { left_ty: left, right_ty: right, }), - }, - (Type::IntLiteral(_), Type::NominalInstance(_)) => self.infer_binary_type_comparison( - KnownClass::Int.to_instance(self.db()), - op, - right, - range, - ), - (Type::NominalInstance(_), Type::IntLiteral(_)) => self.infer_binary_type_comparison( - left, - op, - KnownClass::Int.to_instance(self.db()), - range, - ), + }), + (Type::IntLiteral(_), Type::NominalInstance(_)) => { + Some(self.infer_binary_type_comparison( + KnownClass::Int.to_instance(self.db()), + op, + right, + range, + )) + } + (Type::NominalInstance(_), Type::IntLiteral(_)) => { + Some(self.infer_binary_type_comparison( + left, + op, + KnownClass::Int.to_instance(self.db()), + range, + )) + } // Booleans are coded as integers (False = 0, True = 1) - (Type::IntLiteral(n), Type::BooleanLiteral(b)) => self.infer_binary_type_comparison( - Type::IntLiteral(n), - op, - Type::IntLiteral(i64::from(b)), - range, - ), - (Type::BooleanLiteral(b), Type::IntLiteral(m)) => self.infer_binary_type_comparison( - Type::IntLiteral(i64::from(b)), - op, - Type::IntLiteral(m), - range, - ), - (Type::BooleanLiteral(a), Type::BooleanLiteral(b)) => self - .infer_binary_type_comparison( + (Type::IntLiteral(n), Type::BooleanLiteral(b)) => { + Some(self.infer_binary_type_comparison( + Type::IntLiteral(n), + op, + Type::IntLiteral(i64::from(b)), + range, + )) + } + (Type::BooleanLiteral(b), Type::IntLiteral(m)) => { + Some(self.infer_binary_type_comparison( + Type::IntLiteral(i64::from(b)), + op, + Type::IntLiteral(m), + range, + )) + } + (Type::BooleanLiteral(a), Type::BooleanLiteral(b)) => { + Some(self.infer_binary_type_comparison( Type::IntLiteral(i64::from(a)), op, Type::IntLiteral(i64::from(b)), range, - ), + )) + } (Type::StringLiteral(salsa_s1), Type::StringLiteral(salsa_s2)) => { let s1 = salsa_s1.value(self.db()); let s2 = salsa_s2.value(self.db()); - match op { + let result = match op { ast::CmpOp::Eq => Ok(Type::BooleanLiteral(s1 == s2)), ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(s1 != s2)), ast::CmpOp::Lt => Ok(Type::BooleanLiteral(s1 < s2)), @@ -8063,38 +8080,39 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Ok(Type::BooleanLiteral(true)) } } - } + }; + Some(result) } - (Type::StringLiteral(_), _) => self.infer_binary_type_comparison( + (Type::StringLiteral(_), _) => Some(self.infer_binary_type_comparison( KnownClass::Str.to_instance(self.db()), op, right, range, - ), - (_, Type::StringLiteral(_)) => self.infer_binary_type_comparison( + )), + (_, Type::StringLiteral(_)) => Some(self.infer_binary_type_comparison( left, op, KnownClass::Str.to_instance(self.db()), range, - ), + )), - (Type::LiteralString, _) => self.infer_binary_type_comparison( + (Type::LiteralString, _) => Some(self.infer_binary_type_comparison( KnownClass::Str.to_instance(self.db()), op, right, range, - ), - (_, Type::LiteralString) => self.infer_binary_type_comparison( + )), + (_, Type::LiteralString) => Some(self.infer_binary_type_comparison( left, op, KnownClass::Str.to_instance(self.db()), range, - ), + )), (Type::BytesLiteral(salsa_b1), Type::BytesLiteral(salsa_b2)) => { let b1 = salsa_b1.value(self.db()); let b2 = salsa_b2.value(self.db()); - match op { + let result = match op { ast::CmpOp::Eq => Ok(Type::BooleanLiteral(b1 == b2)), ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(b1 != b2)), ast::CmpOp::Lt => Ok(Type::BooleanLiteral(b1 < b2)), @@ -8121,161 +8139,142 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Ok(Type::BooleanLiteral(true)) } } - } + }; + Some(result) } - (Type::BytesLiteral(_), _) => self.infer_binary_type_comparison( + (Type::BytesLiteral(_), _) => Some(self.infer_binary_type_comparison( KnownClass::Bytes.to_instance(self.db()), op, right, range, - ), - (_, Type::BytesLiteral(_)) => self.infer_binary_type_comparison( + )), + (_, Type::BytesLiteral(_)) => Some(self.infer_binary_type_comparison( left, op, KnownClass::Bytes.to_instance(self.db()), range, - ), + )), (Type::EnumLiteral(literal_1), Type::EnumLiteral(literal_2)) if op == ast::CmpOp::Eq => { - Ok(Type::BooleanLiteral(literal_1 == literal_2)) + Some(Ok(Type::BooleanLiteral(literal_1 == literal_2))) } (Type::EnumLiteral(literal_1), Type::EnumLiteral(literal_2)) if op == ast::CmpOp::NotEq => { - Ok(Type::BooleanLiteral(literal_1 != literal_2)) + Some(Ok(Type::BooleanLiteral(literal_1 != literal_2))) } - (Type::Tuple(_), Type::NominalInstance(instance)) - if instance.class.is_known(self.db(), KnownClass::VersionInfo) => - { - self.infer_binary_type_comparison( - left, - op, - Type::version_info_tuple(self.db()), - range, - ) - } - (Type::NominalInstance(instance), Type::Tuple(_)) - if instance.class.is_known(self.db(), KnownClass::VersionInfo) => - { - self.infer_binary_type_comparison( - Type::version_info_tuple(self.db()), - op, - right, - range, - ) - } - (Type::Tuple(lhs), Type::Tuple(rhs)) => { - let lhs_tuple = lhs.tuple(self.db()); - let rhs_tuple = rhs.tuple(self.db()); + ( + Type::NominalInstance(nominal1), + Type::NominalInstance(nominal2), + ) => nominal1.tuple_spec(self.db()) + .and_then(|lhs_tuple| Some((lhs_tuple, nominal2.tuple_spec(self.db())?))) + .map(|(lhs_tuple, rhs_tuple)| { + let mut tuple_rich_comparison = + |op| self.infer_tuple_rich_comparison(&lhs_tuple, op, &rhs_tuple, range); - let mut tuple_rich_comparison = - |op| self.infer_tuple_rich_comparison(lhs_tuple, op, rhs_tuple, range); + match op { + ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq), + ast::CmpOp::NotEq => tuple_rich_comparison(RichCompareOperator::Ne), + ast::CmpOp::Lt => tuple_rich_comparison(RichCompareOperator::Lt), + ast::CmpOp::LtE => tuple_rich_comparison(RichCompareOperator::Le), + ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt), + ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge), + ast::CmpOp::In | ast::CmpOp::NotIn => { + let mut any_eq = false; + let mut any_ambiguous = false; - match op { - ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq), - ast::CmpOp::NotEq => tuple_rich_comparison(RichCompareOperator::Ne), - ast::CmpOp::Lt => tuple_rich_comparison(RichCompareOperator::Lt), - ast::CmpOp::LtE => tuple_rich_comparison(RichCompareOperator::Le), - ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt), - ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge), - ast::CmpOp::In | ast::CmpOp::NotIn => { - let mut any_eq = false; - let mut any_ambiguous = false; - - for ty in rhs_tuple.all_elements().copied() { - let eq_result = self.infer_binary_type_comparison( - Type::Tuple(lhs), + for ty in rhs_tuple.all_elements().copied() { + let eq_result = self.infer_binary_type_comparison( + left, ast::CmpOp::Eq, ty, range, ).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); - match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for different union variants. Instead, this is just for us to - // evaluate a possibly truthy value to `false` or `true`. - ty => match ty.bool(self.db()) { - Truthiness::AlwaysTrue => any_eq = true, - Truthiness::AlwaysFalse => (), - Truthiness::Ambiguous => any_ambiguous = true, - }, + match eq_result { + todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), + // It's okay to ignore errors here because Python doesn't call `__bool__` + // for different union variants. Instead, this is just for us to + // evaluate a possibly truthy value to `false` or `true`. + ty => match ty.bool(self.db()) { + Truthiness::AlwaysTrue => any_eq = true, + Truthiness::AlwaysFalse => (), + Truthiness::Ambiguous => any_ambiguous = true, + }, + } + } + + if any_eq { + Ok(Type::BooleanLiteral(op.is_in())) + } else if !any_ambiguous { + Ok(Type::BooleanLiteral(op.is_not_in())) + } else { + Ok(KnownClass::Bool.to_instance(self.db())) } } - - if any_eq { - Ok(Type::BooleanLiteral(op.is_in())) - } else if !any_ambiguous { - Ok(Type::BooleanLiteral(op.is_not_in())) - } else { - Ok(KnownClass::Bool.to_instance(self.db())) - } - } - ast::CmpOp::Is | ast::CmpOp::IsNot => { - // - `[ast::CmpOp::Is]`: returns `false` if the elements are definitely unequal, otherwise `bool` - // - `[ast::CmpOp::IsNot]`: returns `true` if the elements are definitely unequal, otherwise `bool` - let eq_result = tuple_rich_comparison(RichCompareOperator::Eq).expect( + ast::CmpOp::Is | ast::CmpOp::IsNot => { + // - `[ast::CmpOp::Is]`: returns `false` if the elements are definitely unequal, otherwise `bool` + // - `[ast::CmpOp::IsNot]`: returns `true` if the elements are definitely unequal, otherwise `bool` + let eq_result = tuple_rich_comparison(RichCompareOperator::Eq).expect( "infer_binary_type_comparison should never return None for `CmpOp::Eq`", ); - Ok(match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => todo, - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for `is` and `is not` comparisons. This is an implementation detail - // for how we determine the truthiness of a type. - ty => match ty.bool(self.db()) { - Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()), - _ => KnownClass::Bool.to_instance(self.db()), - }, - }) + Ok(match eq_result { + todo @ Type::Dynamic(DynamicType::Todo(_)) => todo, + // It's okay to ignore errors here because Python doesn't call `__bool__` + // for `is` and `is not` comparisons. This is an implementation detail + // for how we determine the truthiness of a type. + ty => match ty.bool(self.db()) { + Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()), + _ => KnownClass::Bool.to_instance(self.db()), + }, + }) + } } } - } + ), - // Lookup the rich comparison `__dunder__` methods - _ => { - let rich_comparison = |op| self.infer_rich_comparison(left, right, op); - let membership_test_comparison = |op, range: TextRange| { - self.infer_membership_test_comparison(left, right, op, range) - }; - match op { - ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq), - ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne), - ast::CmpOp::Lt => rich_comparison(RichCompareOperator::Lt), - ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le), - ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt), - ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge), - ast::CmpOp::In => { - membership_test_comparison(MembershipTestCompareOperator::In, range) - } - ast::CmpOp::NotIn => { - membership_test_comparison(MembershipTestCompareOperator::NotIn, range) - } - ast::CmpOp::Is => { - if left.is_disjoint_from(self.db(), right) { - Ok(Type::BooleanLiteral(false)) - } else if left.is_singleton(self.db()) - && left.is_equivalent_to(self.db(), right) - { - Ok(Type::BooleanLiteral(true)) - } else { - Ok(KnownClass::Bool.to_instance(self.db())) - } - } - ast::CmpOp::IsNot => { - if left.is_disjoint_from(self.db(), right) { - Ok(Type::BooleanLiteral(true)) - } else if left.is_singleton(self.db()) - && left.is_equivalent_to(self.db(), right) - { - Ok(Type::BooleanLiteral(false)) - } else { - Ok(KnownClass::Bool.to_instance(self.db())) - } - } + _ => None, + }; + + if let Some(result) = comparison_result { + return result; + } + + // Final generalized fallback: lookup the rich comparison `__dunder__` methods + let rich_comparison = |op| self.infer_rich_comparison(left, right, op); + let membership_test_comparison = + |op, range: TextRange| self.infer_membership_test_comparison(left, right, op, range); + match op { + ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq), + ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne), + ast::CmpOp::Lt => rich_comparison(RichCompareOperator::Lt), + ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le), + ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt), + ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge), + ast::CmpOp::In => membership_test_comparison(MembershipTestCompareOperator::In, range), + ast::CmpOp::NotIn => { + membership_test_comparison(MembershipTestCompareOperator::NotIn, range) + } + ast::CmpOp::Is => { + if left.is_disjoint_from(self.db(), right) { + Ok(Type::BooleanLiteral(false)) + } else if left.is_singleton(self.db()) && left.is_equivalent_to(self.db(), right) { + Ok(Type::BooleanLiteral(true)) + } else { + Ok(KnownClass::Bool.to_instance(self.db())) + } + } + ast::CmpOp::IsNot => { + if left.is_disjoint_from(self.db(), right) { + Ok(Type::BooleanLiteral(true)) + } else if left.is_singleton(self.db()) && left.is_equivalent_to(self.db(), right) { + Ok(Type::BooleanLiteral(false)) + } else { + Ok(KnownClass::Bool.to_instance(self.db())) } } } @@ -8527,10 +8526,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let tuple_generic_alias = |db: &'db dyn Db, tuple: Option>| { let tuple = tuple.unwrap_or_else(|| TupleType::homogeneous(db, Type::unknown()).unwrap()); - tuple - .to_class_type(db) - .map(Type::from) - .unwrap_or_else(Type::unknown) + Type::from(tuple.to_class_type(db)) }; // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the @@ -8619,17 +8615,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let value_node = subscript.value.as_ref(); let inferred = match (value_ty, slice_ty) { - (Type::NominalInstance(instance), _) - if instance.class.is_known(db, KnownClass::VersionInfo) => - { - Some(self.infer_subscript_expression_types( - subscript, - Type::version_info_tuple(db), - slice_ty, - expr_context, - )) - } - (Type::Union(union), _) => Some(union.map(db, |element| { self.infer_subscript_expression_types(subscript, *element, slice_ty, expr_context) })), @@ -8643,9 +8628,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` - (Type::Tuple(tuple_ty), Type::IntLiteral(i64_int)) => { - i32::try_from(i64_int).ok().map(|i32_int| { - let tuple = tuple_ty.tuple(db); + (Type::NominalInstance(nominal), Type::IntLiteral(i64_int)) => nominal + .tuple_spec(db) + .and_then(|tuple| Some((tuple, i32::try_from(i64_int).ok()?))) + .map(|(tuple, i32_int)| { tuple.py_index(db, i32_int).unwrap_or_else(|_| { report_index_out_of_bounds( context, @@ -8657,26 +8643,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); Type::unknown() }) - }) - } + }), // Ex) Given `("a", 1, Null)[0:2]`, return `("a", 1)` - (Type::Tuple(tuple_ty), Type::NominalInstance(NominalInstanceType { class, .. })) => { - class - .slice_literal(db) - .map(|SliceLiteral { start, stop, step }| { - let TupleSpec::Fixed(tuple) = tuple_ty.tuple(db) else { - return todo_type!("slice into variable-length tuple"); - }; - + ( + Type::NominalInstance(maybe_tuple_nominal), + Type::NominalInstance(maybe_slice_nominal), + ) => maybe_tuple_nominal + .tuple_spec(db) + .as_deref() + .and_then(|tuple_spec| Some((tuple_spec, maybe_slice_nominal.slice_literal(db)?))) + .map(|(tuple, SliceLiteral { start, stop, step })| match tuple { + TupleSpec::Fixed(tuple) => { if let Ok(new_elements) = tuple.py_slice(db, start, stop, step) { Type::heterogeneous_tuple(db, new_elements) } else { report_slice_step_size_zero(context, value_node.into()); Type::unknown() } - }) - } + } + TupleSpec::Variable(_) => { + todo_type!("slice into variable-length tuple") + } + }), // Ex) Given `"value"[1]`, return `"a"` (Type::StringLiteral(literal_ty), Type::IntLiteral(i64_int)) => { @@ -8700,10 +8689,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // Ex) Given `"value"[1:3]`, return `"al"` - ( - Type::StringLiteral(literal_ty), - Type::NominalInstance(NominalInstanceType { class, .. }), - ) => class + (Type::StringLiteral(literal_ty), Type::NominalInstance(nominal)) => nominal .slice_literal(db) .map(|SliceLiteral { start, stop, step }| { let literal_value = literal_ty.value(db); @@ -8740,10 +8726,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // Ex) Given `b"value"[1:3]`, return `b"al"` - ( - Type::BytesLiteral(literal_ty), - Type::NominalInstance(NominalInstanceType { class, .. }), - ) => class + (Type::BytesLiteral(literal_ty), Type::NominalInstance(nominal)) => nominal .slice_literal(db) .map(|SliceLiteral { start, stop, step }| { let literal_value = literal_ty.value(db); @@ -8758,37 +8741,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }), // Ex) Given `"value"[True]`, return `"a"` - ( - Type::Tuple(_) | Type::StringLiteral(_) | Type::BytesLiteral(_), - Type::BooleanLiteral(bool), - ) => Some(self.infer_subscript_expression_types( - subscript, - value_ty, - Type::IntLiteral(i64::from(bool)), - expr_context, - )), - - (Type::SpecialForm(SpecialFormType::Protocol), Type::Tuple(typevars)) => { - Some(match typevars.tuple(db) { - TupleSpec::Fixed(typevars) => self - .legacy_generic_class_context( - value_node, - typevars.elements_slice(), - LegacyGenericBase::Protocol, - ) - .map(|context| { - Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context)) - }) - .unwrap_or_else(GenericContextError::into_type), - // TODO: emit a diagnostic - TupleSpec::Variable(_) => Type::unknown(), - }) + (Type::StringLiteral(_) | Type::BytesLiteral(_), Type::BooleanLiteral(bool)) => { + Some(self.infer_subscript_expression_types( + subscript, + value_ty, + Type::IntLiteral(i64::from(bool)), + expr_context, + )) } - (Type::SpecialForm(SpecialFormType::Protocol), typevar) => Some( + (Type::NominalInstance(nominal), Type::BooleanLiteral(bool)) + if nominal.tuple_spec(db).is_some() => + { + Some(self.infer_subscript_expression_types( + subscript, + value_ty, + Type::IntLiteral(i64::from(bool)), + expr_context, + )) + } + + (Type::SpecialForm(SpecialFormType::Protocol), typevars) => Some( self.legacy_generic_class_context( value_node, - std::slice::from_ref(&typevar), + typevars, LegacyGenericBase::Protocol, ) .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context))) @@ -8800,31 +8776,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(todo_type!("doubly-specialized typing.Protocol")) } - (Type::SpecialForm(SpecialFormType::Generic), Type::Tuple(typevars)) => { - Some(match typevars.tuple(db) { - TupleSpec::Fixed(typevars) => self - .legacy_generic_class_context( - value_node, - typevars.elements_slice(), - LegacyGenericBase::Generic, - ) - .map(|context| { - Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context)) - }) - .unwrap_or_else(GenericContextError::into_type), - // TODO: emit a diagnostic - TupleSpec::Variable(_) => Type::unknown(), - }) - } - - (Type::SpecialForm(SpecialFormType::Generic), typevar) => Some( - self.legacy_generic_class_context( - value_node, - std::slice::from_ref(&typevar), - LegacyGenericBase::Generic, - ) - .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context))) - .unwrap_or_else(GenericContextError::into_type), + (Type::SpecialForm(SpecialFormType::Generic), typevars) => Some( + self.legacy_generic_class_context(value_node, typevars, LegacyGenericBase::Generic) + .map(|context| { + Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context)) + }) + .unwrap_or_else(GenericContextError::into_type), ), (Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_)), _) => { @@ -9010,9 +8967,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn legacy_generic_class_context( &self, value_node: &ast::Expr, - typevars: &[Type<'db>], + typevars: Type<'db>, origin: LegacyGenericBase, ) -> Result, GenericContextError> { + let typevars_class_tuple_spec = typevars.exact_tuple_instance_spec(self.db()); + + let typevars = if let Some(tuple_spec) = typevars_class_tuple_spec.as_deref() { + match tuple_spec { + Tuple::Fixed(typevars) => typevars.elements_slice(), + // TODO: emit a diagnostic + Tuple::Variable(_) => return Err(GenericContextError::VariadicTupleArguments), + } + } else { + std::slice::from_ref(&typevars) + }; + let typevars: Result, GenericContextError> = typevars .iter() .map(|typevar| match typevar { @@ -9026,9 +8995,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) .ok_or(GenericContextError::InvalidArgument), Type::Dynamic(DynamicType::TodoUnpack) => Err(GenericContextError::NotYetSupported), - Type::NominalInstance(NominalInstanceType { class, .. }) + Type::NominalInstance(nominal) if matches!( - class.known(self.db()), + nominal.class(self.db()).known(self.db()), Some(KnownClass::TypeVarTuple | KnownClass::ParamSpec) ) => { @@ -9071,7 +9040,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let type_to_slice_argument = |ty: Option>| match ty { Some(ty @ (Type::IntLiteral(_) | Type::BooleanLiteral(_))) => SliceArg::Arg(ty), Some(ty @ Type::NominalInstance(instance)) - if instance.class.is_known(self.db(), KnownClass::NoneType) => + if instance + .class(self.db()) + .is_known(self.db(), KnownClass::NoneType) => { SliceArg::Arg(ty) } @@ -9984,7 +9955,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } = starred; let starred_type = self.infer_type_expression(value); - if let Type::Tuple(_) = starred_type { + if starred_type.exact_tuple_instance_spec(self.db()).is_some() { starred_type } else { todo_type!("PEP 646") @@ -10042,7 +10013,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } match element { - ast::Expr::Starred(_) => !matches!(element_ty, Type::Tuple(_)), + ast::Expr::Starred(_) => { + element_ty.exact_tuple_instance_spec(builder.db()).is_none() + } ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => { let value_ty = if builder.deferred_state.in_string_annotation() { // Using `.expression_type` does not work in string annotations, because @@ -10079,10 +10052,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let element_ty = self.infer_type_expression(element); return_todo |= element_could_alter_type_of_whole_tuple(element, element_ty, self); + if let ast::Expr::Starred(_) = element { - if let Type::Tuple(inner_tuple) = element_ty { - element_types = - element_types.concat(self.db(), inner_tuple.tuple(self.db())); + if let Some(inner_tuple) = element_ty.exact_tuple_instance_spec(self.db()) { + element_types = element_types.concat(self.db(), &inner_tuple); } else { // TODO: emit a diagnostic } @@ -10941,8 +10914,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::Dynamic(DynamicType::TodoPEP695ParamSpec) => { return Some(Parameters::todo()); } - Type::NominalInstance(NominalInstanceType { class, .. }) - if class.is_known(self.db(), KnownClass::ParamSpec) => + Type::NominalInstance(nominal) + if nominal + .class(self.db()) + .is_known(self.db(), KnownClass::ParamSpec) => { return Some(Parameters::todo()); } @@ -10966,6 +10941,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { enum GenericContextError { /// It's invalid to subscript `Generic` or `Protocol` with this type InvalidArgument, + /// It's invalid to subscript `Generic` or `Protocol` with a variadic tuple type. + /// We should emit a diagnostic for this, but we don't yet. + VariadicTupleArguments, /// It's valid to subscribe `Generic` or `Protocol` with this type, /// but the type is not yet supported. NotYetSupported, @@ -10974,7 +10952,9 @@ enum GenericContextError { impl GenericContextError { const fn into_type<'db>(self) -> Type<'db> { match self { - GenericContextError::InvalidArgument => Type::unknown(), + GenericContextError::InvalidArgument | GenericContextError::VariadicTupleArguments => { + Type::unknown() + } GenericContextError::NotYetSupported => todo_type!("ParamSpecs and TypeVarTuples"), } } diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 116c85e63b..5e61cd3f13 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -1,5 +1,6 @@ //! Instance types: both nominal and structural. +use std::borrow::Cow; use std::marker::PhantomData; use super::protocol_class::ProtocolInterface; @@ -9,35 +10,48 @@ use crate::semantic_index::definition::Definition; use crate::types::cyclic::PairVisitor; use crate::types::enums::is_single_member_enum; use crate::types::protocol_class::walk_protocol_interface; -use crate::types::tuple::TupleType; -use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeTransformer, TypedDictType}; -use crate::{Db, FxOrderSet}; +use crate::types::tuple::{TupleSpec, TupleType}; +use crate::types::{ + ClassBase, DynamicType, TypeMapping, TypeRelation, TypeTransformer, TypedDictType, UnionType, +}; +use crate::{Db, FxOrderSet, Program}; pub(super) use synthesized_protocol::SynthesizedProtocolType; impl<'db> Type<'db> { pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { - match (class, class.known(db)) { - (_, Some(KnownClass::Any)) => Self::Dynamic(DynamicType::Any), - (ClassType::NonGeneric(_), Some(KnownClass::Tuple)) => { - Type::tuple(TupleType::homogeneous(db, Type::unknown())) - } - (ClassType::Generic(alias), Some(KnownClass::Tuple)) => { - Self::tuple(TupleType::new(db, alias.specialization(db).tuple(db))) - } - _ => { - let class_literal = class.class_literal(db).0; - if class_literal.is_protocol(db) { - Self::ProtocolInstance(ProtocolInstanceType::from_class(class)) - } else if class_literal.is_typed_dict(db) { - TypedDictType::from(db, class) - } else { - Self::NominalInstance(NominalInstanceType::from_class(class)) - } + let (class_literal, specialization) = class.class_literal(db); + + match class_literal.known(db) { + Some(KnownClass::Any) => Type::Dynamic(DynamicType::Any), + Some(KnownClass::Tuple) => Type::tuple(TupleType::new( + db, + specialization + .and_then(|spec| Some(Cow::Borrowed(spec.tuple(db)?))) + .unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown()))) + .as_ref(), + )), + _ if class_literal.is_protocol(db) => { + Self::ProtocolInstance(ProtocolInstanceType::from_class(class)) } + _ if class_literal.is_typed_dict(db) => TypedDictType::from(db, class), + _ => Type::non_tuple_instance(class), } } + pub(crate) fn tuple(tuple: Option>) -> Self { + let Some(tuple) = tuple else { + return Type::Never; + }; + Type::NominalInstance(NominalInstanceType(NominalInstanceInner::ExactTuple(tuple))) + } + + /// **Private** helper function to create a `Type::NominalInstance` from a class that + /// is known not to be `Any`, a protocol class, or a typed dict class. + fn non_tuple_instance(class: ClassType<'db>) -> Self { + Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple(class))) + } + pub(crate) const fn into_nominal_instance(self) -> Option> { match self { Type::NominalInstance(instance_type) => Some(instance_type), @@ -76,42 +90,176 @@ impl<'db> Type<'db> { /// A type representing the set of runtime objects which are instances of a certain nominal class. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)] -pub struct NominalInstanceType<'db> { - pub(super) class: ClassType<'db>, - +pub struct NominalInstanceType<'db>( // Keep this field private, so that the only way of constructing `NominalInstanceType` instances // is through the `Type::instance` constructor function. - _phantom: PhantomData<()>, -} + NominalInstanceInner<'db>, +); pub(super) fn walk_nominal_instance_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, nominal: NominalInstanceType<'db>, visitor: &mut V, ) { - visitor.visit_type(db, nominal.class.into()); + visitor.visit_type(db, nominal.class(db).into()); } impl<'db> NominalInstanceType<'db> { - // Keep this method private, so that the only way of constructing `NominalInstanceType` - // instances is through the `Type::instance` constructor function. - fn from_class(class: ClassType<'db>) -> Self { - Self { - class, - _phantom: PhantomData, + pub(super) fn class(&self, db: &'db dyn Db) -> ClassType<'db> { + match self.0 { + NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db), + NominalInstanceInner::NonTuple(class) => class, } } + /// If this is an instance type where the class has a tuple spec, returns the tuple spec. + /// + /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. + /// For a subclass of `tuple[int, str]`, it will return the same tuple spec. + pub(super) fn tuple_spec(&self, db: &'db dyn Db) -> Option>> { + fn own_tuple_spec_of_class<'db>( + db: &'db dyn Db, + class: ClassType<'db>, + ) -> Option>> { + let (class_literal, specialization) = class.class_literal(db); + match class_literal.known(db)? { + KnownClass::Tuple => Some( + specialization + .and_then(|spec| Some(Cow::Borrowed(spec.tuple(db)?))) + .unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown()))), + ), + KnownClass::VersionInfo => { + let python_version = Program::get(db).python_version(db); + let int_instance_ty = KnownClass::Int.to_instance(db); + + // TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there) + let release_level_ty = { + let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"] + .iter() + .map(|level| Type::string_literal(db, level)) + .collect(); + + // For most unions, it's better to go via `UnionType::from_elements` or use `UnionBuilder`; + // those techniques ensure that union elements are deduplicated and unions are eagerly simplified + // into other types where necessary. Here, however, we know that there are no duplicates + // in this union, so it's probably more efficient to use `UnionType::new()` directly. + Type::Union(UnionType::new(db, elements)) + }; + + Some(Cow::Owned(TupleSpec::from_elements([ + Type::IntLiteral(python_version.major.into()), + Type::IntLiteral(python_version.minor.into()), + int_instance_ty, + release_level_ty, + int_instance_ty, + ]))) + } + _ => None, + } + } + + match self.0 { + NominalInstanceInner::ExactTuple(tuple) => Some(Cow::Borrowed(tuple.tuple(db))), + NominalInstanceInner::NonTuple(class) => { + // Avoid an expensive MRO traversal for common stdlib classes. + if class + .known(db) + .is_some_and(|known_class| !known_class.is_tuple_subclass()) + { + return None; + } + class + .iter_mro(db) + .filter_map(ClassBase::into_class) + .find_map(|class| own_tuple_spec_of_class(db, class)) + } + } + } + + /// Return `true` if this type represents instances of the class `builtins.object`. + pub(super) fn is_object(self, db: &'db dyn Db) -> bool { + match self.0 { + NominalInstanceInner::ExactTuple(_) => false, + NominalInstanceInner::NonTuple(class) => class.is_object(db), + } + } + + /// If this type is an *exact* tuple type (*not* a subclass of `tuple`), returns the + /// tuple spec. + /// + /// You usually don't want to use this method, as you usually want to consider a subclass + /// of a tuple type in the same way as the `tuple` type itself. Only use this method if you + /// are certain that a *literal tuple* is required, and that a subclass of tuple will not + /// do. + /// + /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. + /// But for a subclass of `tuple[int, str]`, it will return `None`. + pub(super) fn own_tuple_spec(&self, db: &'db dyn Db) -> Option>> { + match self.0 { + NominalInstanceInner::ExactTuple(tuple) => Some(Cow::Borrowed(tuple.tuple(db))), + NominalInstanceInner::NonTuple(_) => None, + } + } + + /// If this is a specialized instance of `slice`, returns a [`SliceLiteral`] describing it. + /// Otherwise returns `None`. + /// + /// The specialization must be one in which the typevars are solved as being statically known + /// integers or `None`. + pub(crate) fn slice_literal(self, db: &'db dyn Db) -> Option { + let class = match self.0 { + NominalInstanceInner::ExactTuple(_) => return None, + NominalInstanceInner::NonTuple(class) => class, + }; + let (class, Some(specialization)) = class.class_literal(db) else { + return None; + }; + if !class.is_known(db, KnownClass::Slice) { + return None; + } + let [start, stop, step] = specialization.types(db) else { + return None; + }; + + let to_u32 = |ty: &Type<'db>| match ty { + Type::IntLiteral(n) => i32::try_from(*n).map(Some).ok(), + Type::BooleanLiteral(b) => Some(Some(i32::from(*b))), + Type::NominalInstance(instance) + if instance.class(db).is_known(db, KnownClass::NoneType) => + { + Some(None) + } + _ => None, + }; + Some(SliceLiteral { + start: to_u32(start)?, + stop: to_u32(stop)?, + step: to_u32(step)?, + }) + } + pub(super) fn normalized_impl( self, db: &'db dyn Db, visitor: &mut TypeTransformer<'db>, - ) -> Self { - Self::from_class(self.class.normalized_impl(db, visitor)) + ) -> Type<'db> { + match self.0 { + NominalInstanceInner::ExactTuple(tuple) => { + Type::tuple(tuple.normalized_impl(db, visitor)) + } + NominalInstanceInner::NonTuple(class) => { + Type::non_tuple_instance(class.normalized_impl(db, visitor)) + } + } } - pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Self { - Self::from_class(self.class.materialize(db, variance)) + pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> { + match self.0 { + NominalInstanceInner::ExactTuple(tuple) => Type::tuple(tuple.materialize(db, variance)), + NominalInstanceInner::NonTuple(class) => { + Type::non_tuple_instance(class.materialize(db, variance)) + } + } } pub(super) fn has_relation_to( @@ -120,41 +268,93 @@ impl<'db> NominalInstanceType<'db> { other: Self, relation: TypeRelation, ) -> bool { - self.class.has_relation_to(db, other.class, relation) + match (self.0, other.0) { + ( + NominalInstanceInner::ExactTuple(tuple1), + NominalInstanceInner::ExactTuple(tuple2), + ) => tuple1.has_relation_to(db, tuple2, relation), + _ => self + .class(db) + .has_relation_to(db, other.class(db), relation), + } } pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - self.class.is_equivalent_to(db, other.class) + match (self.0, other.0) { + ( + NominalInstanceInner::ExactTuple(tuple1), + NominalInstanceInner::ExactTuple(tuple2), + ) => tuple1.is_equivalent_to(db, tuple2), + (NominalInstanceInner::NonTuple(class1), NominalInstanceInner::NonTuple(class2)) => { + class1.is_equivalent_to(db, class2) + } + _ => false, + } } - pub(super) fn is_disjoint_from_impl(self, db: &'db dyn Db, other: Self) -> bool { - !self.class.could_coexist_in_mro_with(db, other.class) + pub(super) fn is_disjoint_from_impl( + self, + db: &'db dyn Db, + other: Self, + visitor: &mut PairVisitor<'db>, + ) -> bool { + let self_spec = self.tuple_spec(db); + if let Some(self_spec) = self_spec.as_deref() { + let other_spec = other.tuple_spec(db); + if let Some(other_spec) = other_spec.as_deref() { + if self_spec.is_disjoint_from_impl(db, other_spec, visitor) { + return true; + } + } + } + !self + .class(db) + .could_coexist_in_mro_with(db, other.class(db)) } pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool { - self.class - .known(db) - .map(KnownClass::is_singleton) - .unwrap_or_else(|| is_single_member_enum(db, self.class.class_literal(db).0)) + match self.0 { + // The empty tuple is a singleton on CPython and PyPy, but not on other Python + // implementations such as GraalPy. Its *use* as a singleton is discouraged and + // should not be relied on for type narrowing, so we do not treat it as one. + // See: + // https://docs.python.org/3/reference/expressions.html#parenthesized-forms + NominalInstanceInner::ExactTuple(_) => false, + NominalInstanceInner::NonTuple(class) => class + .known(db) + .map(KnownClass::is_singleton) + .unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db).0)), + } } pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool { - self.class - .known(db) - .map(KnownClass::is_single_valued) - .unwrap_or_else(|| is_single_member_enum(db, self.class.class_literal(db).0)) + match self.0 { + NominalInstanceInner::ExactTuple(tuple) => tuple.is_single_valued(db), + NominalInstanceInner::NonTuple(class) => class + .known(db) + .and_then(KnownClass::is_single_valued) + .or_else(|| Some(self.tuple_spec(db)?.is_single_valued(db))) + .unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db).0)), + } } pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - SubclassOfType::from(db, self.class) + SubclassOfType::from(db, self.class(db)) } pub(super) fn apply_type_mapping<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, - ) -> Self { - Self::from_class(self.class.apply_type_mapping(db, type_mapping)) + ) -> Type<'db> { + match self.0 { + NominalInstanceInner::ExactTuple(tuple) => { + Type::tuple(tuple.apply_type_mapping(db, type_mapping)) + } + NominalInstanceInner::NonTuple(class) => { + Type::non_tuple_instance(class.apply_type_mapping(db, type_mapping)) + } + } } pub(super) fn find_legacy_typevars( @@ -163,8 +363,14 @@ impl<'db> NominalInstanceType<'db> { binding_context: Option>, typevars: &mut FxOrderSet>, ) { - self.class - .find_legacy_typevars(db, binding_context, typevars); + match self.0 { + NominalInstanceInner::ExactTuple(tuple) => { + tuple.find_legacy_typevars(db, binding_context, typevars); + } + NominalInstanceInner::NonTuple(class) => { + class.find_legacy_typevars(db, binding_context, typevars); + } + } } } @@ -174,6 +380,30 @@ impl<'db> From> for Type<'db> { } } +/// [`NominalInstanceType`] is split into two variants internally as a pure +/// optimization to avoid having to materialize the [`ClassType`] for tuple +/// instances where it would be unnecessary (this is somewhat expensive!). +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, salsa::Update, get_size2::GetSize)] +enum NominalInstanceInner<'db> { + /// A tuple type, e.g. `tuple[int, str]`. + /// + /// Note that the type `tuple[int, str]` includes subtypes of `tuple[int, str]`, + /// but those subtypes would be represented using the `NonTuple` variant. + ExactTuple(TupleType<'db>), + /// Any instance type that does not represent some kind of instance of the + /// builtin `tuple` class. + /// + /// This variant includes types that are subtypes of "exact tuple" types, + /// because they represent "all instances of a class that is a tuple subclass". + NonTuple(ClassType<'db>), +} + +pub(crate) struct SliceLiteral { + pub(crate) start: Option, + pub(crate) stop: Option, + pub(crate) step: Option, +} + /// A `ProtocolInstanceType` represents the set of all possible runtime objects /// that conform to the interface described by a certain protocol. #[derive( @@ -253,7 +483,7 @@ impl<'db> ProtocolInstanceType<'db> { db: &'db dyn Db, visitor: &mut TypeTransformer<'db>, ) -> Type<'db> { - let object = KnownClass::Object.to_instance(db); + let object = Type::object(db); if object.satisfies_protocol(db, self, TypeRelation::Subtyping) { return object; } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index cb3f8c658f..3f02677f41 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -182,14 +182,6 @@ impl ClassInfoConstraintFunction { }; match classinfo { - Type::Tuple(tuple) => UnionType::try_from_elements( - db, - tuple - .tuple(db) - .all_elements() - .copied() - .map(|element| self.generate_constraint(db, element)), - ), Type::ClassLiteral(class_literal) => { // At runtime (on Python 3.11+), this will return `True` for classes that actually // do inherit `typing.Any` and `False` otherwise. We could accurately model that? @@ -236,6 +228,15 @@ impl ClassInfoConstraintFunction { // e.g. `isinstance(x, list[int])` fails at runtime. Type::GenericAlias(_) => None, + Type::NominalInstance(nominal) => nominal.tuple_spec(db).and_then(|tuple| { + UnionType::try_from_elements( + db, + tuple + .all_elements() + .map(|element| self.generate_constraint(db, *element)), + ) + }), + Type::AlwaysFalsy | Type::AlwaysTruthy | Type::BooleanLiteral(_) @@ -252,7 +253,6 @@ impl ClassInfoConstraintFunction { | Type::ProtocolInstance(_) | Type::PropertyInstance(_) | Type::SpecialForm(_) - | Type::NominalInstance(_) | Type::LiteralString | Type::StringLiteral(_) | Type::IntLiteral(_) @@ -560,7 +560,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } // Treat `bool` as `Literal[True, False]`. Type::NominalInstance(instance) - if instance.class.is_known(db, KnownClass::Bool) => + if instance.class(db).is_known(db, KnownClass::Bool) => { UnionType::from_elements( db, @@ -571,11 +571,11 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } // Treat enums as a union of their members. Type::NominalInstance(instance) - if enum_metadata(db, instance.class.class_literal(db).0).is_some() => + if enum_metadata(db, instance.class(db).class_literal(db).0).is_some() => { UnionType::from_elements( db, - enum_member_literals(db, instance.class.class_literal(db).0, None) + enum_member_literals(db, instance.class(db).class_literal(db).0, None) .expect("Calling `enum_member_literals` on an enum class") .map(|ty| filter_to_cannot_be_equal(db, ty, rhs_ty)), ) @@ -602,7 +602,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { fn evaluate_expr_ne(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { match (lhs_ty, rhs_ty) { (Type::NominalInstance(instance), Type::IntLiteral(i)) - if instance.class.is_known(self.db, KnownClass::Bool) => + if instance.class(self.db).is_known(self.db, KnownClass::Bool) => { if i == 0 { Some(Type::BooleanLiteral(false).negate(self.db)) @@ -623,20 +623,21 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { fn evaluate_expr_in(&mut self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { if lhs_ty.is_single_valued(self.db) || lhs_ty.is_union_of_single_valued(self.db) { - match rhs_ty { - Type::Tuple(rhs_tuple) => Some(UnionType::from_elements( - self.db, - rhs_tuple.tuple(self.db).all_elements(), - )), - - Type::StringLiteral(string_literal) => Some(UnionType::from_elements( + if let Type::StringLiteral(string_literal) = rhs_ty { + Some(UnionType::from_elements( self.db, string_literal .iter_each_char(self.db) .map(Type::StringLiteral), - )), - - _ => None, + )) + } else if let Some(tuple_spec) = rhs_ty.tuple_instance_spec(self.db) { + // N.B. Strictly speaking this is unsound, since a tuple subclass might override `__contains__` + // but we'd still apply the narrowing here. This seems unlikely, however, and narrowing is + // generally unsound in numerous ways anyway (attribute narrowing, subscript, narrowing, + // narrowing of globals, etc.). So this doesn't seem worth worrying about too much. + Some(UnionType::from_elements(self.db, tuple_spec.all_elements())) + } else { + None } } else { None diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index 012b37ecce..0a296088ff 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -153,7 +153,7 @@ impl Ty { .place .expect_type(); debug_assert!( - matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class.class_literal(db).0)) + matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class(db).class_literal(db).0)) ); ty } diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index adde95b2fd..f04346d7c8 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -23,12 +23,12 @@ use std::hash::Hash; use itertools::{Either, EitherOrBoth, Itertools}; use crate::semantic_index::definition::Definition; +use crate::types::Truthiness; use crate::types::class::{ClassType, KnownClass}; use crate::types::{ BoundTypeVarInstance, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarVariance, UnionBuilder, UnionType, cyclic::PairVisitor, }; -use crate::types::{SubclassOfType, Truthiness}; use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; use crate::{Db, FxOrderSet}; @@ -146,13 +146,6 @@ pub(super) fn walk_tuple_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized> impl get_size2::GetSize for TupleType<'_> {} impl<'db> Type<'db> { - pub(crate) fn tuple(tuple: Option>) -> Self { - let Some(tuple) = tuple else { - return Type::Never; - }; - Self::Tuple(tuple) - } - pub(crate) fn homogeneous_tuple(db: &'db dyn Db, element: Type<'db>) -> Self { Type::tuple(TupleType::homogeneous(db, element)) } @@ -169,7 +162,7 @@ impl<'db> Type<'db> { } pub(crate) fn empty_tuple(db: &'db dyn Db) -> Self { - Type::Tuple(TupleType::empty(db)) + Type::tuple(Some(TupleType::empty(db))) } } @@ -227,23 +220,22 @@ impl<'db> TupleType<'db> { TupleType::new(db, TupleSpec::homogeneous(element)) } + // N.B. If this method is not Salsa-tracked, we take 10 minutes to check + // `static-frame` as part of a mypy_primer run! This is because it's called + // from `NominalInstanceType::class()`, which is a very hot method. #[salsa::tracked] - pub(crate) fn to_class_type(self, db: &'db dyn Db) -> Option> { - KnownClass::Tuple + pub(crate) fn to_class_type(self, db: &'db dyn Db) -> ClassType<'db> { + let tuple_class = KnownClass::Tuple .try_to_class_literal(db) - .and_then(|class_literal| match class_literal.generic_context(db) { - None => Some(ClassType::NonGeneric(class_literal)), - Some(generic_context) if generic_context.variables(db).len() != 1 => None, - Some(generic_context) => Some( - class_literal - .apply_specialization(db, |_| generic_context.specialize_tuple(db, self)), - ), - }) - } + .expect("Typeshed should always have a `tuple` class in `builtins.pyi`"); - pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Option> { - self.to_class_type(db) - .map(|class| SubclassOfType::from(db, class)) + tuple_class.apply_specialization(db, |generic_context| { + if generic_context.variables(db).len() == 1 { + generic_context.specialize_tuple(db, self) + } else { + generic_context.default_specialization(db, Some(KnownClass::Tuple)) + } + }) } /// Return a normalized version of `self`. @@ -294,23 +286,9 @@ impl<'db> TupleType<'db> { self.tuple(db).is_equivalent_to(db, other.tuple(db)) } - pub(crate) fn is_disjoint_from_impl( - self, - db: &'db dyn Db, - other: Self, - visitor: &mut PairVisitor<'db>, - ) -> bool { - self.tuple(db) - .is_disjoint_from_impl(db, other.tuple(db), visitor) - } - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { self.tuple(db).is_single_valued(db) } - - pub(crate) fn truthiness(self, db: &'db dyn Db) -> Truthiness { - self.tuple(db).truthiness() - } } /// A tuple spec describes the contents of a tuple type, which might be fixed- or variable-length. @@ -361,10 +339,6 @@ impl FixedLengthTuple { self.0.len() } - fn is_empty(&self) -> bool { - self.0.is_empty() - } - pub(crate) fn push(&mut self, element: T) { self.0.push(element); } @@ -1029,10 +1003,6 @@ impl Tuple { } } - pub(crate) const fn is_variadic(&self) -> bool { - matches!(self, Tuple::Variable(_)) - } - /// Returns the length of this tuple. pub(crate) fn len(&self) -> TupleLength { match self { @@ -1052,13 +1022,6 @@ impl Tuple { } } - pub(crate) fn is_empty(&self) -> bool { - match self { - Tuple::Fixed(tuple) => tuple.is_empty(), - Tuple::Variable(_) => false, - } - } - pub(crate) fn push(&mut self, element: T) { match self { Tuple::Fixed(tuple) => tuple.push(element), @@ -1154,10 +1117,10 @@ impl<'db> Tuple> { } } - fn is_disjoint_from_impl( - &'db self, + pub(super) fn is_disjoint_from_impl( + &self, db: &'db dyn Db, - other: &'db Self, + other: &Self, visitor: &mut PairVisitor<'db>, ) -> bool { // Two tuples with an incompatible number of required elements must always be disjoint. @@ -1172,12 +1135,15 @@ impl<'db> Tuple> { // If any of the required elements are pairwise disjoint, the tuples are disjoint as well. #[allow(clippy::items_after_statements)] - fn any_disjoint<'db>( + fn any_disjoint<'s, 'db>( db: &'db dyn Db, - a: impl IntoIterator>, - b: impl IntoIterator>, + a: impl IntoIterator>, + b: impl IntoIterator>, visitor: &mut PairVisitor<'db>, - ) -> bool { + ) -> bool + where + 'db: 's, + { a.into_iter().zip(b).any(|(self_element, other_element)| { self_element.is_disjoint_from_impl(db, *other_element, visitor) }) @@ -1231,7 +1197,7 @@ impl<'db> Tuple> { false } - fn is_single_valued(&self, db: &'db dyn Db) -> bool { + pub(crate) fn is_single_valued(&self, db: &'db dyn Db) -> bool { match self { Tuple::Fixed(tuple) => tuple.is_single_valued(db), Tuple::Variable(_) => false, diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 729a181b15..f5d1910f25 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -100,10 +100,6 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::Callable(_), _) => Ordering::Less, (_, Type::Callable(_)) => Ordering::Greater, - (Type::Tuple(left), Type::Tuple(right)) => left.cmp(right), - (Type::Tuple(_), _) => Ordering::Less, - (_, Type::Tuple(_)) => Ordering::Greater, - (Type::ModuleLiteral(left), Type::ModuleLiteral(right)) => left.cmp(right), (Type::ModuleLiteral(_), _) => Ordering::Less, (_, Type::ModuleLiteral(_)) => Ordering::Greater, @@ -134,7 +130,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::TypeIs(_), _) => Ordering::Less, (_, Type::TypeIs(_)) => Ordering::Greater, - (Type::NominalInstance(left), Type::NominalInstance(right)) => left.class.cmp(&right.class), + (Type::NominalInstance(left), Type::NominalInstance(right)) => { + left.class(db).cmp(&right.class(db)) + } (Type::NominalInstance(_), _) => Ordering::Less, (_, Type::NominalInstance(_)) => Ordering::Greater, @@ -178,7 +176,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (SuperOwnerKind::Class(_), _) => Ordering::Less, (_, SuperOwnerKind::Class(_)) => Ordering::Greater, (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => { - left.class.cmp(&right.class) + left.class(db).cmp(&right.class(db)) } (SuperOwnerKind::Instance(_), _) => Ordering::Less, (_, SuperOwnerKind::Instance(_)) => Ordering::Greater, diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index f371598b17..705ecd03d0 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -9,7 +9,6 @@ use crate::{ function::{FunctionType, walk_function_type}, instance::{walk_nominal_instance_type, walk_protocol_instance_type}, subclass_of::walk_subclass_of_type, - tuple::{TupleType, walk_tuple_type}, walk_bound_method_type, walk_bound_super_type, walk_bound_type_var_type, walk_callable_type, walk_intersection_type, walk_known_instance_type, walk_method_wrapper_type, walk_property_instance_type, walk_type_alias_type, @@ -33,10 +32,6 @@ pub(crate) trait TypeVisitor<'db> { walk_intersection_type(db, intersection, self); } - fn visit_tuple_type(&mut self, db: &'db dyn Db, tuple: TupleType<'db>) { - walk_tuple_type(db, tuple, self); - } - fn visit_callable_type(&mut self, db: &'db dyn Db, callable: CallableType<'db>) { walk_callable_type(db, callable, self); } @@ -127,7 +122,6 @@ pub(crate) trait TypeVisitor<'db> { enum NonAtomicType<'db> { Union(UnionType<'db>), Intersection(IntersectionType<'db>), - Tuple(TupleType<'db>), FunctionLiteral(FunctionType<'db>), BoundMethod(BoundMethodType<'db>), BoundSuper(BoundSuperType<'db>), @@ -177,7 +171,6 @@ impl<'db> From> for TypeKind<'db> { TypeKind::NonAtomic(NonAtomicType::Intersection(intersection)) } Type::Union(union) => TypeKind::NonAtomic(NonAtomicType::Union(union)), - Type::Tuple(tuple) => TypeKind::NonAtomic(NonAtomicType::Tuple(tuple)), Type::BoundMethod(method) => TypeKind::NonAtomic(NonAtomicType::BoundMethod(method)), Type::BoundSuper(bound_super) => { TypeKind::NonAtomic(NonAtomicType::BoundSuper(bound_super)) @@ -224,7 +217,6 @@ fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( visitor.visit_intersection_type(db, intersection); } NonAtomicType::Union(union) => visitor.visit_union_type(db, union), - NonAtomicType::Tuple(tuple) => visitor.visit_tuple_type(db, tuple), NonAtomicType::BoundMethod(method) => visitor.visit_bound_method_type(db, method), NonAtomicType::BoundSuper(bound_super) => visitor.visit_bound_super_type(db, bound_super), NonAtomicType::MethodWrapper(method_wrapper) => {