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 ae6631216c..558c455c4e 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 @@ -334,4 +334,30 @@ 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 are always considered self-equivalent, even if they +have unannotated parameters, or parameters with not-fully-static annotations. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from ty_extensions import is_equivalent_to, TypeOf, static_assert + +def f(): ... + +static_assert(is_equivalent_to(TypeOf[f], TypeOf[f])) + +class A: + def method(self) -> int: + return 42 + +static_assert(is_equivalent_to(TypeOf[A.method], TypeOf[A.method])) +type X = TypeOf[A.method] +static_assert(is_equivalent_to(X, X)) +``` + [the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 2823a00022..6533cec7f2 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7146,10 +7146,11 @@ impl<'db> FunctionType<'db> { // However, our representation of a function literal includes any specialization that // should be applied to the signature. Different specializations of the same function // literal are only subtypes of each other if they result in subtype signatures. - self.body_scope(db) == other.body_scope(db) - && self - .into_callable_type(db) - .is_subtype_of(db, other.into_callable_type(db)) + self.normalized(db) == other.normalized(db) + || (self.body_scope(db) == other.body_scope(db) + && self + .into_callable_type(db) + .is_subtype_of(db, other.into_callable_type(db))) } fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool { @@ -7164,10 +7165,11 @@ impl<'db> FunctionType<'db> { } fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { - self.body_scope(db) == other.body_scope(db) - && self - .into_callable_type(db) - .is_equivalent_to(db, other.into_callable_type(db)) + self.normalized(db) == other.normalized(db) + || (self.body_scope(db) == other.body_scope(db) + && self + .into_callable_type(db) + .is_equivalent_to(db, other.into_callable_type(db))) } fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 35d86f5b1e..2b68e7c731 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -302,8 +302,8 @@ impl<'db> Signature<'db> { pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self { Self { - generic_context: self.generic_context, - inherited_generic_context: self.inherited_generic_context, + generic_context: self.generic_context.map(|ctx| ctx.normalized(db)), + inherited_generic_context: self.inherited_generic_context.map(|ctx| ctx.normalized(db)), parameters: self .parameters .iter()