[ty] Carry generic context through when converting class into Callable (#21798)
When converting a class (whether specialized or not) into a `Callable` type, we should carry through any generic context that the constructor has. This includes both the generic context of the class itself (if it's generic) and of the constructor methods (if they are separately generic). To help test this, this also updates the `generic_context` extension function to work on `Callable` types and unions; and adds a new `into_callable` extension function that works just like `CallableTypeOf`, but on value forms instead of type forms. Pulled this out of #21551 for separate review.
This commit is contained in:
@@ -8535,12 +8535,9 @@ impl<'db> TypeMapping<'_, 'db> {
|
||||
| TypeMapping::Materialize(_)
|
||||
| TypeMapping::ReplaceParameterDefaults
|
||||
| TypeMapping::EagerExpansion => context,
|
||||
TypeMapping::BindSelf { .. } => GenericContext::from_typevar_instances(
|
||||
db,
|
||||
context
|
||||
.variables(db)
|
||||
.filter(|var| !var.typevar(db).is_self(db)),
|
||||
),
|
||||
TypeMapping::BindSelf {
|
||||
binding_context, ..
|
||||
} => context.remove_self(db, *binding_context),
|
||||
TypeMapping::ReplaceSelf { new_upper_bound } => GenericContext::from_typevar_instances(
|
||||
db,
|
||||
context.variables(db).map(|typevar| {
|
||||
|
||||
@@ -32,7 +32,9 @@ use crate::types::function::{
|
||||
use crate::types::generics::{
|
||||
InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError,
|
||||
};
|
||||
use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters};
|
||||
use crate::types::signatures::{
|
||||
CallableSignature, Parameter, ParameterForm, ParameterKind, Parameters,
|
||||
};
|
||||
use crate::types::tuple::{TupleLength, TupleType};
|
||||
use crate::types::{
|
||||
BoundMethodType, BoundTypeVarIdentity, ClassLiteral, DATACLASS_FLAGS, DataclassFlags,
|
||||
@@ -788,51 +790,67 @@ impl<'db> Bindings<'db> {
|
||||
))
|
||||
};
|
||||
|
||||
let function_generic_context = |function: FunctionType<'db>| {
|
||||
let union = UnionType::from_elements(
|
||||
db,
|
||||
function
|
||||
.signature(db)
|
||||
.overloads
|
||||
.iter()
|
||||
.filter_map(|signature| signature.generic_context)
|
||||
.map(wrap_generic_context),
|
||||
);
|
||||
if union.is_never() {
|
||||
Type::none(db)
|
||||
} else {
|
||||
union
|
||||
}
|
||||
};
|
||||
let signature_generic_context =
|
||||
|signature: &CallableSignature<'db>| {
|
||||
UnionType::try_from_elements(
|
||||
db,
|
||||
signature.overloads.iter().map(|signature| {
|
||||
signature.generic_context.map(wrap_generic_context)
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
// TODO: Handle generic functions, and unions/intersections of
|
||||
// generic types
|
||||
overload.set_return_type(match ty {
|
||||
Type::ClassLiteral(class) => class
|
||||
.generic_context(db)
|
||||
.map(wrap_generic_context)
|
||||
.unwrap_or_else(|| Type::none(db)),
|
||||
let generic_context_for_simple_type = |ty: Type<'db>| match ty {
|
||||
Type::ClassLiteral(class) => {
|
||||
class.generic_context(db).map(wrap_generic_context)
|
||||
}
|
||||
|
||||
Type::FunctionLiteral(function) => {
|
||||
function_generic_context(*function)
|
||||
signature_generic_context(function.signature(db))
|
||||
}
|
||||
|
||||
Type::BoundMethod(bound_method) => {
|
||||
function_generic_context(bound_method.function(db))
|
||||
Type::BoundMethod(bound_method) => signature_generic_context(
|
||||
bound_method.function(db).signature(db),
|
||||
),
|
||||
|
||||
Type::Callable(callable) => {
|
||||
signature_generic_context(callable.signatures(db))
|
||||
}
|
||||
|
||||
Type::KnownInstance(KnownInstanceType::TypeAliasType(
|
||||
TypeAliasType::PEP695(alias),
|
||||
)) => alias
|
||||
.generic_context(db)
|
||||
.map(wrap_generic_context)
|
||||
.unwrap_or_else(|| Type::none(db)),
|
||||
)) => alias.generic_context(db).map(wrap_generic_context),
|
||||
|
||||
_ => Type::none(db),
|
||||
});
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let generic_context = match ty {
|
||||
Type::Union(union_type) => UnionType::try_from_elements(
|
||||
db,
|
||||
union_type
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| generic_context_for_simple_type(*ty)),
|
||||
),
|
||||
_ => generic_context_for_simple_type(*ty),
|
||||
};
|
||||
|
||||
overload.set_return_type(
|
||||
generic_context.unwrap_or_else(|| Type::none(db)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IntoCallable) => {
|
||||
let [Some(ty)] = overload.parameter_types() else {
|
||||
continue;
|
||||
};
|
||||
let Some(callables) = ty.try_upcast_to_callable(db) else {
|
||||
continue;
|
||||
};
|
||||
overload.set_return_type(callables.into_type(db));
|
||||
}
|
||||
|
||||
Some(KnownFunction::DunderAllNames) => {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(match ty {
|
||||
|
||||
@@ -1133,6 +1133,13 @@ impl<'db> ClassType<'db> {
|
||||
/// 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> {
|
||||
// TODO: This mimics a lot of the logic in Type::try_call_from_constructor. Can we
|
||||
// consolidate the two? Can we invoke a class by upcasting the class into a Callable, and
|
||||
// then relying on the call binding machinery to Just Work™?
|
||||
|
||||
let (class_literal, _) = self.class_literal(db);
|
||||
let class_generic_context = class_literal.generic_context(db);
|
||||
|
||||
let self_ty = Type::from(self);
|
||||
let metaclass_dunder_call_function_symbol = self_ty
|
||||
.member_lookup_with_policy(
|
||||
@@ -1206,39 +1213,58 @@ impl<'db> ClassType<'db> {
|
||||
// If the class defines an `__init__` method, then we synthesize a callable type with the
|
||||
// same parameters as the `__init__` method after it is bound, and with the return type of
|
||||
// the concrete type of `Self`.
|
||||
let synthesized_dunder_init_callable =
|
||||
if let Place::Defined(ty, _, _) = dunder_init_function_symbol {
|
||||
let signature = match ty {
|
||||
Type::FunctionLiteral(dunder_init_function) => {
|
||||
Some(dunder_init_function.signature(db))
|
||||
}
|
||||
Type::Callable(callable) => Some(callable.signatures(db)),
|
||||
_ => None,
|
||||
let synthesized_dunder_init_callable = if let Place::Defined(ty, _, _) =
|
||||
dunder_init_function_symbol
|
||||
{
|
||||
let signature = match ty {
|
||||
Type::FunctionLiteral(dunder_init_function) => {
|
||||
Some(dunder_init_function.signature(db))
|
||||
}
|
||||
Type::Callable(callable) => Some(callable.signatures(db)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(signature) = signature {
|
||||
let synthesized_signature = |signature: &Signature<'db>| {
|
||||
let self_annotation = signature
|
||||
.parameters()
|
||||
.get_positional(0)
|
||||
.and_then(Parameter::annotated_type)
|
||||
.filter(|ty| {
|
||||
ty.as_typevar()
|
||||
.is_none_or(|bound_typevar| !bound_typevar.typevar(db).is_self(db))
|
||||
});
|
||||
let return_type = self_annotation.unwrap_or(correct_return_type);
|
||||
let instance_ty = self_annotation.unwrap_or_else(|| Type::instance(db, self));
|
||||
let generic_context = GenericContext::merge_optional(
|
||||
db,
|
||||
class_generic_context,
|
||||
signature.generic_context,
|
||||
);
|
||||
Signature::new_generic(
|
||||
generic_context,
|
||||
signature.parameters().clone(),
|
||||
Some(return_type),
|
||||
)
|
||||
.with_definition(signature.definition())
|
||||
.bind_self(db, Some(instance_ty))
|
||||
};
|
||||
|
||||
if let Some(signature) = signature {
|
||||
let synthesized_signature = |signature: &Signature<'db>| {
|
||||
let instance_ty = Type::instance(db, self);
|
||||
Signature::new(signature.parameters().clone(), Some(correct_return_type))
|
||||
.with_definition(signature.definition())
|
||||
.bind_self(db, Some(instance_ty))
|
||||
};
|
||||
let synthesized_dunder_init_signature = CallableSignature::from_overloads(
|
||||
signature.overloads.iter().map(synthesized_signature),
|
||||
);
|
||||
|
||||
let synthesized_dunder_init_signature = CallableSignature::from_overloads(
|
||||
signature.overloads.iter().map(synthesized_signature),
|
||||
);
|
||||
|
||||
Some(CallableType::new(
|
||||
db,
|
||||
synthesized_dunder_init_signature,
|
||||
true,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(CallableType::new(
|
||||
db,
|
||||
synthesized_dunder_init_signature,
|
||||
true,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match (dunder_new_function, synthesized_dunder_init_callable) {
|
||||
(Some(dunder_new_function), Some(synthesized_dunder_init_callable)) => {
|
||||
@@ -1261,9 +1287,13 @@ impl<'db> ClassType<'db> {
|
||||
)
|
||||
.place;
|
||||
|
||||
if let Place::Defined(Type::FunctionLiteral(new_function), _, _) =
|
||||
if let Place::Defined(Type::FunctionLiteral(mut new_function), _, _) =
|
||||
new_function_symbol
|
||||
{
|
||||
if let Some(class_generic_context) = class_generic_context {
|
||||
new_function =
|
||||
new_function.with_inherited_generic_context(db, class_generic_context);
|
||||
}
|
||||
CallableTypes::one(
|
||||
new_function
|
||||
.into_bound_method_type(db, correct_return_type)
|
||||
@@ -1273,7 +1303,11 @@ impl<'db> ClassType<'db> {
|
||||
// Fallback if no `object.__new__` is found.
|
||||
CallableTypes::one(CallableType::single(
|
||||
db,
|
||||
Signature::new(Parameters::empty(), Some(correct_return_type)),
|
||||
Signature::new_generic(
|
||||
class_generic_context,
|
||||
Parameters::empty(),
|
||||
Some(correct_return_type),
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1339,6 +1339,8 @@ pub enum KnownFunction {
|
||||
IsSingleValued,
|
||||
/// `ty_extensions.generic_context`
|
||||
GenericContext,
|
||||
/// `ty_extensions.into_callable`
|
||||
IntoCallable,
|
||||
/// `ty_extensions.dunder_all_names`
|
||||
DunderAllNames,
|
||||
/// `ty_extensions.enum_members`
|
||||
@@ -1411,6 +1413,7 @@ impl KnownFunction {
|
||||
| Self::IsSingleton
|
||||
| Self::IsSubtypeOf
|
||||
| Self::GenericContext
|
||||
| Self::IntoCallable
|
||||
| Self::DunderAllNames
|
||||
| Self::EnumMembers
|
||||
| Self::StaticAssert
|
||||
@@ -1946,6 +1949,7 @@ pub(crate) mod tests {
|
||||
KnownFunction::IsSingleton
|
||||
| KnownFunction::IsSubtypeOf
|
||||
| KnownFunction::GenericContext
|
||||
| KnownFunction::IntoCallable
|
||||
| KnownFunction::DunderAllNames
|
||||
| KnownFunction::EnumMembers
|
||||
| KnownFunction::StaticAssert
|
||||
|
||||
@@ -17,11 +17,12 @@ use crate::types::signatures::Parameters;
|
||||
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
|
||||
use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard};
|
||||
use crate::types::{
|
||||
ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, ClassLiteral,
|
||||
FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor,
|
||||
KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeContext,
|
||||
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance,
|
||||
TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_bound_type_var_type,
|
||||
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance,
|
||||
ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor,
|
||||
IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor,
|
||||
Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity,
|
||||
TypeVarInstance, TypeVarKind, TypeVarVariance, UnionType, declaration_type,
|
||||
walk_bound_type_var_type,
|
||||
};
|
||||
use crate::{Db, FxOrderMap, FxOrderSet};
|
||||
|
||||
@@ -261,6 +262,34 @@ impl<'db> GenericContext<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn merge_optional(
|
||||
db: &'db dyn Db,
|
||||
left: Option<Self>,
|
||||
right: Option<Self>,
|
||||
) -> Option<Self> {
|
||||
match (left, right) {
|
||||
(None, None) => None,
|
||||
(Some(one), None) | (None, Some(one)) => Some(one),
|
||||
(Some(left), Some(right)) => Some(left.merge(db, right)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_self(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
binding_context: Option<BindingContext<'db>>,
|
||||
) -> Self {
|
||||
Self::from_typevar_instances(
|
||||
db,
|
||||
self.variables(db).filter(|bound_typevar| {
|
||||
!(bound_typevar.typevar(db).is_self(db)
|
||||
&& binding_context.is_none_or(|binding_context| {
|
||||
bound_typevar.binding_context(db) == binding_context
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> {
|
||||
#[derive(Default)]
|
||||
struct CollectTypeVars<'db> {
|
||||
|
||||
@@ -667,10 +667,11 @@ impl<'db> Signature<'db> {
|
||||
|
||||
let mut parameters = Parameters::new(db, parameters);
|
||||
let mut return_ty = self.return_ty;
|
||||
let binding_context = self.definition.map(BindingContext::Definition);
|
||||
if let Some(self_type) = self_type {
|
||||
let self_mapping = TypeMapping::BindSelf {
|
||||
self_type,
|
||||
binding_context: self.definition.map(BindingContext::Definition),
|
||||
binding_context,
|
||||
};
|
||||
parameters = parameters.apply_type_mapping_impl(
|
||||
db,
|
||||
@@ -682,7 +683,9 @@ impl<'db> Signature<'db> {
|
||||
.map(|ty| ty.apply_type_mapping(db, &self_mapping, TypeContext::default()));
|
||||
}
|
||||
Self {
|
||||
generic_context: self.generic_context,
|
||||
generic_context: self
|
||||
.generic_context
|
||||
.map(|generic_context| generic_context.remove_self(db, binding_context)),
|
||||
definition: self.definition,
|
||||
parameters,
|
||||
return_ty,
|
||||
|
||||
Reference in New Issue
Block a user