ty: make constructor calls use bindings

This commit is contained in:
Hugo Polloli 2025-12-21 03:08:33 +01:00
parent b4c2825afd
commit ebb4b8bbb7
8 changed files with 545 additions and 489 deletions

View file

@ -60,6 +60,8 @@ class Foo:
reveal_type(Foo(1)) # revealed: Foo
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `int`, found `Literal["x"]`"
reveal_type(Foo("x")) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
reveal_type(Foo()) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 2, got 3"
@ -88,6 +90,23 @@ reveal_type(Foo()) # revealed: Foo
reveal_type(Foo(1, 2)) # revealed: Foo
```
## `__new__` present but `__init__` missing
`object.__init__` allows arbitrary arguments when a custom `__new__` exists. This should not trigger
`__init__` argument errors.
```py
class Foo:
def __new__(cls, x: int):
return object.__new__(cls)
reveal_type(Foo(1)) # revealed: Foo
Foo(1)
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 2, got 3"
Foo(1, 2)
```
## Conditional `__new__`
```py
@ -130,6 +149,30 @@ reveal_type(Foo(1)) # revealed: Foo
reveal_type(Foo()) # revealed: Foo
```
## `__new__` defined as a staticmethod
```py
class Foo:
@staticmethod
def __new__(cls, x: int):
return object.__new__(cls)
reveal_type(Foo(1)) # revealed: Foo
```
## `__new__` defined as a classmethod
```py
class Foo:
@classmethod
def __new__(cls, x: int):
return object.__new__(cls)
# error: [invalid-argument-type] "Argument to bound method `__new__` is incorrect: Expected `int`, found `<class 'Foo'>`"
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__new__`: expected 1, got 2"
Foo(1)
```
## A callable instance in place of `__new__`
### Bound
@ -202,6 +245,35 @@ reveal_type(Foo()) # revealed: Foo
reveal_type(Foo(1, 2)) # revealed: Foo
```
## Generic constructor inference
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, x: T) -> None: ...
reveal_type(Box(1)) # revealed: Box[int]
```
## Union of constructors
```py
class A:
def __init__(self, x: int) -> None:
self.x = x
class B:
def __init__(self, x: int) -> None:
self.x = x
def f(flag: bool):
cls = A if flag else B
reveal_type(cls(1)) # revealed: A | B
```
## `__init__` present on a superclass
If the `__init__` method is defined on a superclass, we can still infer the signature of the
@ -348,6 +420,23 @@ reveal_type(Foo(1)) # revealed: Foo
reveal_type(Foo(1, 2)) # revealed: Foo
```
### Conflicting parameter types
```py
class Foo:
def __new__(cls, x: int):
return object.__new__(cls)
def __init__(self, x: str) -> None:
self.x = x
# error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `str`, found `Literal[1]`"
Foo(1)
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `int`, found `Literal["x"]`"
Foo("x")
```
### Incompatible signatures
```py

View file

@ -150,6 +150,19 @@ reveal_type(f)
reveal_type(f(1))
```
### `functools.cached_property`
```py
from functools import cached_property
class Foo:
@cached_property
def foo(self) -> str:
return "a"
reveal_type(Foo().foo) # revealed: str
```
## Lambdas as decorators
```py

View file

@ -263,15 +263,13 @@ class C(Generic[T]):
x: T
c: C[int] = C()
# TODO: revealed: C[int]
reveal_type(c) # revealed: C[Unknown]
reveal_type(c) # revealed: C[int]
```
The typevars of a fully specialized generic class should no longer be visible:
```py
# TODO: revealed: int
reveal_type(c.x) # revealed: Unknown
reveal_type(c.x) # revealed: int
```
If the type parameter is not specified explicitly, and there are no constraints that let us infer a

View file

@ -226,15 +226,13 @@ class C[T]:
x: T
c: C[int] = C()
# TODO: revealed: C[int]
reveal_type(c) # revealed: C[Unknown]
reveal_type(c) # revealed: C[int]
```
The typevars of a fully specialized generic class should no longer be visible:
```py
# TODO: revealed: int
reveal_type(c.x) # revealed: Unknown
reveal_type(c.x) # revealed: int
```
If the type parameter is not specified explicitly, and there are no constraints that let us infer a

View file

@ -928,7 +928,9 @@ TMsg = TypeVar("TMsg", bound=Msg)
class Builder(Generic[TMsg]):
def build(self) -> Stream[TMsg]:
stream: Stream[TMsg] = Stream()
# TODO: no error
# `Stream` is invariant, so `Stream[Msg]` is not a supertype of `Stream[TMsg]`;
# therefore `_handler` is not compatible with `apply` here.
# error: [invalid-argument-type]
# error: [invalid-assignment]
stream = stream.apply(self._handler)
return stream

View file

@ -9,7 +9,7 @@ use std::time::Duration;
use bitflags::bitflags;
use call::{CallDunderError, CallError, CallErrorKind};
use context::InferContext;
use diagnostic::{INVALID_CONTEXT_MANAGER, NOT_ITERABLE, POSSIBLY_MISSING_IMPLICIT_CALL};
use diagnostic::{INVALID_CONTEXT_MANAGER, NOT_ITERABLE};
use ruff_db::Instant;
use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity};
use ruff_db::files::File;
@ -5988,11 +5988,11 @@ impl<'db> Type<'db> {
},
Type::ClassLiteral(class) => match class.known(db) {
// TODO: Ideally we'd use `try_call_constructor` for all constructor calls.
// TODO: Ideally we'd use `constructor_bindings` for all constructor calls.
// Currently we don't for a few special known types, either because their
// constructors are defined with overloads, or because we want to special case
// their return type beyond what typeshed provides (though this support could
// likely be moved into the `try_call_constructor` path). Once we support
// likely be moved into the `constructor_bindings` path). Once we support
// overloads, re-evaluate the need for these arms.
Some(KnownClass::Bool) => {
// ```py
@ -6365,20 +6365,22 @@ impl<'db> Type<'db> {
.into()
}
// Most class literal constructor calls are handled by `try_call_constructor` and
// Most class literal constructor calls are handled by `constructor_bindings` and
// not via getting the signature here. This signature can still be used in some
// cases (e.g. evaluating callable subtyping). TODO improve this definition
// (intersection of `__new__` and `__init__` signatures? and respect metaclass
// `__call__`).
_ => Binding::single(
self,
Signature::new_generic(
class.generic_context(db),
Parameters::gradual_form(),
self.to_instance(db),
),
)
.into(),
_ => self.constructor_bindings(db).unwrap_or_else(|| {
Binding::single(
self,
Signature::new_generic(
class.generic_context(db),
Parameters::gradual_form(),
self.to_instance(db),
),
)
.into()
}),
},
Type::SpecialForm(SpecialFormType::TypedDict) => {
@ -6411,7 +6413,7 @@ impl<'db> Type<'db> {
Binding::single(self, Signature::todo("functional `NamedTuple` syntax")).into()
}
Type::GenericAlias(_) => {
Type::GenericAlias(_) => self.constructor_bindings(db).unwrap_or_else(|| {
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
Binding::single(
@ -6419,16 +6421,19 @@ impl<'db> Type<'db> {
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
)
.into()
}
}),
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).bindings(db),
// Most type[] constructor calls are handled by `try_call_constructor` and not via
// Most type[] constructor calls are handled by `constructor_bindings` and not via
// getting the signature here. This signature can still be used in some cases (e.g.
// evaluating callable subtyping). TODO improve this definition (intersection of
// `__new__` and `__init__` signatures? and respect metaclass `__call__`).
SubclassOfInner::Class(class) => Type::from(class).bindings(db),
// evaluating callable subtyping) or when constructor bindings are not
// available. TODO improve this definition (intersection of `__new__` and
// `__init__` signatures? and respect metaclass `__call__`).
SubclassOfInner::Class(class) => self
.constructor_bindings(db)
.unwrap_or_else(|| Type::from(class).bindings(db)),
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
@ -6540,6 +6545,231 @@ impl<'db> Type<'db> {
}
}
// Build bindings for constructor calls by combining `__new__`/`__init__` signatures; return
// `None` for cases that keep their manual call signatures.
fn constructor_bindings(self, db: &'db dyn Db) -> Option<Bindings<'db>> {
fn resolve_dunder_new_callable<'db>(
db: &'db dyn Db,
owner: Type<'db>,
place: Place<'db>,
) -> Option<(Type<'db>, Definedness)> {
match place.try_call_dunder_get(db, owner) {
Place::Defined(callable, _, definedness) => Some((callable, definedness)),
Place::Undefined => None,
}
}
fn bind_constructor_new<'db>(
db: &'db dyn Db,
bindings: Bindings<'db>,
self_type: Type<'db>,
) -> Bindings<'db> {
bindings.map(|binding| {
let mut binding = binding;
// If descriptor binding produced a bound callable, bake that into the signature
// first, then bind `cls` for constructor-call semantics (the call site omits `cls`).
// Note: This intentionally preserves `type.__call__` behavior for `@classmethod __new__`,
// which receives an extra implicit `cls` and errors at call sites.
binding.bake_bound_type_into_overloads(db);
binding.bound_type = Some(self_type);
binding
})
}
let class = match self {
Type::ClassLiteral(class) => ClassType::NonGeneric(class),
Type::GenericAlias(alias) => ClassType::Generic(alias),
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
SubclassOfInner::Class(class) => class,
SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => return None,
},
_ => return None,
};
let (class_literal, class_specialization) = class.class_literal(db);
let class_generic_context = class_literal.generic_context(db);
// This helper is called from multiple `Type` variants (not just the `ClassLiteral` arm in
// `Type::bindings`), so manual-constructor exemptions must live here too.
if class_literal.is_typed_dict(db)
|| class::CodeGeneratorKind::TypedDict.matches(db, class_literal, class_specialization)
{
return None;
}
let known = class.known(db);
if matches!(
known,
Some(
KnownClass::Bool
| KnownClass::Str
| KnownClass::Type
| KnownClass::Object
| KnownClass::Property
| KnownClass::Super
| KnownClass::TypeAliasType
| KnownClass::Deprecated
)
) {
// Manual signatures for known constructors.
return None;
}
if matches!(known, Some(KnownClass::Tuple)) && !class.is_generic() {
// Non-generic tuple constructors are defined via overloads.
return None;
}
// temporary special-casing for all subclasses of `enum.Enum`
// until we support the functional syntax for creating enum classes
if KnownClass::Enum
.to_class_literal(db)
.to_class_type(db)
.is_some_and(|enum_class| class.is_subclass_of(db, enum_class))
{
return None;
}
// If we are trying to construct a non-specialized generic class, we should use the
// constructor parameters to try to infer the class specialization. To do this, we need to
// tweak our member lookup logic a bit. Normally, when looking up a class or instance
// member, we first apply the class's default specialization, and apply that specialization
// to the type of the member. To infer a specialization from the argument types, we need to
// have the class's typevars still in the method signature when we attempt to call it. To
// do this, we instead use the _identity_ specialization, which maps each of the class's
// generic typevars to itself.
let self_type = match self {
Type::ClassLiteral(class) if class.generic_context(db).is_some() => {
Type::from(class.identity_specialization(db))
}
_ => self,
};
// As of now we do not model custom `__call__` on meta-classes, so the code below
// only deals with interplay between `__new__` and `__init__` methods.
// The logic is roughly as follows:
// 1. If `__new__` is defined anywhere in the MRO (except for `object`, since it is always
// present), we validate the constructor arguments against it. We then validate `__init__`,
// but only if it is defined somewhere except `object`. This is because `object.__init__`
// allows arbitrary arguments if and only if `__new__` is defined, but typeshed
// defines `__init__` for `object` with no arguments.
// 2. If `__new__` is not found, we call `__init__`. Here, we allow it to fallback all
// the way to `object` (single `self` argument call). This time it is correct to
// fallback to `object.__init__`, since it will indeed check that no arguments are
// passed.
//
// Note that we currently ignore `__new__` return type, since we do not yet support `Self`
// and most builtin classes use it as return type annotation. We always return the instance
// type.
// Lookup `__new__` method in the MRO up to, but not including, `object`. Also, we must
// avoid `__new__` on `type` since per descriptor protocol, if `__new__` is not defined on
// a class, metaclass attribute would take precedence. But by avoiding `__new__` on
// `object` we would inadvertently unhide `__new__` on `type`, which is not what we want.
// An alternative might be to not skip `object.__new__` but instead mark it such that it's
// easy to check if that's the one we found?
// Note that `__new__` is a static method, so we must bind the `cls` argument when forming
// constructor-call bindings.
let new_method = self_type.lookup_dunder_new(db);
// Construct an instance type to look up `__init__`. We use `self_type` (possibly identity-
// specialized) so the instance retains inferable class typevars during constructor checking.
// TODO: we should use the actual return type of `__new__` to determine the instance type
let init_ty = self_type.to_instance(db)?;
// Lookup the `__init__` instance method in the MRO, excluding `object` initially; we only
// fall back to `object.__init__` in the `__new__`-absent case (see rules above).
let init_method_no_object = init_ty.member_lookup_with_policy(
db,
"__init__".into(),
MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
);
let mut missing_init_bindings = None;
let (new_bindings, has_any_new) = match new_method.as_ref().map(|method| method.place) {
Some(place) => match resolve_dunder_new_callable(db, self_type, place) {
Some((new_callable, definedness)) => {
let mut bindings =
bind_constructor_new(db, new_callable.bindings(db), self_type);
if definedness == Definedness::PossiblyUndefined {
bindings.set_implicit_dunder_new_is_possibly_unbound();
}
(Some((bindings, new_callable)), true)
}
None => (None, false),
},
None => (None, false),
};
// Only fall back to `object.__init__` when `__new__` is absent.
let init_bindings = match (&init_method_no_object.place, has_any_new) {
(Place::Defined(init_method, _, definedness), _) => {
let mut bindings = init_method.bindings(db);
if *definedness == Definedness::PossiblyUndefined {
bindings.set_implicit_dunder_init_is_possibly_unbound();
}
Some((bindings, *init_method))
}
(Place::Undefined, false) => {
let init_method_with_object = init_ty.member_lookup_with_policy(
db,
"__init__".into(),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
);
match init_method_with_object.place {
Place::Defined(init_method, _, definedness) => {
let mut bindings = init_method.bindings(db);
if definedness == Definedness::PossiblyUndefined {
bindings.set_implicit_dunder_init_is_possibly_unbound();
}
Some((bindings, init_method))
}
Place::Undefined => {
// If we are using vendored typeshed, it should be impossible to have missing
// or unbound `__init__` method on a class, as all classes have `object` in MRO.
// Thus the following may only trigger if a custom typeshed is used.
// Custom/broken typeshed: no `__init__` available even after falling back
// to `object`. Keep analysis going and surface the missing-implicit-call
// lint via the builder.
let mut bindings: Bindings<'db> = Binding::single(
self_type,
Signature::new(Parameters::gradual_form(), Some(init_ty)),
)
.into();
bindings.set_implicit_dunder_init_is_possibly_unbound();
missing_init_bindings = Some(bindings);
None
}
}
}
(Place::Undefined, true) => None,
};
let bindings = if let Some(bindings) = missing_init_bindings {
bindings
} else {
match (new_bindings, init_bindings) {
(Some((new_bindings, new_callable)), Some((init_bindings, init_callable))) => {
let callable_type = UnionBuilder::new(db)
.add(new_callable)
.add(init_callable)
.build();
// Use both `__new__` and `__init__` bindings so argument inference/checking
// happens under the combined constructor-call type context.
// In ty unions of callables are checked as "all must accept".
Bindings::from_union(callable_type, [new_bindings, init_bindings])
}
(Some((new_bindings, _)), None) => new_bindings,
(None, Some((init_bindings, _))) => init_bindings,
(None, None) => return None,
}
};
Some(
bindings
.with_generic_context(db, class_generic_context)
.with_constructor_instance_type(init_ty),
)
}
/// Calls `self`. Returns a [`CallError`] if `self` is (always or possibly) not callable, or if
/// the arguments are not compatible with the formal parameters.
///
@ -7076,258 +7306,6 @@ impl<'db> Type<'db> {
}
}
/// Given a class literal or non-dynamic `SubclassOf` type, try calling it (creating an instance)
/// and return the resulting instance type.
///
/// The `infer_argument_types` closure should be invoked with the signatures of `__new__` and
/// `__init__`, such that the argument types can be inferred with the correct type context.
///
/// Models `type.__call__` behavior.
/// TODO: model metaclass `__call__`.
///
/// E.g., for the following code, infer the type of `Foo()`:
/// ```python
/// class Foo:
/// pass
///
/// Foo()
/// ```
fn try_call_constructor<'ast>(
self,
db: &'db dyn Db,
infer_argument_types: impl FnOnce(Option<Bindings<'db>>) -> CallArguments<'ast, 'db>,
tcx: TypeContext<'db>,
) -> Result<Type<'db>, ConstructorCallError<'db>> {
debug_assert!(matches!(
self,
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_)
));
// If we are trying to construct a non-specialized generic class, we should use the
// constructor parameters to try to infer the class specialization. To do this, we need to
// tweak our member lookup logic a bit. Normally, when looking up a class or instance
// member, we first apply the class's default specialization, and apply that specialization
// to the type of the member. To infer a specialization from the argument types, we need to
// have the class's typevars still in the method signature when we attempt to call it. To
// do this, we instead use the _identity_ specialization, which maps each of the class's
// generic typevars to itself.
let (generic_origin, generic_context, self_type) = match self {
Type::ClassLiteral(class) => match class.generic_context(db) {
Some(generic_context) => (
Some(class),
Some(generic_context),
// It is important that identity_specialization specializes the class with
// _inferable_ typevars, so that our specialization inference logic will
// try to find a specialization for them.
Type::from(class.identity_specialization(db)),
),
_ => (None, None, self),
},
_ => (None, None, self),
};
// As of now we do not model custom `__call__` on meta-classes, so the code below
// only deals with interplay between `__new__` and `__init__` methods.
// The logic is roughly as follows:
// 1. If `__new__` is defined anywhere in the MRO (except for `object`, since it is always
// present), we call it and analyze outcome. We then analyze `__init__` call, but only
// if it is defined somewhere except object. This is because `object.__init__`
// allows arbitrary arguments if and only if `__new__` is defined, but typeshed
// defines `__init__` for `object` with no arguments.
// 2. If `__new__` is not found, we call `__init__`. Here, we allow it to fallback all
// the way to `object` (single `self` argument call). This time it is correct to
// fallback to `object.__init__`, since it will indeed check that no arguments are
// passed.
//
// Note that we currently ignore `__new__` return type, since we do not yet support `Self`
// and most builtin classes use it as return type annotation. We always return the instance
// type.
// Lookup `__new__` method in the MRO up to, but not including, `object`. Also, we must
// avoid `__new__` on `type` since per descriptor protocol, if `__new__` is not defined on
// a class, metaclass attribute would take precedence. But by avoiding `__new__` on
// `object` we would inadvertently unhide `__new__` on `type`, which is not what we want.
// An alternative might be to not skip `object.__new__` but instead mark it such that it's
// easy to check if that's the one we found?
// Note that `__new__` is a static method, so we must inject the `cls` argument.
let new_method = self_type.lookup_dunder_new(db);
// Construct an instance type that we can use to look up the `__init__` instance method.
// This performs the same logic as `Type::to_instance`, except for generic class literals.
// TODO: we should use the actual return type of `__new__` to determine the instance type
let init_ty = self_type
.to_instance(db)
.expect("type should be convertible to instance type");
// Lookup the `__init__` instance method in the MRO.
let init_method = init_ty.member_lookup_with_policy(
db,
"__init__".into(),
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)),
),
(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,
};
let argument_types = infer_argument_types(bindings);
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) => {
let argument_types = argument_types.with_self(Some(self_type));
let result = new_method
.bindings(db)
.with_constructor_instance_type(init_ty)
.match_parameters(db, &argument_types)
.check_types(db, &argument_types, tcx, &[]);
if boundness == Definedness::PossiblyUndefined {
Some(Err(DunderNewCallError::PossiblyUnbound(result.err())))
} else {
Some(result.map_err(DunderNewCallError::CallError))
}
}
Place::Undefined => None,
}
});
let init_call_outcome = if new_call_outcome.is_none() || !init_method.is_undefined() {
let call_result = match init_ty
.member_lookup_with_policy(
db,
"__init__".into(),
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.place
{
Place::Undefined => Err(CallDunderError::MethodNotAvailable),
Place::Defined(dunder_callable, _, boundness) => {
let bindings = dunder_callable
.bindings(db)
.with_constructor_instance_type(init_ty);
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)
}
})
}
};
Some(call_result)
} else {
None
};
// Note that we use `self` here, not `self_type`, so that if constructor argument inference
// fails, we fail back to the default specialization.
let instance_ty = self
.to_instance(db)
.expect("type should be convertible to instance type");
match (generic_origin, new_call_outcome, init_call_outcome) {
// All calls are successful or not called at all
(
Some(generic_origin),
new_call_outcome @ (None | Some(Ok(_))),
init_call_outcome @ (None | Some(Ok(_))),
) => {
fn combine_specializations<'db>(
db: &'db dyn Db,
s1: Option<Specialization<'db>>,
s2: Option<Specialization<'db>>,
) -> Option<Specialization<'db>> {
match (s1, s2) {
(None, None) => None,
(Some(s), None) | (None, Some(s)) => Some(s),
(Some(s1), Some(s2)) => Some(s1.combine(db, s2)),
}
}
let specialize_constructor = |outcome: Option<Bindings<'db>>| {
let (_, binding) = outcome
.as_ref()?
.single_element()?
.matching_overloads()
.next()?;
binding.specialization()?.restrict(db, generic_context?)
};
let new_specialization =
specialize_constructor(new_call_outcome.and_then(Result::ok));
let init_specialization =
specialize_constructor(init_call_outcome.and_then(Result::ok));
let specialization =
combine_specializations(db, new_specialization, init_specialization);
let specialized = specialization
.map(|specialization| {
Type::instance(
db,
generic_origin.apply_specialization(db, |_| specialization),
)
})
.unwrap_or(instance_ty);
Ok(specialized)
}
(None, None | Some(Ok(_)), None | Some(Ok(_))) => Ok(instance_ty),
(_, None | Some(Ok(_)), Some(Err(error))) => {
// no custom `__new__` or it was called and succeeded, but `__init__` failed.
Err(ConstructorCallError::Init(instance_ty, error))
}
(_, Some(Err(error)), None | Some(Ok(_))) => {
// custom `__new__` was called and failed, but init is ok
Err(ConstructorCallError::New(instance_ty, error))
}
(_, Some(Err(new_error)), Some(Err(init_error))) => {
// custom `__new__` was called and failed, and `__init__` is also not ok
Err(ConstructorCallError::NewAndInit(
instance_ty,
new_error,
init_error,
))
}
}
}
#[must_use]
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
@ -11872,107 +11850,6 @@ impl<'db> BoolError<'db> {
}
}
/// Represents possibly failure modes of implicit `__new__` calls.
#[derive(Debug)]
enum DunderNewCallError<'db> {
/// The call to `__new__` failed.
CallError(CallError<'db>),
/// The `__new__` method could be unbound. If the call to the
/// method has also failed, this variant also includes the
/// corresponding `CallError`.
PossiblyUnbound(Option<CallError<'db>>),
}
/// Error returned if a class instantiation call failed
#[derive(Debug)]
enum ConstructorCallError<'db> {
Init(Type<'db>, CallDunderError<'db>),
New(Type<'db>, DunderNewCallError<'db>),
NewAndInit(Type<'db>, DunderNewCallError<'db>, CallDunderError<'db>),
}
impl<'db> ConstructorCallError<'db> {
fn return_type(&self) -> Type<'db> {
match self {
Self::Init(ty, _) => *ty,
Self::New(ty, _) => *ty,
Self::NewAndInit(ty, _, _) => *ty,
}
}
fn report_diagnostic(
&self,
context: &InferContext<'db, '_>,
context_expression_type: Type<'db>,
context_expression_node: ast::AnyNodeRef,
) {
let report_init_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error {
CallDunderError::MethodNotAvailable => {
if let Some(builder) =
context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, context_expression_node)
{
// If we are using vendored typeshed, it should be impossible to have missing
// or unbound `__init__` method on a class, as all classes have `object` in MRO.
// Thus the following may only trigger if a custom typeshed is used.
builder.into_diagnostic(format_args!(
"`__init__` method is missing on type `{}`. \
Make sure your `object` in typeshed has its definition.",
context_expression_type.display(context.db()),
));
}
}
CallDunderError::PossiblyUnbound(bindings) => {
if let Some(builder) =
context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, context_expression_node)
{
builder.into_diagnostic(format_args!(
"Method `__init__` on type `{}` may be missing.",
context_expression_type.display(context.db()),
));
}
bindings.report_diagnostics(context, context_expression_node);
}
CallDunderError::CallError(_, bindings) => {
bindings.report_diagnostics(context, context_expression_node);
}
};
let report_new_error = |error: &DunderNewCallError<'db>| match error {
DunderNewCallError::PossiblyUnbound(call_error) => {
if let Some(builder) =
context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, context_expression_node)
{
builder.into_diagnostic(format_args!(
"Method `__new__` on type `{}` may be missing.",
context_expression_type.display(context.db()),
));
}
if let Some(CallError(_kind, bindings)) = call_error {
bindings.report_diagnostics(context, context_expression_node);
}
}
DunderNewCallError::CallError(CallError(_kind, bindings)) => {
bindings.report_diagnostics(context, context_expression_node);
}
};
match self {
Self::Init(_, init_call_dunder_error) => {
report_init_error(init_call_dunder_error);
}
Self::New(_, new_call_error) => {
report_new_error(new_call_error);
}
Self::NewAndInit(_, new_call_error, init_call_dunder_error) => {
report_new_error(new_call_error);
report_init_error(init_call_dunder_error);
}
}
}
}
/// A non-exhaustive enumeration of relations that can exist between types.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub(crate) enum TypeRelation<'db> {

View file

@ -37,7 +37,7 @@ use crate::types::function::{
OverloadLiteral,
};
use crate::types::generics::{
InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError,
GenericContext, InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError,
};
use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters};
use crate::types::tuple::{TupleLength, TupleSpec, TupleType};
@ -67,6 +67,12 @@ pub(crate) struct Bindings<'db> {
/// The type of the instance being constructed, if this signature is for a constructor.
constructor_instance_type: Option<Type<'db>>,
/// Whether implicit `__new__` calls may be missing in constructor bindings.
implicit_dunder_new_is_possibly_unbound: bool,
/// Whether implicit `__init__` calls may be missing in constructor bindings.
implicit_dunder_init_is_possibly_unbound: bool,
/// By using `SmallVec`, we avoid an extra heap allocation for the common case of a non-union
/// type.
elements: SmallVec<[CallableBinding<'db>; 1]>,
@ -82,16 +88,26 @@ impl<'db> Bindings<'db> {
where
I: IntoIterator<Item = Bindings<'db>>,
{
let elements: SmallVec<_> = elements
.into_iter()
.flat_map(|s| s.elements.into_iter())
.collect();
let mut implicit_dunder_new_is_possibly_unbound = false;
let mut implicit_dunder_init_is_possibly_unbound = false;
let mut elements_acc = SmallVec::new();
for set in elements {
implicit_dunder_new_is_possibly_unbound |= set.implicit_dunder_new_is_possibly_unbound;
implicit_dunder_init_is_possibly_unbound |=
set.implicit_dunder_init_is_possibly_unbound;
elements_acc.extend(set.elements);
}
let elements = elements_acc;
assert!(!elements.is_empty());
Self {
callable_type,
elements,
argument_forms: ArgumentForms::new(0),
constructor_instance_type: None,
implicit_dunder_new_is_possibly_unbound,
implicit_dunder_init_is_possibly_unbound,
}
}
@ -114,22 +130,59 @@ impl<'db> Bindings<'db> {
binding.constructor_instance_type = Some(constructor_instance_type);
for binding in &mut binding.overloads {
binding.constructor_instance_type = Some(constructor_instance_type);
binding.signature.return_ty = Some(constructor_instance_type);
}
}
self
}
pub(crate) fn with_generic_context(
mut self,
db: &'db dyn Db,
generic_context: Option<GenericContext<'db>>,
) -> Self {
let Some(generic_context) = generic_context else {
return self;
};
for binding in &mut self.elements {
for overload in &mut binding.overloads {
overload.signature.generic_context = GenericContext::merge_optional(
db,
overload.signature.generic_context,
Some(generic_context),
);
}
}
self
}
pub(crate) fn set_dunder_call_is_possibly_unbound(&mut self) {
for binding in &mut self.elements {
binding.dunder_call_is_possibly_unbound = true;
}
}
pub(crate) fn set_implicit_dunder_new_is_possibly_unbound(&mut self) {
self.implicit_dunder_new_is_possibly_unbound = true;
}
pub(crate) fn set_implicit_dunder_init_is_possibly_unbound(&mut self) {
self.implicit_dunder_init_is_possibly_unbound = true;
}
pub(crate) fn argument_forms(&self) -> &[Option<ParameterForm>] {
&self.argument_forms.values
}
pub(crate) fn has_implicit_dunder_new_is_possibly_unbound(&self) -> bool {
self.implicit_dunder_new_is_possibly_unbound
}
pub(crate) fn has_implicit_dunder_init_is_possibly_unbound(&self) -> bool {
self.implicit_dunder_init_is_possibly_unbound
}
pub(crate) fn iter(&self) -> std::slice::Iter<'_, CallableBinding<'db>> {
self.elements.iter()
}
@ -139,6 +192,8 @@ impl<'db> Bindings<'db> {
callable_type: self.callable_type,
argument_forms: self.argument_forms,
constructor_instance_type: self.constructor_instance_type,
implicit_dunder_new_is_possibly_unbound: self.implicit_dunder_new_is_possibly_unbound,
implicit_dunder_init_is_possibly_unbound: self.implicit_dunder_init_is_possibly_unbound,
elements: self.elements.into_iter().map(f).collect(),
}
}
@ -276,10 +331,47 @@ impl<'db> Bindings<'db> {
self.constructor_instance_type
}
// Constructor calls should combine `__new__`/`__init__` specializations instead of unioning.
fn constructor_return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
let constructor_instance_type = self.constructor_instance_type?;
let Some(class_specialization) = constructor_instance_type.class_specialization(db) else {
return Some(constructor_instance_type);
};
let class_context = class_specialization.generic_context(db);
let mut combined: Option<Specialization<'db>> = None;
for binding in &self.elements {
// For constructors, use the first matching overload (declaration order) to avoid
// merging incompatible constructor specializations.
let Some((_, overload)) = binding.matching_overloads().next() else {
continue;
};
let Some(specialization) = overload.specialization else {
continue;
};
let Some(specialization) = specialization.restrict(db, class_context) else {
continue;
};
combined = Some(match combined {
None => specialization,
Some(previous) => previous.combine(db, specialization),
});
}
// If constructor inference doesn't yield a specialization, fall back to the default
// specialization to avoid leaking inferable typevars in the constructed instance.
let specialization =
combined.unwrap_or_else(|| class_context.default_specialization(db, None));
Some(constructor_instance_type.apply_specialization(db, specialization))
}
/// Returns the return type of the call. For successful calls, this is the actual return type.
/// For calls with binding errors, this is a type that best approximates the return type. For
/// types that are not callable, returns `Type::Unknown`.
pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> {
if let Some(return_ty) = self.constructor_return_type(db) {
return return_ty;
}
if let [binding] = self.elements.as_slice() {
return binding.return_type();
}
@ -1451,6 +1543,8 @@ impl<'db> From<CallableBinding<'db>> for Bindings<'db> {
elements: smallvec_inline![from],
argument_forms: ArgumentForms::new(0),
constructor_instance_type: None,
implicit_dunder_new_is_possibly_unbound: false,
implicit_dunder_init_is_possibly_unbound: false,
}
}
}
@ -1474,6 +1568,8 @@ impl<'db> From<Binding<'db>> for Bindings<'db> {
elements: smallvec_inline![callable_binding],
argument_forms: ArgumentForms::new(0),
constructor_instance_type: None,
implicit_dunder_new_is_possibly_unbound: false,
implicit_dunder_init_is_possibly_unbound: false,
}
}
}
@ -1576,6 +1672,15 @@ impl<'db> CallableBinding<'db> {
}
}
pub(crate) fn bake_bound_type_into_overloads(&mut self, db: &'db dyn Db) {
let Some(bound_self) = self.bound_type.take() else {
return;
};
for overload in &mut self.overloads {
overload.signature = overload.signature.bind_self(db, Some(bound_self));
}
}
pub(crate) fn with_bound_type(mut self, bound_type: Type<'db>) -> Self {
self.bound_type = Some(bound_type);
self
@ -3677,7 +3782,11 @@ impl<'db> Binding<'db> {
for (keywords_index, keywords_type) in keywords_arguments {
matcher.match_keyword_variadic(db, keywords_index, keywords_type);
}
self.return_ty = self.signature.return_ty.unwrap_or(Type::unknown());
// For constructor calls, we currently return the constructed instance type (not `__init__`'s `None`).
self.return_ty = self
.constructor_instance_type
.or(self.signature.return_ty)
.unwrap_or(Type::unknown());
self.parameter_tys = vec![None; parameters.len()].into_boxed_slice();
self.variadic_argument_matched_to_variadic_parameter =
matcher.variadic_argument_matched_to_variadic_parameter;
@ -3719,10 +3828,6 @@ impl<'db> Binding<'db> {
self.return_ty
}
pub(crate) fn specialization(&self) -> Option<Specialization<'db>> {
self.specialization
}
/// Returns the bound types for each parameter, in parameter source order, or `None` if no
/// argument was matched to that parameter.
pub(crate) fn parameter_types(&self) -> &[Option<Type<'db>>] {

View file

@ -8458,6 +8458,36 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
callable_type: Type<'db>,
tcx: TypeContext<'db>,
) -> Type<'db> {
fn report_missing_implicit_constructor_call<'db>(
context: &InferContext<'db, '_>,
db: &'db dyn Db,
callable_type: Type<'db>,
call_expression: &ast::ExprCall,
bindings: &Bindings<'db>,
) {
if bindings.has_implicit_dunder_new_is_possibly_unbound() {
if let Some(builder) =
context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, call_expression)
{
builder.into_diagnostic(format_args!(
"Method `__new__` on type `{}` may be missing.",
callable_type.display(db),
));
}
}
if bindings.has_implicit_dunder_init_is_possibly_unbound() {
if let Some(builder) =
context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, call_expression)
{
builder.into_diagnostic(format_args!(
"Method `__init__` on type `{}` may be missing.",
callable_type.display(db),
));
}
}
}
let ast::ExprCall {
range: _,
node_index: _,
@ -8622,106 +8652,42 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// For class literals we model the entire class instantiation logic, so it is handled
// in a separate function. For some known classes we have manual signatures defined and use
// the `try_call` path below.
// TODO: it should be possible to move these special cases into the `try_call_constructor`
// path instead, or even remove some entirely once we support overloads fully.
let has_special_cased_constructor = matches!(
class.known(self.db()),
Some(
KnownClass::Bool
| KnownClass::Str
| KnownClass::Type
| KnownClass::Object
| KnownClass::Property
| KnownClass::Super
| KnownClass::TypeAliasType
| 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()`).
class.is_known(self.db(), KnownClass::Tuple) && !class.is_generic()
) || CodeGeneratorKind::TypedDict.matches(
self.db(),
class.class_literal(self.db()).0,
class.class_literal(self.db()).1,
);
// temporary special-casing for all subclasses of `enum.Enum`
// until we support the functional syntax for creating enum classes
if !has_special_cased_constructor
&& KnownClass::Enum
.to_class_literal(self.db())
.to_class_type(self.db())
.is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class))
{
// Inference of correctly-placed `TypeVar`, `ParamSpec`, and `NewType` definitions
// is done in `infer_legacy_typevar`, `infer_paramspec`, and
// `infer_newtype_expression`, and doesn't use the full call-binding machinery. If
// we reach here, it means that someone is trying to instantiate one of these in an
// invalid context.
match class.known(self.db()) {
Some(KnownClass::TypeVar | KnownClass::ExtensionsTypeVar) => {
if let Some(builder) = self
.context
.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic(
"A `TypeVar` definition must be a simple variable assignment",
);
}
}
Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec) => {
if let Some(builder) = self
.context
.report_lint(&INVALID_PARAMSPEC, call_expression)
{
builder.into_diagnostic(
"A `ParamSpec` definition must be a simple variable assignment",
);
}
}
Some(KnownClass::NewType) => {
if let Some(builder) =
self.context.report_lint(&INVALID_NEWTYPE, call_expression)
{
builder.into_diagnostic(
"A `NewType` definition must be a simple variable assignment",
);
}
}
_ => {}
}
let db = self.db();
let infer_call_arguments = |bindings: Option<Bindings<'db>>| {
if let Some(bindings) = bindings {
let bindings = bindings.match_parameters(self.db(), &call_arguments);
self.infer_all_argument_types(
arguments,
&mut call_arguments,
&bindings,
tcx,
MultiInferenceState::Intersect,
// Inference of correctly-placed `TypeVar`, `ParamSpec`, and `NewType` definitions
// is done in `infer_legacy_typevar`, `infer_paramspec`, and
// `infer_newtype_expression`, and doesn't use the full call-binding machinery. If
// we reach here, it means that someone is trying to instantiate one of these in an
// invalid context.
match class.known(self.db()) {
Some(KnownClass::TypeVar | KnownClass::ExtensionsTypeVar) => {
if let Some(builder) = self
.context
.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
{
builder.into_diagnostic(
"A `TypeVar` definition must be a simple variable assignment",
);
} else {
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
self.infer_argument_types(arguments, &mut call_arguments, &argument_forms);
}
call_arguments
};
return callable_type
.try_call_constructor(db, infer_call_arguments, tcx)
.unwrap_or_else(|err| {
err.report_diagnostic(&self.context, callable_type, call_expression.into());
err.return_type()
});
}
Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec) => {
if let Some(builder) = self
.context
.report_lint(&INVALID_PARAMSPEC, call_expression)
{
builder.into_diagnostic(
"A `ParamSpec` definition must be a simple variable assignment",
);
}
}
Some(KnownClass::NewType) => {
if let Some(builder) =
self.context.report_lint(&INVALID_NEWTYPE, call_expression)
{
builder.into_diagnostic(
"A `NewType` definition must be a simple variable assignment",
);
}
}
_ => {}
}
}
@ -8729,6 +8695,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.bindings(self.db())
.match_parameters(self.db(), &call_arguments);
report_missing_implicit_constructor_call(
&self.context,
self.db(),
callable_type,
call_expression,
&bindings,
);
let bindings_result =
self.infer_and_check_argument_types(arguments, &mut call_arguments, &mut bindings, tcx);