From 296d3e6628f35c6388aeaf376d36eca8c8432bf1 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 21 Nov 2025 10:31:37 -0500 Subject: [PATCH 01/13] upcast into CallableType not Type --- .../reachability_constraints.rs | 2 +- crates/ty_python_semantic/src/types.rs | 169 +++++++++++++----- crates/ty_python_semantic/src/types/class.rs | 38 ++-- .../src/types/ide_support.rs | 8 +- .../src/types/infer/builder.rs | 1 + .../types/infer/builder/type_expression.rs | 3 +- .../src/types/protocol_class.rs | 3 +- 7 files changed, 151 insertions(+), 73 deletions(-) diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index 9e6d60668f2cd..d1f6451e176bf 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -872,7 +872,7 @@ impl ReachabilityConstraints { } let overloads_iterator = - if let Some(Type::Callable(callable)) = ty.try_upcast_to_callable(db) { + if let Some(callable) = ty.try_upcast_to_callable(db).exactly_one() { callable.signatures(db).overloads.iter() } else { return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive); diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 5447ec84a6dd3..1a7efcecdbed2 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -16,6 +16,7 @@ use ruff_db::files::File; use ruff_python_ast as ast; use ruff_python_ast::name::Name; use ruff_text_size::{Ranged, TextRange}; +use smallvec::{SmallVec, smallvec}; use type_ordering::union_or_intersection_elements_ordering; @@ -1524,17 +1525,20 @@ impl<'db> Type<'db> { } } - pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option> { + pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { match self { - Type::Callable(_) => Some(self), + Type::Callable(callable) => CallableTypes::one(callable), - Type::Dynamic(_) => Some(CallableType::function_like(db, Signature::dynamic(self))), + Type::Dynamic(_) => CallableTypes::one(CallableType::function_like_callable( + db, + Signature::dynamic(self), + )), Type::FunctionLiteral(function_literal) => { - Some(Type::Callable(function_literal.into_callable_type(db))) + CallableTypes::one(function_literal.into_callable_type(db)) } Type::BoundMethod(bound_method) => { - Some(Type::Callable(bound_method.into_callable_type(db))) + CallableTypes::one(bound_method.into_callable_type(db)) } Type::NominalInstance(_) | Type::ProtocolInstance(_) => { @@ -1549,14 +1553,14 @@ impl<'db> Type<'db> { if let Place::Defined(ty, _, Definedness::AlwaysDefined) = call_symbol { ty.try_upcast_to_callable(db) } else { - None + CallableTypes::none() } } Type::ClassLiteral(class_literal) => { - Some(ClassType::NonGeneric(class_literal).into_callable(db)) + ClassType::NonGeneric(class_literal).into_callable(db) } - Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)), + Type::GenericAlias(alias) => ClassType::Generic(alias).into_callable(db), Type::NewTypeInstance(newtype) => { Type::instance(db, newtype.base_class_type(db)).try_upcast_to_callable(db) @@ -1564,14 +1568,32 @@ impl<'db> Type<'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))), - )), + SubclassOfInner::Class(class) => class.into_callable(db), + SubclassOfInner::Dynamic(dynamic) => { + CallableTypes::one(CallableType::single_callable( + db, + Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))), + )) + } }, - Type::Union(union) => union.try_map(db, |element| element.try_upcast_to_callable(db)), + Type::Union(union) => { + let mut any_not_callable = false; + let callables: SmallVec<_> = union + .elements(db) + .iter() + .flat_map(|element| { + let element_callable = element.try_upcast_to_callable(db); + any_not_callable |= element_callable.is_empty(); + element_callable.into_inner().into_iter() + }) + .collect(); + if any_not_callable { + CallableTypes::none() + } else { + CallableTypes(callables) + } + } Type::EnumLiteral(enum_literal) => enum_literal .enum_class_instance(db) @@ -1579,27 +1601,29 @@ impl<'db> Type<'db> { Type::TypeAlias(alias) => alias.value_type(db).try_upcast_to_callable(db), - Type::KnownBoundMethod(method) => Some(Type::Callable(CallableType::new( + Type::KnownBoundMethod(method) => CallableTypes::one(CallableType::new( db, CallableSignature::from_overloads(method.signatures(db)), false, - ))), + )), - Type::WrapperDescriptor(wrapper_descriptor) => Some(Type::Callable(CallableType::new( + Type::WrapperDescriptor(wrapper_descriptor) => CallableTypes::one(CallableType::new( db, CallableSignature::from_overloads(wrapper_descriptor.signatures(db)), false, - ))), - - Type::KnownInstance(KnownInstanceType::NewType(newtype)) => Some(CallableType::single( - db, - Signature::new( - Parameters::new([Parameter::positional_only(None) - .with_annotated_type(newtype.base(db).instance_type(db))]), - Some(Type::NewTypeInstance(newtype)), - ), )), + Type::KnownInstance(KnownInstanceType::NewType(newtype)) => { + CallableTypes::one(CallableType::single_callable( + db, + Signature::new( + Parameters::new([Parameter::positional_only(None) + .with_annotated_type(newtype.base(db).instance_type(db))]), + Some(Type::NewTypeInstance(newtype)), + ), + )) + } + Type::Never | Type::DataclassTransformer(_) | Type::AlwaysTruthy @@ -1610,7 +1634,7 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(_) | Type::TypeIs(_) - | Type::TypedDict(_) => None, + | Type::TypedDict(_) => CallableTypes::none(), // TODO Type::DataclassDecorator(_) @@ -1620,7 +1644,7 @@ impl<'db> Type<'db> { | Type::PropertyInstance(_) | Type::Intersection(_) | Type::TypeVar(_) - | Type::BoundSuper(_) => None, + | Type::BoundSuper(_) => CallableTypes::none(), } } @@ -2175,16 +2199,18 @@ impl<'db> Type<'db> { }), (_, Type::Callable(_)) => relation_visitor.visit((self, target, relation), || { - self.try_upcast_to_callable(db).when_some_and(|callable| { - callable.has_relation_to_impl( - db, - target, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) + self.try_upcast_to_callable(db) + .into_type(db) + .when_some_and(|callable| { + callable.has_relation_to_impl( + db, + target, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) }), (_, Type::ProtocolInstance(protocol)) => { @@ -10950,22 +10976,25 @@ impl get_size2::GetSize for CallableType<'_> {} impl<'db> CallableType<'db> { /// Create a callable type with a single non-overloaded signature. pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { - Type::Callable(CallableType::new( - db, - CallableSignature::single(signature), - false, - )) + Type::Callable(Self::single_callable(db, signature)) + } + + pub(crate) fn single_callable(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { + CallableType::new(db, CallableSignature::single(signature), false) } /// Create a non-overloaded, function-like callable type with a single signature. /// /// A function-like callable will bind `self` when accessed as an attribute on an instance. pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { - Type::Callable(CallableType::new( - db, - CallableSignature::single(signature), - true, - )) + Type::Callable(Self::function_like_callable(db, signature)) + } + + pub(crate) fn function_like_callable( + db: &'db dyn Db, + signature: Signature<'db>, + ) -> CallableType<'db> { + CallableType::new(db, CallableSignature::single(signature), true) } /// Create a callable type which accepts any parameters and returns an `Unknown` type. @@ -11068,6 +11097,50 @@ impl<'db> CallableType<'db> { } } +/// Converting a type "into a callable" can possibly return a _union_ of callables. Eventually, +/// when coercing that result to a single type, you'll get a `UnionType`. But this lets you handle +/// that result as a list of `CallableType`s before merging them into a `UnionType` should that be +/// helpful. +#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update)] +pub(crate) struct CallableTypes<'db>(SmallVec<[CallableType<'db>; 1]>); + +impl<'db> CallableTypes<'db> { + pub(crate) fn none() -> Self { + CallableTypes(smallvec![]) + } + + pub(crate) fn one(callable: CallableType<'db>) -> Self { + CallableTypes(smallvec![callable]) + } + + pub(crate) fn from_elements(callables: impl IntoIterator>) -> Self { + CallableTypes(callables.into_iter().collect()) + } + + pub(crate) fn exactly_one(self) -> Option> { + self.0.into_iter().exactly_one().ok() + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> { + self.0 + } + + pub(crate) fn into_type(self, db: &'db dyn Db) -> Option> { + match self.0.as_slice() { + [] => None, + [single] => Some(Type::Callable(*single)), + slice => Some(UnionType::from_elements( + db, + slice.iter().copied().map(Type::Callable), + )), + } + } +} + /// Represents a specific instance of a bound method type for a builtin class. /// /// Unlike bound methods of user-defined classes, these are not generally instances diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index fc3b9d4227aad..435d361455eee 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -31,7 +31,7 @@ use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::typed_dict::typed_dict_params_from_class_def; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ - ApplyTypeMappingVisitor, Binding, BoundSuperType, CallableType, DATACLASS_FLAGS, + ApplyTypeMappingVisitor, Binding, BoundSuperType, CallableType, CallableTypes, DATACLASS_FLAGS, DataclassFlags, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType, @@ -1051,7 +1051,7 @@ impl<'db> ClassType<'db> { /// Return a callable type (or union of callable types) that represents the callable /// constructor signature of this class. #[salsa::tracked(cycle_initial=into_callable_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(super) fn into_callable(self, db: &'db dyn Db) -> Type<'db> { + pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { let self_ty = Type::from(self); let metaclass_dunder_call_function_symbol = self_ty .member_lookup_with_policy( @@ -1069,7 +1069,7 @@ impl<'db> ClassType<'db> { // https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable // by always respecting the signature of the metaclass `__call__`, rather than // using a heuristic which makes unwarranted assumptions to sometimes ignore it. - return Type::Callable(metaclass_dunder_call_function.into_callable_type(db)); + return CallableTypes::one(metaclass_dunder_call_function.into_callable_type(db)); } let dunder_new_function_symbol = self_ty.lookup_dunder_new(db); @@ -1097,14 +1097,14 @@ impl<'db> ClassType<'db> { }); let instance_ty = Type::instance(db, self); - let dunder_new_bound_method = Type::Callable(CallableType::new( + let dunder_new_bound_method = CallableType::new( db, dunder_new_signature.bind_self(db, Some(instance_ty)), true, - )); + ); if returns_non_subclass { - return dunder_new_bound_method; + return CallableTypes::one(dunder_new_bound_method); } Some(dunder_new_bound_method) } else { @@ -1147,11 +1147,11 @@ impl<'db> ClassType<'db> { signature.overloads.iter().map(synthesized_signature), ); - Some(Type::Callable(CallableType::new( + Some(CallableType::new( db, synthesized_dunder_init_signature, true, - ))) + )) } else { None } @@ -1161,12 +1161,14 @@ impl<'db> ClassType<'db> { match (dunder_new_function, synthesized_dunder_init_callable) { (Some(dunder_new_function), Some(synthesized_dunder_init_callable)) => { - UnionType::from_elements( - db, - vec![dunder_new_function, synthesized_dunder_init_callable], - ) + CallableTypes::from_elements([ + dunder_new_function, + synthesized_dunder_init_callable, + ]) + } + (Some(constructor), None) | (None, Some(constructor)) => { + CallableTypes::one(constructor) } - (Some(constructor), None) | (None, Some(constructor)) => constructor, (None, None) => { // If no `__new__` or `__init__` method is found, then we fall back to looking for // an `object.__new__` method. @@ -1181,17 +1183,17 @@ impl<'db> ClassType<'db> { if let Place::Defined(Type::FunctionLiteral(new_function), _, _) = new_function_symbol { - Type::Callable( + CallableTypes::one( new_function .into_bound_method_type(db, correct_return_type) .into_callable_type(db), ) } else { // Fallback if no `object.__new__` is found. - CallableType::single( + CallableTypes::one(CallableType::single_callable( db, Signature::new(Parameters::empty(), Some(correct_return_type)), - ) + )) } } } @@ -1210,8 +1212,8 @@ fn into_callable_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, _self: ClassType<'db>, -) -> Type<'db> { - Type::Never +) -> CallableTypes<'db> { + CallableTypes::none() } impl<'db> From> for ClassType<'db> { diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 66172279603f2..24706d33dfcae 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -876,7 +876,7 @@ pub fn definitions_for_keyword_argument<'db>( let mut resolved_definitions = Vec::new(); - if let Some(Type::Callable(callable_type)) = func_type.try_upcast_to_callable(db) { + if let Some(callable_type) = func_type.try_upcast_to_callable(db).exactly_one() { let signatures = callable_type.signatures(db); // For each signature, find the parameter with the matching name @@ -987,12 +987,12 @@ pub fn call_signature_details<'db>( let func_type = call_expr.func.inferred_type(model); // Use into_callable to handle all the complex type conversions - if let Some(callable_type) = func_type.try_upcast_to_callable(db) { + if let Some(callable) = func_type.try_upcast_to_callable(db).exactly_one() { let call_arguments = CallArguments::from_arguments(&call_expr.arguments, |_, splatted_value| { splatted_value.inferred_type(model) }); - let bindings = callable_type + let bindings = Type::Callable(callable) .bindings(db) .match_parameters(db, &call_arguments); @@ -1042,7 +1042,7 @@ pub fn call_type_simplified_by_overloads<'db>( let func_type = call_expr.func.inferred_type(model); // Use into_callable to handle all the complex type conversions - let callable_type = func_type.try_upcast_to_callable(db)?; + let callable_type = func_type.try_upcast_to_callable(db).into_type(db)?; let bindings = callable_type.bindings(db); // If the callable is trivial this analysis is useless, bail out diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3ffbd12425887..435d9ed0f0069 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2287,6 +2287,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let is_input_function_like = inferred_ty .try_upcast_to_callable(self.db()) + .into_type(self.db()) .and_then(Type::as_callable) .is_some_and(|callable| callable.is_function_like(self.db())); diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index eff43e23e83a5..1213799f96c8c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1227,7 +1227,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let argument_type = self.infer_expression(&arguments[0], TypeContext::default()); - let Some(callable_type) = argument_type.try_upcast_to_callable(db) else { + let Some(callable) = argument_type.try_upcast_to_callable(db).exactly_one() else { if let Some(builder) = self .context .report_lint(&INVALID_TYPE_FORM, arguments_slice) @@ -1245,6 +1245,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Type::unknown(); }; + let callable_type = Type::Callable(callable); if arguments_slice.is_tuple_expr() { self.store_expression_type(arguments_slice, callable_type); } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 8e3835b386a76..b1fbb2ce41600 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -676,7 +676,8 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { // unfortunately not sufficient to obtain the `Callable` supertypes of these types, due to the // complex interaction between `__new__`, `__init__` and metaclass `__call__`. let attribute_type = if self.name == "__call__" { - let Some(attribute_type) = other.try_upcast_to_callable(db) else { + let Some(attribute_type) = other.try_upcast_to_callable(db).into_type(db) + else { return ConstraintSet::from(false); }; attribute_type From 062f148609a09a07b012759e498bab962b1c19df Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 21 Nov 2025 12:55:51 -0500 Subject: [PATCH 02/13] apply Self types when checking protocols --- .../resources/mdtest/protocols.md | 3 +- crates/ty_python_semantic/src/types.rs | 68 ++++++++++++++----- .../src/types/constraints.rs | 14 +++- .../src/types/protocol_class.rs | 31 ++++----- .../src/types/signatures.rs | 35 ++++++++++ 5 files changed, 115 insertions(+), 36 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 8d41073902c7e..60d1857baefcf 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -2112,8 +2112,7 @@ static_assert(not is_assignable_to(NominalLegacy, UsesSelf)) static_assert(not is_assignable_to(NominalWithSelf, NewStyleFunctionScoped)) static_assert(not is_assignable_to(NominalWithSelf, LegacyFunctionScoped)) static_assert(is_assignable_to(NominalWithSelf, UsesSelf)) -# TODO: should pass -static_assert(is_subtype_of(NominalWithSelf, UsesSelf)) # error: [static-assert-error] +static_assert(is_subtype_of(NominalWithSelf, UsesSelf)) # TODO: these should pass static_assert(not is_assignable_to(NominalNotGeneric, NewStyleFunctionScoped)) # error: [static-assert-error] diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 1a7efcecdbed2..b9e1b6629df4c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2198,20 +2198,18 @@ impl<'db> Type<'db> { ) }), - (_, Type::Callable(_)) => relation_visitor.visit((self, target, relation), || { - self.try_upcast_to_callable(db) - .into_type(db) - .when_some_and(|callable| { - callable.has_relation_to_impl( - db, - target, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - }) - }), + (_, Type::Callable(other_callable)) => { + relation_visitor.visit((self, target, relation), || { + self.try_upcast_to_callable(db).has_relation_to_impl( + db, + other_callable, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } (_, Type::ProtocolInstance(protocol)) => { relation_visitor.visit((self, target, relation), || { @@ -4109,7 +4107,7 @@ impl<'db> Type<'db> { Some((self, AttributeKind::NormalOrNonDataDescriptor)) } else { Some(( - Type::Callable(callable.bind_self(db)), + Type::Callable(callable.bind_self(db, None)), AttributeKind::NormalOrNonDataDescriptor, )) }; @@ -11002,8 +11000,16 @@ impl<'db> CallableType<'db> { Self::single(db, Signature::unknown()) } - pub(crate) fn bind_self(self, db: &'db dyn Db) -> CallableType<'db> { - CallableType::new(db, self.signatures(db).bind_self(db, None), false) + pub(crate) fn bind_self( + self, + db: &'db dyn Db, + self_type: Option>, + ) -> CallableType<'db> { + CallableType::new(db, self.signatures(db).bind_self(db, self_type), false) + } + + pub(crate) fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> { + CallableType::new(db, self.signatures(db).apply_self(db, self_type), false) } /// Create a callable type which represents a fully-static "bottom" callable. @@ -11139,6 +11145,34 @@ impl<'db> CallableTypes<'db> { )), } } + + pub(crate) fn map(self, mut f: impl FnMut(CallableType<'db>) -> CallableType<'db>) -> Self { + Self::from_elements(self.0.iter().map(|element| f(*element))) + } + + pub(crate) fn has_relation_to_impl( + self, + db: &'db dyn Db, + other: CallableType<'db>, + inferable: InferableTypeVars<'_, 'db>, + relation: TypeRelation<'db>, + relation_visitor: &HasRelationToVisitor<'db>, + disjointness_visitor: &IsDisjointVisitor<'db>, + ) -> ConstraintSet<'db> { + if self.is_empty() { + return ConstraintSet::from(false); + } + self.0.iter().when_all(db, |element| { + element.has_relation_to_impl( + db, + other, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } } /// Represents a specific instance of a bound method type for a builtin class. diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index ab21b49bf381e..cf4e2917cca63 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -1790,7 +1790,11 @@ impl<'db> InteriorNode<'db> { /// Returns a sequent map for this BDD, which records the relationships between the constraints /// that appear in the BDD. - #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked( + returns(ref), + cycle_initial=sequent_map_cycle_initial, + heap_size=ruff_memory_usage::heap_size, + )] fn sequent_map(self, db: &'db dyn Db) -> SequentMap<'db> { let mut map = SequentMap::default(); Node::Interior(self).for_each_constraint(db, &mut |constraint| { @@ -2109,6 +2113,14 @@ impl<'db> InteriorNode<'db> { } } +fn sequent_map_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: InteriorNode<'db>, +) -> SequentMap<'db> { + SequentMap::default() +} + /// An assignment of one BDD variable to either `true` or `false`. (When evaluating a BDD, we /// must provide an assignment for each variable present in the BDD.) #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index b1fbb2ce41600..c915517135b2e 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -300,7 +300,7 @@ impl<'db> ProtocolInterface<'db> { .and(db, || { our_type.has_relation_to_impl( db, - Type::Callable(other_type.bind_self(db)), + Type::Callable(other_type.bind_self(db, None)), inferable, relation, relation_visitor, @@ -311,9 +311,9 @@ impl<'db> ProtocolInterface<'db> { ( ProtocolMemberKind::Method(our_method), ProtocolMemberKind::Method(other_method), - ) => our_method.bind_self(db).has_relation_to_impl( + ) => our_method.bind_self(db, None).has_relation_to_impl( db, - other_method.bind_self(db), + other_method.bind_self(db, None), inferable, relation, relation_visitor, @@ -676,11 +676,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { // unfortunately not sufficient to obtain the `Callable` supertypes of these types, due to the // complex interaction between `__new__`, `__init__` and metaclass `__call__`. let attribute_type = if self.name == "__call__" { - let Some(attribute_type) = other.try_upcast_to_callable(db).into_type(db) - else { - return ConstraintSet::from(false); - }; - attribute_type + other } else { let Place::Defined(attribute_type, _, Definedness::AlwaysDefined) = other .invoke_descriptor_protocol( @@ -697,14 +693,17 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { attribute_type }; - attribute_type.has_relation_to_impl( - db, - Type::Callable(method.bind_self(db)), - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + attribute_type + .try_upcast_to_callable(db) + .map(|callable| callable.apply_self(db, other)) + .has_relation_to_impl( + db, + method.bind_self(db, Some(other)), + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) } // TODO: consider the types of the attribute on `other` for property members ProtocolMemberKind::Property(_) => ConstraintSet::from(matches!( diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 74fe451e50a76..0460f31f08d3b 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -213,6 +213,19 @@ impl<'db> CallableSignature<'db> { } } + /// Replaces any occurrences of `typing.Self` in the parameter and return annotations with the + /// given type. (Does not bind the `self` parameter; to do that, use + /// [`bind_self`][Self::bind_self].) + pub(crate) fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self { + Self { + overloads: self + .overloads + .iter() + .map(|signature| signature.apply_self(db, self_type)) + .collect(), + } + } + fn is_subtype_of_impl( &self, db: &'db dyn Db, @@ -628,6 +641,28 @@ impl<'db> Signature<'db> { } } + pub(crate) fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self { + let parameters = self.parameters.apply_type_mapping_impl( + db, + &TypeMapping::BindSelf(self_type), + TypeContext::default(), + &ApplyTypeMappingVisitor::default(), + ); + let return_ty = self.return_ty.map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::BindSelf(self_type), + TypeContext::default(), + ) + }); + Self { + generic_context: self.generic_context, + definition: self.definition, + parameters, + return_ty, + } + } + fn inferable_typevars(&self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> { match self.generic_context { Some(generic_context) => generic_context.inferable_typevars(db), From c065a73680969d0380db7f70b056ee56811a8d74 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 21 Nov 2025 14:22:44 -0500 Subject: [PATCH 03/13] fix that test --- crates/ty_python_semantic/src/types/ide_support.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 24706d33dfcae..3c65637539856 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -987,12 +987,12 @@ pub fn call_signature_details<'db>( let func_type = call_expr.func.inferred_type(model); // Use into_callable to handle all the complex type conversions - if let Some(callable) = func_type.try_upcast_to_callable(db).exactly_one() { + if let Some(callable_type) = func_type.try_upcast_to_callable(db).into_type(db) { let call_arguments = CallArguments::from_arguments(&call_expr.arguments, |_, splatted_value| { splatted_value.inferred_type(model) }); - let bindings = Type::Callable(callable) + let bindings = callable_type .bindings(db) .match_parameters(db, &call_arguments); From 58ab6b7395a00246e3de02892454b72cb84348b0 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 21 Nov 2025 14:39:44 -0500 Subject: [PATCH 04/13] fix some calls --- crates/ty_python_semantic/src/types/infer/builder.rs | 3 +-- .../src/types/infer/builder/type_expression.rs | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 435d9ed0f0069..6ee17142e4e53 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2287,8 +2287,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let is_input_function_like = inferred_ty .try_upcast_to_callable(self.db()) - .into_type(self.db()) - .and_then(Type::as_callable) + .exactly_one() .is_some_and(|callable| callable.is_function_like(self.db())); if is_input_function_like diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 1213799f96c8c..1a3c151909455 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1227,7 +1227,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let argument_type = self.infer_expression(&arguments[0], TypeContext::default()); - let Some(callable) = argument_type.try_upcast_to_callable(db).exactly_one() else { + let Some(callable_type) = argument_type + .try_upcast_to_callable(db) + .into_type(self.db()) + else { if let Some(builder) = self .context .report_lint(&INVALID_TYPE_FORM, arguments_slice) @@ -1245,7 +1248,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Type::unknown(); }; - let callable_type = Type::Callable(callable); if arguments_slice.is_tuple_expr() { self.store_expression_type(arguments_slice, callable_type); } From 1ea65e8516309b1d1fbb5c26b006cf9600377e7f Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 21 Nov 2025 14:41:26 -0500 Subject: [PATCH 05/13] early return --- crates/ty_python_semantic/src/types.rs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index b9e1b6629df4c..9b82b0e52cb24 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1578,21 +1578,15 @@ impl<'db> Type<'db> { }, Type::Union(union) => { - let mut any_not_callable = false; - let callables: SmallVec<_> = union - .elements(db) - .iter() - .flat_map(|element| { - let element_callable = element.try_upcast_to_callable(db); - any_not_callable |= element_callable.is_empty(); - element_callable.into_inner().into_iter() - }) - .collect(); - if any_not_callable { - CallableTypes::none() - } else { - CallableTypes(callables) + let mut callables = SmallVec::new(); + for element in union.elements(db) { + let element_callable = element.try_upcast_to_callable(db); + if element_callable.is_empty() { + return CallableTypes::none(); + } + callables.extend(element_callable.into_inner()); } + CallableTypes(callables) } Type::EnumLiteral(enum_literal) => enum_literal From 1d428cbdb2750c453d405d7211fe75e4ad961f73 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 21 Nov 2025 16:02:27 -0500 Subject: [PATCH 06/13] fix stack overflow --- crates/ty_python_semantic/src/types.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 9b82b0e52cb24..4849651fa0257 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7147,6 +7147,15 @@ impl<'db> Type<'db> { tcx: TypeContext<'db>, visitor: &ApplyTypeMappingVisitor<'db>, ) -> Type<'db> { + // If we are binding `typing.Self`, and this type is what we are binding `Self` to, return + // early. This is not just an optimization, it also prevents us from infinitely expanding + // the type, if it's something that can contain a `Self` reference. + if let TypeMapping::BindSelf(self_type) = type_mapping + && self == *self_type + { + return self; + } + match self { Type::TypeVar(bound_typevar) => match type_mapping { TypeMapping::Specialization(specialization) => { From 9cd52546da16a58eb82020fdaac9f17a1015ed52 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 21 Nov 2025 16:35:32 -0500 Subject: [PATCH 07/13] use literal fallback --- .../src/types/protocol_class.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index c915517135b2e..a8b98d5f54edf 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -693,12 +693,24 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { attribute_type }; + // TODO: Instances of `typing.Self` in the protocol member should specialize to the + // type that we are checking. Without this, we will treat `Self` as an inferable + // typevar, and allow it to match against _any_ type. + // + // It's not very principled, but we also use the literal fallback type, instead of + // `other` directly. This lets us check whether things like `Literal[0]` satisfy a + // protocol that includes methods that have `typing.Self` annotations, without + // overly constraining `Self` to that specific literal. + // + // With the new solver, we should be to replace all of this with an additional + // constraint that enforces what `Self` can specialize to. + let fallback_other = other.literal_fallback_instance(db).unwrap_or(other); attribute_type .try_upcast_to_callable(db) - .map(|callable| callable.apply_self(db, other)) + .map(|callable| callable.apply_self(db, fallback_other)) .has_relation_to_impl( db, - method.bind_self(db, Some(other)), + method.bind_self(db, Some(fallback_other)), inferable, relation, relation_visitor, From 15753811de7182627a5ff69801b024f4cb0522be Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 24 Nov 2025 12:34:04 -0500 Subject: [PATCH 08/13] Update crates/ty_python_semantic/src/types.rs Co-authored-by: Alex Waygood --- crates/ty_python_semantic/src/types.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 4849651fa0257..33a434dd97c70 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -11127,7 +11127,10 @@ impl<'db> CallableTypes<'db> { } pub(crate) fn exactly_one(self) -> Option> { - self.0.into_iter().exactly_one().ok() + match self.0.as_slice() { + [single] => Some(*single), + _ => None, + } } fn is_empty(&self) -> bool { From 08145db00d968961c3a345043bd3f11c4b7000ea Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 24 Nov 2025 12:46:00 -0500 Subject: [PATCH 09/13] option for no callable --- .../reachability_constraints.rs | 14 +-- crates/ty_python_semantic/src/types.rs | 96 +++++++++---------- crates/ty_python_semantic/src/types/class.rs | 26 ++--- .../src/types/ide_support.rs | 12 ++- .../src/types/infer/builder.rs | 2 +- .../types/infer/builder/type_expression.rs | 2 +- .../src/types/protocol_class.rs | 21 ++-- 7 files changed, 89 insertions(+), 84 deletions(-) diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index d1f6451e176bf..b561d3365f757 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -871,12 +871,14 @@ impl ReachabilityConstraints { return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive); } - let overloads_iterator = - if let Some(callable) = ty.try_upcast_to_callable(db).exactly_one() { - callable.signatures(db).overloads.iter() - } else { - return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive); - }; + let overloads_iterator = if let Some(callable) = ty + .try_upcast_to_callable(db) + .and_then(|callables| callables.exactly_one()) + { + callable.signatures(db).overloads.iter() + } else { + return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive); + }; let (no_overloads_return_never, all_overloads_return_never) = overloads_iterator .fold((true, true), |(none, all), overload| { diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 33a434dd97c70..7f78bc8e6517c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1525,20 +1525,20 @@ impl<'db> Type<'db> { } } - pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { + pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option> { match self { - Type::Callable(callable) => CallableTypes::one(callable), + Type::Callable(callable) => Some(CallableTypes::one(callable)), - Type::Dynamic(_) => CallableTypes::one(CallableType::function_like_callable( + Type::Dynamic(_) => Some(CallableTypes::one(CallableType::function_like_callable( db, Signature::dynamic(self), - )), + ))), Type::FunctionLiteral(function_literal) => { - CallableTypes::one(function_literal.into_callable_type(db)) + Some(CallableTypes::one(function_literal.into_callable_type(db))) } Type::BoundMethod(bound_method) => { - CallableTypes::one(bound_method.into_callable_type(db)) + Some(CallableTypes::one(bound_method.into_callable_type(db))) } Type::NominalInstance(_) | Type::ProtocolInstance(_) => { @@ -1553,7 +1553,7 @@ impl<'db> Type<'db> { if let Place::Defined(ty, _, Definedness::AlwaysDefined) = call_symbol { ty.try_upcast_to_callable(db) } else { - CallableTypes::none() + None } } Type::ClassLiteral(class_literal) => { @@ -1570,23 +1570,20 @@ impl<'db> Type<'db> { Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { SubclassOfInner::Class(class) => class.into_callable(db), SubclassOfInner::Dynamic(dynamic) => { - CallableTypes::one(CallableType::single_callable( + Some(CallableTypes::one(CallableType::single_callable( db, Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))), - )) + ))) } }, Type::Union(union) => { let mut callables = SmallVec::new(); for element in union.elements(db) { - let element_callable = element.try_upcast_to_callable(db); - if element_callable.is_empty() { - return CallableTypes::none(); - } + let element_callable = element.try_upcast_to_callable(db)?; callables.extend(element_callable.into_inner()); } - CallableTypes(callables) + Some(CallableTypes(callables)) } Type::EnumLiteral(enum_literal) => enum_literal @@ -1595,27 +1592,29 @@ impl<'db> Type<'db> { Type::TypeAlias(alias) => alias.value_type(db).try_upcast_to_callable(db), - Type::KnownBoundMethod(method) => CallableTypes::one(CallableType::new( + Type::KnownBoundMethod(method) => Some(CallableTypes::one(CallableType::new( db, CallableSignature::from_overloads(method.signatures(db)), false, - )), + ))), - Type::WrapperDescriptor(wrapper_descriptor) => CallableTypes::one(CallableType::new( - db, - CallableSignature::from_overloads(wrapper_descriptor.signatures(db)), - false, - )), + Type::WrapperDescriptor(wrapper_descriptor) => { + Some(CallableTypes::one(CallableType::new( + db, + CallableSignature::from_overloads(wrapper_descriptor.signatures(db)), + false, + ))) + } Type::KnownInstance(KnownInstanceType::NewType(newtype)) => { - CallableTypes::one(CallableType::single_callable( + Some(CallableTypes::one(CallableType::single_callable( db, Signature::new( Parameters::new([Parameter::positional_only(None) .with_annotated_type(newtype.base(db).instance_type(db))]), Some(Type::NewTypeInstance(newtype)), ), - )) + ))) } Type::Never @@ -1628,7 +1627,7 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(_) | Type::TypeIs(_) - | Type::TypedDict(_) => CallableTypes::none(), + | Type::TypedDict(_) => None, // TODO Type::DataclassDecorator(_) @@ -1638,7 +1637,7 @@ impl<'db> Type<'db> { | Type::PropertyInstance(_) | Type::Intersection(_) | Type::TypeVar(_) - | Type::BoundSuper(_) => CallableTypes::none(), + | Type::BoundSuper(_) => None, } } @@ -2194,14 +2193,16 @@ impl<'db> Type<'db> { (_, Type::Callable(other_callable)) => { relation_visitor.visit((self, target, relation), || { - self.try_upcast_to_callable(db).has_relation_to_impl( - db, - other_callable, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + self.try_upcast_to_callable(db).when_some_and(|callables| { + callables.has_relation_to_impl( + db, + other_callable, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) }) } @@ -11110,20 +11111,21 @@ impl<'db> CallableType<'db> { /// when coercing that result to a single type, you'll get a `UnionType`. But this lets you handle /// that result as a list of `CallableType`s before merging them into a `UnionType` should that be /// helpful. +/// +/// Note that this type is guaranteed to contain at least one callable. If you need to support "no +/// callables" as a possibility, use `Option`. #[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update)] pub(crate) struct CallableTypes<'db>(SmallVec<[CallableType<'db>; 1]>); impl<'db> CallableTypes<'db> { - pub(crate) fn none() -> Self { - CallableTypes(smallvec![]) - } - pub(crate) fn one(callable: CallableType<'db>) -> Self { CallableTypes(smallvec![callable]) } pub(crate) fn from_elements(callables: impl IntoIterator>) -> Self { - CallableTypes(callables.into_iter().collect()) + let callables: SmallVec<_> = callables.into_iter().collect(); + assert!(!callables.is_empty(), "CallableTypes should not be empty"); + CallableTypes(callables) } pub(crate) fn exactly_one(self) -> Option> { @@ -11133,22 +11135,15 @@ impl<'db> CallableTypes<'db> { } } - fn is_empty(&self) -> bool { - self.0.is_empty() - } - fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> { self.0 } - pub(crate) fn into_type(self, db: &'db dyn Db) -> Option> { + pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> { match self.0.as_slice() { - [] => None, - [single] => Some(Type::Callable(*single)), - slice => Some(UnionType::from_elements( - db, - slice.iter().copied().map(Type::Callable), - )), + [] => unreachable!("CallableTypes should not be empty"), + [single] => Type::Callable(*single), + slice => UnionType::from_elements(db, slice.iter().copied().map(Type::Callable)), } } @@ -11165,9 +11160,6 @@ impl<'db> CallableTypes<'db> { relation_visitor: &HasRelationToVisitor<'db>, disjointness_visitor: &IsDisjointVisitor<'db>, ) -> ConstraintSet<'db> { - if self.is_empty() { - return ConstraintSet::from(false); - } self.0.iter().when_all(db, |element| { element.has_relation_to_impl( db, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 435d361455eee..25151bddb29ba 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1051,7 +1051,7 @@ impl<'db> ClassType<'db> { /// Return a callable type (or union of callable types) that represents the callable /// constructor signature of this class. #[salsa::tracked(cycle_initial=into_callable_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { + pub(super) fn into_callable(self, db: &'db dyn Db) -> Option> { let self_ty = Type::from(self); let metaclass_dunder_call_function_symbol = self_ty .member_lookup_with_policy( @@ -1069,7 +1069,9 @@ impl<'db> ClassType<'db> { // https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable // by always respecting the signature of the metaclass `__call__`, rather than // using a heuristic which makes unwarranted assumptions to sometimes ignore it. - return CallableTypes::one(metaclass_dunder_call_function.into_callable_type(db)); + return Some(CallableTypes::one( + metaclass_dunder_call_function.into_callable_type(db), + )); } let dunder_new_function_symbol = self_ty.lookup_dunder_new(db); @@ -1104,7 +1106,7 @@ impl<'db> ClassType<'db> { ); if returns_non_subclass { - return CallableTypes::one(dunder_new_bound_method); + return Some(CallableTypes::one(dunder_new_bound_method)); } Some(dunder_new_bound_method) } else { @@ -1161,13 +1163,13 @@ impl<'db> ClassType<'db> { match (dunder_new_function, synthesized_dunder_init_callable) { (Some(dunder_new_function), Some(synthesized_dunder_init_callable)) => { - CallableTypes::from_elements([ + Some(CallableTypes::from_elements([ dunder_new_function, synthesized_dunder_init_callable, - ]) + ])) } (Some(constructor), None) | (None, Some(constructor)) => { - CallableTypes::one(constructor) + Some(CallableTypes::one(constructor)) } (None, None) => { // If no `__new__` or `__init__` method is found, then we fall back to looking for @@ -1183,17 +1185,17 @@ impl<'db> ClassType<'db> { if let Place::Defined(Type::FunctionLiteral(new_function), _, _) = new_function_symbol { - CallableTypes::one( + Some(CallableTypes::one( new_function .into_bound_method_type(db, correct_return_type) .into_callable_type(db), - ) + )) } else { // Fallback if no `object.__new__` is found. - CallableTypes::one(CallableType::single_callable( + Some(CallableTypes::one(CallableType::single_callable( db, Signature::new(Parameters::empty(), Some(correct_return_type)), - )) + ))) } } } @@ -1212,8 +1214,8 @@ fn into_callable_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, _self: ClassType<'db>, -) -> CallableTypes<'db> { - CallableTypes::none() +) -> Option> { + None } impl<'db> From> for ClassType<'db> { diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 3c65637539856..3fa00ad1d0b8f 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -876,7 +876,10 @@ pub fn definitions_for_keyword_argument<'db>( let mut resolved_definitions = Vec::new(); - if let Some(callable_type) = func_type.try_upcast_to_callable(db).exactly_one() { + if let Some(callable_type) = func_type + .try_upcast_to_callable(db) + .and_then(|callables| callables.exactly_one()) + { let signatures = callable_type.signatures(db); // For each signature, find the parameter with the matching name @@ -987,7 +990,10 @@ pub fn call_signature_details<'db>( let func_type = call_expr.func.inferred_type(model); // Use into_callable to handle all the complex type conversions - if let Some(callable_type) = func_type.try_upcast_to_callable(db).into_type(db) { + if let Some(callable_type) = func_type + .try_upcast_to_callable(db) + .map(|callables| callables.into_type(db)) + { let call_arguments = CallArguments::from_arguments(&call_expr.arguments, |_, splatted_value| { splatted_value.inferred_type(model) @@ -1042,7 +1048,7 @@ pub fn call_type_simplified_by_overloads<'db>( let func_type = call_expr.func.inferred_type(model); // Use into_callable to handle all the complex type conversions - let callable_type = func_type.try_upcast_to_callable(db).into_type(db)?; + let callable_type = func_type.try_upcast_to_callable(db)?.into_type(db); let bindings = callable_type.bindings(db); // If the callable is trivial this analysis is useless, bail out diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6ee17142e4e53..be74e524b89c4 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2287,7 +2287,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let is_input_function_like = inferred_ty .try_upcast_to_callable(self.db()) - .exactly_one() + .and_then(|callables| callables.exactly_one()) .is_some_and(|callable| callable.is_function_like(self.db())); if is_input_function_like diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 1a3c151909455..25e83eb9251ac 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1229,7 +1229,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let Some(callable_type) = argument_type .try_upcast_to_callable(db) - .into_type(self.db()) + .map(|callables| callables.into_type(self.db())) else { if let Some(builder) = self .context diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index a8b98d5f54edf..b38cb576cd773 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -707,15 +707,18 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { let fallback_other = other.literal_fallback_instance(db).unwrap_or(other); attribute_type .try_upcast_to_callable(db) - .map(|callable| callable.apply_self(db, fallback_other)) - .has_relation_to_impl( - db, - method.bind_self(db, Some(fallback_other)), - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) + .when_some_and(|callables| { + callables + .map(|callable| callable.apply_self(db, fallback_other)) + .has_relation_to_impl( + db, + method.bind_self(db, Some(fallback_other)), + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) } // TODO: consider the types of the attribute on `other` for property members ProtocolMemberKind::Property(_) => ConstraintSet::from(matches!( From b7ef551ab535f597f36612dc1ca9f89a1ff79237 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 24 Nov 2025 12:55:48 -0500 Subject: [PATCH 10/13] simpler callable construction --- crates/ty_python_semantic/src/place.rs | 6 +-- crates/ty_python_semantic/src/types.rs | 39 +++++++++---------- crates/ty_python_semantic/src/types/class.rs | 18 ++++----- .../src/types/infer/builder.rs | 6 +-- .../types/infer/builder/type_expression.rs | 2 +- .../types/property_tests/type_generation.rs | 6 +-- .../src/types/protocol_class.rs | 2 +- 7 files changed, 38 insertions(+), 41 deletions(-) diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 0f22048cccf82..4957a53914315 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1376,9 +1376,7 @@ mod implicit_globals { use crate::place::{Definedness, PlaceAndQualifiers, TypeOrigin}; use crate::semantic_index::symbol::Symbol; use crate::semantic_index::{place_table, use_def_map}; - use crate::types::{ - CallableType, KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type, - }; + use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type}; use ruff_python_ast::PythonVersion; use super::{Place, place_from_declarations}; @@ -1461,7 +1459,7 @@ mod implicit_globals { )), ); Place::Defined( - CallableType::function_like(db, signature), + Type::function_like_callable(db, signature), TypeOrigin::Inferred, Definedness::PossiblyUndefined, ) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 988ac328b4596..8d894b6c916ec 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1537,7 +1537,7 @@ impl<'db> Type<'db> { match self { Type::Callable(callable) => Some(CallableTypes::one(callable)), - Type::Dynamic(_) => Some(CallableTypes::one(CallableType::function_like_callable( + Type::Dynamic(_) => Some(CallableTypes::one(CallableType::function_like( db, Signature::dynamic(self), ))), @@ -1578,7 +1578,7 @@ impl<'db> Type<'db> { Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { SubclassOfInner::Class(class) => class.into_callable(db), SubclassOfInner::Dynamic(dynamic) => { - Some(CallableTypes::one(CallableType::single_callable( + Some(CallableTypes::one(CallableType::single( db, Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))), ))) @@ -1615,7 +1615,7 @@ impl<'db> Type<'db> { } Type::KnownInstance(KnownInstanceType::NewType(newtype)) => { - Some(CallableTypes::one(CallableType::single_callable( + Some(CallableTypes::one(CallableType::single( db, Signature::new( Parameters::new([Parameter::positional_only(None) @@ -5645,7 +5645,7 @@ impl<'db> Type<'db> { .with_annotated_type(UnionType::from_elements( db, [ - CallableType::single(db, getter_signature), + Type::single_callable(db, getter_signature), Type::none(db), ], )) @@ -5654,7 +5654,7 @@ impl<'db> Type<'db> { .with_annotated_type(UnionType::from_elements( db, [ - CallableType::single(db, setter_signature), + Type::single_callable(db, setter_signature), Type::none(db), ], )) @@ -5663,7 +5663,7 @@ impl<'db> Type<'db> { .with_annotated_type(UnionType::from_elements( db, [ - CallableType::single(db, deleter_signature), + Type::single_callable(db, deleter_signature), Type::none(db), ], )) @@ -10984,33 +10984,32 @@ pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( // The Salsa heap is tracked separately. impl get_size2::GetSize for CallableType<'_> {} -impl<'db> CallableType<'db> { +impl<'db> Type<'db> { /// Create a callable type with a single non-overloaded signature. - pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { - Type::Callable(Self::single_callable(db, signature)) - } - - pub(crate) fn single_callable(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { - CallableType::new(db, CallableSignature::single(signature), false) + pub(crate) fn single_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { + Type::Callable(CallableType::single(db, signature)) } /// Create a non-overloaded, function-like callable type with a single signature. /// /// A function-like callable will bind `self` when accessed as an attribute on an instance. - pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { - Type::Callable(Self::function_like_callable(db, signature)) + pub(crate) fn function_like_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { + Type::Callable(CallableType::function_like(db, signature)) + } +} + +impl<'db> CallableType<'db> { + pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { + CallableType::new(db, CallableSignature::single(signature), false) } - pub(crate) fn function_like_callable( - db: &'db dyn Db, - signature: Signature<'db>, - ) -> CallableType<'db> { + pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { CallableType::new(db, CallableSignature::single(signature), true) } /// Create a callable type which accepts any parameters and returns an `Unknown` type. pub(crate) fn unknown(db: &'db dyn Db) -> Type<'db> { - Self::single(db, Signature::unknown()) + Type::Callable(Self::single(db, Signature::unknown())) } pub(crate) fn bind_self( diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d4d93e6dbed1b..bf97c60c93ed1 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -791,7 +791,7 @@ impl<'db> ClassType<'db> { .with_annotated_type(Type::instance(db, self))]); let synthesized_dunder_method = - CallableType::function_like(db, Signature::new(parameters, Some(return_type))); + Type::function_like_callable(db, Signature::new(parameters, Some(return_type))); Member::definitely_declared(synthesized_dunder_method) } @@ -1013,7 +1013,7 @@ impl<'db> ClassType<'db> { iterable_parameter, ]); - let synthesized_dunder = CallableType::function_like( + let synthesized_dunder = Type::function_like_callable( db, Signature::new_generic(inherited_generic_context, parameters, None), ); @@ -1193,7 +1193,7 @@ impl<'db> ClassType<'db> { )) } else { // Fallback if no `object.__new__` is found. - Some(CallableTypes::one(CallableType::single_callable( + Some(CallableTypes::one(CallableType::single( db, Signature::new(Parameters::empty(), Some(correct_return_type)), ))) @@ -2160,7 +2160,7 @@ impl<'db> ClassLiteral<'db> { Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]), Some(field.declared_ty), ); - let property_getter = CallableType::single(db, property_getter_signature); + let property_getter = Type::single_callable(db, property_getter_signature); let property = PropertyInstanceType::new(db, Some(property_getter), None); return Member::definitely_declared(Type::PropertyInstance(property)); } @@ -2374,7 +2374,7 @@ impl<'db> ClassLiteral<'db> { ), _ => Signature::new(Parameters::new(parameters), return_ty), }; - Some(CallableType::function_like(db, signature)) + Some(Type::function_like_callable(db, signature)) }; match (field_policy, name) { @@ -2410,7 +2410,7 @@ impl<'db> ClassLiteral<'db> { Some(KnownClass::Bool.to_instance(db)), ); - Some(CallableType::function_like(db, signature)) + Some(Type::function_like_callable(db, signature)) } (CodeGeneratorKind::DataclassLike(_), "__hash__") => { let unsafe_hash = has_dataclass_param(DataclassFlags::UNSAFE_HASH); @@ -2426,7 +2426,7 @@ impl<'db> ClassLiteral<'db> { Some(KnownClass::Int.to_instance(db)), ); - Some(CallableType::function_like(db, signature)) + Some(Type::function_like_callable(db, signature)) } else if eq && !frozen { Some(Type::none(db)) } else { @@ -2513,7 +2513,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::Never), ); - return Some(CallableType::function_like(db, signature)); + return Some(Type::function_like_callable(db, signature)); } None } @@ -2791,7 +2791,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::none(db)), ); - Some(CallableType::function_like(db, signature)) + Some(Type::function_like_callable(db, signature)) } _ => None, } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 1040a37c584dd..5c10bae8b62e8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3284,7 +3284,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // specified in this context. match default { ast::Expr::EllipsisLiteral(_) => { - CallableType::single(self.db(), Signature::new(Parameters::gradual_form(), None)) + Type::single_callable(self.db(), Signature::new(Parameters::gradual_form(), None)) } ast::Expr::List(ast::ExprList { elts, .. }) => { let mut parameter_types = Vec::with_capacity(elts.len()); @@ -3312,7 +3312,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { })) }; - CallableType::single(self.db(), Signature::new(parameters, None)) + Type::single_callable(self.db(), Signature::new(parameters, None)) } ast::Expr::Name(name) => { let name_ty = self.infer_name_load(name); @@ -7945,7 +7945,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // TODO: Useful inference of a lambda's return type will require a different approach, // which does the inference of the body expression based on arguments at each call site, // rather than eagerly computing a return type without knowing the argument types. - CallableType::function_like(self.db(), Signature::new(parameters, Some(Type::unknown()))) + Type::function_like_callable(self.db(), Signature::new(parameters, Some(Type::unknown()))) } fn infer_call_expression( diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 25e83eb9251ac..9abb18cbd4192 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1011,7 +1011,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let callable_type = if let (Some(parameters), Some(return_type), true) = (parameters, return_type, correct_argument_number) { - CallableType::single(db, Signature::new(parameters, Some(return_type))) + Type::single_callable(db, Signature::new(parameters, Some(return_type))) } else { CallableType::unknown(db) }; 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 52136e4046015..58808e48c2490 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 @@ -3,8 +3,8 @@ use crate::place::{builtins_symbol, known_module_symbol}; use crate::types::enums::is_single_member_enum; use crate::types::tuple::TupleType; use crate::types::{ - BoundMethodType, CallableType, EnumLiteralType, IntersectionBuilder, KnownClass, Parameter, - Parameters, Signature, SpecialFormType, SubclassOfType, Type, UnionType, + BoundMethodType, EnumLiteralType, IntersectionBuilder, KnownClass, Parameter, Parameters, + Signature, SpecialFormType, SubclassOfType, Type, UnionType, }; use crate::{Db, module_resolver::KnownModule}; use quickcheck::{Arbitrary, Gen}; @@ -229,7 +229,7 @@ impl Ty { create_bound_method(db, function, builtins_class) } - Ty::Callable { params, returns } => CallableType::single( + Ty::Callable { params, returns } => Type::single_callable( db, Signature::new( params.into_parameters(db), diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index b38cb576cd773..185bb9d2d552e 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -202,7 +202,7 @@ impl<'db> ProtocolInterface<'db> { Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]), Some(ty.normalized(db)), ); - let property_getter = CallableType::single(db, property_getter_signature); + let property_getter = Type::single_callable(db, property_getter_signature); let property = PropertyInstanceType::new(db, Some(property_getter), None); ( Name::new(name), From e4f47ee79af1863a25b417ffe45265ec7526c74a Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 24 Nov 2025 13:08:01 -0500 Subject: [PATCH 11/13] clippy --- .../src/semantic_index/reachability_constraints.rs | 4 ++-- crates/ty_python_semantic/src/types/ide_support.rs | 4 ++-- crates/ty_python_semantic/src/types/infer/builder.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs index b561d3365f757..d7ab0f621cd78 100644 --- a/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs +++ b/crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs @@ -208,7 +208,7 @@ use crate::semantic_index::predicate::{ Predicates, ScopedPredicateId, }; use crate::types::{ - IntersectionBuilder, Truthiness, Type, TypeContext, UnionBuilder, UnionType, + CallableTypes, IntersectionBuilder, Truthiness, Type, TypeContext, UnionBuilder, UnionType, infer_expression_type, static_expression_truthiness, }; @@ -873,7 +873,7 @@ impl ReachabilityConstraints { let overloads_iterator = if let Some(callable) = ty .try_upcast_to_callable(db) - .and_then(|callables| callables.exactly_one()) + .and_then(CallableTypes::exactly_one) { callable.signatures(db).overloads.iter() } else { diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 9546505e1b26a..779a34b0fe26e 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -15,7 +15,7 @@ use crate::types::generics::Specialization; use crate::types::signatures::Signature; use crate::types::{CallDunderError, UnionType}; use crate::types::{ - ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext, + CallableTypes, ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext, TypeVarBoundOrConstraints, class::CodeGeneratorKind, }; use crate::{Db, DisplaySettings, HasType, NameKind, SemanticModel}; @@ -878,7 +878,7 @@ pub fn definitions_for_keyword_argument<'db>( if let Some(callable_type) = func_type .try_upcast_to_callable(db) - .and_then(|callables| callables.exactly_one()) + .and_then(CallableTypes::exactly_one) { let signatures = callable_type.signatures(db); diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 5c10bae8b62e8..5ee5559d01c56 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -101,8 +101,8 @@ use crate::types::typed_dict::{ }; use crate::types::visitor::any_over_type; use crate::types::{ - CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams, - DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass, + CallDunderError, CallableBinding, CallableType, CallableTypes, ClassLiteral, ClassType, + DataclassParams, DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, @@ -2291,7 +2291,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let is_input_function_like = inferred_ty .try_upcast_to_callable(self.db()) - .and_then(|callables| callables.exactly_one()) + .and_then(CallableTypes::exactly_one) .is_some_and(|callable| callable.is_function_like(self.db())); if is_input_function_like From 9b288dcd21e182608008bd23e9b978cfa5275b6a Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 24 Nov 2025 13:23:33 -0500 Subject: [PATCH 12/13] add mdtest for new typing conformance pass --- .../ty_python_semantic/resources/mdtest/protocols.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index fc31c63876362..1e1ac07aabdc0 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -2003,6 +2003,7 @@ python-version = "3.12" ``` ```py +from typing import final from typing_extensions import TypeVar, Self, Protocol from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of @@ -2094,6 +2095,13 @@ class NominalReturningSelfNotGeneric: def g(self) -> "NominalReturningSelfNotGeneric": return self +@final +class Other: ... + +class NominalReturningOtherClass: + def g(self) -> Other: + raise NotImplementedError + # TODO: should pass static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error] @@ -2125,6 +2133,8 @@ static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, LegacyFunctio # TODO: should pass static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, UsesSelf)) # error: [static-assert-error] +static_assert(not is_assignable_to(NominalReturningOtherClass, UsesSelf)) + # These test cases are taken from the typing conformance suite: class ShapeProtocolImplicitSelf(Protocol): def set_scale(self, scale: float) -> Self: ... From a5c87aaf561ced56f3e909018dede524a24ba360 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 24 Nov 2025 13:47:43 -0500 Subject: [PATCH 13/13] remove unneeded option --- crates/ty_python_semantic/src/types.rs | 6 ++--- crates/ty_python_semantic/src/types/class.rs | 28 +++++++++----------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 8d894b6c916ec..2ba63d825618a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1565,10 +1565,10 @@ impl<'db> Type<'db> { } } Type::ClassLiteral(class_literal) => { - ClassType::NonGeneric(class_literal).into_callable(db) + Some(ClassType::NonGeneric(class_literal).into_callable(db)) } - Type::GenericAlias(alias) => ClassType::Generic(alias).into_callable(db), + Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)), Type::NewTypeInstance(newtype) => { Type::instance(db, newtype.base_class_type(db)).try_upcast_to_callable(db) @@ -1576,7 +1576,7 @@ impl<'db> Type<'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) => class.into_callable(db), + SubclassOfInner::Class(class) => Some(class.into_callable(db)), SubclassOfInner::Dynamic(dynamic) => { Some(CallableTypes::one(CallableType::single( db, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index bf97c60c93ed1..7de3a86263b21 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1052,7 +1052,7 @@ impl<'db> ClassType<'db> { /// Return a callable type (or union of callable types) that represents the callable /// constructor signature of this class. #[salsa::tracked(cycle_initial=into_callable_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(super) fn into_callable(self, db: &'db dyn Db) -> Option> { + pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { let self_ty = Type::from(self); let metaclass_dunder_call_function_symbol = self_ty .member_lookup_with_policy( @@ -1070,9 +1070,7 @@ impl<'db> ClassType<'db> { // https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable // by always respecting the signature of the metaclass `__call__`, rather than // using a heuristic which makes unwarranted assumptions to sometimes ignore it. - return Some(CallableTypes::one( - metaclass_dunder_call_function.into_callable_type(db), - )); + return CallableTypes::one(metaclass_dunder_call_function.into_callable_type(db)); } let dunder_new_function_symbol = self_ty.lookup_dunder_new(db); @@ -1107,7 +1105,7 @@ impl<'db> ClassType<'db> { ); if returns_non_subclass { - return Some(CallableTypes::one(dunder_new_bound_method)); + return CallableTypes::one(dunder_new_bound_method); } Some(dunder_new_bound_method) } else { @@ -1164,13 +1162,13 @@ impl<'db> ClassType<'db> { match (dunder_new_function, synthesized_dunder_init_callable) { (Some(dunder_new_function), Some(synthesized_dunder_init_callable)) => { - Some(CallableTypes::from_elements([ + CallableTypes::from_elements([ dunder_new_function, synthesized_dunder_init_callable, - ])) + ]) } (Some(constructor), None) | (None, Some(constructor)) => { - Some(CallableTypes::one(constructor)) + CallableTypes::one(constructor) } (None, None) => { // If no `__new__` or `__init__` method is found, then we fall back to looking for @@ -1186,17 +1184,17 @@ impl<'db> ClassType<'db> { if let Place::Defined(Type::FunctionLiteral(new_function), _, _) = new_function_symbol { - Some(CallableTypes::one( + CallableTypes::one( new_function .into_bound_method_type(db, correct_return_type) .into_callable_type(db), - )) + ) } else { // Fallback if no `object.__new__` is found. - Some(CallableTypes::one(CallableType::single( + CallableTypes::one(CallableType::single( db, Signature::new(Parameters::empty(), Some(correct_return_type)), - ))) + )) } } } @@ -1212,11 +1210,11 @@ impl<'db> ClassType<'db> { } fn into_callable_cycle_initial<'db>( - _db: &'db dyn Db, + db: &'db dyn Db, _id: salsa::Id, _self: ClassType<'db>, -) -> Option> { - None +) -> CallableTypes<'db> { + CallableTypes::one(CallableType::bottom(db)) } impl<'db> From> for ClassType<'db> {