diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/new_types.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/new_types.md index c2f2fc2820..825ca9b27d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/new_types.md @@ -8,7 +8,11 @@ Currently, red-knot doesn't support `typing.NewType` in type annotations. from typing_extensions import NewType from types import GenericAlias +X = GenericAlias(type, ()) A = NewType("A", int) +# TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased +# to be compatible with `type` +# error: [invalid-argument-type] "Object of type `NewType` cannot be assigned to parameter 2 (`origin`) of function `__new__`; expected type `type`" B = GenericAlias(A, ()) def _( diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md b/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md index b9927fe086..763b6bd30b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md @@ -1,7 +1,325 @@ # Constructor +When classes are instantiated, Python calls the meta-class `__call__` method, which can either be +customized by the user or `type.__call__` is used. + +The latter calls the `__new__` method of the class, which is responsible for creating the instance +and then calls the `__init__` method on the resulting instance to initialize it with the same +arguments. + +Both `__new__` and `__init__` are looked up using full descriptor protocol, but `__new__` is then +called as an implicit static, rather than bound method with `cls` passed as the first argument. +`__init__` has no special handling, it is fetched as bound method and is called just like any other +dunder method. + +`type.__call__` does other things too, but this is not yet handled by us. + +Since every class has `object` in it's MRO, the default implementations are `object.__new__` and +`object.__init__`. They have some special behavior, namely: + +- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for `object`) + \- no arguments are accepted and `TypeError` is raised if any are passed. +- If `__new__` is defined, but `__init__` is not - `object.__init__` will allow arbitrary arguments! + +As of today there are a number of behaviors that we do not support: + +- `__new__` is assumed to return an instance of the class on which it is called +- User defined `__call__` on metaclass is ignored + +## Creating an instance of the `object` class itself + +Test the behavior of the `object` class itself. As implementation has to ignore `object` own methods +as defined in typeshed due to behavior not expressible in typeshed (see above how `__init__` behaves +differently depending on whether `__new__` is defined or not), we have to test the behavior of +`object` itself. + +```py +reveal_type(object()) # revealed: object + +# error: [too-many-positional-arguments] "Too many positional arguments to class `object`: expected 0, got 1" +reveal_type(object(1)) # revealed: object +``` + +## No init or new + ```py class Foo: ... reveal_type(Foo()) # revealed: Foo + +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1" +reveal_type(Foo(1)) # revealed: Foo +``` + +## `__new__` present on the class itself + +```py +class Foo: + def __new__(cls, x: int) -> "Foo": + return object.__new__(cls) + +reveal_type(Foo(1)) # 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 1, got 2" +reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## `__new__` present on a superclass + +If the `__new__` method is defined on a superclass, we can still infer the signature of the +constructor from it. + +```py +from typing_extensions import Self + +class Base: + def __new__(cls, x: int) -> Self: ... + +class Foo(Base): ... + +reveal_type(Foo(1)) # 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 1, got 2" +reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## Conditional `__new__` + +```py +def _(flag: bool) -> None: + class Foo: + if flag: + def __new__(cls, x: int): ... + else: + def __new__(cls, x: int, y: int = 1): ... + + reveal_type(Foo(1)) # revealed: Foo + # error: [invalid-argument-type] "Object of type `Literal["1"]` cannot be assigned to parameter 2 (`x`) of function `__new__`; expected type `int`" + reveal_type(Foo("1")) # 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 1, got 2" + reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## A descriptor in place of `__new__` + +```py +class SomeCallable: + def __call__(self, cls, x: int) -> "Foo": + obj = object.__new__(cls) + obj.x = x + return obj + +class Descriptor: + def __get__(self, instance, owner) -> SomeCallable: + return SomeCallable() + +class Foo: + __new__: Descriptor = Descriptor() + +reveal_type(Foo(1)) # revealed: Foo +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" +reveal_type(Foo()) # revealed: Foo +``` + +## A callable instance in place of `__new__` + +### Bound + +```py +class Callable: + def __call__(self, cls, x: int) -> "Foo": + return object.__new__(cls) + +class Foo: + __new__ = Callable() + +reveal_type(Foo(1)) # revealed: Foo +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" +reveal_type(Foo()) # revealed: Foo +``` + +### Possibly Unbound + +```py +def _(flag: bool) -> None: + class Callable: + if flag: + def __call__(self, cls, x: int) -> "Foo": + return object.__new__(cls) + + class Foo: + __new__ = Callable() + + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + reveal_type(Foo(1)) # revealed: Foo + # TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" + # but we currently infer the signature of `__call__` as unknown, so it accepts any arguments + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + reveal_type(Foo()) # revealed: Foo +``` + +## `__init__` present on the class itself + +If the class has an `__init__` method, we can infer the signature of the constructor from it. + +```py +class Foo: + def __init__(self, x: int): ... + +reveal_type(Foo(1)) # revealed: Foo + +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo()) # revealed: Foo +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" +reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## `__init__` present on a superclass + +If the `__init__` method is defined on a superclass, we can still infer the signature of the +constructor from it. + +```py +class Base: + def __init__(self, x: int): ... + +class Foo(Base): ... + +reveal_type(Foo(1)) # revealed: Foo + +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo()) # revealed: Foo +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" +reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## Conditional `__init__` + +```py +def _(flag: bool) -> None: + class Foo: + if flag: + def __init__(self, x: int): ... + else: + def __init__(self, x: int, y: int = 1): ... + + reveal_type(Foo(1)) # revealed: Foo + # error: [invalid-argument-type] "Object of type `Literal["1"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `int`" + reveal_type(Foo("1")) # revealed: Foo + # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" + reveal_type(Foo()) # revealed: Foo + # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" + reveal_type(Foo(1, 2)) # revealed: Foo +``` + +## A descriptor in place of `__init__` + +```py +class SomeCallable: + # TODO: at runtime `__init__` is checked to return `None` and + # a `TypeError` is raised if it doesn't. However, apparently + # this is not true when the descriptor is used as `__init__`. + # However, we may still want to check this. + def __call__(self, x: int) -> str: + return "a" + +class Descriptor: + def __get__(self, instance, owner) -> SomeCallable: + return SomeCallable() + +class Foo: + __init__: Descriptor = Descriptor() + +reveal_type(Foo(1)) # revealed: Foo +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" +reveal_type(Foo()) # revealed: Foo +``` + +## A callable instance in place of `__init__` + +### Bound + +```py +class Callable: + def __call__(self, x: int) -> None: + pass + +class Foo: + __init__ = Callable() + +reveal_type(Foo(1)) # revealed: Foo +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" +reveal_type(Foo()) # revealed: Foo +``` + +### Possibly Unbound + +```py +def _(flag: bool) -> None: + class Callable: + if flag: + def __call__(self, x: int) -> None: + pass + + class Foo: + __init__ = Callable() + + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + reveal_type(Foo(1)) # revealed: Foo + # TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" + # but we currently infer the signature of `__call__` as unknown, so it accepts any arguments + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + reveal_type(Foo()) # revealed: Foo +``` + +## `__new__` and `__init__` both present + +### Identical signatures + +A common case is to have `__new__` and `__init__` with identical signatures (except for the first +argument). We report errors for both `__new__` and `__init__` if the arguments are incorrect. + +At runtime `__new__` is called first and will fail without executing `__init__` if the arguments are +incorrect. However, we decided that it is better to report errors for both methods, since after +fixing the `__new__` method, the user may forget to fix the `__init__` method. + +```py +class Foo: + def __new__(cls, x: int) -> "Foo": + return object.__new__(cls) + + def __init__(self, x: int): ... + +# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`" +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo()) # revealed: Foo + +reveal_type(Foo(1)) # revealed: Foo +``` + +### Compatible signatures + +But they can also be compatible, but not identical. We should correctly report errors only for the +mthod that would fail. + +```py +class Foo: + def __new__(cls, *args, **kwargs): + return object.__new__(cls) + + def __init__(self, x: int) -> None: + self.x = x + +# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" +reveal_type(Foo()) # revealed: Foo +reveal_type(Foo(1)) # revealed: Foo + +# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" +reveal_type(Foo(1, 2)) # revealed: Foo ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/subclass_of.md b/crates/red_knot_python_semantic/resources/mdtest/call/subclass_of.md index 37082cfaa4..747367b03f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/subclass_of.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/subclass_of.md @@ -20,9 +20,11 @@ class C: def _(subclass_of_c: type[C]): reveal_type(subclass_of_c(1)) # revealed: C - # TODO: Those should all be errors + # error: [invalid-argument-type] "Object of type `Literal["a"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `int`" reveal_type(subclass_of_c("a")) # revealed: C + # error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`" reveal_type(subclass_of_c()) # revealed: C + # error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2" reveal_type(subclass_of_c(1, 2)) # revealed: C ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md index d6f78f1fed..d22660fe81 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -111,6 +111,8 @@ class E[T]: def __init__(self, x: T) -> None: ... # TODO: revealed: E[int] or E[Literal[1]] +# TODO should not emit an error +# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `T`" reveal_type(E(1)) # revealed: E ``` @@ -118,7 +120,8 @@ The types inferred from a type context and from a constructor parameter must be other: ```py -# TODO: error +# TODO: the error should not leak the `T` typevar and should mention `E[int]` +# error: [invalid-argument-type] "Object of type `Literal["five"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `T`" wrong_innards: E[int] = E("five") ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/invert_add_usub.md b/crates/red_knot_python_semantic/resources/mdtest/unary/invert_add_usub.md index 98d9c9b1c0..6b07cb1d2e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unary/invert_add_usub.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unary/invert_add_usub.md @@ -18,7 +18,7 @@ class Number: def __invert__(self) -> Literal[True]: return True -a = Number() +a = Number(0) reveal_type(+a) # revealed: int reveal_type(-a) # revealed: int diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index a6fc3c289b..4351f41488 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use bitflags::bitflags; use call::{CallDunderError, CallError, CallErrorKind}; use context::InferContext; -use diagnostic::{INVALID_CONTEXT_MANAGER, NOT_ITERABLE}; +use diagnostic::{CALL_POSSIBLY_UNBOUND_METHOD, INVALID_CONTEXT_MANAGER, NOT_ITERABLE}; use itertools::EitherOrBoth; use ruff_db::files::{File, FileRange}; use ruff_python_ast as ast; @@ -151,20 +151,60 @@ enum InstanceFallbackShadowsNonDataDescriptor { No, } -/// Dunder methods are looked up on the meta-type of a type without potentially falling -/// back on attributes on the type itself. For example, when implicitly invoked on an -/// instance, dunder methods are not looked up as instance attributes. And when invoked -/// on a class, dunder methods are only looked up on the metaclass, not the class itself. -/// -/// All other attributes use the `WithInstanceFallback` policy. -#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)] -enum MemberLookupPolicy { +bitflags! { + #[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)] + pub(crate) struct MemberLookupPolicy: u8 { + /// Dunder methods are looked up on the meta-type of a type without potentially falling + /// back on attributes on the type itself. For example, when implicitly invoked on an + /// instance, dunder methods are not looked up as instance attributes. And when invoked + /// on a class, dunder methods are only looked up on the metaclass, not the class itself. + /// + /// All other attributes use the `WithInstanceFallback` policy. + /// + /// If this flag is set - look up the attribute on the meta-type only. + const NO_INSTANCE_FALLBACK = 1 << 0; + + /// When looking up an attribute on a class, we sometimes need to avoid + /// looking up attributes defined on the `object` class. Usually because + /// typeshed doesn't properly encode runtime behavior (e.g. see how `__new__` & `__init__` + /// are handled during class creation). + /// + /// If this flag is set - exclude attributes defined on `object` when looking up attributes. + const MRO_NO_OBJECT_FALLBACK = 1 << 1; + + /// When looking up an attribute on a class, we sometimes need to avoid + /// looking up attributes defined on `type` if this is the metaclass of the class. + /// + /// This is similar to no object fallback above + const META_CLASS_NO_TYPE_FALLBACK = 1 << 2; + } +} + +impl MemberLookupPolicy { /// Only look up the attribute on the meta-type. - NoInstanceFallback, - /// Look up the attribute on the meta-type, but fall back to attributes on the instance + /// + /// If false - Look up the attribute on the meta-type, but fall back to attributes on the instance /// if the meta-type attribute is not found or if the meta-type attribute is not a data /// descriptor. - WithInstanceFallback, + pub(crate) const fn no_instance_fallback(self) -> bool { + self.contains(Self::NO_INSTANCE_FALLBACK) + } + + /// Exclude attributes defined on `object` when looking up attributes. + pub(crate) const fn mro_no_object_fallback(self) -> bool { + self.contains(Self::MRO_NO_OBJECT_FALLBACK) + } + + /// Exclude attributes defined on `type` when looking up meta-class-attributes. + pub(crate) const fn meta_class_no_type_fallback(self) -> bool { + self.contains(Self::META_CLASS_NO_TYPE_FALLBACK) + } +} + +impl Default for MemberLookupPolicy { + fn default() -> Self { + Self::empty() + } } impl AttributeKind { @@ -444,6 +484,10 @@ impl<'db> Type<'db> { .expect("Expected a Type::ClassLiteral variant") } + pub const fn is_subclass_of(&self) -> bool { + matches!(self, Type::SubclassOf(..)) + } + pub const fn is_class_literal(&self) -> bool { matches!(self, Type::ClassLiteral(..)) } @@ -1840,9 +1884,18 @@ impl<'db> Type<'db> { /// [descriptor guide]: https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance /// [`_PyType_Lookup`]: https://github.com/python/cpython/blob/e285232c76606e3be7bf216efb1be1e742423e4b/Objects/typeobject.c#L5223 fn find_name_in_mro(&self, db: &'db dyn Db, name: &str) -> Option> { + self.find_name_in_mro_with_policy(db, name, MemberLookupPolicy::default()) + } + + fn find_name_in_mro_with_policy( + &self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> Option> { match self { Type::Union(union) => Some(union.map_with_boundness_and_qualifiers(db, |elem| { - elem.find_name_in_mro(db, name) + elem.find_name_in_mro_with_policy(db, name, policy) // If some elements are classes, and some are not, we simply fall back to `Unbound` for the non-class // elements instead of short-circuiting the whole result to `None`. We would need a more detailed // return type otherwise, and since `find_name_in_mro` is usually called via `class_member`, this is @@ -1851,7 +1904,7 @@ impl<'db> Type<'db> { })), Type::Intersection(inter) => { Some(inter.map_with_boundness_and_qualifiers(db, |elem| { - elem.find_name_in_mro(db, name) + elem.find_name_in_mro_with_policy(db, name, policy) // Fall back to Unbound, similar to the union case (see above). .unwrap_or_default() })) @@ -1903,7 +1956,7 @@ impl<'db> Type<'db> { "__get__" | "__set__" | "__delete__", ) => Some(Symbol::Unbound.into()), - _ => Some(class_literal.class_member(db, name)), + _ => Some(class_literal.class_member(db, name, policy)), } } @@ -1926,7 +1979,9 @@ impl<'db> Type<'db> { { Some(Symbol::Unbound.into()) } - Type::SubclassOf(subclass_of_ty) => subclass_of_ty.find_name_in_mro(db, name), + Type::SubclassOf(subclass_of_ty) => { + subclass_of_ty.find_name_in_mro_with_policy(db, name, policy) + } // We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type). // So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the @@ -1934,7 +1989,7 @@ impl<'db> Type<'db> { Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Type) => { KnownClass::Object .to_class_literal(db) - .find_name_in_mro(db, name) + .find_name_in_mro_with_policy(db, name, policy) } Type::FunctionLiteral(_) @@ -1964,17 +2019,28 @@ impl<'db> Type<'db> { /// /// Basically corresponds to `self.to_meta_type().find_name_in_mro(name)`, except for the handling /// of union and intersection types. - #[salsa::tracked] fn class_member(self, db: &'db dyn Db, name: Name) -> SymbolAndQualifiers<'db> { + self.class_member_with_policy(db, name, MemberLookupPolicy::default()) + } + + #[salsa::tracked] + fn class_member_with_policy( + self, + db: &'db dyn Db, + name: Name, + policy: MemberLookupPolicy, + ) -> SymbolAndQualifiers<'db> { tracing::trace!("class_member: {}.{}", self.display(db), name); match self { - Type::Union(union) => union - .map_with_boundness_and_qualifiers(db, |elem| elem.class_member(db, name.clone())), - Type::Intersection(inter) => inter - .map_with_boundness_and_qualifiers(db, |elem| elem.class_member(db, name.clone())), + Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| { + elem.class_member_with_policy(db, name.clone(), policy) + }), + Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, |elem| { + elem.class_member_with_policy(db, name.clone(), policy) + }), _ => self .to_meta_type(db) - .find_name_in_mro(db, name.as_str()) + .find_name_in_mro_with_policy(db, name.as_str(), policy) .expect( "`Type::find_name_in_mro()` should return `Some()` when called on a meta-type", ), @@ -2235,6 +2301,7 @@ impl<'db> Type<'db> { name: &str, fallback: SymbolAndQualifiers<'db>, policy: InstanceFallbackShadowsNonDataDescriptor, + member_policy: MemberLookupPolicy, ) -> SymbolAndQualifiers<'db> { let ( SymbolAndQualifiers { @@ -2244,7 +2311,7 @@ impl<'db> Type<'db> { meta_attr_kind, ) = Self::try_call_dunder_get_on_attribute( db, - self.class_member(db, name.into()), + self.class_member_with_policy(db, name.into(), member_policy), self, self.to_meta_type(db), ); @@ -2323,7 +2390,7 @@ impl<'db> Type<'db> { /// lookup, like a failed `__get__` call on a descriptor. #[must_use] pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - self.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::WithInstanceFallback) + self.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::default()) } /// Similar to [`Type::member`], but allows the caller to specify what policy should be used @@ -2454,15 +2521,17 @@ impl<'db> Type<'db> { Type::ModuleLiteral(module) => module.static_member(db, name_str).into(), - Type::AlwaysFalsy | Type::AlwaysTruthy => self.class_member(db, name), + Type::AlwaysFalsy | Type::AlwaysTruthy => { + self.class_member_with_policy(db, name, policy) + } - _ if policy == MemberLookupPolicy::NoInstanceFallback => self - .invoke_descriptor_protocol( - db, - name_str, - Symbol::Unbound.into(), - InstanceFallbackShadowsNonDataDescriptor::No, - ), + _ if policy.no_instance_fallback() => self.invoke_descriptor_protocol( + db, + name_str, + Symbol::Unbound.into(), + InstanceFallbackShadowsNonDataDescriptor::No, + policy, + ), Type::Instance(..) | Type::BooleanLiteral(..) @@ -2483,15 +2552,21 @@ impl<'db> Type<'db> { name_str, fallback, InstanceFallbackShadowsNonDataDescriptor::No, + policy, ); let custom_getattr_result = || { - // Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with dynamic imports. - // We explicitly hide it here to prevent arbitrary attributes from being available on modules. - if self - .into_instance() - .is_some_and(|instance| instance.class.is_known(db, KnownClass::ModuleType)) - { + // Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with + // dynamic imports. We explicitly hide it here to prevent arbitrary attributes + // from being available on modules. Same for `types.GenericAlias` - its + // `__getattr__` method will delegate to `__origin__` to allow looking up + // attributes on the original type. But in typeshed its return type is `Any`. + // It will need a special handling, so it remember the origin type to properly + // resolve the attribute. + if self.into_instance().is_some_and(|instance| { + instance.class.is_known(db, KnownClass::ModuleType) + || instance.class.is_known(db, KnownClass::GenericAlias) + }) { return Symbol::Unbound.into(); } @@ -2525,7 +2600,7 @@ impl<'db> Type<'db> { } Type::ClassLiteral(..) | Type::SubclassOf(..) => { - let class_attr_plain = self.find_name_in_mro(db, name_str).expect( + let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str, policy).expect( "Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`", ); @@ -2550,6 +2625,7 @@ impl<'db> Type<'db> { name_str, class_attr_fallback, InstanceFallbackShadowsNonDataDescriptor::Yes, + policy, ) } } @@ -3066,6 +3142,12 @@ impl<'db> Type<'db> { }, Type::ClassLiteral(ClassLiteralType { class }) => match class.known(db) { + // TODO: Ideally we'd use `try_call_constructor` 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 + // overloads, re-evaluate the need for these arms. Some(KnownClass::Bool) => { // ```py // class bool(int): @@ -3165,6 +3247,21 @@ impl<'db> Type<'db> { ); Signatures::single(signature) } + Some(KnownClass::Object) => { + // ```py + // class object: + // def __init__(self) -> None: ... + // def __new__(cls) -> Self: ... + // ``` + let signature = CallableSignature::from_overloads( + self, + [Signature::new( + Parameters::empty(), + Some(KnownClass::Object.to_instance(db)), + )], + ); + Signatures::single(signature) + } Some(KnownClass::Property) => { let getter_signature = Signature::new( @@ -3234,8 +3331,11 @@ impl<'db> Type<'db> { Signatures::single(signature) } - // TODO annotated return type on `__new__` or metaclass `__call__` - // TODO check call vs signatures of `__new__` and/or `__init__` + // Most class literal constructor calls are handled by `try_call_constructor` 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__`). _ => { let signature = CallableSignature::single( self, @@ -3247,6 +3347,10 @@ impl<'db> Type<'db> { Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).signatures(db), + // Most type[] constructor calls are handled by `try_call_constructor` 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__`). ClassBase::Class(class) => Type::class_literal(class).signatures(db), }, @@ -3260,7 +3364,7 @@ impl<'db> Type<'db> { .member_lookup_with_policy( db, Name::new_static("__call__"), - MemberLookupPolicy::NoInstanceFallback, + MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) .symbol { @@ -3324,7 +3428,7 @@ impl<'db> Type<'db> { mut argument_types: CallArgumentTypes<'_, 'db>, ) -> Result, CallDunderError<'db>> { match self - .member_lookup_with_policy(db, name.into(), MemberLookupPolicy::NoInstanceFallback) + .member_lookup_with_policy(db, name.into(), MemberLookupPolicy::NO_INSTANCE_FALLBACK) .symbol { Symbol::Type(dunder_callable, boundness) => { @@ -3485,6 +3589,124 @@ 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. + /// + /// 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( + self, + db: &'db dyn Db, + argument_types: CallArgumentTypes<'_, 'db>, + ) -> Result, ConstructorCallError<'db>> { + debug_assert!(matches!(self, Type::ClassLiteral(_) | Type::SubclassOf(_))); + + // 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? + let new_call_outcome: Option, CallDunderError<'db>>> = match self + .member_lookup_with_policy( + db, + "__new__".into(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ) + .symbol + { + Symbol::Type(dunder_callable, boundness) => { + let signatures = dunder_callable.signatures(db); + // `__new__` is a static method, so we must inject the `cls` argument. + let mut argument_types = argument_types.prepend_synthetic(self); + + Some( + match Bindings::match_parameters(signatures, &mut argument_types) + .check_types(db, &mut argument_types) + { + Ok(bindings) => { + if boundness == Boundness::PossiblyUnbound { + Err(CallDunderError::PossiblyUnbound(Box::new(bindings))) + } else { + Ok(bindings) + } + } + Err(err) => Err(err.into()), + }, + ) + } + // No explicit `__new__` method found + Symbol::Unbound => None, + }; + + // TODO: we should use the actual return type of `__new__` to determine the instance type + let instance_ty = self + .to_instance(db) + .expect("Class literal type and subclass-of types should always be convertible to instance type"); + + let init_call_outcome = if new_call_outcome.is_none() + || !instance_ty + .member_lookup_with_policy( + db, + "__init__".into(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ) + .symbol + .is_unbound() + { + Some(instance_ty.try_call_dunder(db, "__init__", argument_types)) + } else { + None + }; + + match (new_call_outcome, init_call_outcome) { + // All calls are successful or not called at all + (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 fn to_instance(&self, db: &'db dyn Db) -> Option> { match self { @@ -4718,6 +4940,98 @@ impl<'db> BoolError<'db> { } } +/// Error returned if a class instantiation call failed +#[derive(Debug)] +enum ConstructorCallError<'db> { + Init(Type<'db>, CallDunderError<'db>), + New(Type<'db>, CallDunderError<'db>), + NewAndInit(Type<'db>, CallDunderError<'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 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. + context.report_lint( + &CALL_POSSIBLY_UNBOUND_METHOD, + context_expression_node, + 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) => { + context.report_lint( + &CALL_POSSIBLY_UNBOUND_METHOD, + context_expression_node, + format_args!( + "Method `__init__` on type `{}` is possibly unbound.", + 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 = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error { + CallDunderError::MethodNotAvailable => { + // We are explicitly checking for `__new__` before attempting to call it, + // so this should never happen. + unreachable!("`__new__` method may not be called if missing"); + } + CallDunderError::PossiblyUnbound(bindings) => { + context.report_lint( + &CALL_POSSIBLY_UNBOUND_METHOD, + context_expression_node, + format_args!( + "Method `__new__` on type `{}` is possibly unbound.", + context_expression_type.display(context.db()), + ), + ); + + bindings.report_diagnostics(context, context_expression_node); + } + CallDunderError::CallError(_, bindings) => { + bindings.report_diagnostics(context, context_expression_node); + } + }; + + match self { + Self::Init(_, call_dunder_error) => { + report_init_error(call_dunder_error); + } + Self::New(_, call_dunder_error) => { + report_new_error(call_dunder_error); + } + Self::NewAndInit(_, new_call_dunder_error, init_call_dunder_error) => { + report_new_error(new_call_dunder_error); + report_init_error(init_call_dunder_error); + } + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Truthiness { /// For an object `x`, `bool(x)` will always return `True` diff --git a/crates/red_knot_python_semantic/src/types/call/arguments.rs b/crates/red_knot_python_semantic/src/types/call/arguments.rs index cce0c81c0b..fd2cd4a313 100644 --- a/crates/red_knot_python_semantic/src/types/call/arguments.rs +++ b/crates/red_knot_python_semantic/src/types/call/arguments.rs @@ -109,6 +109,21 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> { result } + /// Create a new [`CallArgumentTypes`] by prepending a synthetic argument to the front of this + /// argument list. + pub(crate) fn prepend_synthetic(&self, synthetic: Type<'db>) -> Self { + Self { + arguments: CallArguments( + std::iter::once(Argument::Synthetic) + .chain(self.arguments.iter()) + .collect(), + ), + types: std::iter::once(synthetic) + .chain(self.types.iter().copied()) + .collect(), + } + } + pub(crate) fn iter(&self) -> impl Iterator, Type<'db>)> + '_ { self.arguments.iter().zip(self.types.iter().copied()) } diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 8714e48120..a2f25b1289 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -924,8 +924,14 @@ impl<'db> Binding<'db> { first_excess_argument_index, num_synthetic_args, ), - expected_positional_count: parameters.positional().count(), - provided_positional_count: next_positional, + expected_positional_count: parameters + .positional() + .count() + // using saturating_sub to avoid negative values due to invalid syntax in source code + .saturating_sub(num_synthetic_args), + provided_positional_count: next_positional + // using saturating_sub to avoid negative values due to invalid syntax in source code + .saturating_sub(num_synthetic_args), }); } let mut missing = vec![]; diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 21e4c5e6c8..b73a62c792 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -2,8 +2,8 @@ use std::sync::{LazyLock, Mutex}; use super::{ class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder, - KnownFunction, Mro, MroError, MroIterator, SubclassOfType, Truthiness, Type, TypeAliasType, - TypeQualifiers, TypeVarInstance, + KnownFunction, MemberLookupPolicy, Mro, MroError, MroIterator, SubclassOfType, Truthiness, + Type, TypeAliasType, TypeQualifiers, TypeVarInstance, }; use crate::semantic_index::definition::Definition; use crate::{ @@ -323,7 +323,12 @@ impl<'db> Class<'db> { /// The member resolves to a member on the class itself or any of its proper superclasses. /// /// TODO: Should this be made private...? - pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { + pub(super) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> SymbolAndQualifiers<'db> { if name == "__mro__" { let tuple_elements = self.iter_mro(db).map(Type::from); return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into(); @@ -354,6 +359,18 @@ impl<'db> Class<'db> { dynamic_type_to_intersect_with.get_or_insert(Type::from(superclass)); } ClassBase::Class(class) => { + if class.is_known(db, KnownClass::Object) + // Only exclude `object` members if this is not an `object` class itself + && (policy.mro_no_object_fallback() && !self.is_known(db, KnownClass::Object)) + { + continue; + } + + if class.is_known(db, KnownClass::Type) && policy.meta_class_no_type_fallback() + { + continue; + } + lookup_result = lookup_result.or_else(|lookup_error| { lookup_error.or_fall_back_to(db, class.own_class_member(db, name)) }); @@ -776,8 +793,13 @@ impl<'db> ClassLiteralType<'db> { self.class.body_scope(db) } - pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> { - self.class.class_member(db, name) + pub(super) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> SymbolAndQualifiers<'db> { + self.class.class_member(db, name, policy) } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 98502ca061..97f2c56840 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -100,7 +100,7 @@ use super::slots::check_class_slots; use super::string_annotation::{ parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, }; -use super::CallDunderError; +use super::{CallDunderError, ClassLiteralType}; /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. /// Use when checking a scope, or needing to provide a type for an arbitrary expression in the @@ -2380,10 +2380,11 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::WrapperDescriptor(_) | Type::TypeVar(..) | Type::AlwaysTruthy - | Type::AlwaysFalsy => match object_ty.class_member(db, attribute.into()) { - meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => { - if emit_diagnostics { - self.context.report_lint( + | Type::AlwaysFalsy => { + match object_ty.class_member(db, attribute.into()) { + meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => { + if emit_diagnostics { + self.context.report_lint( &INVALID_ATTRIBUTE_ACCESS, target, format_args!( @@ -2391,26 +2392,30 @@ impl<'db> TypeInferenceBuilder<'db> { ty = object_ty.display(self.db()), ), ); + } + false } - false - } - SymbolAndQualifiers { - symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness), - qualifiers: _, - } => { - let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) = - meta_attr_ty.class_member(db, "__set__".into()).symbol - { - let successful_call = meta_dunder_set - .try_call( - db, - CallArgumentTypes::positional([meta_attr_ty, object_ty, value_ty]), - ) - .is_ok(); + SymbolAndQualifiers { + symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness), + qualifiers: _, + } => { + let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) = + meta_attr_ty.class_member(db, "__set__".into()).symbol + { + let successful_call = meta_dunder_set + .try_call( + db, + CallArgumentTypes::positional([ + meta_attr_ty, + object_ty, + value_ty, + ]), + ) + .is_ok(); - if !successful_call && emit_diagnostics { - // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed - self.context.report_lint( + if !successful_call && emit_diagnostics { + // TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed + self.context.report_lint( &INVALID_ASSIGNMENT, target, format_args!( @@ -2418,15 +2423,16 @@ impl<'db> TypeInferenceBuilder<'db> { object_ty.display(db) ), ); - } + } - successful_call - } else { - ensure_assignable_to(meta_attr_ty) - }; + successful_call + } else { + ensure_assignable_to(meta_attr_ty) + }; - let assignable_to_instance_attribute = - if meta_attr_boundness == Boundness::PossiblyUnbound { + let assignable_to_instance_attribute = if meta_attr_boundness + == Boundness::PossiblyUnbound + { let (assignable, boundness) = if let Symbol::Type(instance_attr_ty, instance_attr_boundness) = object_ty.instance_member(db, attribute).symbol @@ -2453,43 +2459,44 @@ impl<'db> TypeInferenceBuilder<'db> { true }; - assignable_to_meta_attr && assignable_to_instance_attribute - } + assignable_to_meta_attr && assignable_to_instance_attribute + } - SymbolAndQualifiers { - symbol: Symbol::Unbound, - .. - } => { - if let Symbol::Type(instance_attr_ty, instance_attr_boundness) = - object_ty.instance_member(db, attribute).symbol - { - if instance_attr_boundness == Boundness::PossiblyUnbound { - report_possibly_unbound_attribute( - &self.context, - target, - attribute, - object_ty, - ); - } - - ensure_assignable_to(instance_attr_ty) - } else { - if emit_diagnostics { - self.context.report_lint( - &UNRESOLVED_ATTRIBUTE, - target, - format_args!( - "Unresolved attribute `{}` on type `{}`.", + SymbolAndQualifiers { + symbol: Symbol::Unbound, + .. + } => { + if let Symbol::Type(instance_attr_ty, instance_attr_boundness) = + object_ty.instance_member(db, attribute).symbol + { + if instance_attr_boundness == Boundness::PossiblyUnbound { + report_possibly_unbound_attribute( + &self.context, + target, attribute, - object_ty.display(db) - ), - ); - } + object_ty, + ); + } - false + ensure_assignable_to(instance_attr_ty) + } else { + if emit_diagnostics { + self.context.report_lint( + &UNRESOLVED_ATTRIBUTE, + target, + format_args!( + "Unresolved attribute `{}` on type `{}`.", + attribute, + object_ty.display(db) + ), + ); + } + + false + } } } - }, + } Type::ClassLiteral(..) | Type::SubclassOf(..) => { match object_ty.class_member(db, attribute.into()) { @@ -3970,8 +3977,48 @@ impl<'db> TypeInferenceBuilder<'db> { // arguments after matching them to parameters, but before checking that the argument types // are assignable to any parameter annotations. let mut call_arguments = Self::parse_arguments(arguments); - let function_type = self.infer_expression(func); - let signatures = function_type.signatures(self.db()); + let callable_type = self.infer_expression(func); + + // For class literals we model the entire class instantiation logic, so it is handled + // in a separate function. + let class = match callable_type { + Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { + ClassBase::Dynamic(_) => None, + ClassBase::Class(class) => Some(class), + }, + Type::ClassLiteral(ClassLiteralType { class }) => Some(class), + _ => None, + }; + + if class.is_some_and(|class| { + // 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. + class.known(self.db()).is_none_or(|class| { + !matches!( + class, + KnownClass::Bool + | KnownClass::Str + | KnownClass::Type + | KnownClass::Object + | KnownClass::Property + ) + }) + }) { + let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; + let call_argument_types = + self.infer_argument_types(arguments, call_arguments, &argument_forms); + + return callable_type + .try_call_constructor(self.db(), call_argument_types) + .unwrap_or_else(|err| { + err.report_diagnostic(&self.context, callable_type, call_expression.into()); + err.return_type() + }); + } + + let signatures = callable_type.signatures(self.db()); let bindings = Bindings::match_parameters(signatures, &mut call_arguments); let mut call_argument_types = self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms); diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/red_knot_python_semantic/src/types/subclass_of.rs index 903fce42a4..b69c7505c8 100644 --- a/crates/red_knot_python_semantic/src/types/subclass_of.rs +++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs @@ -1,6 +1,6 @@ use crate::symbol::SymbolAndQualifiers; -use super::{ClassBase, ClassLiteralType, Db, KnownClass, Type}; +use super::{ClassBase, ClassLiteralType, Db, KnownClass, MemberLookupPolicy, Type}; /// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] @@ -66,12 +66,13 @@ impl<'db> SubclassOfType<'db> { !self.is_dynamic() } - pub(crate) fn find_name_in_mro( + pub(crate) fn find_name_in_mro_with_policy( self, db: &'db dyn Db, name: &str, + policy: MemberLookupPolicy, ) -> Option> { - Type::from(self.subclass_of).find_name_in_mro(db, name) + Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy) } /// Return `true` if `self` is a subtype of `other`.