mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-10 02:12:09 +00:00
[ty] Make tuple subclass constructors sound (#19469)
This commit is contained in:
parent
fcdffe4ac9
commit
cb5a9ff8dc
7 changed files with 201 additions and 66 deletions
|
@ -4427,31 +4427,14 @@ impl<'db> Type<'db> {
|
|||
.into()
|
||||
}
|
||||
|
||||
Type::GenericAlias(alias) => {
|
||||
let instantiated = Type::instance(db, ClassType::from(alias));
|
||||
|
||||
let parameters = if alias.origin(db).is_known(db, KnownClass::Tuple) {
|
||||
// ```py
|
||||
// class tuple:
|
||||
// @overload
|
||||
// def __new__(cls: type[tuple[()]], iterable: tuple[()] = ()) -> tuple[()]: ...
|
||||
// @overload
|
||||
// def __new__[T](cls: type[tuple[T, ...]], iterable: tuple[T, ...]) -> tuple[T, ...]: ...
|
||||
// ```
|
||||
let spec = alias.specialization(db).tuple(db);
|
||||
let mut parameter =
|
||||
Parameter::positional_only(Some(Name::new_static("iterable")))
|
||||
.with_annotated_type(instantiated);
|
||||
if matches!(spec.len().maximum(), Some(0)) {
|
||||
parameter = parameter.with_default_type(TupleType::empty(db));
|
||||
}
|
||||
Parameters::new([parameter])
|
||||
} else {
|
||||
Parameters::gradual_form()
|
||||
};
|
||||
Type::GenericAlias(_) => {
|
||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
||||
// TODO check call vs signatures of `__new__` and/or `__init__`
|
||||
Binding::single(self, Signature::new(parameters, Some(instantiated))).into()
|
||||
Binding::single(
|
||||
self,
|
||||
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
||||
|
@ -4636,7 +4619,7 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
|
||||
if let Type::GenericAlias(alias) = self {
|
||||
if alias.origin(db).is_known(db, KnownClass::Tuple) {
|
||||
if alias.origin(db).is_tuple(db) {
|
||||
return Ok(todo_type!("*tuple[] annotations"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -575,7 +575,7 @@ impl<'db> ClassType<'db> {
|
|||
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
||||
let (class_literal, specialization) = self.class_literal(db);
|
||||
|
||||
let synthesize_tuple_method = |return_type| {
|
||||
let synthesize_simple_tuple_method = |return_type| {
|
||||
let parameters =
|
||||
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(Type::instance(db, self))]);
|
||||
|
@ -587,22 +587,88 @@ impl<'db> ClassType<'db> {
|
|||
};
|
||||
|
||||
match name {
|
||||
"__len__" if class_literal.is_known(db, KnownClass::Tuple) => {
|
||||
"__len__" if class_literal.is_tuple(db) => {
|
||||
let return_type = specialization
|
||||
.and_then(|spec| spec.tuple(db).len().into_fixed_length())
|
||||
.and_then(|len| i64::try_from(len).ok())
|
||||
.map(Type::IntLiteral)
|
||||
.unwrap_or_else(|| KnownClass::Int.to_instance(db));
|
||||
|
||||
synthesize_tuple_method(return_type)
|
||||
synthesize_simple_tuple_method(return_type)
|
||||
}
|
||||
"__bool__" if class_literal.is_known(db, KnownClass::Tuple) => {
|
||||
|
||||
"__bool__" if class_literal.is_tuple(db) => {
|
||||
let return_type = specialization
|
||||
.map(|spec| spec.tuple(db).truthiness().into_type(db))
|
||||
.unwrap_or_else(|| KnownClass::Bool.to_instance(db));
|
||||
|
||||
synthesize_tuple_method(return_type)
|
||||
synthesize_simple_tuple_method(return_type)
|
||||
}
|
||||
|
||||
// ```py
|
||||
// class tuple:
|
||||
// @overload
|
||||
// def __new__(cls: type[tuple[()]], iterable: tuple[()] = ()) -> tuple[()]: ...
|
||||
// @overload
|
||||
// def __new__[T](cls: type[tuple[T, ...]], iterable: tuple[T, ...]) -> tuple[T, ...]: ...
|
||||
// ```
|
||||
"__new__" if class_literal.is_tuple(db) => {
|
||||
let mut iterable_parameter =
|
||||
Parameter::positional_only(Some(Name::new_static("iterable")));
|
||||
|
||||
match specialization {
|
||||
Some(spec) => {
|
||||
let tuple = spec.tuple(db);
|
||||
let tuple_len = tuple.len();
|
||||
|
||||
if tuple_len.minimum() == 0 && tuple_len.maximum().is_none() {
|
||||
// If the tuple has no length restrictions,
|
||||
// any iterable is allowed as long as the iterable has the correct element type.
|
||||
let mut tuple_elements = tuple.all_elements();
|
||||
iterable_parameter = iterable_parameter.with_annotated_type(
|
||||
KnownClass::Iterable
|
||||
.to_specialized_instance(db, [*tuple_elements.next().unwrap()]),
|
||||
);
|
||||
assert_eq!(
|
||||
tuple_elements.next(),
|
||||
None,
|
||||
"Tuple specialization should not have more than one element when it has no length restriction"
|
||||
);
|
||||
} else {
|
||||
// But if the tuple is of a fixed length, or has a minimum length, we require a tuple rather
|
||||
// than an iterable, as a tuple is the only kind of iterable for which we can
|
||||
// specify a fixed length, or that the iterable must be at least a certain length.
|
||||
iterable_parameter =
|
||||
iterable_parameter.with_annotated_type(Type::instance(db, self));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// If the tuple isn't specialized at all, we allow any argument as long as it is iterable.
|
||||
iterable_parameter = iterable_parameter
|
||||
.with_annotated_type(KnownClass::Iterable.to_instance(db));
|
||||
}
|
||||
}
|
||||
|
||||
// We allow the `iterable` parameter to be omitted for:
|
||||
// - a zero-length tuple
|
||||
// - an unspecialized tuple
|
||||
// - a tuple with no minimum length
|
||||
if specialization.is_none_or(|spec| spec.tuple(db).len().minimum() == 0) {
|
||||
iterable_parameter = iterable_parameter.with_default_type(TupleType::empty(db));
|
||||
}
|
||||
|
||||
let parameters = Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(SubclassOfType::from(db, self)),
|
||||
iterable_parameter,
|
||||
]);
|
||||
|
||||
let synthesized_dunder =
|
||||
CallableType::function_like(db, Signature::new(parameters, None));
|
||||
|
||||
Place::bound(synthesized_dunder).into()
|
||||
}
|
||||
|
||||
_ => class_literal
|
||||
.own_class_member(db, specialization, name)
|
||||
.map_type(|ty| ty.apply_optional_specialization(db, specialization)),
|
||||
|
@ -659,38 +725,41 @@ impl<'db> ClassType<'db> {
|
|||
)
|
||||
.place;
|
||||
|
||||
let dunder_new_function =
|
||||
if let Place::Type(Type::FunctionLiteral(dunder_new_function), _) =
|
||||
dunder_new_function_symbol
|
||||
{
|
||||
// Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class,
|
||||
// then we should ignore the `__init__` and just return the `__new__` method.
|
||||
let returns_non_subclass =
|
||||
dunder_new_function
|
||||
.signature(db)
|
||||
.overloads
|
||||
.iter()
|
||||
.any(|signature| {
|
||||
signature.return_ty.is_some_and(|return_ty| {
|
||||
!return_ty.is_assignable_to(
|
||||
db,
|
||||
self_ty
|
||||
.to_instance(db)
|
||||
.expect("ClassType should be instantiable"),
|
||||
)
|
||||
})
|
||||
});
|
||||
let dunder_new_signature = dunder_new_function_symbol
|
||||
.ignore_possibly_unbound()
|
||||
.and_then(|ty| match ty {
|
||||
Type::FunctionLiteral(function) => Some(function.signature(db)),
|
||||
Type::Callable(callable) => Some(callable.signatures(db)),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let dunder_new_bound_method =
|
||||
dunder_new_function.into_bound_method_type(db, self_ty);
|
||||
let dunder_new_function = if let Some(dunder_new_signature) = dunder_new_signature {
|
||||
// Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class,
|
||||
// then we should ignore the `__init__` and just return the `__new__` method.
|
||||
let returns_non_subclass = dunder_new_signature.overloads.iter().any(|signature| {
|
||||
signature.return_ty.is_some_and(|return_ty| {
|
||||
!return_ty.is_assignable_to(
|
||||
db,
|
||||
self_ty
|
||||
.to_instance(db)
|
||||
.expect("ClassType should be instantiable"),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
if returns_non_subclass {
|
||||
return dunder_new_bound_method;
|
||||
}
|
||||
Some(dunder_new_bound_method)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let dunder_new_bound_method = Type::Callable(CallableType::new(
|
||||
db,
|
||||
dunder_new_signature.bind_self(),
|
||||
true,
|
||||
));
|
||||
|
||||
if returns_non_subclass {
|
||||
return dunder_new_bound_method;
|
||||
}
|
||||
Some(dunder_new_bound_method)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let dunder_init_function_symbol = self_ty
|
||||
.member_lookup_with_policy(
|
||||
|
@ -864,6 +933,10 @@ impl<'db> ClassLiteral<'db> {
|
|||
self.known(db) == Some(known_class)
|
||||
}
|
||||
|
||||
pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool {
|
||||
self.is_known(db, KnownClass::Tuple)
|
||||
}
|
||||
|
||||
pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
|
||||
// Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code
|
||||
// the knowledge that this class is not generic.
|
||||
|
|
|
@ -511,7 +511,7 @@ impl TupleSpecialization {
|
|||
}
|
||||
|
||||
fn from_class(db: &dyn Db, class: ClassLiteral) -> Self {
|
||||
if class.is_known(db, KnownClass::Tuple) {
|
||||
if class.is_tuple(db) {
|
||||
Self::Yes
|
||||
} else {
|
||||
Self::No
|
||||
|
|
|
@ -5637,10 +5637,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
| KnownClass::TypeVar
|
||||
| KnownClass::NamedTuple
|
||||
| KnownClass::TypeAliasType
|
||||
| KnownClass::Tuple
|
||||
| KnownClass::Deprecated
|
||||
)
|
||||
)
|
||||
|
||||
// Constructor calls to `tuple` and subclasses of `tuple` are handled in `Type::Bindings`,
|
||||
// but constructor calls to `tuple[int]`, `tuple[int, ...]`, `tuple[int, *tuple[str, ...]]` (etc.)
|
||||
// are handled by the default constructor-call logic (we synthesize a `__new__` method for them
|
||||
// in `ClassType::own_class_member()`).
|
||||
&& (callable_type.is_generic_alias() || !class.is_known(self.db(), KnownClass::Tuple))
|
||||
|
||||
// temporary special-casing for all subclasses of `enum.Enum`
|
||||
// until we support the functional syntax for creating enum classes
|
||||
&& KnownClass::Enum
|
||||
|
@ -8003,7 +8009,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
// updating all of the subscript logic below to use custom callables for all of the _other_
|
||||
// special cases, too.
|
||||
if let Type::ClassLiteral(class) = value_ty {
|
||||
if class.is_known(self.db(), KnownClass::Tuple) {
|
||||
if class.is_tuple(self.db()) {
|
||||
return self
|
||||
.infer_tuple_type_expression(slice)
|
||||
.to_meta_type(self.db());
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue