From 3be83d36a56bc7eca18d7068d638e0acae7db208 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Fri, 4 Jul 2025 03:04:03 +0100 Subject: [PATCH] [ty] Add into_callable method for Type (#19130) ## Summary Was just playing around with this, there's definitely more to do with this function, but it seems like maybe a better option than having so many arms in has_relation_to for (_, Callable). --------- Co-authored-by: Alex Waygood Co-authored-by: Carl Meyer --- crates/ty_python_semantic/src/types.rs | 124 +++++++++++++++---------- 1 file changed, 74 insertions(+), 50 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index b68489a78b..4ebc60b768 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1095,6 +1095,74 @@ impl<'db> Type<'db> { } } + pub(crate) fn into_callable(self, db: &'db dyn Db) -> Option> { + match self { + Type::Callable(_) => Some(self), + + Type::Dynamic(_) => Some(CallableType::single(db, Signature::dynamic(self))), + + Type::FunctionLiteral(function_literal) => { + Some(function_literal.into_callable_type(db)) + } + Type::BoundMethod(bound_method) => Some(bound_method.into_callable_type(db)), + + Type::NominalInstance(_) | Type::ProtocolInstance(_) => { + let call_symbol = self + .member_lookup_with_policy( + db, + Name::new_static("__call__"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place; + + if let Place::Type(ty, Boundness::Bound) = call_symbol { + ty.into_callable(db) + } else { + None + } + } + Type::ClassLiteral(class_literal) => { + Some(ClassType::NonGeneric(class_literal).into_callable(db)) + } + + Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)), + + // TODO: This is unsound so in future we can consider an opt-in option to disable it. + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { + SubclassOfInner::Class(class) => Some(class.into_callable(db)), + SubclassOfInner::Dynamic(dynamic) => Some(CallableType::single( + db, + Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))), + )), + }, + + Type::Union(union) => union.try_map(db, |element| element.into_callable(db)), + + Type::Never + | Type::DataclassTransformer(_) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::IntLiteral(_) + | Type::BooleanLiteral(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::BytesLiteral(_) + | Type::Tuple(_) + | Type::TypeIs(_) => None, + + // TODO + Type::MethodWrapper(_) + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::ModuleLiteral(_) + | Type::SpecialForm(_) + | Type::KnownInstance(_) + | Type::PropertyInstance(_) + | Type::Intersection(_) + | Type::TypeVar(_) + | Type::BoundSuper(_) => None, + } + } /// Return true if this type is a [subtype of] type `target`. /// /// For fully static types, this means that the set of objects represented by `self` is a @@ -1305,24 +1373,14 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(_), ) => false, - (Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => { - let call_symbol = self - .member_lookup_with_policy( - db, - Name::new_static("__call__"), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place; - // If the type of __call__ is a subtype of a callable type, this instance is. - // Don't add other special cases here; our subtyping of a callable type - // shouldn't get out of sync with the calls we will actually allow. - if let Place::Type(t, Boundness::Bound) = call_symbol { - t.has_relation_to(db, target, relation) - } else { - false - } + (Type::Callable(self_callable), Type::Callable(other_callable)) => { + self_callable.has_relation_to(db, other_callable, relation) } + (_, Type::Callable(_)) => self + .into_callable(db) + .is_some_and(|callable| callable.has_relation_to(db, target, relation)), + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { left.has_relation_to(db, right, relation) } @@ -1349,16 +1407,6 @@ impl<'db> Type<'db> { ) => (self.literal_fallback_instance(db)) .is_some_and(|instance| instance.has_relation_to(db, target, relation)), - (Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => { - self_function_literal - .into_callable_type(db) - .has_relation_to(db, target, relation) - } - - (Type::BoundMethod(self_bound_method), Type::Callable(_)) => self_bound_method - .into_callable_type(db) - .has_relation_to(db, target, relation), - // A `FunctionLiteral` type is a single-valued type like the other literals handled above, // so it also, for now, just delegates to its instance fallback. (Type::FunctionLiteral(_), _) => KnownClass::FunctionType @@ -1376,10 +1424,6 @@ impl<'db> Type<'db> { .to_instance(db) .has_relation_to(db, target, relation), - (Type::Callable(self_callable), Type::Callable(other_callable)) => { - self_callable.has_relation_to(db, other_callable, relation) - } - (Type::DataclassDecorator(_) | Type::DataclassTransformer(_), _) => { // TODO: Implement subtyping using an equivalent `Callable` type. false @@ -1456,26 +1500,6 @@ impl<'db> Type<'db> { self_subclass_ty.has_relation_to(db, target_subclass_ty, relation) } - (Type::ClassLiteral(class_literal), Type::Callable(_)) => { - ClassType::NonGeneric(class_literal) - .into_callable(db) - .has_relation_to(db, target, relation) - } - - (Type::GenericAlias(alias), Type::Callable(_)) => ClassType::Generic(alias) - .into_callable(db) - .has_relation_to(db, target, relation), - - // TODO: This is unsound so in future we can consider an opt-in option to disable it. - (Type::SubclassOf(subclass_of_ty), Type::Callable(_)) - if subclass_of_ty.subclass_of().into_class().is_some() => - { - let class = subclass_of_ty.subclass_of().into_class().unwrap(); - class - .into_callable(db) - .has_relation_to(db, target, relation) - } - // `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`. // `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object // is an instance of its metaclass `abc.ABCMeta`.