diff --git a/crates/ty_python_semantic/resources/mdtest/call/constructor.md b/crates/ty_python_semantic/resources/mdtest/call/constructor.md index b6e65fcdf2..69a701b884 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/ty_python_semantic/resources/mdtest/call/constructor.md @@ -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 ``" +# 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 diff --git a/crates/ty_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md index 124e2d9a82..149afe50f4 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 30d6a89ec0..48a95c66b1 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 06680d2168..2d36359d82 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 3cdebe848e..342f5448f3 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -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 diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 2ec65ea8bf..4cd13e8c5a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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> { + 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>) -> CallArguments<'ast, 'db>, - tcx: TypeContext<'db>, - ) -> Result, 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>, - s2: Option>, - ) -> Option> { - 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>| { - 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> { 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>), -} - -/// 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> { diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 14bb62ff4e..57075b4bc8 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -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>, + /// 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>, { - 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>, + ) -> 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] { &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> { + 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> = 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> 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> 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> { - 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>] { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 28061e2e92..86d8e3066f 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -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>| { - 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);