Always validate

This commit is contained in:
Charlie Marsh
2025-12-31 12:34:23 -05:00
parent 817336ad35
commit ebe4f00b82
4 changed files with 144 additions and 94 deletions

View File

@@ -7211,13 +7211,11 @@ impl<'db> Type<'db> {
_ => (None, None, self),
};
// Check for a custom `__call__` on the metaclass. If present and it returns a type
// that is not assignable to the class instance type, it takes precedence over the
// `__new__`/`__init__` logic (e.g., returning `int | Meta2` instead of `Class2`).
//
// However, if the metaclass `__call__` returns the class instance type (or a type
// variable that resolves to it), we still need to check `__new__`/`__init__` parameters,
// since the metaclass `__call__` is likely just delegating to `super().__call__()`.
// Check for a custom `__call__` on the metaclass. Following pyright's behavior:
// 1. If the metaclass has a custom `__call__` with a declared return type, validate it first
// 2. If there are argument errors, report them and return
// 3. If the return type is not assignable to the instance type, skip `__new__`/`__init__`
// 4. Otherwise, also validate `__new__`/`__init__`
let metaclass_dunder_call = self_type.member_lookup_with_policy(
db,
"__call__".into(),
@@ -7225,61 +7223,26 @@ impl<'db> Type<'db> {
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
);
if let Place::Defined(Type::BoundMethod(metaclass_dunder_call), _, boundness, _) =
metaclass_dunder_call.place
{
// Get the declared return type directly from the function signature.
// This avoids needing to perform argument matching just to determine the return type.
let signature = metaclass_dunder_call.function(db).signature(db);
let declared_return_type = signature
.overloads
.first()
.and_then(|sig| sig.return_ty)
.map(|ty| ty.filter_union(db, |elem| !elem.is_dynamic()))
.unwrap_or(Type::unknown());
// Get the instance type for comparison.
let instance_ty = self
.to_instance(db)
.expect("type should be convertible to instance type");
// Only use the metaclass `__call__` directly if:
// 1. The return type is not dynamic (has an explicit annotation).
// 2. The return type doesn't contain unresolved type variables (like `T`).
// 3. The return type is not assignable to the instance type.
//
// This handles cases like `Meta2.__call__` returning `int | Meta2`.
// If the return type has typevars (like `T` which would resolve to the instance),
// is dynamic, or is assignable to the instance, fall through to `__new__`/`__init__`.
if !declared_return_type.is_dynamic()
&& !declared_return_type.has_typevar(db)
&& !declared_return_type.is_assignable_to(db, instance_ty)
// Extract metaclass `__call__` info if it exists and has a declared return type.
let metaclass_call_info =
if let Place::Defined(Type::BoundMethod(metaclass_dunder_call), _, boundness, _) =
metaclass_dunder_call.place
{
// Use the metaclass `__call__` for argument checking.
let bindings = Type::BoundMethod(metaclass_dunder_call).bindings(db);
let argument_types = infer_argument_types(Some(bindings.clone()));
let call_result = bindings
.match_parameters(db, &argument_types)
.check_types(db, &argument_types, tcx, &[])
.map_err(CallDunderError::from)
.and_then(|bindings| {
if boundness == Definedness::PossiblyUndefined {
Err(CallDunderError::PossiblyUnbound(Box::new(bindings)))
} else {
Ok(bindings)
}
});
return match call_result {
Ok(bindings) => Ok(bindings.return_type(db)),
Err(error) => Err(ConstructorCallError::MetaclassCall(
error.fallback_return_type(db),
error,
)),
};
}
}
let signature = metaclass_dunder_call.function(db).signature(db);
// Only use metaclass `__call__` if it has a declared return type.
// If return type is unannotated, fall through to `__new__`/`__init__`.
let has_declared_return = signature
.overloads
.iter()
.any(|sig| sig.return_ty.is_some());
if has_declared_return {
Some((metaclass_dunder_call, boundness))
} else {
None
}
} else {
None
};
// The code below deals with interplay between `__new__` and `__init__` methods.
// The logic is roughly as follows:
@@ -7318,42 +7281,105 @@ impl<'db> Type<'db> {
MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
);
// Infer the call argument types, using both `__new__` and `__init__` for type-context.
let bindings = match (
new_method.as_ref().map(|method| &method.place),
&init_method.place,
) {
(Some(Place::Defined(new_method, ..)), Place::Undefined) => Some(
new_method
.bindings(db)
.map(|binding| binding.with_bound_type(self_type)),
),
// Infer the call argument types using the appropriate bindings for type-context.
// If metaclass `__call__` has a declared return type, use its bindings.
// Otherwise, use combined `__new__` and `__init__` bindings.
let bindings = if metaclass_call_info.is_some() {
// Use metaclass `__call__` bindings for argument inference.
metaclass_call_info
.as_ref()
.map(|(method, _)| Type::BoundMethod(*method).bindings(db))
} else {
// Use `__new__` and `__init__` bindings for argument inference.
match (
new_method.as_ref().map(|method| &method.place),
&init_method.place,
) {
(Some(Place::Defined(new_method, ..)), Place::Undefined) => Some(
new_method
.bindings(db)
.map(|binding| binding.with_bound_type(self_type)),
),
(Some(Place::Undefined) | None, Place::Defined(init_method, ..)) => {
Some(init_method.bindings(db))
(Some(Place::Undefined) | None, Place::Defined(init_method, ..)) => {
Some(init_method.bindings(db))
}
(Some(Place::Defined(new_method, ..)), Place::Defined(init_method, ..)) => {
let callable = UnionBuilder::new(db)
.add(*new_method)
.add(*init_method)
.build();
let new_method_bindings = new_method
.bindings(db)
.map(|binding| binding.with_bound_type(self_type));
Some(Bindings::from_union(
callable,
[new_method_bindings, init_method.bindings(db)],
))
}
_ => None,
}
(Some(Place::Defined(new_method, ..)), Place::Defined(init_method, ..)) => {
let callable = UnionBuilder::new(db)
.add(*new_method)
.add(*init_method)
.build();
let new_method_bindings = new_method
.bindings(db)
.map(|binding| binding.with_bound_type(self_type));
Some(Bindings::from_union(
callable,
[new_method_bindings, init_method.bindings(db)],
))
}
_ => None,
};
let argument_types = infer_argument_types(bindings);
// If metaclass `__call__` exists with a declared return type, validate it first.
// Following pyright's behavior:
// - If there are argument errors, return the metaclass error
// - If the return type is not assignable to the instance type, return the metaclass result
// - Otherwise, continue to validate `__new__`/`__init__`
if let Some((metaclass_dunder_call, boundness)) = metaclass_call_info {
let bindings = Type::BoundMethod(metaclass_dunder_call).bindings(db);
let call_result = bindings
.clone()
.match_parameters(db, &argument_types)
.check_types(db, &argument_types, tcx, &[]);
let metaclass_return_type = call_result
.as_ref()
.map_or_else(|err| err.1.return_type(db), |b| b.return_type(db));
// Get the instance type for comparison.
let instance_ty = self
.to_instance(db)
.expect("type should be convertible to instance type");
// Check if we should skip `__new__`/`__init__` evaluation.
// Skip if: return type is not assignable to instance, is Never, or contains Any.
let skip_new_init = !metaclass_return_type.is_assignable_to(db, instance_ty)
|| metaclass_return_type.is_never()
|| matches!(metaclass_return_type, Type::Dynamic(DynamicType::Any));
// If there are argument errors or we should skip `__new__`/`__init__`, return metaclass result.
if call_result.is_err() || skip_new_init {
let call_result = call_result
.map_err(CallDunderError::from)
.and_then(|bindings| {
if boundness == Definedness::PossiblyUndefined {
Err(CallDunderError::PossiblyUnbound(Box::new(bindings)))
} else {
Ok(bindings)
}
});
return match call_result {
Ok(bindings) => Ok(bindings.return_type(db)),
Err(error) => Err(ConstructorCallError::MetaclassCall(
error.fallback_return_type(db),
error,
)),
};
}
// Metaclass `__call__` succeeded and returns instance type.
// Continue to validate `__new__`/`__init__` below.
}
let new_call_outcome = new_method.and_then(|new_method| {
match new_method.place.try_call_dunder_get(db, self_type) {
Place::Defined(new_method, _, boundness, _) => {