diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index d036fb64bce89..9f83b1ac417b6 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -71,12 +71,12 @@ Such calls have confusing semantics and often indicate a logic error. ### Examples ```python from typing import reveal_type -from ty_extensions import is_fully_static +from ty_extensions import is_singleton if flag: f = repr # Expects a value else: - f = is_fully_static # Expects a type form + f = is_singleton # Expects a type form f(int) # error ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index e6e4771050140..847377ae00529 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -313,7 +313,7 @@ len([], 1) ### Type API predicates ```py -from ty_extensions import is_subtype_of, is_fully_static +from ty_extensions import is_subtype_of # error: [missing-argument] is_subtype_of() @@ -326,10 +326,4 @@ is_subtype_of(int, int, int) # error: [too-many-positional-arguments] is_subtype_of(int, int, int, int) - -# error: [missing-argument] -is_fully_static() - -# error: [too-many-positional-arguments] -is_fully_static(int, int) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md b/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md index fc54aefd00c75..a68bae12d2616 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md +++ b/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md @@ -144,8 +144,7 @@ from typing import Any def _(a: Any, tuple_of_any: tuple[Any]): reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"] - # TODO: Ideally, this would just be `def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int` - # revealed: (def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int) | Literal["default"] + # revealed: def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int reveal_type(inspect.getattr_static(tuple_of_any, "index", "default")) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index 5edbdb29819b6..b7df4043834f4 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -203,15 +203,15 @@ def _( ## Cannot use an argument as both a value and a type form ```py -from ty_extensions import is_fully_static +from ty_extensions import is_singleton def _(flag: bool): if flag: f = repr else: - f = is_fully_static + f = is_singleton # error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call" - reveal_type(f(int)) # revealed: str | Literal[True] + reveal_type(f(int)) # revealed: str | Literal[False] ``` ## Size limit on unions of literals diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses.md index 98d1af487deaf..41c7cb7078fd9 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses.md @@ -90,11 +90,12 @@ from typing import Any @dataclass class C: + w: type[Any] x: Any y: int | Any z: tuple[int, Any] -reveal_type(C.__init__) # revealed: (self: C, x: Any, y: int | Any, z: tuple[int, Any]) -> None +reveal_type(C.__init__) # revealed: (self: C, w: type[Any], x: Any, y: int | Any, z: tuple[int, Any]) -> None ``` Variables without annotations are ignored: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md index d66e2d913d284..4c2a7032ab04b 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md @@ -22,7 +22,7 @@ Types that "produce" data on demand are covariant in their typevar. If you expec get from the sequence is a valid `int`. ```py -from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any, Generic, TypeVar class A: ... @@ -53,11 +53,13 @@ static_assert(is_assignable_to(D[Any], C[A])) static_assert(is_assignable_to(D[Any], C[B])) static_assert(is_subtype_of(C[B], C[A])) +static_assert(is_subtype_of(C[A], C[A])) static_assert(not is_subtype_of(C[A], C[B])) static_assert(not is_subtype_of(C[A], C[Any])) static_assert(not is_subtype_of(C[B], C[Any])) static_assert(not is_subtype_of(C[Any], C[A])) static_assert(not is_subtype_of(C[Any], C[B])) +static_assert(not is_subtype_of(C[Any], C[Any])) static_assert(is_subtype_of(D[B], C[A])) static_assert(not is_subtype_of(D[A], C[B])) @@ -84,27 +86,11 @@ static_assert(not is_equivalent_to(D[B], C[Any])) static_assert(not is_equivalent_to(D[Any], C[A])) static_assert(not is_equivalent_to(D[Any], C[B])) -static_assert(is_gradual_equivalent_to(C[A], C[A])) -static_assert(is_gradual_equivalent_to(C[B], C[B])) -static_assert(is_gradual_equivalent_to(C[Any], C[Any])) -static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(C[B], C[A])) -static_assert(not is_gradual_equivalent_to(C[A], C[B])) -static_assert(not is_gradual_equivalent_to(C[A], C[Any])) -static_assert(not is_gradual_equivalent_to(C[B], C[Any])) -static_assert(not is_gradual_equivalent_to(C[Any], C[A])) -static_assert(not is_gradual_equivalent_to(C[Any], C[B])) - -static_assert(not is_gradual_equivalent_to(D[A], C[A])) -static_assert(not is_gradual_equivalent_to(D[B], C[B])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(D[B], C[A])) -static_assert(not is_gradual_equivalent_to(D[A], C[B])) -static_assert(not is_gradual_equivalent_to(D[A], C[Any])) -static_assert(not is_gradual_equivalent_to(D[B], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[A])) -static_assert(not is_gradual_equivalent_to(D[Any], C[B])) +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) ``` ## Contravariance @@ -117,7 +103,7 @@ Types that "consume" data are contravariant in their typevar. If you expect a co that you pass into the consumer is a valid `int`. ```py -from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any, Generic, TypeVar class A: ... @@ -178,27 +164,11 @@ static_assert(not is_equivalent_to(D[B], C[Any])) static_assert(not is_equivalent_to(D[Any], C[A])) static_assert(not is_equivalent_to(D[Any], C[B])) -static_assert(is_gradual_equivalent_to(C[A], C[A])) -static_assert(is_gradual_equivalent_to(C[B], C[B])) -static_assert(is_gradual_equivalent_to(C[Any], C[Any])) -static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(C[B], C[A])) -static_assert(not is_gradual_equivalent_to(C[A], C[B])) -static_assert(not is_gradual_equivalent_to(C[A], C[Any])) -static_assert(not is_gradual_equivalent_to(C[B], C[Any])) -static_assert(not is_gradual_equivalent_to(C[Any], C[A])) -static_assert(not is_gradual_equivalent_to(C[Any], C[B])) - -static_assert(not is_gradual_equivalent_to(D[A], C[A])) -static_assert(not is_gradual_equivalent_to(D[B], C[B])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(D[B], C[A])) -static_assert(not is_gradual_equivalent_to(D[A], C[B])) -static_assert(not is_gradual_equivalent_to(D[A], C[Any])) -static_assert(not is_gradual_equivalent_to(D[B], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[A])) -static_assert(not is_gradual_equivalent_to(D[Any], C[B])) +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) ``` ## Invariance @@ -224,7 +194,7 @@ In the end, if you expect a mutable list, you must always be given a list of exa since we can't know in advance which of the allowed methods you'll want to use. ```py -from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any, Generic, TypeVar class A: ... @@ -287,27 +257,11 @@ static_assert(not is_equivalent_to(D[B], C[Any])) static_assert(not is_equivalent_to(D[Any], C[A])) static_assert(not is_equivalent_to(D[Any], C[B])) -static_assert(is_gradual_equivalent_to(C[A], C[A])) -static_assert(is_gradual_equivalent_to(C[B], C[B])) -static_assert(is_gradual_equivalent_to(C[Any], C[Any])) -static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(C[B], C[A])) -static_assert(not is_gradual_equivalent_to(C[A], C[B])) -static_assert(not is_gradual_equivalent_to(C[A], C[Any])) -static_assert(not is_gradual_equivalent_to(C[B], C[Any])) -static_assert(not is_gradual_equivalent_to(C[Any], C[A])) -static_assert(not is_gradual_equivalent_to(C[Any], C[B])) - -static_assert(not is_gradual_equivalent_to(D[A], C[A])) -static_assert(not is_gradual_equivalent_to(D[B], C[B])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(D[B], C[A])) -static_assert(not is_gradual_equivalent_to(D[A], C[B])) -static_assert(not is_gradual_equivalent_to(D[A], C[Any])) -static_assert(not is_gradual_equivalent_to(D[B], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[A])) -static_assert(not is_gradual_equivalent_to(D[Any], C[B])) +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) ``` ## Bivariance diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md index d8149b808f09f..1ea17def0a735 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -113,33 +113,6 @@ class C[T]: reveal_type(x) # revealed: T ``` -## Fully static typevars - -We consider a typevar to be fully static unless it has a non-fully-static bound or constraint. This -is true even though a fully static typevar might be specialized to a gradual form like `Any`. (This -is similar to how you can assign an expression whose type is not fully static to a target whose type -is.) - -```py -from ty_extensions import is_fully_static, static_assert -from typing import Any - -def unbounded_unconstrained[T](t: T) -> None: - static_assert(is_fully_static(T)) - -def bounded[T: int](t: T) -> None: - static_assert(is_fully_static(T)) - -def bounded_by_gradual[T: Any](t: T) -> None: - static_assert(not is_fully_static(T)) - -def constrained[T: (int, str)](t: T) -> None: - static_assert(is_fully_static(T)) - -def constrained_by_gradual[T: (int, Any)](t: T) -> None: - static_assert(not is_fully_static(T)) -``` - ## Subtyping and assignability (Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static @@ -372,14 +345,14 @@ def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: ## Equivalence -A fully static `TypeVar` is always equivalent to itself, but never to another `TypeVar`, since there -is no guarantee that they will be specialized to the same type. (This is true even if both typevars -are bounded by the same final class, since you can specialize the typevars to `Never` in addition to +A `TypeVar` is always equivalent to itself, but never to another `TypeVar`, since there is no +guarantee that they will be specialized to the same type. (This is true even if both typevars are +bounded by the same final class, since you can specialize the typevars to `Never` in addition to that final class.) ```py from typing import final -from ty_extensions import is_equivalent_to, static_assert, is_gradual_equivalent_to +from ty_extensions import is_equivalent_to, static_assert @final class FinalClass: ... @@ -395,28 +368,16 @@ def f[A, B, C: FinalClass, D: FinalClass, E: (FinalClass, SecondFinalClass), F: static_assert(is_equivalent_to(E, E)) static_assert(is_equivalent_to(F, F)) - static_assert(is_gradual_equivalent_to(A, A)) - static_assert(is_gradual_equivalent_to(B, B)) - static_assert(is_gradual_equivalent_to(C, C)) - static_assert(is_gradual_equivalent_to(D, D)) - static_assert(is_gradual_equivalent_to(E, E)) - static_assert(is_gradual_equivalent_to(F, F)) - static_assert(not is_equivalent_to(A, B)) static_assert(not is_equivalent_to(C, D)) static_assert(not is_equivalent_to(E, F)) - - static_assert(not is_gradual_equivalent_to(A, B)) - static_assert(not is_gradual_equivalent_to(C, D)) - static_assert(not is_gradual_equivalent_to(E, F)) ``` -TypeVars which have non-fully-static bounds or constraints do not participate in equivalence -relations, but do participate in gradual equivalence relations. +TypeVars which have non-fully-static bounds or constraints are also self-equivalent. ```py from typing import final, Any -from ty_extensions import is_equivalent_to, static_assert, is_gradual_equivalent_to +from ty_extensions import is_equivalent_to, static_assert # fmt: off @@ -426,15 +387,10 @@ def f[ C: (tuple[Any], tuple[Any, Any]), D: (tuple[Any], tuple[Any, Any]) ](): - static_assert(not is_equivalent_to(A, A)) - static_assert(not is_equivalent_to(B, B)) - static_assert(not is_equivalent_to(C, C)) - static_assert(not is_equivalent_to(D, D)) - - static_assert(is_gradual_equivalent_to(A, A)) - static_assert(is_gradual_equivalent_to(B, B)) - static_assert(is_gradual_equivalent_to(C, C)) - static_assert(is_gradual_equivalent_to(D, D)) + static_assert(is_equivalent_to(A, A)) + static_assert(is_equivalent_to(B, B)) + static_assert(is_equivalent_to(C, C)) + static_assert(is_equivalent_to(D, D)) # fmt: on ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md index 2e5b9507c6640..e98d01a35fd02 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -27,7 +27,7 @@ Types that "produce" data on demand are covariant in their typevar. If you expec get from the sequence is a valid `int`. ```py -from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any class A: ... @@ -94,27 +94,11 @@ static_assert(not is_equivalent_to(D[B], C[Any])) static_assert(not is_equivalent_to(D[Any], C[A])) static_assert(not is_equivalent_to(D[Any], C[B])) -static_assert(is_gradual_equivalent_to(C[A], C[A])) -static_assert(is_gradual_equivalent_to(C[B], C[B])) -static_assert(is_gradual_equivalent_to(C[Any], C[Any])) -static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(C[B], C[A])) -static_assert(not is_gradual_equivalent_to(C[A], C[B])) -static_assert(not is_gradual_equivalent_to(C[A], C[Any])) -static_assert(not is_gradual_equivalent_to(C[B], C[Any])) -static_assert(not is_gradual_equivalent_to(C[Any], C[A])) -static_assert(not is_gradual_equivalent_to(C[Any], C[B])) - -static_assert(not is_gradual_equivalent_to(D[A], C[A])) -static_assert(not is_gradual_equivalent_to(D[B], C[B])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(D[B], C[A])) -static_assert(not is_gradual_equivalent_to(D[A], C[B])) -static_assert(not is_gradual_equivalent_to(D[A], C[Any])) -static_assert(not is_gradual_equivalent_to(D[B], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[A])) -static_assert(not is_gradual_equivalent_to(D[Any], C[B])) +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) ``` ## Contravariance @@ -127,7 +111,7 @@ Types that "consume" data are contravariant in their typevar. If you expect a co that you pass into the consumer is a valid `int`. ```py -from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any class A: ... @@ -193,27 +177,11 @@ static_assert(not is_equivalent_to(D[B], C[Any])) static_assert(not is_equivalent_to(D[Any], C[A])) static_assert(not is_equivalent_to(D[Any], C[B])) -static_assert(is_gradual_equivalent_to(C[A], C[A])) -static_assert(is_gradual_equivalent_to(C[B], C[B])) -static_assert(is_gradual_equivalent_to(C[Any], C[Any])) -static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(C[B], C[A])) -static_assert(not is_gradual_equivalent_to(C[A], C[B])) -static_assert(not is_gradual_equivalent_to(C[A], C[Any])) -static_assert(not is_gradual_equivalent_to(C[B], C[Any])) -static_assert(not is_gradual_equivalent_to(C[Any], C[A])) -static_assert(not is_gradual_equivalent_to(C[Any], C[B])) - -static_assert(not is_gradual_equivalent_to(D[A], C[A])) -static_assert(not is_gradual_equivalent_to(D[B], C[B])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(D[B], C[A])) -static_assert(not is_gradual_equivalent_to(D[A], C[B])) -static_assert(not is_gradual_equivalent_to(D[A], C[Any])) -static_assert(not is_gradual_equivalent_to(D[B], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[A])) -static_assert(not is_gradual_equivalent_to(D[Any], C[B])) +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) ``` ## Invariance @@ -239,7 +207,7 @@ In the end, if you expect a mutable list, you must always be given a list of exa since we can't know in advance which of the allowed methods you'll want to use. ```py -from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any class A: ... @@ -299,27 +267,11 @@ static_assert(not is_equivalent_to(D[B], C[Any])) static_assert(not is_equivalent_to(D[Any], C[A])) static_assert(not is_equivalent_to(D[Any], C[B])) -static_assert(is_gradual_equivalent_to(C[A], C[A])) -static_assert(is_gradual_equivalent_to(C[B], C[B])) -static_assert(is_gradual_equivalent_to(C[Any], C[Any])) -static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(C[B], C[A])) -static_assert(not is_gradual_equivalent_to(C[A], C[B])) -static_assert(not is_gradual_equivalent_to(C[A], C[Any])) -static_assert(not is_gradual_equivalent_to(C[B], C[Any])) -static_assert(not is_gradual_equivalent_to(C[Any], C[A])) -static_assert(not is_gradual_equivalent_to(C[Any], C[B])) - -static_assert(not is_gradual_equivalent_to(D[A], C[A])) -static_assert(not is_gradual_equivalent_to(D[B], C[B])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(D[B], C[A])) -static_assert(not is_gradual_equivalent_to(D[A], C[B])) -static_assert(not is_gradual_equivalent_to(D[A], C[Any])) -static_assert(not is_gradual_equivalent_to(D[B], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[A])) -static_assert(not is_gradual_equivalent_to(D[Any], C[B])) +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) ``` ## Bivariance @@ -333,7 +285,7 @@ at all. (If it did, it would have to be covariant, contravariant, or invariant, the typevar was used.) ```py -from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown +from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown from typing import Any class A: ... @@ -359,6 +311,7 @@ static_assert(is_assignable_to(C[Any], C[B])) # TODO: no error # error: [static-assert-error] static_assert(is_assignable_to(D[B], C[A])) +static_assert(is_subtype_of(C[A], C[A])) # TODO: no error # error: [static-assert-error] static_assert(is_assignable_to(D[A], C[B])) @@ -377,6 +330,7 @@ static_assert(not is_subtype_of(C[A], C[Any])) static_assert(not is_subtype_of(C[B], C[Any])) static_assert(not is_subtype_of(C[Any], C[A])) static_assert(not is_subtype_of(C[Any], C[B])) +static_assert(not is_subtype_of(C[Any], C[Any])) # TODO: no error # error: [static-assert-error] @@ -397,10 +351,18 @@ static_assert(is_equivalent_to(C[B], C[A])) # TODO: no error # error: [static-assert-error] static_assert(is_equivalent_to(C[A], C[B])) -static_assert(not is_equivalent_to(C[A], C[Any])) -static_assert(not is_equivalent_to(C[B], C[Any])) -static_assert(not is_equivalent_to(C[Any], C[A])) -static_assert(not is_equivalent_to(C[Any], C[B])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[A], C[Any])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[B], C[Any])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[Any], C[A])) +# TODO: no error +# error: [static-assert-error] +static_assert(is_equivalent_to(C[Any], C[B])) static_assert(not is_equivalent_to(D[A], C[A])) static_assert(not is_equivalent_to(D[B], C[B])) @@ -411,39 +373,11 @@ static_assert(not is_equivalent_to(D[B], C[Any])) static_assert(not is_equivalent_to(D[Any], C[A])) static_assert(not is_equivalent_to(D[Any], C[B])) -static_assert(is_gradual_equivalent_to(C[A], C[A])) -static_assert(is_gradual_equivalent_to(C[B], C[B])) -static_assert(is_gradual_equivalent_to(C[Any], C[Any])) -static_assert(is_gradual_equivalent_to(C[Any], C[Unknown])) -# TODO: no error -# error: [static-assert-error] -static_assert(is_gradual_equivalent_to(C[B], C[A])) -# TODO: no error -# error: [static-assert-error] -static_assert(is_gradual_equivalent_to(C[A], C[B])) -# TODO: no error -# error: [static-assert-error] -static_assert(is_gradual_equivalent_to(C[A], C[Any])) -# TODO: no error -# error: [static-assert-error] -static_assert(is_gradual_equivalent_to(C[B], C[Any])) -# TODO: no error -# error: [static-assert-error] -static_assert(is_gradual_equivalent_to(C[Any], C[A])) -# TODO: no error -# error: [static-assert-error] -static_assert(is_gradual_equivalent_to(C[Any], C[B])) - -static_assert(not is_gradual_equivalent_to(D[A], C[A])) -static_assert(not is_gradual_equivalent_to(D[B], C[B])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[Unknown])) -static_assert(not is_gradual_equivalent_to(D[B], C[A])) -static_assert(not is_gradual_equivalent_to(D[A], C[B])) -static_assert(not is_gradual_equivalent_to(D[A], C[Any])) -static_assert(not is_gradual_equivalent_to(D[B], C[Any])) -static_assert(not is_gradual_equivalent_to(D[Any], C[A])) -static_assert(not is_gradual_equivalent_to(D[Any], C[B])) +static_assert(is_equivalent_to(C[Any], C[Any])) +static_assert(is_equivalent_to(C[Any], C[Unknown])) + +static_assert(not is_equivalent_to(D[Any], C[Any])) +static_assert(not is_equivalent_to(D[Any], C[Unknown])) ``` [spec]: https://typing.python.org/en/latest/spec/generics.html#variance diff --git a/crates/ty_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md index 256727dd2f745..66f350bfe936d 100644 --- a/crates/ty_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md @@ -717,6 +717,18 @@ def never( reveal_type(d) # revealed: Never ``` +Regression tests for complex nested simplifications: + +```py +from typing_extensions import Any, assert_type + +def _(x: Intersection[bool, Not[Intersection[Any, Not[AlwaysTruthy], Not[AlwaysFalsy]]]]): + assert_type(x, bool) + +def _(x: Intersection[bool, Any] | Literal[True] | Literal[False]): + assert_type(x, bool) +``` + ## Simplification of `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy` Similarly, intersections between `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy` can be diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 51fe5e1b19a51..62aa534ae1709 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -1346,107 +1346,6 @@ def h(obj: InstanceAttrBool): reveal_type(bool(obj)) # revealed: bool ``` -## Fully static protocols; gradual protocols - -A protocol is only fully static if all of its members are fully static: - -```py -from typing import Protocol, Any -from ty_extensions import is_fully_static, static_assert - -class FullyStatic(Protocol): - x: int - -class NotFullyStatic(Protocol): - x: Any - -static_assert(is_fully_static(FullyStatic)) -static_assert(not is_fully_static(NotFullyStatic)) -``` - -Non-fully-static protocols do not participate in subtyping or equivalence, only assignability and -gradual equivalence: - -```py -from ty_extensions import is_subtype_of, is_assignable_to, is_equivalent_to, is_gradual_equivalent_to - -class NominalWithX: - x: int = 42 - -static_assert(is_assignable_to(NominalWithX, FullyStatic)) -static_assert(is_assignable_to(NominalWithX, NotFullyStatic)) - -static_assert(not is_subtype_of(FullyStatic, NotFullyStatic)) -static_assert(is_assignable_to(FullyStatic, NotFullyStatic)) - -static_assert(not is_subtype_of(NotFullyStatic, FullyStatic)) -static_assert(is_assignable_to(NotFullyStatic, FullyStatic)) - -static_assert(not is_subtype_of(NominalWithX, NotFullyStatic)) -static_assert(is_assignable_to(NominalWithX, NotFullyStatic)) - -static_assert(is_subtype_of(NominalWithX, FullyStatic)) - -static_assert(is_equivalent_to(FullyStatic, FullyStatic)) -static_assert(not is_equivalent_to(NotFullyStatic, NotFullyStatic)) - -static_assert(is_gradual_equivalent_to(FullyStatic, FullyStatic)) -static_assert(is_gradual_equivalent_to(NotFullyStatic, NotFullyStatic)) - -class AlsoNotFullyStatic(Protocol): - x: Any - -static_assert(not is_equivalent_to(NotFullyStatic, AlsoNotFullyStatic)) -static_assert(is_gradual_equivalent_to(NotFullyStatic, AlsoNotFullyStatic)) -``` - -Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to -the nominal type `object` (as described above): - -```py -class Empty(Protocol): ... - -static_assert(is_fully_static(Empty)) -``` - -A method member is only considered fully static if all its parameter annotations and its return -annotation are fully static: - -```py -class FullyStaticMethodMember(Protocol): - def method(self, x: int) -> str: ... - -class DynamicParameter(Protocol): - def method(self, x: Any) -> str: ... - -class DynamicReturn(Protocol): - def method(self, x: int) -> Any: ... - -static_assert(is_fully_static(FullyStaticMethodMember)) - -# TODO: these should pass -static_assert(not is_fully_static(DynamicParameter)) # error: [static-assert-error] -static_assert(not is_fully_static(DynamicReturn)) # error: [static-assert-error] -``` - -The [typing spec][spec_protocol_members] states: - -> If any parameters of a protocol method are not annotated, then their types are assumed to be `Any` - -Thus, a partially unannotated method member can also not be considered to be fully static: - -```py -class NoParameterAnnotation(Protocol): - def method(self, x) -> str: ... - -class NoReturnAnnotation(Protocol): - def method(self, x: int): ... - -# TODO: these should pass -static_assert(not is_fully_static(NoParameterAnnotation)) # error: [static-assert-error] -static_assert(not is_fully_static(NoReturnAnnotation)) # error: [static-assert-error] -``` - ## Callable protocols An instance of a protocol type is callable if the protocol defines a `__call__` method: @@ -1560,7 +1459,7 @@ def two(some_list: list, some_tuple: tuple[int, str], some_sized: Sized): from __future__ import annotations from typing import Protocol, Any -from ty_extensions import is_fully_static, static_assert, is_assignable_to, is_subtype_of, is_equivalent_to +from ty_extensions import static_assert, is_assignable_to, is_subtype_of, is_equivalent_to class RecursiveFullyStatic(Protocol): parent: RecursiveFullyStatic @@ -1570,11 +1469,9 @@ class RecursiveNonFullyStatic(Protocol): parent: RecursiveNonFullyStatic x: Any -static_assert(is_fully_static(RecursiveFullyStatic)) -static_assert(not is_fully_static(RecursiveNonFullyStatic)) - -static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic)) -static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic)) +# TODO: these should pass, once we take into account types of members +static_assert(not is_subtype_of(RecursiveFullyStatic, RecursiveNonFullyStatic)) # error: [static-assert-error] +static_assert(not is_subtype_of(RecursiveNonFullyStatic, RecursiveFullyStatic)) # error: [static-assert-error] static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveNonFullyStatic)) static_assert(is_assignable_to(RecursiveFullyStatic, RecursiveNonFullyStatic)) @@ -1589,8 +1486,6 @@ static_assert(is_equivalent_to(AlsoRecursiveFullyStatic, RecursiveFullyStatic)) class RecursiveOptionalParent(Protocol): parent: RecursiveOptionalParent | None -static_assert(is_fully_static(RecursiveOptionalParent)) - static_assert(is_assignable_to(RecursiveOptionalParent, RecursiveOptionalParent)) static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveOptionalParent)) @@ -1635,7 +1530,7 @@ python-version = "3.12" from __future__ import annotations from typing import Protocol, Callable -from ty_extensions import Intersection, Not, is_fully_static, is_assignable_to, is_equivalent_to, static_assert +from ty_extensions import Intersection, Not, is_assignable_to, is_equivalent_to, static_assert class C: ... @@ -1663,7 +1558,6 @@ class Recursive(Protocol): nested: Recursive | Callable[[Recursive | Recursive, tuple[Recursive, Recursive]], Recursive | Recursive] -static_assert(is_fully_static(Recursive)) static_assert(is_equivalent_to(Recursive, Recursive)) static_assert(is_assignable_to(Recursive, Recursive)) diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" index 1224cb6610f4b..0403b492357ad 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" @@ -40,7 +40,7 @@ error[parameter-already-assigned]: Multiple values provided for parameter `name` | ^^^^^^^^^^ | info: Union variant `def f1(name: str) -> int` is incompatible with this call site -info: Attempted to call union type `(def f1(name: str) -> int) | (def any(*args, **kwargs) -> int)` +info: Attempted to call union type `(def f1(name: str) -> int) | (def any(...) -> int)` info: rule `parameter-already-assigned` is enabled by default ``` @@ -55,7 +55,7 @@ error[unknown-argument]: Argument `unknown` does not match any known parameter o | ^^^^^^^^^^^^^^ | info: Union variant `def f1(name: str) -> int` is incompatible with this call site -info: Attempted to call union type `(def f1(name: str) -> int) | (def any(*args, **kwargs) -> int)` +info: Attempted to call union type `(def f1(name: str) -> int) | (def any(...) -> int)` info: rule `unknown-argument` is enabled by default ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_api.md b/crates/ty_python_semantic/resources/mdtest/type_api.md index 7608f59311d41..6f7c2c8180272 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_api.md +++ b/crates/ty_python_semantic/resources/mdtest/type_api.md @@ -91,13 +91,11 @@ The `Unknown` type is a special type that we use to represent actually unknown t annotation), as opposed to `Any` which represents an explicitly unknown type. ```py -from ty_extensions import Unknown, static_assert, is_assignable_to, is_fully_static +from ty_extensions import Unknown, static_assert, is_assignable_to static_assert(is_assignable_to(Unknown, int)) static_assert(is_assignable_to(int, Unknown)) -static_assert(not is_fully_static(Unknown)) - def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None: reveal_type(x) # revealed: Unknown reveal_type(y) # revealed: tuple[str, Unknown] @@ -333,19 +331,6 @@ static_assert(is_disjoint_from(None, int)) static_assert(not is_disjoint_from(Literal[2] | str, int)) ``` -### Fully static types - -```py -from ty_extensions import is_fully_static, static_assert -from typing import Any - -static_assert(is_fully_static(int | str)) -static_assert(is_fully_static(type[int])) - -static_assert(not is_fully_static(int | Any)) -static_assert(not is_fully_static(type[Any])) -``` - ### Singleton types ```py diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md index da545f70a22f9..a0de2576b98b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/any.md @@ -2,24 +2,13 @@ ## Introduction -The type `Any` is the dynamic type in Python's gradual type system. It represents an unknown -fully-static type, which means that it represents an *unknown* set of runtime values. - -```py -from ty_extensions import static_assert, is_fully_static -from typing import Any -``` - -`Any` is a dynamic type: - -```py -static_assert(not is_fully_static(Any)) -``` +The type `Any` is the dynamic type in Python's gradual type system. It represents an unknown static +type, which means that it represents an *unknown* set of runtime values. ## Every type is assignable to `Any`, and `Any` is assignable to every type ```py -from ty_extensions import static_assert, is_fully_static, is_assignable_to +from ty_extensions import static_assert, is_assignable_to from typing_extensions import Never, Any class C: ... diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md index 962e30f507129..261452e000209 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/not_t.md @@ -113,8 +113,8 @@ static_assert(is_equivalent_to(Not[Intersection[P, Q]], Not[P] | Not[Q])) The two gradual types are equivalent: ```py -from ty_extensions import static_assert, is_gradual_equivalent_to, Not +from ty_extensions import static_assert, is_equivalent_to, Not from typing import Any -static_assert(is_gradual_equivalent_to(Not[Any], Any)) +static_assert(is_equivalent_to(Not[Any], Any)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 6b96210b54a63..ee5ce9c8dabad 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -36,8 +36,7 @@ static_assert(not is_assignable_to(Child1, Child2)) ### Gradual types -Gradual types do not participate in subtyping, but can still be assignable to other types (and -static types can be assignable to gradual types): +The dynamic type is assignable to or from any type. ```py from ty_extensions import static_assert, is_assignable_to, Unknown @@ -47,13 +46,6 @@ static_assert(is_assignable_to(Unknown, Literal[1])) static_assert(is_assignable_to(Any, Literal[1])) static_assert(is_assignable_to(Literal[1], Unknown)) static_assert(is_assignable_to(Literal[1], Any)) - -class SubtypeOfAny(Any): ... - -static_assert(is_assignable_to(SubtypeOfAny, Any)) -static_assert(is_assignable_to(SubtypeOfAny, int)) -static_assert(is_assignable_to(Any, SubtypeOfAny)) -static_assert(not is_assignable_to(int, SubtypeOfAny)) ``` ## Literal types @@ -239,7 +231,9 @@ from ty_extensions import is_assignable_to, static_assert static_assert(not is_assignable_to(type[Any], None)) ``` -## Class-literals that inherit from `Any` +## Inheriting `Any` + +### Class-literal types Class-literal types that inherit from `Any` are assignable to any type `T` where `T` is assignable to `type`: @@ -267,6 +261,39 @@ def test(x: Any): This is because the `Any` element in the MRO could materialize to any subtype of `type`. +### Nominal instance and subclass-of types + +Instances of classes that inherit `Any` are assignable to any non-final type. + +```py +from ty_extensions import is_assignable_to, static_assert +from typing_extensions import Any, final + +class InheritsAny(Any): + pass + +class Arbitrary: + pass + +@final +class FinalClass: + pass + +static_assert(is_assignable_to(InheritsAny, Arbitrary)) +static_assert(is_assignable_to(InheritsAny, Any)) +static_assert(is_assignable_to(InheritsAny, object)) +static_assert(not is_assignable_to(InheritsAny, FinalClass)) +``` + +Similar for subclass-of types: + +```py +static_assert(is_assignable_to(type[Any], type[Any])) +static_assert(is_assignable_to(type[object], type[Any])) +static_assert(is_assignable_to(type[Any], type[Arbitrary])) +static_assert(is_assignable_to(type[Any], type[object])) +``` + ## Heterogeneous tuple types ```py diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index d8094d2e78cb3..b3ae6150b5418 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -193,8 +193,8 @@ static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3])) ## Intersections ```py -from typing_extensions import Literal, final, Any -from ty_extensions import Intersection, is_disjoint_from, static_assert, Not +from typing_extensions import Literal, final, Any, LiteralString +from ty_extensions import Intersection, is_disjoint_from, static_assert, Not, AlwaysFalsy @final class P: ... @@ -249,6 +249,9 @@ static_assert(not is_disjoint_from(Intersection[Any, Not[Y]], Intersection[Any, static_assert(is_disjoint_from(Intersection[int, Any], Not[int])) static_assert(is_disjoint_from(Not[int], Intersection[int, Any])) + +# TODO https://github.com/astral-sh/ty/issues/216 +static_assert(is_disjoint_from(AlwaysFalsy, LiteralString & ~Literal[""])) # error: [static-assert-error] ``` ## Special types diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index 558c455c4ed62..42cfa0f217b8c 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -1,41 +1,76 @@ # Equivalence relation -`is_equivalent_to` implements [the equivalence relation] for fully static types. +`is_equivalent_to` implements [the equivalence relation] on types. -Two types `A` and `B` are equivalent iff `A` is a subtype of `B` and `B` is a subtype of `A`. +For fully static types, two types `A` and `B` are equivalent iff `A` is a subtype of `B` and `B` is +a subtype of `A` (that is, the two types represent the same set of values). + +Two gradual types `A` and `B` are equivalent if all [materializations] of `A` are also +materializations of `B`, and all materializations of `B` are also materializations of `A`. ## Basic +### Fully static + ```py -from typing import Any -from typing_extensions import Literal -from ty_extensions import Unknown, is_equivalent_to, static_assert +from typing_extensions import Literal, LiteralString, Never +from ty_extensions import Unknown, is_equivalent_to, static_assert, TypeOf, AlwaysTruthy, AlwaysFalsy static_assert(is_equivalent_to(Literal[1, 2], Literal[1, 2])) static_assert(is_equivalent_to(type[object], type)) +static_assert(is_equivalent_to(type, type[object])) -static_assert(not is_equivalent_to(Any, Any)) -static_assert(not is_equivalent_to(Unknown, Unknown)) -static_assert(not is_equivalent_to(Any, None)) static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 0])) +static_assert(not is_equivalent_to(Literal[1, 0], Literal[1, 2])) static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 2, 3])) +static_assert(not is_equivalent_to(Literal[1, 2, 3], Literal[1, 2])) + +static_assert(is_equivalent_to(Never, Never)) +static_assert(is_equivalent_to(AlwaysTruthy, AlwaysTruthy)) +static_assert(is_equivalent_to(AlwaysFalsy, AlwaysFalsy)) +static_assert(is_equivalent_to(LiteralString, LiteralString)) + +static_assert(is_equivalent_to(Literal[True], Literal[True])) +static_assert(is_equivalent_to(Literal[False], Literal[False])) +static_assert(is_equivalent_to(TypeOf[0:1:2], TypeOf[0:1:2])) + +static_assert(is_equivalent_to(TypeOf[str], TypeOf[str])) +static_assert(is_equivalent_to(type, type[object])) ``` -## Equivalence is commutative +### Gradual ```py -from typing_extensions import Literal -from ty_extensions import is_equivalent_to, static_assert +from typing import Any +from typing_extensions import Literal, LiteralString, Never +from ty_extensions import Unknown, is_equivalent_to, static_assert -static_assert(is_equivalent_to(type, type[object])) -static_assert(not is_equivalent_to(Literal[1, 0], Literal[1, 2])) -static_assert(not is_equivalent_to(Literal[1, 2, 3], Literal[1, 2])) +static_assert(is_equivalent_to(Any, Any)) +static_assert(is_equivalent_to(Unknown, Unknown)) +static_assert(is_equivalent_to(Any, Unknown)) +static_assert(not is_equivalent_to(Any, None)) + +static_assert(not is_equivalent_to(type, type[Any])) +static_assert(not is_equivalent_to(type[object], type[Any])) ``` -## Differently ordered intersections and unions are equivalent +## Unions and intersections ```py -from ty_extensions import is_equivalent_to, static_assert, Intersection, Not +from typing import Any +from ty_extensions import Intersection, Not, Unknown, is_equivalent_to, static_assert + +static_assert(is_equivalent_to(str | int, str | int)) +static_assert(is_equivalent_to(str | int | Any, str | int | Unknown)) +static_assert(is_equivalent_to(str | int, int | str)) +static_assert(is_equivalent_to(Intersection[str, int, Not[bytes], Not[None]], Intersection[int, str, Not[None], Not[bytes]])) +static_assert(is_equivalent_to(Intersection[str | int, Not[type[Any]]], Intersection[int | str, Not[type[Unknown]]])) + +static_assert(not is_equivalent_to(str | int, int | str | bytes)) +static_assert(not is_equivalent_to(str | int | bytes, int | str | dict)) + +static_assert(is_equivalent_to(Unknown, Unknown | Any)) +static_assert(is_equivalent_to(Unknown, Intersection[Unknown, Any])) class P: ... class Q: ... @@ -66,6 +101,18 @@ static_assert(is_equivalent_to(Intersection[Q, R, Not[P]], Intersection[Not[P], static_assert(is_equivalent_to(Intersection[Q | R, Not[P | S]], Intersection[Not[S | P], R | Q])) ``` +## Tuples + +```py +from ty_extensions import Unknown, is_equivalent_to, static_assert +from typing import Any + +static_assert(is_equivalent_to(tuple[str, Any], tuple[str, Unknown])) + +static_assert(not is_equivalent_to(tuple[str, int], tuple[str, int, bytes])) +static_assert(not is_equivalent_to(tuple[str, int], tuple[int, str])) +``` + ## Tuples containing equivalent but differently ordered unions/intersections are equivalent ```py @@ -193,21 +240,14 @@ def f2(a: int, b: int) -> None: ... static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) ``` -When either of the callable types uses a gradual form for the parameters: - -```py -static_assert(not is_equivalent_to(Callable[..., None], Callable[[int], None])) -static_assert(not is_equivalent_to(Callable[[int], None], Callable[..., None])) -``` - -When the return types are not equivalent or absent in one or both of the callable types: +When the return types are not equivalent in one or both of the callable types: ```py def f3(): ... def f4() -> None: ... static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None])) -static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3])) +static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3])) static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) static_assert(not is_equivalent_to(CallableTypeOf[f4], CallableTypeOf[f3])) ``` @@ -247,7 +287,7 @@ def f11(a) -> None: ... static_assert(not is_equivalent_to(CallableTypeOf[f9], CallableTypeOf[f10])) static_assert(not is_equivalent_to(CallableTypeOf[f10], CallableTypeOf[f11])) static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f10])) -static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11])) +static_assert(is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11])) ``` When the default value for a parameter is present only in one of the callable type: @@ -334,10 +374,9 @@ static_assert(is_equivalent_to(CallableTypeOf[pg], CallableTypeOf[cpg])) static_assert(is_equivalent_to(CallableTypeOf[cpg], CallableTypeOf[pg])) ``` -## Function-literal types and bound-method types +### Function-literal types and bound-method types -Function-literal types and bound-method types are always considered self-equivalent, even if they -have unannotated parameters, or parameters with not-fully-static annotations. +Function-literal types and bound-method types are always considered self-equivalent. ```toml [environment] @@ -360,4 +399,94 @@ type X = TypeOf[A.method] static_assert(is_equivalent_to(X, X)) ``` +### Non-fully-static callable types + +The examples provided below are only a subset of the possible cases and only include the ones with +gradual types. The cases with fully static types and using different combinations of parameter kinds +are covered above. + +```py +from ty_extensions import Unknown, CallableTypeOf, is_equivalent_to, static_assert +from typing import Any, Callable + +static_assert(is_equivalent_to(Callable[..., int], Callable[..., int])) +static_assert(is_equivalent_to(Callable[..., Any], Callable[..., Unknown])) +static_assert(is_equivalent_to(Callable[[int, Any], None], Callable[[int, Unknown], None])) + +static_assert(not is_equivalent_to(Callable[[int, Any], None], Callable[[Any, int], None])) +static_assert(not is_equivalent_to(Callable[[int, str], None], Callable[[int, str, bytes], None])) +static_assert(not is_equivalent_to(Callable[..., None], Callable[[], None])) +``` + +A function with no explicit return type should be gradual equivalent to a callable with a return +type of `Any`. + +```py +def f1(): + return + +static_assert(is_equivalent_to(CallableTypeOf[f1], Callable[[], Any])) +``` + +And, similarly for parameters with no annotations. + +```py +def f2(a, b, /) -> None: + return + +static_assert(is_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None])) +``` + +Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs` +parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable +with `...` as the parameter type. + +```py +def variadic_without_annotation(*args, **kwargs): + return + +def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any: + return + +static_assert(is_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any])) +static_assert(is_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any])) +``` + +But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a +callable with `...` as the parameter type. + +```py +def variadic_args(*args): + return + +def variadic_kwargs(**kwargs): + return + +static_assert(not is_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any])) +static_assert(not is_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any])) +``` + +Parameter names, default values, and it's kind should also be considered when checking for gradual +equivalence. + +```py +def f1(a): ... +def f2(b): ... + +static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) + +def f3(a=1): ... +def f4(a=2): ... +def f5(a): ... + +static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) +static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3])) +static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5])) + +def f6(a, /): ... + +static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6])) +``` + +[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize [the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md deleted file mode 100644 index 53bf7b6cf4637..0000000000000 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md +++ /dev/null @@ -1,133 +0,0 @@ -# Fully-static types - -A type is fully static iff it does not contain any gradual forms. - -## Fully-static - -```py -from typing_extensions import Literal, LiteralString, Never, Callable -from ty_extensions import Intersection, Not, TypeOf, is_fully_static, static_assert - -static_assert(is_fully_static(Never)) -static_assert(is_fully_static(None)) - -static_assert(is_fully_static(Literal[1])) -static_assert(is_fully_static(Literal[True])) -static_assert(is_fully_static(Literal["abc"])) -static_assert(is_fully_static(Literal[b"abc"])) - -static_assert(is_fully_static(LiteralString)) - -static_assert(is_fully_static(str)) -static_assert(is_fully_static(object)) -static_assert(is_fully_static(type)) - -static_assert(is_fully_static(TypeOf[str])) -static_assert(is_fully_static(TypeOf[Literal])) - -static_assert(is_fully_static(str | None)) -static_assert(is_fully_static(Intersection[str, Not[LiteralString]])) - -static_assert(is_fully_static(tuple[()])) -static_assert(is_fully_static(tuple[int, object])) - -static_assert(is_fully_static(type[str])) -static_assert(is_fully_static(type[object])) -``` - -## Non-fully-static - -```py -from typing_extensions import Any, Literal, LiteralString, Callable -from ty_extensions import Intersection, Not, TypeOf, Unknown, is_fully_static, static_assert - -static_assert(not is_fully_static(Any)) -static_assert(not is_fully_static(Unknown)) - -static_assert(not is_fully_static(Any | str)) -static_assert(not is_fully_static(str | Unknown)) -static_assert(not is_fully_static(Intersection[Any, Not[LiteralString]])) - -static_assert(not is_fully_static(tuple[Any, ...])) - -static_assert(not is_fully_static(tuple[int, Any])) -static_assert(not is_fully_static(type[Any])) -``` - -## Callable - -```py -from typing_extensions import Callable, Any -from ty_extensions import Unknown, is_fully_static, static_assert - -static_assert(is_fully_static(Callable[[], int])) -static_assert(is_fully_static(Callable[[int, str], int])) - -static_assert(not is_fully_static(Callable[..., int])) -static_assert(not is_fully_static(Callable[[], Any])) -static_assert(not is_fully_static(Callable[[int, Unknown], int])) -``` - -The invalid forms of `Callable` annotation are never fully static because we represent them with the -`(...) -> Unknown` signature. - -```py -static_assert(not is_fully_static(Callable)) -# error: [invalid-type-form] -static_assert(not is_fully_static(Callable[int, int])) -``` - -Using function literals, we can check more variations of callable types as it allows us to define -parameters without annotations and no return type. - -```py -from ty_extensions import CallableTypeOf, is_fully_static, static_assert - -def f00() -> None: ... -def f01(a: int, b: str) -> None: ... -def f11(): ... -def f12(a, b): ... -def f13(a, b: int): ... -def f14(a, b: int) -> None: ... -def f15(a, b) -> None: ... - -static_assert(is_fully_static(CallableTypeOf[f00])) -static_assert(is_fully_static(CallableTypeOf[f01])) - -static_assert(not is_fully_static(CallableTypeOf[f11])) -static_assert(not is_fully_static(CallableTypeOf[f12])) -static_assert(not is_fully_static(CallableTypeOf[f13])) -static_assert(not is_fully_static(CallableTypeOf[f14])) -static_assert(not is_fully_static(CallableTypeOf[f15])) -``` - -## Overloads - -`overloaded.pyi`: - -```pyi -from typing import Any, overload - -@overload -def gradual() -> None: ... -@overload -def gradual(a: Any) -> None: ... - -@overload -def static() -> None: ... -@overload -def static(x: int) -> None: ... -@overload -def static(x: str) -> str: ... -``` - -```py -from ty_extensions import CallableTypeOf, TypeOf, is_fully_static, static_assert -from overloaded import gradual, static - -static_assert(is_fully_static(TypeOf[gradual])) -static_assert(is_fully_static(TypeOf[static])) - -static_assert(not is_fully_static(CallableTypeOf[gradual])) -static_assert(is_fully_static(CallableTypeOf[static])) -``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md deleted file mode 100644 index e3de427e4ee92..0000000000000 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md +++ /dev/null @@ -1,159 +0,0 @@ -# Gradual equivalence relation - -Two gradual types `A` and `B` are equivalent if all [materializations] of `A` are also -materializations of `B`, and all materializations of `B` are also materializations of `A`. - -## Basic - -```py -from typing import Any -from typing_extensions import Literal, LiteralString, Never -from ty_extensions import AlwaysFalsy, AlwaysTruthy, TypeOf, Unknown, is_gradual_equivalent_to, static_assert - -static_assert(is_gradual_equivalent_to(Any, Any)) -static_assert(is_gradual_equivalent_to(Unknown, Unknown)) -static_assert(is_gradual_equivalent_to(Any, Unknown)) - -static_assert(is_gradual_equivalent_to(Never, Never)) -static_assert(is_gradual_equivalent_to(AlwaysTruthy, AlwaysTruthy)) -static_assert(is_gradual_equivalent_to(AlwaysFalsy, AlwaysFalsy)) -static_assert(is_gradual_equivalent_to(LiteralString, LiteralString)) - -static_assert(is_gradual_equivalent_to(Literal[True], Literal[True])) -static_assert(is_gradual_equivalent_to(Literal[False], Literal[False])) -static_assert(is_gradual_equivalent_to(TypeOf[0:1:2], TypeOf[0:1:2])) - -static_assert(is_gradual_equivalent_to(TypeOf[str], TypeOf[str])) -static_assert(is_gradual_equivalent_to(type, type[object])) - -static_assert(not is_gradual_equivalent_to(type, type[Any])) -static_assert(not is_gradual_equivalent_to(type[object], type[Any])) -``` - -## Unions and intersections - -```py -from typing import Any -from ty_extensions import Intersection, Not, Unknown, is_gradual_equivalent_to, static_assert - -static_assert(is_gradual_equivalent_to(str | int, str | int)) -static_assert(is_gradual_equivalent_to(str | int | Any, str | int | Unknown)) -static_assert(is_gradual_equivalent_to(str | int, int | str)) -static_assert( - is_gradual_equivalent_to(Intersection[str, int, Not[bytes], Not[None]], Intersection[int, str, Not[None], Not[bytes]]) -) -static_assert(is_gradual_equivalent_to(Intersection[str | int, Not[type[Any]]], Intersection[int | str, Not[type[Unknown]]])) - -static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes)) -static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict)) - -static_assert(is_gradual_equivalent_to(Unknown, Unknown | Any)) -static_assert(is_gradual_equivalent_to(Unknown, Intersection[Unknown, Any])) -``` - -## Tuples - -```py -from ty_extensions import Unknown, is_gradual_equivalent_to, static_assert -from typing import Any - -static_assert(is_gradual_equivalent_to(tuple[str, Any], tuple[str, Unknown])) - -static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[str, int, bytes])) -static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str])) -``` - -## Callable - -The examples provided below are only a subset of the possible cases and only include the ones with -gradual types. The cases with fully static types and using different combinations of parameter kinds -are covered in the [equivalence tests](./is_equivalent_to.md#callable). - -```py -from ty_extensions import Unknown, CallableTypeOf, is_gradual_equivalent_to, static_assert -from typing import Any, Callable - -static_assert(is_gradual_equivalent_to(Callable[..., int], Callable[..., int])) -static_assert(is_gradual_equivalent_to(Callable[..., Any], Callable[..., Unknown])) -static_assert(is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[int, Unknown], None])) - -static_assert(not is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[Any, int], None])) -static_assert(not is_gradual_equivalent_to(Callable[[int, str], None], Callable[[int, str, bytes], None])) -static_assert(not is_gradual_equivalent_to(Callable[..., None], Callable[[], None])) -``` - -A function with no explicit return type should be gradual equivalent to a callable with a return -type of `Any`. - -```py -def f1(): - return - -static_assert(is_gradual_equivalent_to(CallableTypeOf[f1], Callable[[], Any])) -``` - -And, similarly for parameters with no annotations. - -```py -def f2(a, b, /) -> None: - return - -static_assert(is_gradual_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None])) -``` - -Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs` -parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable -with `...` as the parameter type. - -```py -def variadic_without_annotation(*args, **kwargs): - return - -def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any: - return - -static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any])) -static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any])) -``` - -But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a -callable with `...` as the parameter type. - -```py -def variadic_args(*args): - return - -def variadic_kwargs(**kwargs): - return - -static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any])) -static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any])) -``` - -Parameter names, default values, and it's kind should also be considered when checking for gradual -equivalence. - -```py -def f1(a): ... -def f2(b): ... - -static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2])) - -def f3(a=1): ... -def f4(a=2): ... -def f5(a): ... - -static_assert(is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])) -static_assert( - is_gradual_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3]) -) -static_assert(not is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5])) - -def f6(a, /): ... - -static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6])) -``` - -TODO: Overloads - -[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 1619e3a027f56..7476b57c0f5ce 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -10,6 +10,10 @@ The `is_subtype_of(S, T)` relation below checks if type `S` is a subtype of type A fully static type `S` is a subtype of another fully static type `T` iff the set of values represented by `S` is a subset of the set of values represented by `T`. +A non fully static type `S` can also be safely considered a subtype of a non fully static type `T`, +if all possible materializations of `S` represent sets of values that are a subset of every possible +set of values represented by a materialization of `T`. + See the [typing documentation] for more information. ## Basic builtin types @@ -316,12 +320,13 @@ static_assert( python-version = "3.12" ``` -As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type. -However, the special-case behavior of assignability does not also apply to subtyping, since gradual -types to not participate in subtyping. +As a [special case][gradual tuple], `tuple[Any, ...]` is a [gradual][gradual form] tuple type, not +only in the type of its elements, but also in its length. + +Its subtyping follows the general rule for subtyping of gradual types. ```py -from typing import Any +from typing import Any, Never from ty_extensions import static_assert, is_subtype_of static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, ...])) @@ -330,9 +335,11 @@ static_assert(not is_subtype_of(tuple[Any, ...], tuple[Any, Any])) static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, ...])) static_assert(not is_subtype_of(tuple[Any, ...], tuple[int])) static_assert(not is_subtype_of(tuple[Any, ...], tuple[int, int])) +static_assert(is_subtype_of(tuple[Any, ...], tuple[object, ...])) +static_assert(is_subtype_of(tuple[Never, ...], tuple[Any, ...])) ``` -Subtyping also does not apply when `tuple[Any, ...]` is unpacked into a mixed tuple. +Same applies when `tuple[Any, ...]` is unpacked into a mixed tuple. ```py static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...]], tuple[int, *tuple[Any, ...]])) @@ -363,9 +370,9 @@ static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int])) static_assert(not is_subtype_of(tuple[int, *tuple[Any, ...], int], tuple[int, int])) ``` -Subtyping does apply to unbounded homogeneous tuples of a fully static type. However, such tuples -are defined to be the _union_ of all tuple lengths, not the _gradual choice_ of them, so no -variable-length tuples are a subtyping of _any_ fixed-length tuple. +Unbounded homogeneous tuples of a non-Any type are defined to be the _union_ of all tuple lengths, +not the _gradual choice_ of them, so no variable-length tuples are a subtype of _any_ fixed-length +tuple. ```py static_assert(not is_subtype_of(tuple[int, ...], tuple[Any, ...])) @@ -642,7 +649,7 @@ static_assert(not is_subtype_of(_SpecialForm, TypeOf[Literal])) ### Basic ```py -from typing import _SpecialForm +from typing import _SpecialForm, Any from typing_extensions import Literal, assert_type from ty_extensions import TypeOf, is_subtype_of, static_assert @@ -674,6 +681,8 @@ static_assert(not is_subtype_of(LiteralBool, bool)) static_assert(not is_subtype_of(type, type[bool])) +static_assert(not is_subtype_of(LiteralBool, type[Any])) + # int static_assert(is_subtype_of(LiteralInt, LiteralInt)) @@ -687,7 +696,9 @@ static_assert(not is_subtype_of(LiteralInt, int)) static_assert(not is_subtype_of(type, type[int])) -# LiteralString +static_assert(not is_subtype_of(LiteralInt, type[Any])) + +# str static_assert(is_subtype_of(LiteralStr, type[str])) static_assert(is_subtype_of(LiteralStr, type)) @@ -695,6 +706,8 @@ static_assert(is_subtype_of(LiteralStr, type[object])) static_assert(not is_subtype_of(type[str], LiteralStr)) +static_assert(not is_subtype_of(LiteralStr, type[Any])) + # custom metaclasses type LiteralHasCustomMetaclass = TypeOf[HasCustomMetaclass] @@ -704,6 +717,18 @@ static_assert(is_subtype_of(Meta, type[object])) static_assert(is_subtype_of(Meta, type)) static_assert(not is_subtype_of(Meta, type[type])) + +static_assert(not is_subtype_of(Meta, type[Any])) + +# generics + +type LiteralListOfInt = TypeOf[list[int]] + +assert_type(list[int], LiteralListOfInt) + +static_assert(is_subtype_of(LiteralListOfInt, type)) + +static_assert(not is_subtype_of(LiteralListOfInt, type[Any])) ``` ### Unions of class literals @@ -740,7 +765,9 @@ static_assert(is_subtype_of(LiteralBase | LiteralUnrelated, object)) ## Non-fully-static types -`Any`, `Unknown`, `Todo` and derivatives thereof do not participate in subtyping. +A non-fully-static type can be considered a subtype of another type if all possible materializations +of the first type represent sets of values that are a subset of every possible set of values +represented by a materialization of the second type. ```py from ty_extensions import Unknown, is_subtype_of, static_assert, Intersection @@ -749,25 +776,58 @@ from typing_extensions import Any static_assert(not is_subtype_of(Any, Any)) static_assert(not is_subtype_of(Any, int)) static_assert(not is_subtype_of(int, Any)) -static_assert(not is_subtype_of(Any, object)) +static_assert(is_subtype_of(Any, object)) static_assert(not is_subtype_of(object, Any)) -static_assert(not is_subtype_of(int, Any | int)) -static_assert(not is_subtype_of(Intersection[Any, int], int)) +static_assert(is_subtype_of(int, Any | int)) +static_assert(is_subtype_of(Intersection[Any, int], int)) static_assert(not is_subtype_of(tuple[int, int], tuple[int, Any])) +``` + +The same for `Unknown`: -# The same for `Unknown`: +```py static_assert(not is_subtype_of(Unknown, Unknown)) static_assert(not is_subtype_of(Unknown, int)) static_assert(not is_subtype_of(int, Unknown)) -static_assert(not is_subtype_of(Unknown, object)) +static_assert(is_subtype_of(Unknown, object)) static_assert(not is_subtype_of(object, Unknown)) -static_assert(not is_subtype_of(int, Unknown | int)) -static_assert(not is_subtype_of(Intersection[Unknown, int], int)) +static_assert(is_subtype_of(int, Unknown | int)) +static_assert(is_subtype_of(Intersection[Unknown, int], int)) static_assert(not is_subtype_of(tuple[int, int], tuple[int, Unknown])) ``` +Instances of classes that inherit `Any` are not subtypes of some other `Arbitrary` class, because +the `Any` they inherit from could materialize to something (e.g. `object`) that is not a subclass of +that class. + +Similarly, they are not subtypes of `Any`, because there are possible materializations of `Any` that +would not satisfy the subtype relation. + +They are subtypes of `object`. + +```py +class InheritsAny(Any): + pass + +class Arbitrary: + pass + +static_assert(not is_subtype_of(InheritsAny, Arbitrary)) +static_assert(not is_subtype_of(InheritsAny, Any)) +static_assert(is_subtype_of(InheritsAny, object)) +``` + +Similar for subclass-of types: + +```py +static_assert(not is_subtype_of(type[Any], type[Any])) +static_assert(not is_subtype_of(type[object], type[Any])) +static_assert(not is_subtype_of(type[Any], type[Arbitrary])) +static_assert(is_subtype_of(type[Any], type[object])) +``` + ## Callable The general principle is that a callable type is a subtype of another if it's more flexible in what @@ -1389,10 +1449,45 @@ static_assert(is_subtype_of(TypeOf[C.foo], object)) static_assert(not is_subtype_of(object, TypeOf[C.foo])) ``` +#### Gradual form + +A callable type with `...` parameters can be considered a supertype of a callable type that accepts +any arguments of any type, but otherwise is not a subtype or supertype of any callable type. + +```py +from typing import Callable, Never +from ty_extensions import CallableTypeOf, is_subtype_of, static_assert + +def bottom(*args: object, **kwargs: object) -> Never: + raise Exception() + +type BottomCallable = CallableTypeOf[bottom] + +static_assert(is_subtype_of(BottomCallable, Callable[..., Never])) +static_assert(is_subtype_of(BottomCallable, Callable[..., int])) + +static_assert(not is_subtype_of(Callable[[], object], Callable[..., object])) +static_assert(not is_subtype_of(Callable[..., object], Callable[[], object])) +``` + +According to the spec, `*args: Any, **kwargs: Any` is equivalent to `...`. This is a subtle but +important distinction. No materialization of the former signature (if taken literally) can have any +required arguments, but `...` can materialize to a signature with required arguments. The below test +would not pass if we didn't handle this special case. + +```py +from typing import Callable, Any +from ty_extensions import is_subtype_of, static_assert, CallableTypeOf + +def f(*args: Any, **kwargs: Any) -> Any: ... + +static_assert(not is_subtype_of(CallableTypeOf[f], Callable[[], object])) +``` + ### Classes with `__call__` ```py -from typing import Callable +from typing import Callable, Any from ty_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to class A: @@ -1404,6 +1499,8 @@ a = A() static_assert(is_subtype_of(A, Callable[[int], int])) static_assert(not is_subtype_of(A, Callable[[], int])) static_assert(not is_subtype_of(Callable[[int], int], A)) +static_assert(not is_subtype_of(A, Callable[[Any], int])) +static_assert(not is_subtype_of(A, Callable[[int], Any])) def f(fn: Callable[[int], int]) -> None: ... diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index 03adadce98697..ebfb45801c571 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -326,29 +326,20 @@ from ty_extensions import ( Unknown, bottom_materialization, top_materialization, - is_fully_static, static_assert, is_subtype_of, ) def bounded_by_gradual[T: Any](t: T) -> None: - static_assert(not is_fully_static(T)) - # Top materialization of `T: Any` is `T: object` - static_assert(is_fully_static(TypeOf[top_materialization(T)])) # Bottom materialization of `T: Any` is `T: Never` - static_assert(is_fully_static(TypeOf[bottom_materialization(T)])) static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], Never)) def constrained_by_gradual[T: (int, Any)](t: T) -> None: - static_assert(not is_fully_static(T)) - # Top materialization of `T: (int, Any)` is `T: (int, object)` - static_assert(is_fully_static(TypeOf[top_materialization(T)])) # Bottom materialization of `T: (int, Any)` is `T: (int, Never)` - static_assert(is_fully_static(TypeOf[bottom_materialization(T)])) static_assert(is_subtype_of(TypeOf[bottom_materialization(T)], int)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md index 50d20efb1248d..919fd4921b29e 100644 --- a/crates/ty_python_semantic/resources/mdtest/union_types.md +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -175,8 +175,8 @@ python-version = "3.12" ``` ```py -from typing import Literal -from ty_extensions import AlwaysTruthy, AlwaysFalsy +from typing import Literal, Union +from ty_extensions import AlwaysTruthy, AlwaysFalsy, is_equivalent_to, static_assert type strings = Literal["foo", ""] type ints = Literal[0, 1] @@ -213,4 +213,61 @@ def _( reveal_type(bytes_or_falsy) # revealed: Literal[b"foo"] | AlwaysFalsy reveal_type(falsy_or_bytes) # revealed: AlwaysFalsy | Literal[b"foo"] + +type SA = Union[Literal[""], AlwaysTruthy, Literal["foo"]] +static_assert(is_equivalent_to(SA, Literal[""] | AlwaysTruthy)) + +type SD = Union[Literal[""], AlwaysTruthy, Literal["foo"], AlwaysFalsy, AlwaysTruthy, int] +static_assert(is_equivalent_to(SD, AlwaysTruthy | AlwaysFalsy | int)) + +type BA = Union[Literal[b""], AlwaysTruthy, Literal[b"foo"]] +static_assert(is_equivalent_to(BA, Literal[b""] | AlwaysTruthy)) + +type BD = Union[Literal[b""], AlwaysTruthy, Literal[b"foo"], AlwaysFalsy, AlwaysTruthy, int] +static_assert(is_equivalent_to(BD, AlwaysTruthy | AlwaysFalsy | int)) + +type IA = Union[Literal[0], AlwaysTruthy, Literal[1]] +static_assert(is_equivalent_to(IA, Literal[0] | AlwaysTruthy)) + +type ID = Union[Literal[0], AlwaysTruthy, Literal[1], AlwaysFalsy, AlwaysTruthy, str] +static_assert(is_equivalent_to(ID, AlwaysTruthy | AlwaysFalsy | str)) +``` + +## Unions with intersections of literals and Any + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, Literal +from ty_extensions import Intersection + +type SA = Literal[""] +type SB = Intersection[Literal[""], Any] +type SC = SA | SB +type SD = SB | SA + +def _(c: SC, d: SD): + reveal_type(c) # revealed: Literal[""] + reveal_type(d) # revealed: Literal[""] + +type IA = Literal[0] +type IB = Intersection[Literal[0], Any] +type IC = IA | IB +type ID = IB | IA + +def _(c: IC, d: ID): + reveal_type(c) # revealed: Literal[0] + reveal_type(d) # revealed: Literal[0] + +type BA = Literal[b""] +type BB = Intersection[Literal[b""], Any] +type BC = BA | BB +type BD = BB | BA + +def _(c: BC, d: BD): + reveal_type(c) # revealed: Literal[b""] + reveal_type(d) # revealed: Literal[b""] ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 34ace1ddb5fa8..af1a258b6e325 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -558,7 +558,6 @@ pub enum Type<'db> { BoundSuper(BoundSuperType<'db>), /// A subtype of `bool` that allows narrowing in both positive and negative cases. TypeIs(TypeIsType<'db>), - // TODO protocols, overloads, generics } #[salsa::tracked] @@ -1165,11 +1164,88 @@ impl<'db> Type<'db> { } } + /// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for + /// any `T` of this type. + /// + /// This is true for fully static types, but also for some types that may not be fully static. + /// For example, a `ClassLiteral` may inherit `Any`, but its subtyping is still reflexive. + /// + /// This method may have false negatives, but it should not have false positives. It should be + /// a cheap shallow check, not an exhaustive recursive check. + fn subtyping_is_always_reflexive(self) -> bool { + match self { + Type::Never + | Type::FunctionLiteral(..) + | Type::BoundMethod(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(..) + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::PropertyInstance(_) + // might inherit `Any`, but subtyping is still reflexive + | Type::ClassLiteral(_) => true, + Type::Dynamic(_) + | Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::GenericAlias(_) + | Type::SubclassOf(_) + | Type::Union(_) + | Type::Intersection(_) + | Type::Callable(_) + | Type::Tuple(_) + | Type::TypeVar(_) + | Type::BoundSuper(_) + | Type::TypeIs(_) => false, + } + } + /// Return true if this type is a [subtype of] type `target`. /// - /// This method returns `false` if either `self` or `other` is not fully static. + /// For fully static types, this means that the set of objects represented by `self` is a + /// subset of the objects represented by `target`. + /// + /// For gradual types, it means that the union of all possible sets of values represented by + /// `self` (the "top materialization" of `self`) is a subtype of the intersection of all + /// possible sets of values represented by `target` (the "bottom materialization" of + /// `target`). In other words, for all possible pairs of materializations `self'` and + /// `target'`, `self'` is always a subtype of `target'`. + /// + /// Note that this latter expansion of the subtyping relation to non-fully-static types is not + /// described in the typing spec, but the primary use of the subtyping relation is for + /// simplifying unions and intersections, and this expansion to gradual types is sound and + /// allows us to better simplify many unions and intersections. This definition does mean the + /// subtyping relation is not reflexive for non-fully-static types (e.g. `Any` is not a subtype + /// of `Any`). /// /// [subtype of]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence + /// + /// There would be an even more general definition of subtyping for gradual types, allowing a + /// type `S` to be a subtype of a type `T` if the top materialization of `S` (`S+`) is a + /// subtype of `T+`, and the bottom materialization of `S` (`S-`) is a subtype of `T-`. This + /// definition is attractive in that it would restore reflexivity of subtyping for all types, + /// and would mean that gradual equivalence of `S` and `T` could be defined simply as `S <: T + /// && T <: S`. It would also be sound, in that simplifying unions or intersections according + /// to this definition of subtyping would still result in an equivalent type. + /// + /// Unfortunately using this definition would break transitivity of subtyping when both nominal + /// and structural types are involved, because Liskov enforcement for nominal types is based on + /// assignability, so we can have class `A` with method `def meth(self) -> Any` and a subclass + /// `B(A)` with method `def meth(self) -> int`. In this case, `A` would be a subtype of a + /// protocol `P` with method `def meth(self) -> Any`, but `B` would not be a subtype of `P`, + /// and yet `B` is (by nominal subtyping) a subtype of `A`, so we would have `B <: A` and `A <: + /// P`, but not `B <: P`. Losing transitivity of subtyping is not tenable (it makes union and + /// intersection simplification dependent on the order in which elements are added), so we do + /// not use this more general definition of subtyping. pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { self.has_relation_to(db, target, TypeRelation::Subtyping) } @@ -1182,22 +1258,25 @@ impl<'db> Type<'db> { } fn has_relation_to(self, db: &'db dyn Db, target: Type<'db>, relation: TypeRelation) -> bool { - if !relation.applies_to(db, self, target) { - return false; - } - if relation.are_equivalent(db, self, target) { + // Subtyping implies assignability, so if subtyping is reflexive and the two types are + // equivalent, it is both a subtype and assignable. Assignability is always reflexive. + if (relation.is_assignability() || self.subtyping_is_always_reflexive()) + && self.is_equivalent_to(db, target) + { return true; } match (self, target) { - (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => true, + // Everything is a subtype of `object`. + (_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true, // `Never` is the bottom type, the empty set. - // It is a subtype of all other fully static types. + // It is a subtype of all other types. (Type::Never, _) => true, - // Everything is a subtype of `object`. - (_, Type::NominalInstance(instance)) if instance.class.is_object(db) => true, + // Dynamic is only a subtype of `object` and only a supertype of `Never`; both were + // handled above. It's always assignable, though. + (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => relation.is_assignability(), // In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied: // 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`. @@ -1219,6 +1298,14 @@ impl<'db> Type<'db> { false } + // Two identical typevars must always solve to the same type, so they are always + // subtypes of each other and assignable to each other. + (Type::TypeVar(lhs_typevar), Type::TypeVar(rhs_typevar)) + if lhs_typevar == rhs_typevar => + { + true + } + // A fully static typevar is a subtype of its upper bound, and to something similar to // the union of its constraints. An unbound, unconstrained, fully static typevar has an // implicit upper bound of `object` (which is handled above). @@ -1250,7 +1337,7 @@ impl<'db> Type<'db> { // `Never` is the bottom type, the empty set. // Other than one unlikely edge case (TypeVars bound to `Never`), - // no other fully static type is a subtype of `Never`. + // no other type is a subtype of or assignable to `Never`. (_, Type::Never) => false, (Type::Union(union), _) => union @@ -1295,7 +1382,7 @@ impl<'db> Type<'db> { (left, Type::AlwaysTruthy) => left.bool(db).is_always_true(), // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { - relation.are_equivalent(db, target, Type::object(db)) + target.is_equivalent_to(db, Type::object(db)) } // These clauses handle type variants that include function literals. A function @@ -1410,6 +1497,15 @@ impl<'db> Type<'db> { false } + // `TypeIs` is invariant. + (Type::TypeIs(left), Type::TypeIs(right)) => { + left.return_type(db) + .has_relation_to(db, right.return_type(db), relation) + && right + .return_type(db) + .has_relation_to(db, left.return_type(db), relation) + } + // `TypeIs[T]` is a subtype of `bool`. (Type::TypeIs(_), _) => KnownClass::Bool .to_instance(db) @@ -1425,11 +1521,7 @@ impl<'db> Type<'db> { true } - (Type::Callable(_), _) => { - // TODO: Implement subtyping between callable types and other types like - // function literals, bound methods, class literals, `type[]`, etc.) - false - } + (Type::Callable(_), _) => false, (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { self_tuple.has_relation_to(db, target_tuple, relation) @@ -1449,7 +1541,7 @@ impl<'db> Type<'db> { } (Type::Tuple(_), _) => false, - (Type::BoundSuper(_), Type::BoundSuper(_)) => relation.are_equivalent(db, self, target), + (Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target), (Type::BoundSuper(_), _) => KnownClass::Super .to_instance(db) .has_relation_to(db, target, relation), @@ -1459,15 +1551,17 @@ impl<'db> Type<'db> { (Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() .into_class() - .is_none_or(|subclass_of_class| { + .map(|subclass_of_class| { ClassType::NonGeneric(class).has_relation_to(db, subclass_of_class, relation) - }), + }) + .unwrap_or(relation.is_assignability()), (Type::GenericAlias(alias), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty .subclass_of() .into_class() - .is_none_or(|subclass_of_class| { + .map(|subclass_of_class| { ClassType::Generic(alias).has_relation_to(db, subclass_of_class, relation) - }), + }) + .unwrap_or(relation.is_assignability()), // This branch asks: given two types `type[T]` and `type[S]`, is `type[T]` a subtype of `type[S]`? (Type::SubclassOf(self_subclass_ty), Type::SubclassOf(target_subclass_ty)) => { @@ -1494,25 +1588,20 @@ impl<'db> Type<'db> { .metaclass_instance_type(db) .has_relation_to(db, target, relation), - // This branch upholds two properties: - // - For any type `T` that is assignable to `type`, `T` shall be assignable to `type[Any]`. - // - For any type `T` that is assignable to `type`, `type[Any]` shall be assignable to `T`. - // - // This is really the same as the very first branch in this `match` statement that handles dynamic types. - // That branch upholds two properties: - // - For any type `S` that is assignable to `object` (which is _all_ types), `S` shall be assignable to `Any` - // - For any type `S` that is assignable to `object` (which is _all_ types), `Any` shall be assignable to `S`. - // - // The only difference between this branch and the first branch is that the first branch deals with the type - // `object & Any` (which simplifies to `Any`!) whereas this branch deals with the type `type & Any`. - // - // See also: - (Type::SubclassOf(subclass_of_ty), other) - | (other, Type::SubclassOf(subclass_of_ty)) - if subclass_of_ty.is_dynamic() - && other.has_relation_to(db, KnownClass::Type.to_instance(db), relation) => + // `type[Any]` is a subtype of `type[object]`, and is assignable to any `type[...]` + (Type::SubclassOf(subclass_of_ty), other) if subclass_of_ty.is_dynamic() => { + KnownClass::Type + .to_instance(db) + .has_relation_to(db, other, relation) + || (relation.is_assignability() + && other.has_relation_to(db, KnownClass::Type.to_instance(db), relation)) + } + + // Any `type[...]` type is assignable to `type[Any]` + (other, Type::SubclassOf(subclass_of_ty)) + if subclass_of_ty.is_dynamic() && relation.is_assignability() => { - true + other.has_relation_to(db, KnownClass::Type.to_instance(db), relation) } // `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses @@ -1561,43 +1650,7 @@ impl<'db> Type<'db> { /// Return true if this type is [equivalent to] type `other`. /// - /// This method returns `false` if either `self` or `other` is not fully static. - /// - /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { - // TODO equivalent but not identical types: TypedDicts, Protocols, type aliases, etc. - - match (self, other) { - (Type::Union(left), Type::Union(right)) => left.is_equivalent_to(db, right), - (Type::Intersection(left), Type::Intersection(right)) => { - left.is_equivalent_to(db, right) - } - (Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right), - (Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => { - self_function.is_equivalent_to(db, target_function) - } - (Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => { - self_method.is_equivalent_to(db, target_method) - } - (Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => { - self_method.is_equivalent_to(db, target_method) - } - (Type::Callable(left), Type::Callable(right)) => left.is_equivalent_to(db, right), - (Type::NominalInstance(left), Type::NominalInstance(right)) => { - left.is_equivalent_to(db, right) - } - (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { - left.is_equivalent_to(db, right) - } - (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) - | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { - n.class.is_object(db) && protocol.normalized(db) == nominal - } - _ => self == other && self.is_fully_static(db) && other.is_fully_static(db), - } - } - - /// Returns true if this type and `other` are gradual equivalent. + /// Two equivalent types represent the same sets of values. /// /// > Two gradual types `A` and `B` are equivalent /// > (that is, the same gradual type, not merely consistent with one another) @@ -1606,10 +1659,8 @@ impl<'db> Type<'db> { /// > /// > — [Summary of type relations] /// - /// This powers the `assert_type()` directive. - /// - /// [Summary of type relations]: https://typing.python.org/en/latest/spec/concepts.html#summary-of-type-relations - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { + /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent + pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { if self == other { return true; } @@ -1626,32 +1677,30 @@ impl<'db> Type<'db> { } (Type::NominalInstance(first), Type::NominalInstance(second)) => { - first.is_gradual_equivalent_to(db, second) + first.is_equivalent_to(db, second) } - (Type::Tuple(first), Type::Tuple(second)) => first.is_gradual_equivalent_to(db, second), + (Type::Tuple(first), Type::Tuple(second)) => first.is_equivalent_to(db, second), - (Type::Union(first), Type::Union(second)) => first.is_gradual_equivalent_to(db, second), + (Type::Union(first), Type::Union(second)) => first.is_equivalent_to(db, second), (Type::Intersection(first), Type::Intersection(second)) => { - first.is_gradual_equivalent_to(db, second) + first.is_equivalent_to(db, second) } (Type::FunctionLiteral(self_function), Type::FunctionLiteral(target_function)) => { - self_function.is_gradual_equivalent_to(db, target_function) + self_function.is_equivalent_to(db, target_function) } (Type::BoundMethod(self_method), Type::BoundMethod(target_method)) => { - self_method.is_gradual_equivalent_to(db, target_method) + self_method.is_equivalent_to(db, target_method) } (Type::MethodWrapper(self_method), Type::MethodWrapper(target_method)) => { - self_method.is_gradual_equivalent_to(db, target_method) - } - (Type::Callable(first), Type::Callable(second)) => { - first.is_gradual_equivalent_to(db, second) + self_method.is_equivalent_to(db, target_method) } + (Type::Callable(first), Type::Callable(second)) => first.is_equivalent_to(db, second), (Type::ProtocolInstance(first), Type::ProtocolInstance(second)) => { - first.is_gradual_equivalent_to(db, second) + first.is_equivalent_to(db, second) } (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { @@ -2125,75 +2174,6 @@ impl<'db> Type<'db> { } } - /// Returns true if the type does not contain any gradual forms (as a sub-part). - pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool { - match self { - Type::Dynamic(_) => false, - Type::Never - | Type::FunctionLiteral(..) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::MethodWrapper(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::ModuleLiteral(..) - | Type::IntLiteral(_) - | Type::BooleanLiteral(_) - | Type::StringLiteral(_) - | Type::LiteralString - | Type::BytesLiteral(_) - | Type::SpecialForm(_) - | Type::KnownInstance(_) - | Type::AlwaysFalsy - | Type::AlwaysTruthy - | Type::PropertyInstance(_) => true, - - Type::ProtocolInstance(protocol) => protocol.is_fully_static(db), - - Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { - None => true, - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.is_fully_static(db), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_fully_static(db)), - }, - - Type::SubclassOf(subclass_of_ty) => subclass_of_ty.is_fully_static(), - Type::BoundSuper(bound_super) => { - !matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_)) - && !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_)) - } - Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::NominalInstance(_) => { - // TODO: Ideally, we would iterate over the MRO of the class, check if all - // bases are fully static, and only return `true` if that is the case. - // - // This does not work yet, because we currently infer `Unknown` for some - // generic base classes that we don't understand yet. For example, `str` - // is defined as `class str(Sequence[str])` in typeshed and we currently - // compute its MRO as `(str, Unknown, object)`. This would make us think - // that `str` is a gradual type, which causes all sorts of downstream - // issues because it does not participate in equivalence/subtyping etc. - // - // Another problem is that we run into problems if we eagerly query the - // MRO of class literals here. I have not fully investigated this, but - // iterating over the MRO alone, without even acting on it, causes us to - // infer `Unknown` for many classes. - - true - } - Type::Union(union) => union.is_fully_static(db), - Type::Intersection(intersection) => intersection.is_fully_static(db), - // TODO: Once we support them, make sure that we return `false` for other types - // containing gradual forms such as `tuple[Any, ...]`. - // Conversely, make sure to return `true` for homogeneous tuples such as - // `tuple[int, ...]`, once we add support for them. - Type::Tuple(tuple) => tuple.is_fully_static(db), - Type::Callable(callable) => callable.is_fully_static(db), - Type::TypeIs(type_is) => type_is.return_type(db).is_fully_static(db), - } - } - /// Return true if there is just a single inhabitant for this type. /// /// Note: This function aims to have no false positives, but might return `false` @@ -3744,8 +3724,7 @@ impl<'db> Type<'db> { KnownFunction::IsEquivalentTo | KnownFunction::IsSubtypeOf | KnownFunction::IsAssignableTo - | KnownFunction::IsDisjointFrom - | KnownFunction::IsGradualEquivalentTo, + | KnownFunction::IsDisjointFrom, ) => Binding::single( self, Signature::new( @@ -3762,20 +3741,20 @@ impl<'db> Type<'db> { ) .into(), - Some( - KnownFunction::IsFullyStatic - | KnownFunction::IsSingleton - | KnownFunction::IsSingleValued, - ) => Binding::single( - self, - Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static("a"))) + Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => { + Binding::single( + self, + Signature::new( + Parameters::new([Parameter::positional_only(Some(Name::new_static( + "a", + ))) .type_form() .with_annotated_type(Type::any())]), - Some(KnownClass::Bool.to_instance(db)), - ), - ) - .into(), + Some(KnownClass::Bool.to_instance(db)), + ), + ) + .into() + } Some(KnownFunction::TopMaterialization | KnownFunction::BottomMaterialization) => { Binding::single( @@ -6981,34 +6960,7 @@ pub(crate) enum TypeRelation { } impl TypeRelation { - /// Non-fully-static types do not participate in subtyping, only assignability, - /// so the subtyping relation does not even apply to them. - /// - /// Type `A` can only be a subtype of type `B` if the set of possible runtime objects - /// that `A` represents is a subset of the set of possible runtime objects that `B` represents. - /// But the set of objects described by a non-fully-static type is (either partially or wholly) unknown, - /// so the question is simply unanswerable for non-fully-static types. - /// - /// However, the assignability relation applies to all types, even non-fully-static ones. - fn applies_to<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool { - match self { - TypeRelation::Subtyping => type_1.is_fully_static(db) && type_2.is_fully_static(db), - TypeRelation::Assignability => true, - } - } - - /// Determine whether `type_1` and `type_2` are equivalent. - /// - /// Depending on whether the context is a subtyping test or an assignability test, - /// this method may call [`Type::is_equivalent_to`] or [`Type::is_assignable_to`]. - fn are_equivalent<'db>(self, db: &'db dyn Db, type_1: Type<'db>, type_2: Type<'db>) -> bool { - match self { - TypeRelation::Subtyping => type_1.is_equivalent_to(db, type_2), - TypeRelation::Assignability => type_1.is_gradual_equivalent_to(db, type_2), - } - } - - const fn applies_to_non_fully_static_types(self) -> bool { + pub(crate) const fn is_assignability(self) -> bool { matches!(self, TypeRelation::Assignability) } } @@ -7147,14 +7099,6 @@ impl<'db> BoundMethodType<'db> { .self_instance(db) .is_equivalent_to(db, self.self_instance(db)) } - - fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - self.function(db) - .is_gradual_equivalent_to(db, other.function(db)) - && other - .self_instance(db) - .is_gradual_equivalent_to(db, self.self_instance(db)) - } } /// This type represents the set of all callable objects with a certain, possibly overloaded, @@ -7257,13 +7201,6 @@ impl<'db> CallableType<'db> { self.signatures(db).find_legacy_typevars(db, typevars); } - /// Check whether this callable type is fully static. - /// - /// See [`Type::is_fully_static`] for more details. - fn is_fully_static(self, db: &'db dyn Db) -> bool { - self.signatures(db).is_fully_static(db) - } - /// Check whether this callable type has the given relation to another callable type. /// /// See [`Type::is_subtype_of`] and [`Type::is_assignable_to`] for more details. @@ -7285,16 +7222,6 @@ impl<'db> CallableType<'db> { .is_equivalent_to(db, other.signatures(db)) } - /// Check whether this callable type is gradual equivalent to another callable type. - /// - /// See [`Type::is_gradual_equivalent_to`] for more details. - fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - self.is_function_like(db) == other.is_function_like(db) - && self - .signatures(db) - .is_gradual_equivalent_to(db, other.signatures(db)) - } - /// See [`Type::replace_self_reference`]. fn replace_self_reference(self, db: &'db dyn Db, class: ClassLiteral<'db>) -> Self { CallableType::new( @@ -7391,39 +7318,6 @@ impl<'db> MethodWrapperKind<'db> { } } - fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - match (self, other) { - ( - MethodWrapperKind::FunctionTypeDunderGet(self_function), - MethodWrapperKind::FunctionTypeDunderGet(other_function), - ) => self_function.is_gradual_equivalent_to(db, other_function), - - ( - MethodWrapperKind::FunctionTypeDunderCall(self_function), - MethodWrapperKind::FunctionTypeDunderCall(other_function), - ) => self_function.is_gradual_equivalent_to(db, other_function), - - (MethodWrapperKind::PropertyDunderGet(_), MethodWrapperKind::PropertyDunderGet(_)) - | (MethodWrapperKind::PropertyDunderSet(_), MethodWrapperKind::PropertyDunderSet(_)) - | (MethodWrapperKind::StrStartswith(_), MethodWrapperKind::StrStartswith(_)) => { - self == other - } - - ( - MethodWrapperKind::FunctionTypeDunderGet(_) - | MethodWrapperKind::FunctionTypeDunderCall(_) - | MethodWrapperKind::PropertyDunderGet(_) - | MethodWrapperKind::PropertyDunderSet(_) - | MethodWrapperKind::StrStartswith(_), - MethodWrapperKind::FunctionTypeDunderGet(_) - | MethodWrapperKind::FunctionTypeDunderCall(_) - | MethodWrapperKind::PropertyDunderGet(_) - | MethodWrapperKind::PropertyDunderSet(_) - | MethodWrapperKind::StrStartswith(_), - ) => false, - } - } - fn normalized(self, db: &'db dyn Db) -> Self { match self { MethodWrapperKind::FunctionTypeDunderGet(function) => { @@ -7781,10 +7675,6 @@ impl<'db> UnionType<'db> { } } - pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool { - self.elements(db).iter().all(|ty| ty.is_fully_static(db)) - } - /// Create a new union type with the elements normalized. /// /// See [`Type::normalized`] for more details. @@ -7799,15 +7689,8 @@ impl<'db> UnionType<'db> { UnionType::new(db, new_elements.into_boxed_slice()) } - /// Return `true` if `self` represents the exact same set of possible runtime objects as `other` + /// Return `true` if `self` represents the exact same sets of possible runtime objects as `other` pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - /// Inlined version of [`UnionType::is_fully_static`] to avoid having to lookup - /// `self.elements` multiple times in the Salsa db in this single method. - #[inline] - fn all_fully_static(db: &dyn Db, elements: &[Type]) -> bool { - elements.iter().all(|ty| ty.is_fully_static(db)) - } - let self_elements = self.elements(db); let other_elements = other.elements(db); @@ -7815,14 +7698,6 @@ impl<'db> UnionType<'db> { return false; } - if !all_fully_static(db, self_elements) { - return false; - } - - if !all_fully_static(db, other_elements) { - return false; - } - if self == other { return true; } @@ -7835,39 +7710,6 @@ impl<'db> UnionType<'db> { sorted_self == other.normalized(db) } - - /// Return `true` if `self` has exactly the same set of possible static materializations as `other` - /// (if `self` represents the same set of possible sets of possible runtime objects as `other`) - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - if self == other { - return true; - } - - // TODO: `T | Unknown` should be gradually equivalent to `T | Unknown | Any`, - // since they have exactly the same set of possible static materializations - // (they represent the same set of possible sets of possible runtime objects) - if self.elements(db).len() != other.elements(db).len() { - return false; - } - - let sorted_self = self.normalized(db); - - if sorted_self == other { - return true; - } - - let sorted_other = other.normalized(db); - - if sorted_self == sorted_other { - return true; - } - - sorted_self - .elements(db) - .iter() - .zip(sorted_other.elements(db)) - .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) - } } #[salsa::interned(debug)] @@ -7910,52 +7752,24 @@ impl<'db> IntersectionType<'db> { ) } - pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool { - self.positive(db).iter().all(|ty| ty.is_fully_static(db)) - && self.negative(db).iter().all(|ty| ty.is_fully_static(db)) - } - /// Return `true` if `self` represents exactly the same set of possible runtime objects as `other` pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - /// Inlined version of [`IntersectionType::is_fully_static`] to avoid having to lookup - /// `positive` and `negative` multiple times in the Salsa db in this single method. - #[inline] - fn all_fully_static(db: &dyn Db, elements: &FxOrderSet) -> bool { - elements.iter().all(|ty| ty.is_fully_static(db)) - } - let self_positive = self.positive(db); - if !all_fully_static(db, self_positive) { - return false; - } - let other_positive = other.positive(db); if self_positive.len() != other_positive.len() { return false; } - if !all_fully_static(db, other_positive) { - return false; - } - let self_negative = self.negative(db); - if !all_fully_static(db, self_negative) { - return false; - } - let other_negative = other.negative(db); if self_negative.len() != other_negative.len() { return false; } - if !all_fully_static(db, other_negative) { - return false; - } - if self == other { return true; } @@ -7969,43 +7783,6 @@ impl<'db> IntersectionType<'db> { sorted_self == other.normalized(db) } - /// Return `true` if `self` has exactly the same set of possible static materializations as `other` - /// (if `self` represents the same set of possible sets of possible runtime objects as `other`) - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - if self == other { - return true; - } - - if self.positive(db).len() != other.positive(db).len() - || self.negative(db).len() != other.negative(db).len() - { - return false; - } - - let sorted_self = self.normalized(db); - - if sorted_self == other { - return true; - } - - let sorted_other = other.normalized(db); - - if sorted_self == sorted_other { - return true; - } - - sorted_self - .positive(db) - .iter() - .zip(sorted_other.positive(db)) - .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) - && sorted_self - .negative(db) - .iter() - .zip(sorted_other.negative(db)) - .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) - } - pub(crate) fn map_with_boundness( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 5b55088a8b336..2186310b03c28 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -78,6 +78,7 @@ impl<'db> Type<'db> { } } +#[derive(Debug)] enum UnionElement<'db> { IntLiterals(FxOrderSet), StringLiterals(FxOrderSet>), @@ -87,27 +88,26 @@ enum UnionElement<'db> { impl<'db> UnionElement<'db> { /// Try reducing this `UnionElement` given the presence in the same union of `other_type`. - /// - /// If this `UnionElement` is a group of literals, filter the literals present if needed and - /// return `ReduceResult::KeepIf` with a boolean value indicating whether the remaining group - /// of literals should be kept in the union - /// - /// If this `UnionElement` is some other type, return `ReduceResult::Type` so `UnionBuilder` - /// can perform more complex checks on it. fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> { match self { UnionElement::IntLiterals(literals) => { if other_type.splits_literals(db, LiteralKind::Int) { let mut collapse = false; + let mut ignore = false; let negated = other_type.negate(db); literals.retain(|literal| { let ty = Type::IntLiteral(*literal); if negated.is_subtype_of(db, ty) { collapse = true; } + if other_type.is_subtype_of(db, ty) { + ignore = true; + } !ty.is_subtype_of(db, other_type) }); - if collapse { + if ignore { + ReduceResult::Ignore + } else if collapse { ReduceResult::CollapseToObject } else { ReduceResult::KeepIf(!literals.is_empty()) @@ -121,15 +121,21 @@ impl<'db> UnionElement<'db> { UnionElement::StringLiterals(literals) => { if other_type.splits_literals(db, LiteralKind::String) { let mut collapse = false; + let mut ignore = false; let negated = other_type.negate(db); literals.retain(|literal| { let ty = Type::StringLiteral(*literal); if negated.is_subtype_of(db, ty) { collapse = true; } + if other_type.is_subtype_of(db, ty) { + ignore = true; + } !ty.is_subtype_of(db, other_type) }); - if collapse { + if ignore { + ReduceResult::Ignore + } else if collapse { ReduceResult::CollapseToObject } else { ReduceResult::KeepIf(!literals.is_empty()) @@ -143,15 +149,21 @@ impl<'db> UnionElement<'db> { UnionElement::BytesLiterals(literals) => { if other_type.splits_literals(db, LiteralKind::Bytes) { let mut collapse = false; + let mut ignore = false; let negated = other_type.negate(db); literals.retain(|literal| { let ty = Type::BytesLiteral(*literal); if negated.is_subtype_of(db, ty) { collapse = true; } + if other_type.is_subtype_of(db, ty) { + ignore = true; + } !ty.is_subtype_of(db, other_type) }); - if collapse { + if ignore { + ReduceResult::Ignore + } else if collapse { ReduceResult::CollapseToObject } else { ReduceResult::KeepIf(!literals.is_empty()) @@ -173,6 +185,8 @@ enum ReduceResult<'db> { KeepIf(bool), /// Collapse this entire union to `object`. CollapseToObject, + /// The new element is a subtype of an existing part of the `UnionElement`, ignore it. + Ignore, /// The given `Type` can stand-in for the entire `UnionElement` for further union /// simplification checks. Type(Type<'db>), @@ -229,9 +243,10 @@ impl<'db> UnionBuilder<'db> { // means we shouldn't add it. Otherwise, add a new `UnionElement::StringLiterals` // containing it. Type::StringLiteral(literal) => { - let mut found = false; + let mut found = None; + let mut to_remove = None; let ty_negated = ty.negate(self.db); - for element in &mut self.elements { + for (index, element) in self.elements.iter_mut().enumerate() { match element { UnionElement::StringLiterals(literals) => { if literals.len() >= MAX_UNION_LITERALS { @@ -239,14 +254,16 @@ impl<'db> UnionBuilder<'db> { self.add_in_place(replace_with); return; } - literals.insert(literal); - found = true; - break; + found = Some(literals); + continue; } UnionElement::Type(existing) => { if ty.is_subtype_of(self.db, *existing) { return; } + if existing.is_subtype_of(self.db, ty) { + to_remove = Some(index); + } if ty_negated.is_subtype_of(self.db, *existing) { // The type that includes both this new element, and its negation // (or a supertype of its negation), must be simply `object`. @@ -257,18 +274,24 @@ impl<'db> UnionBuilder<'db> { _ => {} } } - if !found { + if let Some(found) = found { + found.insert(literal); + } else { self.elements .push(UnionElement::StringLiterals(FxOrderSet::from_iter([ literal, ]))); } + if let Some(index) = to_remove { + self.elements.swap_remove(index); + } } // Same for bytes literals as for string literals, above. Type::BytesLiteral(literal) => { - let mut found = false; + let mut found = None; + let mut to_remove = None; let ty_negated = ty.negate(self.db); - for element in &mut self.elements { + for (index, element) in self.elements.iter_mut().enumerate() { match element { UnionElement::BytesLiterals(literals) => { if literals.len() >= MAX_UNION_LITERALS { @@ -276,14 +299,16 @@ impl<'db> UnionBuilder<'db> { self.add_in_place(replace_with); return; } - literals.insert(literal); - found = true; - break; + found = Some(literals); + continue; } UnionElement::Type(existing) => { if ty.is_subtype_of(self.db, *existing) { return; } + if existing.is_subtype_of(self.db, ty) { + to_remove = Some(index); + } if ty_negated.is_subtype_of(self.db, *existing) { // The type that includes both this new element, and its negation // (or a supertype of its negation), must be simply `object`. @@ -294,18 +319,24 @@ impl<'db> UnionBuilder<'db> { _ => {} } } - if !found { + if let Some(found) = found { + found.insert(literal); + } else { self.elements .push(UnionElement::BytesLiterals(FxOrderSet::from_iter([ literal, ]))); } + if let Some(index) = to_remove { + self.elements.swap_remove(index); + } } // And same for int literals as well. Type::IntLiteral(literal) => { - let mut found = false; + let mut found = None; + let mut to_remove = None; let ty_negated = ty.negate(self.db); - for element in &mut self.elements { + for (index, element) in self.elements.iter_mut().enumerate() { match element { UnionElement::IntLiterals(literals) => { if literals.len() >= MAX_UNION_LITERALS { @@ -313,14 +344,16 @@ impl<'db> UnionBuilder<'db> { self.add_in_place(replace_with); return; } - literals.insert(literal); - found = true; - break; + found = Some(literals); + continue; } UnionElement::Type(existing) => { if ty.is_subtype_of(self.db, *existing) { return; } + if existing.is_subtype_of(self.db, ty) { + to_remove = Some(index); + } if ty_negated.is_subtype_of(self.db, *existing) { // The type that includes both this new element, and its negation // (or a supertype of its negation), must be simply `object`. @@ -331,10 +364,15 @@ impl<'db> UnionBuilder<'db> { _ => {} } } - if !found { + if let Some(found) = found { + found.insert(literal); + } else { self.elements .push(UnionElement::IntLiterals(FxOrderSet::from_iter([literal]))); } + if let Some(index) = to_remove { + self.elements.swap_remove(index); + } } // Adding `object` to a union results in `object`. ty if ty.is_object(self.db) => { @@ -347,7 +385,6 @@ impl<'db> UnionBuilder<'db> { None }; - let mut to_add = ty; let mut to_remove = SmallVec::<[usize; 2]>::new(); let ty_negated = ty.negate(self.db); @@ -364,20 +401,17 @@ impl<'db> UnionBuilder<'db> { self.collapse_to_object(); return; } + ReduceResult::Ignore => { + return; + } }; if Some(element_type) == bool_pair { - to_add = KnownClass::Bool.to_instance(self.db); - to_remove.push(index); - // The type we are adding is a BooleanLiteral, which doesn't have any - // subtypes. And we just found that the union already contained our - // mirror-image BooleanLiteral, so it can't also contain bool or any - // supertype of bool. Therefore, we are done. - break; + self.add_in_place(KnownClass::Bool.to_instance(self.db)); + return; } - if ty.is_gradual_equivalent_to(self.db, element_type) + if ty.is_equivalent_to(self.db, element_type) || ty.is_subtype_of(self.db, element_type) - || element_type.is_object(self.db) { return; } else if element_type.is_subtype_of(self.db, ty) { @@ -397,13 +431,13 @@ impl<'db> UnionBuilder<'db> { } } if let Some((&first, rest)) = to_remove.split_first() { - self.elements[first] = UnionElement::Type(to_add); + self.elements[first] = UnionElement::Type(ty); // We iterate in descending order to keep remaining indices valid after `swap_remove`. for &index in rest.iter().rev() { self.elements.swap_remove(index); } } else { - self.elements.push(UnionElement::Type(to_add)); + self.elements.push(UnionElement::Type(ty)); } } } @@ -681,7 +715,7 @@ impl<'db> InnerIntersectionBuilder<'db> { for (index, existing_positive) in self.positive.iter().enumerate() { // S & T = S if S <: T if existing_positive.is_subtype_of(db, new_positive) - || existing_positive.is_gradual_equivalent_to(db, new_positive) + || existing_positive.is_equivalent_to(db, new_positive) { return; } @@ -778,7 +812,7 @@ impl<'db> InnerIntersectionBuilder<'db> { for (index, existing_negative) in self.negative.iter().enumerate() { // ~S & ~T = ~T if S <: T if existing_negative.is_subtype_of(db, new_negative) - || existing_negative.is_gradual_equivalent_to(db, new_negative) + || existing_negative.is_equivalent_to(db, new_negative) { to_remove.push(index); } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 1e1841491ce50..7e2ba6d807e1a 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -599,21 +599,6 @@ impl<'db> Bindings<'db> { } } - Some(KnownFunction::IsGradualEquivalentTo) => { - if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - overload.set_return_type(Type::BooleanLiteral( - ty_a.is_gradual_equivalent_to(db, *ty_b), - )); - } - } - - Some(KnownFunction::IsFullyStatic) => { - if let [Some(ty)] = overload.parameter_types() { - overload - .set_return_type(Type::BooleanLiteral(ty.is_fully_static(db))); - } - } - Some(KnownFunction::IsSingleton) => { if let [Some(ty)] = overload.parameter_types() { overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db))); @@ -801,15 +786,13 @@ impl<'db> Bindings<'db> { overload.set_return_type( match instance_ty.static_member(db, attr_name.value(db)) { Place::Type(ty, Boundness::Bound) => { - if instance_ty.is_fully_static(db) { - ty - } else { + if ty.is_dynamic() { // Here, we attempt to model the fact that an attribute lookup on - // a non-fully static type could fail. This is an approximation, - // as there are gradual types like `tuple[Any]`, on which a lookup - // of (e.g. of the `index` method) would always succeed. + // a dynamic type could fail union_with_default(ty) + } else { + ty } } Place::Type(ty, Boundness::PossiblyUnbound) => { @@ -1396,7 +1379,7 @@ impl<'db> CallableBinding<'db> { .annotated_type() .unwrap_or(Type::unknown()); if let Some(first_parameter_type) = first_parameter_type { - if !first_parameter_type.is_gradual_equivalent_to(db, current_parameter_type) { + if !first_parameter_type.is_equivalent_to(db, current_parameter_type) { participating_parameter_index = Some(parameter_index); break; } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index c03707dfb9ae9..3cd7bffa5b7b1 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -379,9 +379,10 @@ impl<'db> ClassType<'db> { ) -> bool { self.iter_mro(db).any(|base| { match base { - ClassBase::Dynamic(_) => { - relation.applies_to_non_fully_static_types() && !other.is_final(db) - } + ClassBase::Dynamic(_) => match relation { + TypeRelation::Subtyping => other.is_object(db), + TypeRelation::Assignability => !other.is_final(db), + }, // Protocol and Generic are not represented by a ClassType. ClassBase::Protocol | ClassBase::Generic => false, @@ -417,20 +418,6 @@ impl<'db> ClassType<'db> { } } - pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - match (self, other) { - (ClassType::NonGeneric(this), ClassType::NonGeneric(other)) => this == other, - (ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => false, - - (ClassType::Generic(this), ClassType::Generic(other)) => { - this.origin(db) == other.origin(db) - && this - .specialization(db) - .is_gradual_equivalent_to(db, other.specialization(db)) - } - } - } - /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { let (class_literal, specialization) = self.class_literal(db); @@ -1427,14 +1414,15 @@ impl<'db> ClassLiteral<'db> { continue; } - // The descriptor handling below is guarded by this fully-static check, because dynamic - // types like `Any` are valid (data) descriptors: since they have all possible attributes, - // they also have a (callable) `__set__` method. The problem is that we can't determine - // the type of the value parameter this way. Instead, we want to use the dynamic type - // itself in this case, so we skip the special descriptor handling. - if attr_ty.is_fully_static(db) { - let dunder_set = attr_ty.class_member(db, "__set__".into()); - if let Some(dunder_set) = dunder_set.place.ignore_possibly_unbound() { + let dunder_set = attr_ty.class_member(db, "__set__".into()); + if let Place::Type(dunder_set, Boundness::Bound) = dunder_set.place { + // The descriptor handling below is guarded by this not-dynamic check, because + // dynamic types like `Any` are valid (data) descriptors: since they have all + // possible attributes, they also have a (callable) `__set__` method. The + // problem is that we can't determine the type of the value parameter this way. + // Instead, we want to use the dynamic type itself in this case, so we skip the + // special descriptor handling. + if !dunder_set.is_dynamic() { // This type of this attribute is a data descriptor. Instead of overwriting the // descriptor attribute, data-classes will (implicitly) call the `__set__` method // of the descriptor. This means that the synthesized `__init__` parameter for diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 9cd5f861b4fa5..2c41af082e7f4 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -148,12 +148,12 @@ declare_lint! { /// ## Examples /// ```python /// from typing import reveal_type - /// from ty_extensions import is_fully_static + /// from ty_extensions import is_singleton /// /// if flag: /// f = repr # Expects a value /// else: - /// f = is_fully_static # Expects a type form + /// f = is_singleton # Expects a type form /// /// f(int) # error /// ``` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index cc98c7e14feb7..31daf84fcd59b 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -736,9 +736,6 @@ impl<'db> FunctionType<'db> { } let self_signature = self.signature(db); let other_signature = other.signature(db); - if !self_signature.is_fully_static(db) || !other_signature.is_fully_static(db) { - return false; - } self_signature.is_subtype_of(db, other_signature) } @@ -760,19 +757,9 @@ impl<'db> FunctionType<'db> { } let self_signature = self.signature(db); let other_signature = other.signature(db); - if !self_signature.is_fully_static(db) || !other_signature.is_fully_static(db) { - return false; - } self_signature.is_equivalent_to(db, other_signature) } - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - self.literal(db) == other.literal(db) - && self - .signature(db) - .is_gradual_equivalent_to(db, other.signature(db)) - } - pub(crate) fn find_legacy_typevars( self, db: &'db dyn Db, @@ -878,10 +865,6 @@ pub enum KnownFunction { IsAssignableTo, /// `ty_extensions.is_disjoint_from` IsDisjointFrom, - /// `ty_extensions.is_gradual_equivalent_to` - IsGradualEquivalentTo, - /// `ty_extensions.is_fully_static` - IsFullyStatic, /// `ty_extensions.is_singleton` IsSingleton, /// `ty_extensions.is_single_valued` @@ -948,8 +931,6 @@ impl KnownFunction { Self::IsAssignableTo | Self::IsDisjointFrom | Self::IsEquivalentTo - | Self::IsGradualEquivalentTo - | Self::IsFullyStatic | Self::IsSingleValued | Self::IsSingleton | Self::IsSubtypeOf @@ -1009,12 +990,10 @@ pub(crate) mod tests { | KnownFunction::GenericContext | KnownFunction::DunderAllNames | KnownFunction::StaticAssert - | KnownFunction::IsFullyStatic | KnownFunction::IsDisjointFrom | KnownFunction::IsSingleValued | KnownFunction::IsAssignableTo | KnownFunction::IsEquivalentTo - | KnownFunction::IsGradualEquivalentTo | KnownFunction::TopMaterialization | KnownFunction::BottomMaterialization | KnownFunction::AllMembers => KnownModule::TyExtensions, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index bec2a2579e9ae..32c844dc7265e 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -463,10 +463,6 @@ impl<'db> Specialization<'db> { .zip(self.types(db)) .zip(other.types(db)) { - if matches!(self_type, Type::Dynamic(_)) || matches!(other_type, Type::Dynamic(_)) { - return false; - } - // Equivalence of each type in the specialization depends on the variance of the // corresponding typevar: // - covariant: verify that self_type == other_type @@ -487,42 +483,6 @@ impl<'db> Specialization<'db> { true } - pub(crate) fn is_gradual_equivalent_to( - self, - db: &'db dyn Db, - other: Specialization<'db>, - ) -> bool { - let generic_context = self.generic_context(db); - if generic_context != other.generic_context(db) { - return false; - } - - for ((typevar, self_type), other_type) in (generic_context.variables(db).into_iter()) - .zip(self.types(db)) - .zip(other.types(db)) - { - // Equivalence of each type in the specialization depends on the variance of the - // corresponding typevar: - // - covariant: verify that self_type == other_type - // - contravariant: verify that other_type == self_type - // - invariant: verify that self_type == other_type - // - bivariant: skip, can't make equivalence false - let compatible = match typevar.variance(db) { - TypeVarVariance::Invariant - | TypeVarVariance::Covariant - | TypeVarVariance::Contravariant => { - self_type.is_gradual_equivalent_to(db, *other_type) - } - TypeVarVariance::Bivariant => true, - }; - if !compatible { - return false; - } - } - - true - } - pub(crate) fn find_legacy_typevars( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index ef4a2cb177e97..0240c01f8a665 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -5448,8 +5448,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let [Some(actual_ty), Some(asserted_ty)] = overload.parameter_types() { - if !actual_ty - .is_gradual_equivalent_to(self.db(), *asserted_ty) + if !actual_ty.is_equivalent_to(self.db(), *asserted_ty) { if let Some(builder) = self.context.report_lint( &TYPE_ASSERTION_FAILURE, @@ -5586,14 +5585,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let contains_unknown_or_todo = |ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any); if source_type.is_equivalent_to(db, *casted_type) - || (source_type.normalized(db) - == casted_type.normalized(db) - && !casted_type.any_over_type(db, &|ty| { - contains_unknown_or_todo(ty) - }) - && !source_type.any_over_type(db, &|ty| { - contains_unknown_or_todo(ty) - })) + && !casted_type.any_over_type(db, &|ty| { + contains_unknown_or_todo(ty) + }) + && !source_type.any_over_type(db, &|ty| { + contains_unknown_or_todo(ty) + }) { if let Some(builder) = self .context diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 73e05af5c02e5..2ad17b2949018 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -108,10 +108,6 @@ impl<'db> NominalInstanceType<'db> { !self.class.could_coexist_in_mro_with(db, other.class) } - pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - self.class.is_gradual_equivalent_to(db, other.class) - } - pub(super) fn is_singleton(self, db: &'db dyn Db) -> bool { self.class.known(db).is_some_and(KnownClass::is_singleton) } @@ -240,11 +236,6 @@ impl<'db> ProtocolInstanceType<'db> { self.inner.interface(db).any_over_type(db, type_fn) } - /// Return `true` if this protocol type is fully static. - pub(super) fn is_fully_static(self, db: &'db dyn Db) -> bool { - self.inner.interface(db).is_fully_static(db) - } - /// Return `true` if this protocol type has the given type relation to the protocol `other`. /// /// TODO: consider the types of the members as well as their existence @@ -252,13 +243,9 @@ impl<'db> ProtocolInstanceType<'db> { self, db: &'db dyn Db, other: Self, - relation: TypeRelation, + _relation: TypeRelation, ) -> bool { - relation.applies_to( - db, - Type::ProtocolInstance(self), - Type::ProtocolInstance(other), - ) && other + other .inner .interface(db) .is_sub_interface_of(db, self.inner.interface(db)) @@ -268,15 +255,6 @@ impl<'db> ProtocolInstanceType<'db> { /// /// TODO: consider the types of the members as well as their existence pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - self.is_fully_static(db) - && other.is_fully_static(db) - && self.normalized(db) == other.normalized(db) - } - - /// Return `true` if this protocol type is gradually equivalent to the protocol `other`. - /// - /// TODO: consider the types of the members as well as their existence - pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { self.normalized(db) == other.normalized(db) } diff --git a/crates/ty_python_semantic/src/types/property_tests.rs b/crates/ty_python_semantic/src/types/property_tests.rs index de97903296ddc..5f071cbff5a8e 100644 --- a/crates/ty_python_semantic/src/types/property_tests.rs +++ b/crates/ty_python_semantic/src/types/property_tests.rs @@ -50,9 +50,19 @@ macro_rules! type_property_test { $property } }; + ($test_name:ident, $db:ident, forall fully_static_types $($types:ident),+ . $property:expr) => { + #[quickcheck_macros::quickcheck] + #[ignore] + fn $test_name($($types: crate::types::property_tests::type_generation::FullyStaticTy),+) -> bool { + let $db = &crate::types::property_tests::setup::get_cached_db(); + $(let $types = $types.into_type($db);)+ + + $property + } + }; // A property test with a logical implication. - ($name:ident, $db:ident, forall types $($types:ident),+ . $premise:expr => $conclusion:expr) => { - type_property_test!($name, $db, forall types $($types),+ . !($premise) || ($conclusion)); + ($name:ident, $db:ident, forall $typekind:ident $($types:ident),+ . $premise:expr => $conclusion:expr) => { + type_property_test!($name, $db, forall $typekind $($types),+ . !($premise) || ($conclusion)); }; } @@ -63,11 +73,10 @@ mod stable { // Reflexivity: `T` is equivalent to itself. type_property_test!( equivalent_to_is_reflexive, db, - forall types t. t.is_fully_static(db) => t.is_equivalent_to(db, t) + forall types t. t.is_equivalent_to(db, t) ); // Symmetry: If `S` is equivalent to `T`, then `T` must be equivalent to `S`. - // Note that this (trivially) holds true for gradual types as well. type_property_test!( equivalent_to_is_symmetric, db, forall types s, t. s.is_equivalent_to(db, t) => t.is_equivalent_to(db, s) @@ -79,18 +88,6 @@ mod stable { forall types s, t, u. s.is_equivalent_to(db, t) && t.is_equivalent_to(db, u) => s.is_equivalent_to(db, u) ); - // Symmetry: If `S` is gradual equivalent to `T`, `T` is gradual equivalent to `S`. - type_property_test!( - gradual_equivalent_to_is_symmetric, db, - forall types s, t. s.is_gradual_equivalent_to(db, t) => t.is_gradual_equivalent_to(db, s) - ); - - // A fully static type `T` is a subtype of itself. - type_property_test!( - subtype_of_is_reflexive, db, - forall types t. t.is_fully_static(db) => t.is_subtype_of(db, t) - ); - // `S <: T` and `T <: U` implies that `S <: U`. type_property_test!( subtype_of_is_transitive, db, @@ -133,28 +130,16 @@ mod stable { forall types t. t.is_singleton(db) => t.is_single_valued(db) ); - // If `T` contains a gradual form, it should not participate in equivalence - type_property_test!( - non_fully_static_types_do_not_participate_in_equivalence, db, - forall types s, t. !s.is_fully_static(db) => !s.is_equivalent_to(db, t) && !t.is_equivalent_to(db, s) - ); - - // If `T` contains a gradual form, it should not participate in subtyping - type_property_test!( - non_fully_static_types_do_not_participate_in_subtyping, db, - forall types s, t. !s.is_fully_static(db) => !s.is_subtype_of(db, t) && !t.is_subtype_of(db, s) - ); - // All types should be assignable to `object` type_property_test!( all_types_assignable_to_object, db, forall types t. t.is_assignable_to(db, Type::object(db)) ); - // And for fully static types, they should also be subtypes of `object` + // And all types should be subtypes of `object` type_property_test!( - all_fully_static_types_subtype_of_object, db, - forall types t. t.is_fully_static(db) => t.is_subtype_of(db, Type::object(db)) + all_types_subtype_of_object, db, + forall types t. t.is_subtype_of(db, Type::object(db)) ); // Never should be assignable to every type @@ -163,53 +148,62 @@ mod stable { forall types t. Type::Never.is_assignable_to(db, t) ); - // And it should be a subtype of all fully static types + // And it should be a subtype of all types type_property_test!( - never_subtype_of_every_fully_static_type, db, - forall types t. t.is_fully_static(db) => Type::Never.is_subtype_of(db, t) + never_subtype_of_every_type, db, + forall types t. Type::Never.is_subtype_of(db, t) ); - // Similar to `Never`, a fully-static "bottom" callable type should be a subtype of all - // fully-static callable types + // Similar to `Never`, a "bottom" callable type should be a subtype of all callable types type_property_test!( - bottom_callable_is_subtype_of_all_fully_static_callable, db, - forall types t. t.is_callable_type() && t.is_fully_static(db) + bottom_callable_is_subtype_of_all_callable, db, + forall types t. t.is_callable_type() => CallableType::bottom(db).is_subtype_of(db, t) ); - // For any two fully static types, each type in the pair must be a subtype of their union. + // `T` can be assigned to itself. type_property_test!( - all_fully_static_type_pairs_are_subtype_of_their_union, db, - forall types s, t. - s.is_fully_static(db) && t.is_fully_static(db) - => s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t])) + assignable_to_is_reflexive, db, + forall types t. t.is_assignable_to(db, t) ); - // A fully static type does not have any materializations. - // Thus, two equivalent (fully static) types are also gradual equivalent. + // For *any* pair of types, each of the pair should be assignable to the union of the two. type_property_test!( - two_equivalent_types_are_also_gradual_equivalent, db, - forall types s, t. s.is_equivalent_to(db, t) => s.is_gradual_equivalent_to(db, t) + all_type_pairs_are_assignable_to_their_union, db, + forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t])) ); - // Two gradual equivalent fully static types are also equivalent. + // Only `Never` is a subtype of `Any`. type_property_test!( - two_gradual_equivalent_fully_static_types_are_also_equivalent, db, - forall types s, t. - s.is_fully_static(db) && s.is_gradual_equivalent_to(db, t) => s.is_equivalent_to(db, t) + only_never_is_subtype_of_any, db, + forall types s. !s.is_equivalent_to(db, Type::Never) => !s.is_subtype_of(db, Type::any()) ); - // `T` can be assigned to itself. + // Only `object` is a supertype of `Any`. type_property_test!( - assignable_to_is_reflexive, db, - forall types t. t.is_assignable_to(db, t) + only_object_is_supertype_of_any, db, + forall types t. !t.is_equivalent_to(db, Type::object(db)) => !Type::any().is_subtype_of(db, t) ); - // For *any* pair of types, whether fully static or not, - // each of the pair should be assignable to the union of the two. + // Equivalence is commutative. type_property_test!( - all_type_pairs_are_assignable_to_their_union, db, - forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t])) + equivalent_to_is_commutative, db, + forall types s, t. s.is_equivalent_to(db, t) == t.is_equivalent_to(db, s) + ); + + // A fully static type `T` is a subtype of itself. (This is not true for non-fully-static + // types; `Any` is not a subtype of `Any`, only `Never` is.) + type_property_test!( + subtype_of_is_reflexive_for_fully_static_types, db, + forall fully_static_types t. t.is_subtype_of(db, t) + ); + + // For any two fully static types, each type in the pair must be a subtype of their union. + // (This is clearly not true for non-fully-static types, since their subtyping is not + // reflexive.) + type_property_test!( + all_fully_static_type_pairs_are_subtype_of_their_union, db, + forall fully_static_types s, t. s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t])) ); } @@ -231,21 +225,21 @@ mod flaky { forall types t. t.negate(db).negate(db).is_equivalent_to(db, t) ); - // ~T should be disjoint from T + // For any fully static type `T`, `T` should be disjoint from `~T`. + // https://github.com/astral-sh/ty/issues/216 type_property_test!( - negation_is_disjoint, db, - forall types t. t.is_fully_static(db) => t.negate(db).is_disjoint_from(db, t) + negation_of_fully_static_types_is_disjoint, db, + forall fully_static_types t. t.negate(db).is_disjoint_from(db, t) ); - // For two fully static types, their intersection must be a subtype of each type in the pair. + // For two types, their intersection must be a subtype of each type in the pair. type_property_test!( - all_fully_static_type_pairs_are_supertypes_of_their_intersection, db, + all_type_pairs_are_supertypes_of_their_intersection, db, forall types s, t. - s.is_fully_static(db) && t.is_fully_static(db) - => intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t) + intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t) ); - // And for non-fully-static types, the intersection of a pair of types + // And the intersection of a pair of types // should be assignable to both types of the pair. // Currently fails due to https://github.com/astral-sh/ruff/issues/14899 type_property_test!( @@ -258,8 +252,7 @@ mod flaky { type_property_test!( intersection_equivalence_not_order_dependent, db, forall types s, t, u. - s.is_fully_static(db) && t.is_fully_static(db) && u.is_fully_static(db) - => [s, t, u] + [s, t, u] .into_iter() .permutations(3) .map(|trio_of_types| intersection(db, trio_of_types)) @@ -272,8 +265,7 @@ mod flaky { type_property_test!( union_equivalence_not_order_dependent, db, forall types s, t, u. - s.is_fully_static(db) && t.is_fully_static(db) && u.is_fully_static(db) - => [s, t, u] + [s, t, u] .into_iter() .permutations(3) .map(|trio_of_types| union(db, trio_of_types)) 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 180b751250291..6643ca884e2d1 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 @@ -207,16 +207,31 @@ impl Ty { } } -fn arbitrary_core_type(g: &mut Gen) -> Ty { +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct FullyStaticTy(Ty); + +impl FullyStaticTy { + pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { + self.0.into_type(db) + } +} + +fn arbitrary_core_type(g: &mut Gen, fully_static: bool) -> Ty { // We could select a random integer here, but this would make it much less // likely to explore interesting edge cases: let int_lit = Ty::IntLiteral(*g.choose(&[-2, -1, 0, 1, 2]).unwrap()); let bool_lit = Ty::BooleanLiteral(bool::arbitrary(g)); - g.choose(&[ - Ty::Never, + + // Update this if new non-fully-static types are added below. + let fully_static_index = 3; + let types = &[ + Ty::Any, Ty::Unknown, + Ty::SubclassOfAny, + // Add fully static types below, dynamic types above. + // Update `fully_static_index` above if adding new dynamic types! + Ty::Never, Ty::None, - Ty::Any, int_lit, bool_lit, Ty::StringLiteral(""), @@ -241,7 +256,6 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty { Ty::BuiltinInstance("type"), Ty::AbcInstance("ABC"), Ty::AbcInstance("ABCMeta"), - Ty::SubclassOfAny, Ty::SubclassOfBuiltinClass("object"), Ty::SubclassOfBuiltinClass("str"), Ty::SubclassOfBuiltinClass("type"), @@ -261,9 +275,13 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty { class: "int", method: "bit_length", }, - ]) - .unwrap() - .clone() + ]; + let types = if fully_static { + &types[fully_static_index..] + } else { + types + }; + g.choose(types).unwrap().clone() } /// Constructs an arbitrary type. @@ -271,53 +289,54 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty { /// The `size` parameter controls the depth of the type tree. For example, /// a simple type like `int` has a size of 0, `Union[int, str]` has a size /// of 1, `tuple[int, Union[str, bytes]]` has a size of 2, etc. -fn arbitrary_type(g: &mut Gen, size: u32) -> Ty { +/// +/// The `fully_static` parameter, if `true`, limits generation to fully static types. +fn arbitrary_type(g: &mut Gen, size: u32, fully_static: bool) -> Ty { if size == 0 { - arbitrary_core_type(g) + arbitrary_core_type(g, fully_static) } else { match u32::arbitrary(g) % 6 { - 0 => arbitrary_core_type(g), + 0 => arbitrary_core_type(g, fully_static), 1 => Ty::Union( (0..*g.choose(&[2, 3]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) + .map(|_| arbitrary_type(g, size - 1, fully_static)) .collect(), ), 2 => Ty::FixedLengthTuple( (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) + .map(|_| arbitrary_type(g, size - 1, fully_static)) .collect(), ), 3 => Ty::VariableLengthTuple( (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) + .map(|_| arbitrary_type(g, size - 1, fully_static)) .collect(), - Box::new(arbitrary_type(g, size - 1)), + Box::new(arbitrary_type(g, size - 1, fully_static)), (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) + .map(|_| arbitrary_type(g, size - 1, fully_static)) .collect(), ), 4 => Ty::Intersection { pos: (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) + .map(|_| arbitrary_type(g, size - 1, fully_static)) .collect(), neg: (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) + .map(|_| arbitrary_type(g, size - 1, fully_static)) .collect(), }, 5 => Ty::Callable { params: match u32::arbitrary(g) % 2 { - 0 => CallableParams::GradualForm, - 1 => CallableParams::List(arbitrary_parameter_list(g, size)), - _ => unreachable!(), + 0 if !fully_static => CallableParams::GradualForm, + _ => CallableParams::List(arbitrary_parameter_list(g, size, fully_static)), }, - returns: arbitrary_optional_type(g, size - 1).map(Box::new), + returns: arbitrary_annotation(g, size - 1, fully_static).map(Box::new), }, _ => unreachable!(), } } } -fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec { +fn arbitrary_parameter_list(g: &mut Gen, size: u32, fully_static: bool) -> Vec { let mut params: Vec = vec![]; let mut used_names = HashSet::new(); @@ -369,11 +388,11 @@ fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec { params.push(Param { kind: next_kind, name, - annotated_ty: arbitrary_optional_type(g, size), + annotated_ty: arbitrary_annotation(g, size, fully_static), default_ty: if matches!(next_kind, ParamKind::Variadic | ParamKind::KeywordVariadic) { None } else { - arbitrary_optional_type(g, size) + arbitrary_optional_type(g, size, fully_static) }, }); } @@ -381,10 +400,19 @@ fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec { params } -fn arbitrary_optional_type(g: &mut Gen, size: u32) -> Option { +/// An arbitrary optional type, always `Some` if fully static. +fn arbitrary_annotation(g: &mut Gen, size: u32, fully_static: bool) -> Option { + if fully_static { + Some(arbitrary_type(g, size, true)) + } else { + arbitrary_optional_type(g, size, false) + } +} + +fn arbitrary_optional_type(g: &mut Gen, size: u32, fully_static: bool) -> Option { match u32::arbitrary(g) % 2 { 0 => None, - 1 => Some(arbitrary_type(g, size)), + 1 => Some(arbitrary_type(g, size, fully_static)), _ => unreachable!(), } } @@ -404,7 +432,7 @@ fn arbitrary_optional_name(g: &mut Gen) -> Option { impl Arbitrary for Ty { fn arbitrary(g: &mut Gen) -> Ty { const MAX_SIZE: u32 = 2; - arbitrary_type(g, MAX_SIZE) + arbitrary_type(g, MAX_SIZE, false) } fn shrink(&self) -> Box> { @@ -491,6 +519,17 @@ impl Arbitrary for Ty { } } +impl Arbitrary for FullyStaticTy { + fn arbitrary(g: &mut Gen) -> FullyStaticTy { + const MAX_SIZE: u32 = 2; + FullyStaticTy(arbitrary_type(g, MAX_SIZE, true)) + } + + fn shrink(&self) -> Box> { + Box::new(self.0.shrink().map(FullyStaticTy)) + } +} + pub(crate) fn intersection<'db>( db: &'db TestDb, tys: impl IntoIterator>, diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index df3a633367c8b..c091d8adcb74f 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -135,11 +135,6 @@ impl<'db> ProtocolInterface<'db> { } } - /// Return `true` if all members of this protocol are fully static. - pub(super) fn is_fully_static(self, db: &'db dyn Db) -> bool { - self.members(db).all(|member| member.ty.is_fully_static(db)) - } - /// Return `true` if if all members on `self` are also members of `other`. /// /// TODO: this method should consider the types of the members as well as their names. diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index bcaeb6be1204c..2729e64a8ab4f 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -97,15 +97,6 @@ impl<'db> CallableSignature<'db> { } } - /// Check whether this callable type is fully static. - /// - /// See [`Type::is_fully_static`] for more details. - pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool { - self.overloads - .iter() - .all(|signature| signature.is_fully_static(db)) - } - pub(crate) fn has_relation_to( &self, db: &'db dyn Db, @@ -123,9 +114,10 @@ impl<'db> CallableSignature<'db> { /// See [`Type::is_subtype_of`] for more details. pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Self) -> bool { Self::has_relation_to_impl( + db, &self.overloads, &other.overloads, - &|self_signature, other_signature| self_signature.is_subtype_of(db, other_signature), + TypeRelation::Subtyping, ) } @@ -134,54 +126,54 @@ impl<'db> CallableSignature<'db> { /// See [`Type::is_assignable_to`] for more details. pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Self) -> bool { Self::has_relation_to_impl( + db, &self.overloads, &other.overloads, - &|self_signature, other_signature| self_signature.is_assignable_to(db, other_signature), + TypeRelation::Assignability, ) } - /// Implementation for the various relation checks between two, possible overloaded, callable + /// Implementation of subtyping and assignability between two, possible overloaded, callable /// types. - /// - /// The `check_signature` closure is used to check the relation between two [`Signature`]s. - fn has_relation_to_impl( + fn has_relation_to_impl( + db: &'db dyn Db, self_signatures: &[Signature<'db>], other_signatures: &[Signature<'db>], - check_signature: &F, - ) -> bool - where - F: Fn(&Signature<'db>, &Signature<'db>) -> bool, - { + relation: TypeRelation, + ) -> bool { match (self_signatures, other_signatures) { ([self_signature], [other_signature]) => { // Base case: both callable types contain a single signature. - check_signature(self_signature, other_signature) + self_signature.has_relation_to(db, other_signature, relation) } // `self` is possibly overloaded while `other` is definitely not overloaded. (_, [_]) => self_signatures.iter().any(|self_signature| { Self::has_relation_to_impl( + db, std::slice::from_ref(self_signature), other_signatures, - check_signature, + relation, ) }), // `self` is definitely not overloaded while `other` is possibly overloaded. ([_], _) => other_signatures.iter().all(|other_signature| { Self::has_relation_to_impl( + db, self_signatures, std::slice::from_ref(other_signature), - check_signature, + relation, ) }), // `self` is definitely overloaded while `other` is possibly overloaded. (_, _) => other_signatures.iter().all(|other_signature| { Self::has_relation_to_impl( + db, self_signatures, std::slice::from_ref(other_signature), - check_signature, + relation, ) }), } @@ -197,14 +189,7 @@ impl<'db> CallableSignature<'db> { // equivalence check instead of delegating it to the subtype check. self_signature.is_equivalent_to(db, other_signature) } - (self_signatures, other_signatures) => { - if !self_signatures - .iter() - .chain(other_signatures.iter()) - .all(|signature| signature.is_fully_static(db)) - { - return false; - } + (_, _) => { if self == other { return true; } @@ -213,21 +198,6 @@ impl<'db> CallableSignature<'db> { } } - /// Check whether this callable type is gradual equivalent to another callable type. - /// - /// See [`Type::is_gradual_equivalent_to`] for more details. - pub(crate) fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { - match (self.overloads.as_slice(), other.overloads.as_slice()) { - ([self_signature], [other_signature]) => { - self_signature.is_gradual_equivalent_to(db, other_signature) - } - _ => { - // TODO: overloads - false - } - } - } - pub(crate) fn replace_self_reference(&self, db: &'db dyn Db, class: ClassLiteral<'db>) -> Self { Self { overloads: self @@ -435,61 +405,19 @@ impl<'db> Signature<'db> { } } - /// Returns `true` if this is a fully static signature. - /// - /// A signature is fully static if all of its parameters and return type are fully static and - /// if it does not use gradual form (`...`) for its parameters. - pub(crate) fn is_fully_static(&self, db: &'db dyn Db) -> bool { - if self.parameters.is_gradual() { - return false; - } - - if self.parameters.iter().any(|parameter| { - parameter - .annotated_type() - .is_none_or(|annotated_type| !annotated_type.is_fully_static(db)) - }) { - return false; - } - - self.return_ty - .is_some_and(|return_type| return_type.is_fully_static(db)) - } - /// Return `true` if `self` has exactly the same set of possible static materializations as /// `other` (if `self` represents the same set of possible sets of possible runtime objects as /// `other`). - pub(crate) fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool { - self.is_equivalent_to_impl(other, |self_type, other_type| { + pub(crate) fn is_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool { + let check_types = |self_type: Option>, other_type: Option>| { self_type .unwrap_or(Type::unknown()) - .is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown())) - }) - } - - /// Return `true` if `self` represents the exact same set of possible runtime objects as `other`. - pub(crate) fn is_equivalent_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool { - self.is_equivalent_to_impl(other, |self_type, other_type| { - match (self_type, other_type) { - (Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type), - // We need the catch-all case here because it's not guaranteed that this is a fully - // static type. - _ => false, - } - }) - } + .is_equivalent_to(db, other_type.unwrap_or(Type::unknown())) + }; - /// Implementation for the [`is_equivalent_to`] and [`is_gradual_equivalent_to`] for signature. - /// - /// [`is_equivalent_to`]: Self::is_equivalent_to - /// [`is_gradual_equivalent_to`]: Self::is_gradual_equivalent_to - fn is_equivalent_to_impl(&self, other: &Signature<'db>, check_types: F) -> bool - where - F: Fn(Option>, Option>) -> bool, - { - // N.B. We don't need to explicitly check for the use of gradual form (`...`) in the - // parameters because it is internally represented by adding `*Any` and `**Any` to the - // parameter list. + if self.parameters.is_gradual() != other.parameters.is_gradual() { + return false; + } if self.parameters.len() != other.parameters.len() { return false; @@ -554,38 +482,13 @@ impl<'db> Signature<'db> { true } - /// Return `true` if a callable with signature `self` is assignable to a callable with - /// signature `other`. - pub(crate) fn is_assignable_to(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool { - self.is_assignable_to_impl(other, |type1, type2| { - // In the context of a callable type, the `None` variant represents an `Unknown` type. - type1 - .unwrap_or(Type::unknown()) - .is_assignable_to(db, type2.unwrap_or(Type::unknown())) - }) - } - - /// Return `true` if a callable with signature `self` is a subtype of a callable with signature - /// `other`. - /// - /// # Panics - /// - /// Panics if `self` or `other` is not a fully static signature. - pub(crate) fn is_subtype_of(&self, db: &'db dyn Db, other: &Signature<'db>) -> bool { - self.is_assignable_to_impl(other, |type1, type2| { - // SAFETY: Subtype relation is only checked for fully static types. - type1.unwrap().is_subtype_of(db, type2.unwrap()) - }) - } - - /// Implementation for the [`is_assignable_to`] and [`is_subtype_of`] for signature. - /// - /// [`is_assignable_to`]: Self::is_assignable_to - /// [`is_subtype_of`]: Self::is_subtype_of - fn is_assignable_to_impl(&self, other: &Signature<'db>, check_types: F) -> bool - where - F: Fn(Option>, Option>) -> bool, - { + /// Implementation of subtyping and assignability for signature. + fn has_relation_to( + &self, + db: &'db dyn Db, + other: &Signature<'db>, + relation: TypeRelation, + ) -> bool { /// A helper struct to zip two slices of parameters together that provides control over the /// two iterators individually. It also keeps track of the current parameter in each /// iterator. @@ -647,17 +550,40 @@ impl<'db> Signature<'db> { } } + let check_types = |type1: Option>, type2: Option>| { + type1.unwrap_or(Type::unknown()).has_relation_to( + db, + type2.unwrap_or(Type::unknown()), + relation, + ) + }; + // Return types are covariant. if !check_types(self.return_ty, other.return_ty) { return false; } - if self.parameters.is_gradual() || other.parameters.is_gradual() { - // If either of the parameter lists contains a gradual form (`...`), then it is - // assignable / subtype to and from any other callable type. + // A gradual parameter list is a supertype of the "bottom" parameter list (*args: object, + // **kwargs: object). + if other.parameters.is_gradual() + && self + .parameters + .variadic() + .is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object(db))) + && self + .parameters + .keyword_variadic() + .is_some_and(|(_, param)| param.annotated_type().is_some_and(|ty| ty.is_object(db))) + { return true; } + // If either of the parameter lists is gradual (`...`), then it is assignable to and from + // any other parameter list, but not a subtype or supertype of any other parameter list. + if self.parameters.is_gradual() || other.parameters.is_gradual() { + return relation.is_assignability(); + } + let mut parameters = ParametersZip { current_self: None, current_other: None, @@ -979,23 +905,36 @@ pub(crate) struct Parameters<'db> { /// Whether this parameter list represents a gradual form using `...` as the only parameter. /// /// If this is `true`, the `value` will still contain the variadic and keyword-variadic - /// parameters. This flag is used to distinguish between an explicit `...` in the callable type - /// as in `Callable[..., int]` and the variadic arguments in `lambda` expression as in - /// `lambda *args, **kwargs: None`. + /// parameters. + /// + /// Per [the typing specification], any signature with a variadic and a keyword-variadic + /// argument, both annotated (explicitly or implicitly) as `Any` or `Unknown`, is considered + /// equivalent to `...`. /// /// The display implementation utilizes this flag to use `...` instead of displaying the /// individual variadic and keyword-variadic parameters. /// - /// Note: This flag is also used to indicate invalid forms of `Callable` annotations. + /// Note: This flag can also result from invalid forms of `Callable` annotations. + /// + /// TODO: the spec also allows signatures like `Concatenate[int, ...]`, which have some number + /// of required positional parameters followed by a gradual form. Our representation will need + /// some adjustments to represent that. + /// + /// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable is_gradual: bool, } impl<'db> Parameters<'db> { pub(crate) fn new(parameters: impl IntoIterator>) -> Self { - Self { - value: parameters.into_iter().collect(), - is_gradual: false, - } + let value: Vec> = parameters.into_iter().collect(); + let is_gradual = value.len() == 2 + && value + .iter() + .any(|p| p.is_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic())) + && value.iter().any(|p| { + p.is_keyword_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic()) + }); + Self { value, is_gradual } } /// Create an empty parameter list. diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 70f4d5f4e7d32..0351f27cd3529 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -73,10 +73,6 @@ impl<'db> SubclassOfType<'db> { subclass_of.is_dynamic() } - pub(crate) const fn is_fully_static(self) -> bool { - !self.is_dynamic() - } - pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> { match self.subclass_of { SubclassOfInner::Dynamic(_) => match variance { @@ -146,9 +142,13 @@ impl<'db> SubclassOfType<'db> { relation: TypeRelation, ) -> bool { match (self.subclass_of, other.subclass_of) { - (SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => { - relation.applies_to_non_fully_static_types() + (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => { + relation.is_assignability() + } + (SubclassOfInner::Dynamic(_), SubclassOfInner::Class(other_class)) => { + other_class.is_object(db) || relation.is_assignability() } + (SubclassOfInner::Class(_), SubclassOfInner::Dynamic(_)) => relation.is_assignability(), // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, // and `type[int]` describes all possible runtime subclasses of the class `int`. diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 018a6e0023718..b26730334b752 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -137,18 +137,10 @@ impl<'db> TupleType<'db> { self.tuple(db).is_equivalent_to(db, other.tuple(db)) } - pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - self.tuple(db).is_gradual_equivalent_to(db, other.tuple(db)) - } - pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool { self.tuple(db).is_disjoint_from(db, other.tuple(db)) } - pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool { - self.tuple(db).is_fully_static(db) - } - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { self.tuple(db).is_single_valued(db) } @@ -292,17 +284,6 @@ impl<'db> FixedLengthTupleSpec<'db> { .all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty)) } - fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { - self.0.len() == other.0.len() - && (self.0.iter()) - .zip(&other.0) - .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) - } - - fn is_fully_static(&self, db: &'db dyn Db) -> bool { - self.0.iter().all(|ty| ty.is_fully_static(db)) - } - fn is_single_valued(&self, db: &'db dyn Db) -> bool { self.0.iter().all(|ty| ty.is_single_valued(db)) } @@ -667,32 +648,6 @@ impl<'db> VariableLengthTupleSpec<'db> { EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false, }) } - - fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &Self) -> bool { - self.variable.is_gradual_equivalent_to(db, other.variable) - && (self.prenormalized_prefix_elements(db, None)) - .zip_longest(other.prenormalized_prefix_elements(db, None)) - .all(|pair| match pair { - EitherOrBoth::Both(self_ty, other_ty) => { - self_ty.is_gradual_equivalent_to(db, other_ty) - } - EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false, - }) - && (self.prenormalized_suffix_elements(db, None)) - .zip_longest(other.prenormalized_suffix_elements(db, None)) - .all(|pair| match pair { - EitherOrBoth::Both(self_ty, other_ty) => { - self_ty.is_gradual_equivalent_to(db, other_ty) - } - EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false, - }) - } - - fn is_fully_static(&self, db: &'db dyn Db) -> bool { - self.variable.is_fully_static(db) - && self.prefix_elements().all(|ty| ty.is_fully_static(db)) - && self.suffix_elements().all(|ty| ty.is_fully_static(db)) - } } impl<'db> PyIndex<'db> for &VariableLengthTupleSpec<'db> { @@ -873,19 +828,6 @@ impl<'db> TupleSpec<'db> { } } - fn is_gradual_equivalent_to(&self, db: &'db dyn Db, other: &TupleSpec<'db>) -> bool { - match (self, other) { - (TupleSpec::Fixed(self_tuple), TupleSpec::Fixed(other_tuple)) => { - self_tuple.is_gradual_equivalent_to(db, other_tuple) - } - (TupleSpec::Variable(self_tuple), TupleSpec::Variable(other_tuple)) => { - self_tuple.is_gradual_equivalent_to(db, other_tuple) - } - (TupleSpec::Fixed(_), TupleSpec::Variable(_)) - | (TupleSpec::Variable(_), TupleSpec::Fixed(_)) => false, - } - } - fn is_disjoint_from(&self, db: &'db dyn Db, other: &Self) -> bool { // Two tuples with an incompatible number of required elements must always be disjoint. let (self_min, self_max) = self.size_hint(); @@ -950,13 +892,6 @@ impl<'db> TupleSpec<'db> { false } - fn is_fully_static(&self, db: &'db dyn Db) -> bool { - match self { - TupleSpec::Fixed(tuple) => tuple.is_fully_static(db), - TupleSpec::Variable(tuple) => tuple.is_fully_static(db), - } - } - fn is_single_valued(&self, db: &'db dyn Db) -> bool { match self { TupleSpec::Fixed(tuple) => tuple.is_single_valued(db), diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index 81838fdacc8f5..158f0dcbf445d 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -31,8 +31,6 @@ def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ... def is_subtype_of(type_derived: Any, type_base: Any) -> bool: ... def is_assignable_to(type_target: Any, type_source: Any) -> bool: ... def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ... -def is_gradual_equivalent_to(type_a: Any, type_b: Any) -> bool: ... -def is_fully_static(type: Any) -> bool: ... def is_singleton(type: Any) -> bool: ... def is_single_valued(type: Any) -> bool: ... diff --git a/ty.schema.json b/ty.schema.json index ff6cd7e943c09..3c896cb48d700 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -293,7 +293,7 @@ }, "conflicting-argument-forms": { "title": "detects when an argument is used as both a value and a type form in a call", - "description": "## What it does\nChecks whether an argument is used as both a value and a type form in a call.\n\n## Why is this bad?\nSuch calls have confusing semantics and often indicate a logic error.\n\n## Examples\n```python\nfrom typing import reveal_type\nfrom ty_extensions import is_fully_static\n\nif flag:\n f = repr # Expects a value\nelse:\n f = is_fully_static # Expects a type form\n\nf(int) # error\n```", + "description": "## What it does\nChecks whether an argument is used as both a value and a type form in a call.\n\n## Why is this bad?\nSuch calls have confusing semantics and often indicate a logic error.\n\n## Examples\n```python\nfrom typing import reveal_type\nfrom ty_extensions import is_singleton\n\nif flag:\n f = repr # Expects a value\nelse:\n f = is_singleton # Expects a type form\n\nf(int) # error\n```", "default": "error", "oneOf": [ {