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 3064be79d6..8d3194ded2 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -129,7 +129,7 @@ specialization. Thus, the typevar is a subtype of itself and of `object`, but no (including other typevars). ```py -from ty_extensions import is_assignable_to, is_subtype_of, static_assert +from ty_extensions import reveal_when_assignable_to, reveal_when_subtype_of class Super: ... class Base(Super): ... @@ -137,23 +137,23 @@ class Sub(Base): ... class Unrelated: ... def unbounded_unconstrained[T, U](t: T, u: U) -> None: - static_assert(is_assignable_to(T, T)) - static_assert(is_assignable_to(T, object)) - static_assert(not is_assignable_to(T, Super)) - static_assert(is_assignable_to(U, U)) - static_assert(is_assignable_to(U, object)) - static_assert(not is_assignable_to(U, Super)) - static_assert(not is_assignable_to(T, U)) - static_assert(not is_assignable_to(U, T)) + reveal_when_assignable_to(T, T) # revealed: always + reveal_when_assignable_to(T, object) # revealed: always + reveal_when_assignable_to(T, Super) # revealed: never + reveal_when_assignable_to(U, U) # revealed: always + reveal_when_assignable_to(U, object) # revealed: always + reveal_when_assignable_to(U, Super) # revealed: never + reveal_when_assignable_to(T, U) # revealed: never + reveal_when_assignable_to(U, T) # revealed: never - static_assert(is_subtype_of(T, T)) - static_assert(is_subtype_of(T, object)) - static_assert(not is_subtype_of(T, Super)) - static_assert(is_subtype_of(U, U)) - static_assert(is_subtype_of(U, object)) - static_assert(not is_subtype_of(U, Super)) - static_assert(not is_subtype_of(T, U)) - static_assert(not is_subtype_of(U, T)) + reveal_when_subtype_of(T, T) # revealed: always + reveal_when_subtype_of(T, object) # revealed: always + reveal_when_subtype_of(T, Super) # revealed: never + reveal_when_subtype_of(U, U) # revealed: always + reveal_when_subtype_of(U, object) # revealed: always + reveal_when_subtype_of(U, Super) # revealed: never + reveal_when_subtype_of(T, U) # revealed: never + reveal_when_subtype_of(U, T) # revealed: never ``` A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of @@ -167,40 +167,40 @@ from typing import Any from typing_extensions import final def bounded[T: Super](t: T) -> None: - static_assert(is_assignable_to(T, Super)) - static_assert(not is_assignable_to(T, Sub)) - static_assert(not is_assignable_to(Super, T)) - static_assert(not is_assignable_to(Sub, T)) + reveal_when_assignable_to(T, Super) # revealed: always + reveal_when_assignable_to(T, Sub) # revealed: never + reveal_when_assignable_to(Super, T) # revealed: never + reveal_when_assignable_to(Sub, T) # revealed: never - static_assert(is_subtype_of(T, Super)) - static_assert(not is_subtype_of(T, Sub)) - static_assert(not is_subtype_of(Super, T)) - static_assert(not is_subtype_of(Sub, T)) + reveal_when_subtype_of(T, Super) # revealed: always + reveal_when_subtype_of(T, Sub) # revealed: never + reveal_when_subtype_of(Super, T) # revealed: never + reveal_when_subtype_of(Sub, T) # revealed: never def bounded_by_gradual[T: Any](t: T) -> None: - static_assert(is_assignable_to(T, Any)) - static_assert(is_assignable_to(Any, T)) - static_assert(is_assignable_to(T, Super)) - static_assert(not is_assignable_to(Super, T)) - static_assert(is_assignable_to(T, Sub)) - static_assert(not is_assignable_to(Sub, T)) + reveal_when_assignable_to(T, Any) # revealed: always + reveal_when_assignable_to(Any, T) # revealed: always + reveal_when_assignable_to(T, Super) # revealed: always + reveal_when_assignable_to(Super, T) # revealed: never + reveal_when_assignable_to(T, Sub) # revealed: always + reveal_when_assignable_to(Sub, T) # revealed: never - static_assert(not is_subtype_of(T, Any)) - static_assert(not is_subtype_of(Any, T)) - static_assert(not is_subtype_of(T, Super)) - static_assert(not is_subtype_of(Super, T)) - static_assert(not is_subtype_of(T, Sub)) - static_assert(not is_subtype_of(Sub, T)) + reveal_when_subtype_of(T, Any) # revealed: never + reveal_when_subtype_of(Any, T) # revealed: never + reveal_when_subtype_of(T, Super) # revealed: never + reveal_when_subtype_of(Super, T) # revealed: never + reveal_when_subtype_of(T, Sub) # revealed: never + reveal_when_subtype_of(Sub, T) # revealed: never @final class FinalClass: ... def bounded_final[T: FinalClass](t: T) -> None: - static_assert(is_assignable_to(T, FinalClass)) - static_assert(not is_assignable_to(FinalClass, T)) + reveal_when_assignable_to(T, FinalClass) # revealed: always + reveal_when_assignable_to(FinalClass, T) # revealed: never - static_assert(is_subtype_of(T, FinalClass)) - static_assert(not is_subtype_of(FinalClass, T)) + reveal_when_subtype_of(T, FinalClass) # revealed: always + reveal_when_subtype_of(FinalClass, T) # revealed: never ``` Two distinct fully static typevars are not subtypes of each other, even if they have the same @@ -210,18 +210,18 @@ typevars to `Never` in addition to that final class. ```py def two_bounded[T: Super, U: Super](t: T, u: U) -> None: - static_assert(not is_assignable_to(T, U)) - static_assert(not is_assignable_to(U, T)) + reveal_when_assignable_to(T, U) # revealed: never + reveal_when_assignable_to(U, T) # revealed: never - static_assert(not is_subtype_of(T, U)) - static_assert(not is_subtype_of(U, T)) + reveal_when_subtype_of(T, U) # revealed: never + reveal_when_subtype_of(U, T) # revealed: never def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None: - static_assert(not is_assignable_to(T, U)) - static_assert(not is_assignable_to(U, T)) + reveal_when_assignable_to(T, U) # revealed: never + reveal_when_assignable_to(U, T) # revealed: never - static_assert(not is_subtype_of(T, U)) - static_assert(not is_subtype_of(U, T)) + reveal_when_subtype_of(T, U) # revealed: never + reveal_when_subtype_of(U, T) # revealed: never ``` A constrained fully static typevar is assignable to the union of its constraints, but not to any of @@ -232,64 +232,64 @@ intersection of all of its constraints is a subtype of the typevar. from ty_extensions import Intersection def constrained[T: (Base, Unrelated)](t: T) -> None: - static_assert(not is_assignable_to(T, Super)) - static_assert(not is_assignable_to(T, Base)) - static_assert(not is_assignable_to(T, Sub)) - static_assert(not is_assignable_to(T, Unrelated)) - static_assert(is_assignable_to(T, Super | Unrelated)) - static_assert(is_assignable_to(T, Base | Unrelated)) - static_assert(not is_assignable_to(T, Sub | Unrelated)) - static_assert(not is_assignable_to(Super, T)) - static_assert(not is_assignable_to(Unrelated, T)) - static_assert(not is_assignable_to(Super | Unrelated, T)) - static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) + reveal_when_assignable_to(T, Super) # revealed: never + reveal_when_assignable_to(T, Base) # revealed: never + reveal_when_assignable_to(T, Sub) # revealed: never + reveal_when_assignable_to(T, Unrelated) # revealed: never + reveal_when_assignable_to(T, Super | Unrelated) # revealed: always + reveal_when_assignable_to(T, Base | Unrelated) # revealed: always + reveal_when_assignable_to(T, Sub | Unrelated) # revealed: never + reveal_when_assignable_to(Super, T) # revealed: never + reveal_when_assignable_to(Unrelated, T) # revealed: never + reveal_when_assignable_to(Super | Unrelated, T) # revealed: never + reveal_when_assignable_to(Intersection[Base, Unrelated], T) # revealed: always - static_assert(not is_subtype_of(T, Super)) - static_assert(not is_subtype_of(T, Base)) - static_assert(not is_subtype_of(T, Sub)) - static_assert(not is_subtype_of(T, Unrelated)) - static_assert(is_subtype_of(T, Super | Unrelated)) - static_assert(is_subtype_of(T, Base | Unrelated)) - static_assert(not is_subtype_of(T, Sub | Unrelated)) - static_assert(not is_subtype_of(Super, T)) - static_assert(not is_subtype_of(Unrelated, T)) - static_assert(not is_subtype_of(Super | Unrelated, T)) - static_assert(is_subtype_of(Intersection[Base, Unrelated], T)) + reveal_when_subtype_of(T, Super) # revealed: never + reveal_when_subtype_of(T, Base) # revealed: never + reveal_when_subtype_of(T, Sub) # revealed: never + reveal_when_subtype_of(T, Unrelated) # revealed: never + reveal_when_subtype_of(T, Super | Unrelated) # revealed: always + reveal_when_subtype_of(T, Base | Unrelated) # revealed: always + reveal_when_subtype_of(T, Sub | Unrelated) # revealed: never + reveal_when_subtype_of(Super, T) # revealed: never + reveal_when_subtype_of(Unrelated, T) # revealed: never + reveal_when_subtype_of(Super | Unrelated, T) # revealed: never + reveal_when_subtype_of(Intersection[Base, Unrelated], T) # revealed: always def constrained_by_gradual[T: (Base, Any)](t: T) -> None: - static_assert(is_assignable_to(T, Super)) - static_assert(is_assignable_to(T, Base)) - static_assert(not is_assignable_to(T, Sub)) - static_assert(not is_assignable_to(T, Unrelated)) - static_assert(is_assignable_to(T, Any)) - static_assert(is_assignable_to(T, Super | Any)) - static_assert(is_assignable_to(T, Super | Unrelated)) - static_assert(not is_assignable_to(Super, T)) - static_assert(is_assignable_to(Base, T)) - static_assert(not is_assignable_to(Unrelated, T)) - static_assert(is_assignable_to(Any, T)) - static_assert(not is_assignable_to(Super | Any, T)) - static_assert(is_assignable_to(Base | Any, T)) - static_assert(not is_assignable_to(Super | Unrelated, T)) - static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) - static_assert(is_assignable_to(Intersection[Base, Any], T)) + reveal_when_assignable_to(T, Super) # revealed: always + reveal_when_assignable_to(T, Base) # revealed: always + reveal_when_assignable_to(T, Sub) # revealed: never + reveal_when_assignable_to(T, Unrelated) # revealed: never + reveal_when_assignable_to(T, Any) # revealed: always + reveal_when_assignable_to(T, Super | Any) # revealed: always + reveal_when_assignable_to(T, Super | Unrelated) # revealed: always + reveal_when_assignable_to(Super, T) # revealed: never + reveal_when_assignable_to(Base, T) # revealed: always + reveal_when_assignable_to(Unrelated, T) # revealed: never + reveal_when_assignable_to(Any, T) # revealed: always + reveal_when_assignable_to(Super | Any, T) # revealed: never + reveal_when_assignable_to(Base | Any, T) # revealed: always + reveal_when_assignable_to(Super | Unrelated, T) # revealed: never + reveal_when_assignable_to(Intersection[Base, Unrelated], T) # revealed: always + reveal_when_assignable_to(Intersection[Base, Any], T) # revealed: always - static_assert(not is_subtype_of(T, Super)) - static_assert(not is_subtype_of(T, Base)) - static_assert(not is_subtype_of(T, Sub)) - static_assert(not is_subtype_of(T, Unrelated)) - static_assert(not is_subtype_of(T, Any)) - static_assert(not is_subtype_of(T, Super | Any)) - static_assert(not is_subtype_of(T, Super | Unrelated)) - static_assert(not is_subtype_of(Super, T)) - static_assert(not is_subtype_of(Base, T)) - static_assert(not is_subtype_of(Unrelated, T)) - static_assert(not is_subtype_of(Any, T)) - static_assert(not is_subtype_of(Super | Any, T)) - static_assert(not is_subtype_of(Base | Any, T)) - static_assert(not is_subtype_of(Super | Unrelated, T)) - static_assert(not is_subtype_of(Intersection[Base, Unrelated], T)) - static_assert(not is_subtype_of(Intersection[Base, Any], T)) + reveal_when_subtype_of(T, Super) # revealed: never + reveal_when_subtype_of(T, Base) # revealed: never + reveal_when_subtype_of(T, Sub) # revealed: never + reveal_when_subtype_of(T, Unrelated) # revealed: never + reveal_when_subtype_of(T, Any) # revealed: never + reveal_when_subtype_of(T, Super | Any) # revealed: never + reveal_when_subtype_of(T, Super | Unrelated) # revealed: never + reveal_when_subtype_of(Super, T) # revealed: never + reveal_when_subtype_of(Base, T) # revealed: never + reveal_when_subtype_of(Unrelated, T) # revealed: never + reveal_when_subtype_of(Any, T) # revealed: never + reveal_when_subtype_of(Super | Any, T) # revealed: never + reveal_when_subtype_of(Base | Any, T) # revealed: never + reveal_when_subtype_of(Super | Unrelated, T) # revealed: never + reveal_when_subtype_of(Intersection[Base, Unrelated], T) # revealed: never + reveal_when_subtype_of(Intersection[Base, Any], T) # revealed: never ``` Two distinct fully static typevars are not subtypes of each other, even if they have the same @@ -299,58 +299,58 @@ the same type. ```py def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None: - static_assert(not is_assignable_to(T, U)) - static_assert(not is_assignable_to(U, T)) + reveal_when_assignable_to(T, U) # revealed: never + reveal_when_assignable_to(U, T) # revealed: never - static_assert(not is_subtype_of(T, U)) - static_assert(not is_subtype_of(U, T)) + reveal_when_subtype_of(T, U) # revealed: never + reveal_when_subtype_of(U, T) # revealed: never @final class AnotherFinalClass: ... def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None: - static_assert(not is_assignable_to(T, U)) - static_assert(not is_assignable_to(U, T)) + reveal_when_assignable_to(T, U) # revealed: never + reveal_when_assignable_to(U, T) # revealed: never - static_assert(not is_subtype_of(T, U)) - static_assert(not is_subtype_of(U, T)) + reveal_when_subtype_of(T, U) # revealed: never + reveal_when_subtype_of(U, T) # revealed: never ``` A bound or constrained typevar is a subtype of itself in a union: ```py def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: - static_assert(is_assignable_to(T, T | None)) - static_assert(is_assignable_to(U, U | None)) + reveal_when_assignable_to(T, T | None) # revealed: always + reveal_when_assignable_to(U, U | None) # revealed: always - static_assert(is_subtype_of(T, T | None)) - static_assert(is_subtype_of(U, U | None)) + reveal_when_subtype_of(T, T | None) # revealed: always + reveal_when_subtype_of(U, U | None) # revealed: always ``` A bound or constrained typevar in a union with a dynamic type is assignable to the typevar: ```py def union_with_dynamic[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: - static_assert(is_assignable_to(T | Any, T)) - static_assert(is_assignable_to(U | Any, U)) + reveal_when_assignable_to(T | Any, T) # revealed: always + reveal_when_assignable_to(U | Any, U) # revealed: always - static_assert(not is_subtype_of(T | Any, T)) - static_assert(not is_subtype_of(U | Any, U)) + reveal_when_subtype_of(T | Any, T) # revealed: never + reveal_when_subtype_of(U | Any, U) # revealed: never ``` And an intersection of a typevar with another type is always a subtype of the TypeVar: ```py -from ty_extensions import Intersection, Not, is_disjoint_from +from ty_extensions import Intersection, Not, is_disjoint_from, static_assert class A: ... def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: - static_assert(is_assignable_to(Intersection[T, Unrelated], T)) - static_assert(is_subtype_of(Intersection[T, Unrelated], T)) + reveal_when_assignable_to(Intersection[T, Unrelated], T) # revealed: always + reveal_when_subtype_of(Intersection[T, Unrelated], T) # revealed: always - static_assert(is_assignable_to(Intersection[U, A], U)) - static_assert(is_subtype_of(Intersection[U, A], U)) + reveal_when_assignable_to(Intersection[U, A], U) # revealed: always + reveal_when_subtype_of(Intersection[U, A], U) # revealed: always static_assert(is_disjoint_from(Not[T], T)) static_assert(is_disjoint_from(T, Not[T])) @@ -647,14 +647,14 @@ The intersection of a typevar with any other type is assignable to (and if fully of) itself. ```py -from ty_extensions import is_assignable_to, is_subtype_of, static_assert, Not +from ty_extensions import reveal_when_assignable_to, reveal_when_subtype_of, Not def intersection_is_assignable[T](t: T) -> None: - static_assert(is_assignable_to(Intersection[T, None], T)) - static_assert(is_assignable_to(Intersection[T, Not[None]], T)) + reveal_when_assignable_to(Intersection[T, None], T) # revealed: always + reveal_when_assignable_to(Intersection[T, Not[None]], T) # revealed: always - static_assert(is_subtype_of(Intersection[T, None], T)) - static_assert(is_subtype_of(Intersection[T, Not[None]], T)) + reveal_when_subtype_of(Intersection[T, None], T) # revealed: always + reveal_when_subtype_of(Intersection[T, Not[None]], T) # revealed: always ``` ## Narrowing diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index b94f2277a9..7e4e710f09 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -413,13 +413,13 @@ To see the kinds and types of the protocol members, you can use the debugging ai from ty_extensions import reveal_protocol_interface from typing import SupportsIndex, SupportsAbs, ClassVar, Iterator -# error: [revealed-type] "Revealed protocol interface: `{"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }}`" +# revealed: {"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }} reveal_protocol_interface(Foo) -# error: [revealed-type] "Revealed protocol interface: `{"__index__": MethodMember(`(self) -> int`)}`" +# revealed: {"__index__": MethodMember(`(self) -> int`)} reveal_protocol_interface(SupportsIndex) -# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> Unknown`)}`" +# revealed: {"__abs__": MethodMember(`(self) -> Unknown`)} reveal_protocol_interface(SupportsAbs) -# error: [revealed-type] "Revealed protocol interface: `{"__iter__": MethodMember(`(self) -> Iterator[Unknown]`), "__next__": MethodMember(`(self) -> Unknown`)}`" +# revealed: {"__iter__": MethodMember(`(self) -> Iterator[Unknown]`), "__next__": MethodMember(`(self) -> Unknown`)} reveal_protocol_interface(Iterator) # error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`" @@ -439,9 +439,9 @@ do not implement any special handling for generic aliases passed to the function reveal_type(get_protocol_members(SupportsAbs[int])) # revealed: frozenset[str] reveal_type(get_protocol_members(Iterator[int])) # revealed: frozenset[str] -# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> int`)}`" +# revealed: {"__abs__": MethodMember(`(self) -> int`)} reveal_protocol_interface(SupportsAbs[int]) -# error: [revealed-type] "Revealed protocol interface: `{"__iter__": MethodMember(`(self) -> Iterator[int]`), "__next__": MethodMember(`(self) -> int`)}`" +# revealed: {"__iter__": MethodMember(`(self) -> Iterator[int]`), "__next__": MethodMember(`(self) -> int`)} reveal_protocol_interface(Iterator[int]) class BaseProto(Protocol): @@ -450,16 +450,16 @@ class BaseProto(Protocol): class SubProto(BaseProto, Protocol): def member(self) -> bool: ... -# error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> int`)}`" +# revealed: {"member": MethodMember(`(self) -> int`)} reveal_protocol_interface(BaseProto) -# error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> bool`)}`" +# revealed: {"member": MethodMember(`(self) -> bool`)} reveal_protocol_interface(SubProto) class ProtoWithClassVar(Protocol): x: ClassVar[int] -# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`" +# revealed: {"x": AttributeMember(`int`; ClassVar)} reveal_protocol_interface(ProtoWithClassVar) class ProtocolWithDefault(Protocol): @@ -468,7 +468,7 @@ class ProtocolWithDefault(Protocol): # We used to incorrectly report this as having an `x: Literal[0]` member; # declared types should take priority over inferred types for protocol interfaces! # -# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`)}`" +# revealed: {"x": AttributeMember(`int`)} reveal_protocol_interface(ProtocolWithDefault) ``` @@ -2474,7 +2474,7 @@ class Foo(Protocol): from stub import Foo from ty_extensions import reveal_protocol_interface -# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`" +# revealed: {"x": AttributeMember(`int`; ClassVar)} reveal_protocol_interface(Foo) ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6f62b815db..ece4c38ab1 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4168,6 +4168,24 @@ impl<'db> Type<'db> { ) .into(), + Some( + KnownFunction::RevealWhenAssignableTo | KnownFunction::RevealWhenSubtypeOf, + ) => Binding::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("a"))) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("b"))) + .type_form() + .with_annotated_type(Type::any()), + ]), + Some(KnownClass::NoneType.to_instance(db)), + ), + ) + .into(), + Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => { Binding::single( self, diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 77c968b535..0ee28c89e8 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -133,9 +133,6 @@ pub(crate) trait Constraints<'db>: Clone + Sized { } // This is here so that we can easily print constraint sets when debugging. - // TODO: Add a ty_extensions function to reveal constraint sets so that this is no longer dead - // code, and so that we verify the contents of our rendering. - #[expect(dead_code)] fn display(&self, db: &'db dyn Db) -> impl Display; } @@ -345,34 +342,6 @@ impl<'db> ConstraintSet<'db> { } } } - - // This is here so that we can easily print constraint sets when debugging. - // TODO: Add a ty_extensions function to reveal constraint sets so that this is no longer dead - // code, and so that we verify the contents of our rendering. - #[expect(dead_code)] - pub(crate) fn display(&self, db: &'db dyn Db) -> impl Display { - struct DisplayConstraintSet<'a, 'db> { - set: &'a ConstraintSet<'db>, - db: &'db dyn Db, - } - - impl Display for DisplayConstraintSet<'_, '_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.set.clauses.is_empty() { - return f.write_str("0"); - } - for (i, clause) in self.set.clauses.iter().enumerate() { - if i > 0 { - f.write_str(" ∨ ")?; - } - clause.display(self.db).fmt(f)?; - } - Ok(()) - } - } - - DisplayConstraintSet { set: self, db } - } } impl<'db> Constraints<'db> for ConstraintSet<'db> { @@ -411,7 +380,27 @@ impl<'db> Constraints<'db> for ConstraintSet<'db> { } fn display(&self, db: &'db dyn Db) -> impl Display { - self.display(db) + struct DisplayConstraintSet<'a, 'db> { + set: &'a ConstraintSet<'db>, + db: &'db dyn Db, + } + + impl Display for DisplayConstraintSet<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.set.clauses.is_empty() { + return f.write_str("0"); + } + for (i, clause) in self.set.clauses.iter().enumerate() { + if i > 0 { + f.write_str(" ∨ ")?; + } + clause.display(self.db).fmt(f)?; + } + Ok(()) + } + } + + DisplayConstraintSet { set: self, db } } } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index a6357d145e..d91fa42f4e 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -65,7 +65,7 @@ use crate::semantic_index::definition::Definition; use crate::semantic_index::scope::ScopeId; use crate::semantic_index::semantic_index; use crate::types::call::{Binding, CallArguments}; -use crate::types::constraints::Constraints; +use crate::types::constraints::{ConstraintSet, Constraints}; use crate::types::context::InferContext; use crate::types::diagnostic::{ INVALID_ARGUMENT_TYPE, REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE, @@ -1188,6 +1188,10 @@ pub enum KnownFunction { HasMember, /// `ty_extensions.reveal_protocol_interface` RevealProtocolInterface, + /// `ty_extensions.reveal_when_assignable_to` + RevealWhenAssignableTo, + /// `ty_extensions.reveal_when_subtype_of` + RevealWhenSubtypeOf, } impl KnownFunction { @@ -1253,6 +1257,8 @@ impl KnownFunction { | Self::StaticAssert | Self::HasMember | Self::RevealProtocolInterface + | Self::RevealWhenAssignableTo + | Self::RevealWhenSubtypeOf | Self::AllMembers => module.is_ty_extensions(), Self::ImportModule => module.is_importlib(), } @@ -1548,6 +1554,54 @@ impl KnownFunction { overload.set_return_type(Type::module_literal(db, file, module)); } + KnownFunction::RevealWhenAssignableTo => { + let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else { + return; + }; + let constraints = ty_a.when_assignable_to::(db, *ty_b); + let Some(builder) = + context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info) + else { + return; + }; + let mut diag = builder.into_diagnostic("Assignability holds"); + let span = context.span(call_expression); + if constraints.is_always_satisfied(db) { + diag.annotate(Annotation::primary(span).message("always")); + } else if constraints.is_never_satisfied(db) { + diag.annotate(Annotation::primary(span).message("never")); + } else { + diag.annotate( + Annotation::primary(span) + .message(format_args!("when {}", constraints.display(db))), + ); + } + } + + KnownFunction::RevealWhenSubtypeOf => { + let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else { + return; + }; + let constraints = ty_a.when_subtype_of::(db, *ty_b); + let Some(builder) = + context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info) + else { + return; + }; + let mut diag = builder.into_diagnostic("Subtyping holds"); + let span = context.span(call_expression); + if constraints.is_always_satisfied(db) { + diag.annotate(Annotation::primary(span).message("always")); + } else if constraints.is_never_satisfied(db) { + diag.annotate(Annotation::primary(span).message("never")); + } else { + diag.annotate( + Annotation::primary(span) + .message(format_args!("when {}", constraints.display(db))), + ); + } + } + _ => {} } } @@ -1608,6 +1662,8 @@ pub(crate) mod tests { | KnownFunction::IsEquivalentTo | KnownFunction::HasMember | KnownFunction::RevealProtocolInterface + | KnownFunction::RevealWhenAssignableTo + | KnownFunction::RevealWhenSubtypeOf | KnownFunction::AllMembers => KnownModule::TyExtensions, KnownFunction::ImportModule => KnownModule::ImportLib, diff --git a/crates/ty_test/src/matcher.rs b/crates/ty_test/src/matcher.rs index 3d574242bd..39fe8633ca 100644 --- a/crates/ty_test/src/matcher.rs +++ b/crates/ty_test/src/matcher.rs @@ -1,15 +1,19 @@ //! Match [`Diagnostic`]s against assertions and produce test failure //! messages for any mismatches. -use crate::assertion::{InlineFileAssertions, ParsedAssertion, UnparsedAssertion}; -use crate::db::Db; -use crate::diagnostic::SortedDiagnostics; + +use std::borrow::Cow; +use std::cmp::Ordering; +use std::ops::Range; + use colored::Colorize; use ruff_db::diagnostic::{Diagnostic, DiagnosticId}; use ruff_db::files::File; use ruff_db::source::{SourceText, line_index, source_text}; use ruff_source_file::{LineIndex, OneIndexed}; -use std::cmp::Ordering; -use std::ops::Range; + +use crate::assertion::{InlineFileAssertions, ParsedAssertion, UnparsedAssertion}; +use crate::db::Db; +use crate::diagnostic::SortedDiagnostics; #[derive(Debug, Default)] pub(super) struct FailuresByLine { @@ -194,12 +198,17 @@ impl UnmatchedWithColumn for &Diagnostic { /// Discard `@Todo`-type metadata from expected types, which is not available /// when running in release mode. -#[cfg(not(debug_assertions))] -fn discard_todo_metadata(ty: &str) -> std::borrow::Cow<'_, str> { - static TODO_METADATA_REGEX: std::sync::LazyLock = - std::sync::LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap()); +fn discard_todo_metadata(ty: &str) -> Cow<'_, str> { + #[cfg(not(debug_assertions))] + { + static TODO_METADATA_REGEX: std::sync::LazyLock = + std::sync::LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap()); - TODO_METADATA_REGEX.replace_all(ty, "@Todo") + TODO_METADATA_REGEX.replace_all(ty, "@Todo") + } + + #[cfg(debug_assertions)] + Cow::Borrowed(ty) } struct Matcher { @@ -297,21 +306,53 @@ impl Matcher { } } ParsedAssertion::Revealed(expected_type) => { - #[cfg(not(debug_assertions))] - let expected_type = discard_todo_metadata(&expected_type); + let expected_type = discard_todo_metadata(expected_type); + let expected_reveal_type_message = format!("`{expected_type}`"); + + let diagnostic_matches_reveal = |diagnostic: &Diagnostic| { + if diagnostic.id() != DiagnosticId::RevealedType { + return false; + } + let primary_message = diagnostic.primary_message(); + let Some(primary_annotation) = + (diagnostic.primary_annotation()).and_then(|a| a.get_message()) + else { + return false; + }; + + // reveal_type + if primary_message == "Revealed type" + && primary_annotation == expected_reveal_type_message + { + return true; + } + + // reveal_protocol_interface + if primary_message == "Revealed protocol interface" + && primary_annotation == expected_reveal_type_message + { + return true; + } + + // reveal_when_assignable_to + if primary_message == "Assignability holds" + && primary_annotation == expected_type + { + return true; + } + + // reveal_when_subtype_of + if primary_message == "Subtyping holds" && primary_annotation == expected_type { + return true; + } + + false + }; let mut matched_revealed_type = None; let mut matched_undefined_reveal = None; - let expected_reveal_type_message = format!("`{expected_type}`"); for (index, diagnostic) in unmatched.iter().enumerate() { - if matched_revealed_type.is_none() - && diagnostic.id() == DiagnosticId::RevealedType - && diagnostic - .primary_annotation() - .and_then(|a| a.get_message()) - .unwrap_or_default() - == expected_reveal_type_message - { + if matched_revealed_type.is_none() && diagnostic_matches_reveal(diagnostic) { matched_revealed_type = Some(index); } else if matched_undefined_reveal.is_none() && diagnostic.id().is_lint_named("undefined-reveal") diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index 9252693402..41da401099 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -45,15 +45,21 @@ type JustComplex = TypeOf[1.0j] # Ideally, these would be annotated using `TypeForm`, but that has not been # standardized yet (https://peps.python.org/pep-0747). 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_subtype_of(type_a: Any, type_b: Any) -> bool: ... +def is_assignable_to(type_a: Any, type_b: Any) -> bool: ... def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ... -def is_singleton(type: Any) -> bool: ... -def is_single_valued(type: Any) -> bool: ... +def is_singleton(ty: Any) -> bool: ... +def is_single_valued(ty: Any) -> bool: ... + +# These are the same as above, but instead of returning _whether_ the property +# holds, it shows a diagnostic that describes under what constraints the +# property holds. +def reveal_when_assignable_to(type_a: Any, type_b: Any) -> None: ... +def reveal_when_subtype_of(type_a: Any, type_b: Any) -> None: ... # Returns the generic context of a type as a tuple of typevars, or `None` if the # type is not generic. -def generic_context(type: Any) -> Any: ... +def generic_context(ty: Any) -> Any: ... # Returns the `__all__` names of a module as a tuple of sorted strings, or `None` if # either the module does not have `__all__` or it has invalid elements.