diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md index d41d1276d6..7426e4c887 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -67,21 +67,24 @@ import typing #################### ### Built-ins +#################### class ListSubclass(typing.List): ... -# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(ListSubclass.__mro__) class DictSubclass(typing.Dict): ... -# TODO: should have `Generic`, should not have `Unknown` -# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[DictSubclass], Literal[dict], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(DictSubclass.__mro__) class SetSubclass(typing.Set): ... -# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(SetSubclass.__mro__) class FrozenSetSubclass(typing.FrozenSet): ... @@ -92,11 +95,12 @@ reveal_type(FrozenSetSubclass.__mro__) #################### ### `collections` +#################### class ChainMapSubclass(typing.ChainMap): ... -# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(ChainMapSubclass.__mro__) class CounterSubclass(typing.Counter): ... @@ -113,7 +117,8 @@ reveal_type(DefaultDictSubclass.__mro__) class DequeSubclass(typing.Deque): ... -# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(DequeSubclass.__mro__) class OrderedDictSubclass(typing.OrderedDict): ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 80161644ab..3d76ece35d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -41,7 +41,7 @@ class Foo: One thing that is supported is error messages for using special forms in type expressions. ```py -from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec +from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, Generic def _( a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression" @@ -49,6 +49,7 @@ def _( c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression" d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression" e: ParamSpec, + f: Generic, # error: [invalid-type-form] "`typing.Generic` is not allowed in type expressions" ) -> None: reveal_type(a) # revealed: Unknown reveal_type(b) # revealed: Unknown @@ -65,7 +66,7 @@ You can't inherit from most of these. `typing.Callable` is an exception. ```py from typing import Callable -from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate +from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate, Generic class A(Self): ... # error: [invalid-base] class B(Unpack): ... # error: [invalid-base] @@ -73,6 +74,7 @@ class C(TypeGuard): ... # error: [invalid-base] class D(TypeIs): ... # error: [invalid-base] class E(Concatenate): ... # error: [invalid-base] class F(Callable): ... +class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`" reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]] ``` 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 737e3455e0..6359978660 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -45,8 +45,6 @@ from typing import Generic, TypeVar T = TypeVar("T") -# TODO: no error -# error: [invalid-base] class C(Generic[T]): ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index 0d99019894..9712be7213 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -137,8 +137,6 @@ from typing import TypeVar, Generic T = TypeVar("T") S = TypeVar("S") -# TODO: no error -# error: [invalid-base] class Legacy(Generic[T]): def m(self, x: T, y: S) -> S: return y @@ -177,8 +175,6 @@ def f(x: T) -> None: # TODO: invalid-assignment error y: list[S] = [] -# TODO: no error -# error: [invalid-base] class C(Generic[T]): # TODO: error: cannot use S if it's not in the current generic context x: list[S] = [] @@ -259,8 +255,7 @@ def f[T](x: T, y: T) -> None: class Ok[S]: ... # TODO: error for reuse of typevar class Bad1[T]: ... - # TODO: no non-subscriptable error, error for reuse of typevar - # error: [non-subscriptable] + # TODO: error for reuse of typevar class Bad2(Iterable[T]): ... ``` @@ -273,8 +268,7 @@ class C[T]: class Ok1[S]: ... # TODO: error for reuse of typevar class Bad1[T]: ... - # TODO: no non-subscriptable error, error for reuse of typevar - # error: [non-subscriptable] + # TODO: error for reuse of typevar class Bad2(Iterable[T]): ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 2ddb0f404d..ee375cd0cb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -28,8 +28,7 @@ from typing import Protocol class MyProtocol(Protocol): ... -# TODO: at runtime this is `(, , , )` -reveal_type(MyProtocol.__mro__) # revealed: tuple[Literal[MyProtocol], @Todo(protocol), Literal[object]] +reveal_type(MyProtocol.__mro__) # revealed: tuple[Literal[MyProtocol], typing.Protocol, typing.Generic, Literal[object]] ``` Just like for any other class base, it is an error for `Protocol` to appear multiple times in a @@ -72,8 +71,7 @@ it is not sufficient for it to have `Protocol` in its MRO. ```py class SubclassOfMyProtocol(MyProtocol): ... -# TODO -# revealed: tuple[Literal[SubclassOfMyProtocol], Literal[MyProtocol], @Todo(protocol), Literal[object]] +# revealed: tuple[Literal[SubclassOfMyProtocol], Literal[MyProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(SubclassOfMyProtocol.__mro__) # TODO: should be `Literal[False]` @@ -94,8 +92,7 @@ class OtherProtocol(Protocol): class ComplexInheritance(SubProtocol, OtherProtocol, Protocol): ... -# TODO -# revealed: tuple[Literal[ComplexInheritance], Literal[SubProtocol], Literal[MyProtocol], Literal[OtherProtocol], @Todo(protocol), Literal[object]] +# revealed: tuple[Literal[ComplexInheritance], Literal[SubProtocol], Literal[MyProtocol], Literal[OtherProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(ComplexInheritance.__mro__) # TODO: should be `Literal[True]` @@ -109,15 +106,13 @@ or `TypeError` is raised at runtime when the class is created. # TODO: should emit `[invalid-protocol]` class Invalid(NotAProtocol, Protocol): ... -# TODO -# revealed: tuple[Literal[Invalid], Literal[NotAProtocol], @Todo(protocol), Literal[object]] +# revealed: tuple[Literal[Invalid], Literal[NotAProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(Invalid.__mro__) # TODO: should emit an `[invalid-protocol`] error class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ... -# TODO -# revealed: tuple[Literal[AlsoInvalid], Literal[MyProtocol], Literal[OtherProtocol], Literal[NotAProtocol], @Todo(protocol), Literal[object]] +# revealed: tuple[Literal[AlsoInvalid], Literal[MyProtocol], Literal[OtherProtocol], Literal[NotAProtocol], typing.Protocol, typing.Generic, Literal[object]] reveal_type(AlsoInvalid.__mro__) ``` @@ -135,11 +130,9 @@ T = TypeVar("T") # type checkers. class Fine(Protocol, object): ... -# TODO -reveal_type(Fine.__mro__) # revealed: tuple[Literal[Fine], @Todo(protocol), Literal[object]] +reveal_type(Fine.__mro__) # revealed: tuple[Literal[Fine], typing.Protocol, typing.Generic, Literal[object]] -# TODO: should not error -class StillFine(Protocol, Generic[T], object): ... # error: [invalid-base] +class StillFine(Protocol, Generic[T], object): ... class EvenThis[T](Protocol, object): ... ``` @@ -149,8 +142,7 @@ And multiple inheritance from a mix of protocol and non-protocol classes is fine ```py class FineAndDandy(MyProtocol, OtherProtocol, NotAProtocol): ... -# TODO -# revealed: tuple[Literal[FineAndDandy], Literal[MyProtocol], Literal[OtherProtocol], @Todo(protocol), Literal[NotAProtocol], Literal[object]] +# revealed: tuple[Literal[FineAndDandy], Literal[MyProtocol], Literal[OtherProtocol], typing.Protocol, typing.Generic, Literal[NotAProtocol], Literal[object]] reveal_type(FineAndDandy.__mro__) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index 579d7451f6..005e3da1ea 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -117,6 +117,7 @@ from typing import Tuple class C(Tuple): ... -# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]] +# TODO: generic protocols +# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(C.__mro__) ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 52c9f934e8..aeb0c67b8e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -26,7 +26,7 @@ pub(crate) use self::infer::{ }; pub(crate) use self::narrow::KnownConstraintFunction; pub(crate) use self::signatures::{CallableSignature, Signature, Signatures}; -pub(crate) use self::subclass_of::SubclassOfType; +pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; use crate::module_name::ModuleName; use crate::module_resolver::{file_to_module, resolve_module, KnownModule}; use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId}; @@ -499,7 +499,11 @@ impl<'db> Type<'db> { pub fn contains_todo(&self, db: &'db dyn Db) -> bool { match self { - Self::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) => true, + Self::Dynamic( + DynamicType::Todo(_) + | DynamicType::SubscriptedProtocol + | DynamicType::SubscriptedGeneric, + ) => true, Self::AlwaysFalsy | Self::AlwaysTruthy @@ -540,9 +544,13 @@ impl<'db> Type<'db> { } Self::SubclassOf(subclass_of) => match subclass_of.subclass_of() { - ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) => true, - ClassBase::Dynamic(DynamicType::Unknown | DynamicType::Any) => false, - ClassBase::Class(_) => false, + SubclassOfInner::Dynamic( + DynamicType::Todo(_) + | DynamicType::SubscriptedProtocol + | DynamicType::SubscriptedGeneric, + ) => true, + SubclassOfInner::Dynamic(DynamicType::Unknown | DynamicType::Any) => false, + SubclassOfInner::Class(_) => false, }, Self::TypeVar(typevar) => match typevar.bound_or_constraints(db) { @@ -557,10 +565,18 @@ impl<'db> Type<'db> { Self::BoundSuper(bound_super) => { matches!( bound_super.pivot_class(db), - ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) + ClassBase::Dynamic( + DynamicType::Todo(_) + | DynamicType::SubscriptedGeneric + | DynamicType::SubscriptedProtocol + ) ) || matches!( bound_super.owner(db), - SuperOwnerKind::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol) + SuperOwnerKind::Dynamic( + DynamicType::Todo(_) + | DynamicType::SubscriptedGeneric + | DynamicType::SubscriptedProtocol + ) ) } @@ -1466,7 +1482,7 @@ impl<'db> Type<'db> { (Type::SubclassOf(first), Type::SubclassOf(second)) => { match (first.subclass_of(), second.subclass_of()) { (first, second) if first == second => true, - (ClassBase::Dynamic(_), ClassBase::Dynamic(_)) => true, + (SubclassOfInner::Dynamic(_), SubclassOfInner::Dynamic(_)) => true, _ => false, } } @@ -1633,16 +1649,16 @@ impl<'db> Type<'db> { (Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b)) | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => false, - ClassBase::Class(class_a) => !class_b.is_subclass_of(db, None, class_a), + SubclassOfInner::Dynamic(_) => false, + SubclassOfInner::Class(class_a) => !class_b.is_subclass_of(db, None, class_a), } } (Type::SubclassOf(subclass_of_ty), Type::GenericAlias(alias_b)) | (Type::GenericAlias(alias_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => false, - ClassBase::Class(class_a) => { + SubclassOfInner::Dynamic(_) => false, + SubclassOfInner::Class(class_a) => { !ClassType::from(alias_b).is_subclass_of(db, class_a) } } @@ -1691,10 +1707,10 @@ impl<'db> Type<'db> { // so although the type is dynamic we can still determine disjointedness in some situations (Type::SubclassOf(subclass_of_ty), other) | (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => { + SubclassOfInner::Dynamic(_) => { KnownClass::Type.to_instance(db).is_disjoint_from(db, other) } - ClassBase::Class(class) => class + SubclassOfInner::Class(class) => class .metaclass_instance_type(db) .is_disjoint_from(db, other), }, @@ -3073,8 +3089,8 @@ impl<'db> Type<'db> { .try_bool_impl(db, allow_short_circuit)?, Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => Truthiness::Ambiguous, - ClassBase::Class(class) => { + SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, + SubclassOfInner::Class(class) => { Type::from(class).try_bool_impl(db, allow_short_circuit)? } }, @@ -3812,12 +3828,14 @@ 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), + SubclassOfInner::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::from(class).signatures(db), + SubclassOfInner::Class(class) => Type::from(class).signatures(db), }, Type::Instance(_) => { @@ -4380,6 +4398,10 @@ impl<'db> Type<'db> { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol], fallback_type: Type::unknown(), }), + KnownInstanceType::Generic => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Generic], + fallback_type: Type::unknown(), + }), KnownInstanceType::Literal | KnownInstanceType::Union @@ -4564,21 +4586,21 @@ impl<'db> Type<'db> { Type::ClassLiteral(class) => class.metaclass(db), Type::GenericAlias(alias) => ClassType::from(*alias).metaclass(db), Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - ClassBase::Dynamic(_) => *self, - ClassBase::Class(class) => SubclassOfType::from( + SubclassOfInner::Dynamic(_) => *self, + SubclassOfInner::Class(class) => SubclassOfType::from( db, - ClassBase::try_from_type(db, class.metaclass(db)) - .unwrap_or(ClassBase::unknown()), + SubclassOfInner::try_from_type(db, class.metaclass(db)) + .unwrap_or(SubclassOfInner::unknown()), ), }, Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class_literal(db), - Type::Dynamic(dynamic) => SubclassOfType::from(db, ClassBase::Dynamic(*dynamic)), + Type::Dynamic(dynamic) => SubclassOfType::from(db, SubclassOfInner::Dynamic(*dynamic)), // TODO intersections Type::Intersection(_) => SubclassOfType::from( db, - ClassBase::try_from_type(db, todo_type!("Intersection meta-type")) - .expect("Type::Todo should be a valid ClassBase"), + SubclassOfInner::try_from_type(db, todo_type!("Intersection meta-type")) + .expect("Type::Todo should be a valid `SubclassOfInner`"), ), Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), @@ -4780,8 +4802,8 @@ impl<'db> Type<'db> { }, Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - ClassBase::Class(class) => Some(TypeDefinition::Class(class.definition(db))), - ClassBase::Dynamic(_) => None, + SubclassOfInner::Class(class) => Some(TypeDefinition::Class(class.definition(db))), + SubclassOfInner::Dynamic(_) => None, }, Self::StringLiteral(_) @@ -4833,9 +4855,12 @@ pub enum DynamicType { /// /// This variant should be created with the `todo_type!` macro. Todo(TodoType), - /// Temporary type until we support protocols. We use a separate variant (instead of `Todo(…)`) - /// in order to be able to match on them explicitly. - TodoProtocol, + /// Temporary type until we support generic protocols. + /// We use a separate variant (instead of `Todo(…)`) in order to be able to match on them explicitly. + SubscriptedProtocol, + /// Temporary type until we support old-style generics. + /// We use a separate variant (instead of `Todo(…)`) in order to be able to match on them explicitly. + SubscriptedGeneric, } impl std::fmt::Display for DynamicType { @@ -4846,8 +4871,13 @@ impl std::fmt::Display for DynamicType { // `DynamicType::Todo`'s display should be explicit that is not a valid display of // any other type DynamicType::Todo(todo) => write!(f, "@Todo{todo}"), - DynamicType::TodoProtocol => f.write_str(if cfg!(debug_assertions) { - "@Todo(protocol)" + DynamicType::SubscriptedProtocol => f.write_str(if cfg!(debug_assertions) { + "@Todo(`Protocol[]` subscript)" + } else { + "@Todo" + }), + DynamicType::SubscriptedGeneric => f.write_str(if cfg!(debug_assertions) { + "@Todo(`Generic[]` subscript)" } else { "@Todo" }), @@ -4959,8 +4989,10 @@ enum InvalidTypeExpression<'db> { RequiresArguments(Type<'db>), /// Some types always require at least two arguments when used in a type expression RequiresTwoArguments(Type<'db>), - /// The `Protocol` type is invalid in type expressions + /// The `Protocol` class is invalid in type expressions Protocol, + /// Same for `Generic` + Generic, /// Type qualifiers are always invalid in *type expressions*, /// but these ones are okay with 0 arguments in *annotation expressions* TypeQualifier(KnownInstanceType<'db>), @@ -4999,6 +5031,9 @@ impl<'db> InvalidTypeExpression<'db> { InvalidTypeExpression::Protocol => f.write_str( "`typing.Protocol` is not allowed in type expressions" ), + InvalidTypeExpression::Generic => f.write_str( + "`typing.Generic` is not allowed in type expressions" + ), InvalidTypeExpression::TypeQualifier(qualifier) => write!( f, "Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions)", diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index e8e53a2e3f..2f57261650 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -246,7 +246,7 @@ impl<'db> ClassType<'db> { /// cases rather than simply iterating over the inferred resolution order for the class. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - pub(super) fn iter_mro(self, db: &'db dyn Db) -> impl Iterator> { + pub(super) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> { let (class_literal, specialization) = self.class_literal(db); class_literal.iter_mro(db, specialization) } @@ -645,7 +645,7 @@ impl<'db> ClassLiteralType<'db> { self, db: &'db dyn Db, specialization: Option>, - ) -> impl Iterator> { + ) -> MroIterator<'db> { MroIterator::new(db, self, specialization) } @@ -846,7 +846,11 @@ impl<'db> ClassLiteralType<'db> { for superclass in mro_iter { match superclass { - ClassBase::Dynamic(DynamicType::TodoProtocol) => { + ClassBase::Dynamic( + DynamicType::SubscriptedGeneric | DynamicType::SubscriptedProtocol, + ) + | ClassBase::Generic + | ClassBase::Protocol => { // TODO: We currently skip `Protocol` when looking up class members, in order to // avoid creating many dynamic types in our test suite that would otherwise // result from looking up attributes on builtin types like `str`, `list`, `tuple` @@ -865,7 +869,12 @@ impl<'db> ClassLiteralType<'db> { continue; } - if class.is_known(db, KnownClass::Type) && policy.meta_class_no_type_fallback() + // HACK: we should implement some more general logic here that supports arbitrary custom + // metaclasses, not just `type` and `ABCMeta`. + if matches!( + class.known(db), + Some(KnownClass::Type | KnownClass::ABCMeta) + ) && policy.meta_class_no_type_fallback() { continue; } @@ -1158,8 +1167,12 @@ impl<'db> ClassLiteralType<'db> { for superclass in self.iter_mro(db, specialization) { match superclass { - ClassBase::Dynamic(DynamicType::TodoProtocol) => { - // TODO: We currently skip `Protocol` when looking up instance members, in order to + ClassBase::Dynamic( + DynamicType::SubscriptedProtocol | DynamicType::SubscriptedGeneric, + ) + | ClassBase::Generic + | ClassBase::Protocol => { + // TODO: We currently skip these when looking up instance members, in order to // avoid creating many dynamic types in our test suite that would otherwise // result from looking up attributes on builtin types like `str`, `list`, `tuple` } @@ -1671,6 +1684,8 @@ pub(crate) enum KnownClass { Super, // enum Enum, + // abc + ABCMeta, // Types GenericAlias, ModuleType, @@ -1780,6 +1795,7 @@ impl<'db> KnownClass { | Self::Float | Self::Sized | Self::Enum + | Self::ABCMeta // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 // and raises a `TypeError` in Python >=3.14 // (see https://docs.python.org/3/library/constants.html#NotImplemented) @@ -1836,6 +1852,7 @@ impl<'db> KnownClass { Self::Sized => "Sized", Self::OrderedDict => "OrderedDict", Self::Enum => "Enum", + Self::ABCMeta => "ABCMeta", Self::Super => "super", // For example, `typing.List` is defined as `List = _Alias()` in typeshed Self::StdlibAlias => "_Alias", @@ -1992,6 +2009,7 @@ impl<'db> KnownClass { | Self::Super | Self::Property => KnownModule::Builtins, Self::VersionInfo => KnownModule::Sys, + Self::ABCMeta => KnownModule::Abc, Self::Enum => KnownModule::Enum, Self::GenericAlias | Self::ModuleType @@ -2096,6 +2114,7 @@ impl<'db> KnownClass { | Self::TypeVarTuple | Self::Sized | Self::Enum + | Self::ABCMeta | Self::Super | Self::NewType => false, } @@ -2155,6 +2174,7 @@ impl<'db> KnownClass { | Self::TypeVarTuple | Self::Sized | Self::Enum + | Self::ABCMeta | Self::Super | Self::UnionType | Self::NewType => false, @@ -2216,6 +2236,7 @@ impl<'db> KnownClass { "SupportsIndex" => Self::SupportsIndex, "Sized" => Self::Sized, "Enum" => Self::Enum, + "ABCMeta" => Self::ABCMeta, "super" => Self::Super, "_version_info" => Self::VersionInfo, "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { @@ -2271,6 +2292,7 @@ impl<'db> KnownClass { | Self::MethodType | Self::MethodWrapperType | Self::Enum + | Self::ABCMeta | Self::Super | Self::NotImplementedType | Self::UnionType @@ -2393,6 +2415,8 @@ pub enum KnownInstanceType<'db> { OrderedDict, /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) Protocol, + /// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`) + Generic, /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) Type, /// A single instance of `typing.TypeVar` @@ -2467,6 +2491,7 @@ impl<'db> KnownInstanceType<'db> { | Self::ChainMap | Self::OrderedDict | Self::Protocol + | Self::Generic | Self::ReadOnly | Self::TypeAliasType(_) | Self::Unknown @@ -2513,6 +2538,7 @@ impl<'db> KnownInstanceType<'db> { Self::ChainMap => "typing.ChainMap", Self::OrderedDict => "typing.OrderedDict", Self::Protocol => "typing.Protocol", + Self::Generic => "typing.Generic", Self::ReadOnly => "typing.ReadOnly", Self::TypeVar(typevar) => typevar.name(db), Self::TypeAliasType(_) => "typing.TypeAliasType", @@ -2560,7 +2586,8 @@ impl<'db> KnownInstanceType<'db> { Self::Deque => KnownClass::StdlibAlias, Self::ChainMap => KnownClass::StdlibAlias, Self::OrderedDict => KnownClass::StdlibAlias, - Self::Protocol => KnownClass::SpecialForm, + Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says + Self::Generic => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says Self::TypeVar(_) => KnownClass::TypeVar, Self::TypeAliasType(_) => KnownClass::TypeAliasType, Self::TypeOf => KnownClass::SpecialForm, @@ -2604,6 +2631,7 @@ impl<'db> KnownInstanceType<'db> { "Counter" => Self::Counter, "ChainMap" => Self::ChainMap, "OrderedDict" => Self::OrderedDict, + "Generic" => Self::Generic, "Protocol" => Self::Protocol, "Optional" => Self::Optional, "Union" => Self::Union, @@ -2662,6 +2690,7 @@ impl<'db> KnownInstanceType<'db> { | Self::NoReturn | Self::Tuple | Self::Type + | Self::Generic | Self::Callable => module.is_typing(), Self::Annotated | Self::Protocol diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 4b33c60b8c..5f342a3f3a 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -1,11 +1,12 @@ -use crate::types::{todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, Type}; +use crate::types::{ + todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, MroIterator, Type, +}; use crate::Db; -use itertools::Either; /// Enumeration of the possible kinds of types we allow in class bases. /// /// This is much more limited than the [`Type`] enum: all types that would be invalid to have as a -/// class base are transformed into [`ClassBase::unknown`] +/// class base are transformed into [`ClassBase::unknown()`] /// /// Note that a non-specialized generic class _cannot_ be a class base. When we see a /// non-specialized generic class in any type expression (including the list of base classes), we @@ -14,6 +15,13 @@ use itertools::Either; pub enum ClassBase<'db> { Dynamic(DynamicType), Class(ClassType<'db>), + /// Although `Protocol` is not a class in typeshed's stubs, it is at runtime, + /// and can appear in the MRO of a class. + Protocol, + /// Bare `Generic` cannot be subclassed directly in user code, + /// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`, + /// `Protocol[T]`, or bare `Protocol`. + Generic, } impl<'db> ClassBase<'db> { @@ -25,13 +33,6 @@ impl<'db> ClassBase<'db> { Self::Dynamic(DynamicType::Unknown) } - pub(crate) const fn is_dynamic(self) -> bool { - match self { - ClassBase::Dynamic(_) => true, - ClassBase::Class(_) => false, - } - } - pub(crate) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { struct Display<'db> { base: ClassBase<'db>, @@ -48,6 +49,8 @@ impl<'db> ClassBase<'db> { ClassBase::Class(ClassType::Generic(alias)) => { write!(f, "", alias.display(self.db)) } + ClassBase::Protocol => f.write_str("typing.Protocol"), + ClassBase::Generic => f.write_str("typing.Generic"), } } } @@ -165,7 +168,8 @@ impl<'db> ClassBase<'db> { KnownInstanceType::Callable => { Self::try_from_type(db, todo_type!("Support for Callable as a base class")) } - KnownInstanceType::Protocol => Some(ClassBase::Dynamic(DynamicType::TodoProtocol)), + KnownInstanceType::Protocol => Some(ClassBase::Protocol), + KnownInstanceType::Generic => Some(ClassBase::Generic), }, } } @@ -173,18 +177,21 @@ impl<'db> ClassBase<'db> { pub(super) fn into_class(self) -> Option> { match self { Self::Class(class) => Some(class), - Self::Dynamic(_) => None, + Self::Dynamic(_) | Self::Generic | Self::Protocol => None, } } /// Iterate over the MRO of this base - pub(super) fn mro( - self, - db: &'db dyn Db, - ) -> Either>, impl Iterator>> { + pub(super) fn mro(self, db: &'db dyn Db) -> impl Iterator> { match self { - ClassBase::Dynamic(_) => Either::Left([self, ClassBase::object(db)].into_iter()), - ClassBase::Class(class) => Either::Right(class.iter_mro(db)), + ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic), + ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => ClassBaseMroIterator::length_3( + db, + self, + ClassBase::Dynamic(DynamicType::SubscriptedGeneric), + ), + ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self), + ClassBase::Class(class) => ClassBaseMroIterator::from_class(db, class), } } } @@ -200,6 +207,8 @@ impl<'db> From> for Type<'db> { match value { ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic), ClassBase::Class(class) => class.into(), + ClassBase::Protocol => Type::KnownInstance(KnownInstanceType::Protocol), + ClassBase::Generic => Type::KnownInstance(KnownInstanceType::Generic), } } } @@ -209,3 +218,41 @@ impl<'db> From<&ClassBase<'db>> for Type<'db> { Self::from(*value) } } + +/// An iterator over the MRO of a class base. +enum ClassBaseMroIterator<'db> { + Length2(core::array::IntoIter, 2>), + Length3(core::array::IntoIter, 3>), + FromClass(MroIterator<'db>), +} + +impl<'db> ClassBaseMroIterator<'db> { + /// Iterate over an MRO of length 2 that consists of `first_element` and then `object`. + fn length_2(db: &'db dyn Db, first_element: ClassBase<'db>) -> Self { + ClassBaseMroIterator::Length2([first_element, ClassBase::object(db)].into_iter()) + } + + /// Iterate over an MRO of length 3 that consists of `first_element`, then `second_element`, then `object`. + fn length_3(db: &'db dyn Db, element_1: ClassBase<'db>, element_2: ClassBase<'db>) -> Self { + ClassBaseMroIterator::Length3([element_1, element_2, ClassBase::object(db)].into_iter()) + } + + /// Iterate over the MRO of an arbitrary class. The MRO may be of any length. + fn from_class(db: &'db dyn Db, class: ClassType<'db>) -> Self { + ClassBaseMroIterator::FromClass(class.iter_mro(db)) + } +} + +impl<'db> Iterator for ClassBaseMroIterator<'db> { + type Item = ClassBase<'db>; + + fn next(&mut self) -> Option { + match self { + Self::Length2(iter) => iter.next(), + Self::Length3(iter) => iter.next(), + Self::FromClass(iter) => iter.next(), + } + } +} + +impl std::iter::FusedIterator for ClassBaseMroIterator<'_> {} diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 96c3c5c535..b420d3ef72 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -7,13 +7,12 @@ use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_literal::escape::AsciiEscape; use crate::types::class::{ClassType, GenericAlias, GenericClass}; -use crate::types::class_base::ClassBase; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ FunctionSignature, InstanceType, IntersectionType, KnownClass, MethodWrapperKind, - StringLiteralType, Type, TypeVarBoundOrConstraints, TypeVarInstance, UnionType, - WrapperDescriptorKind, + StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance, + UnionType, WrapperDescriptorKind, }; use crate::Db; use rustc_hash::FxHashMap; @@ -92,8 +91,8 @@ impl Display for DisplayRepresentation<'_> { Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { // Only show the bare class name here; ClassBase::display would render this as // type[] instead of type[Foo]. - ClassBase::Class(class) => write!(f, "type[{}]", class.name(self.db)), - ClassBase::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), + SubclassOfInner::Class(class) => write!(f, "type[{}]", class.name(self.db)), + SubclassOfInner::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), }, Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)), Type::FunctionLiteral(function) => { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 4107b39775..e8f4ac2c6e 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -94,7 +94,6 @@ use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; use crate::Db; -use super::class_base::ClassBase; use super::context::{InNoTypeCheck, InferContext}; use super::diagnostic::{ report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause, @@ -107,7 +106,8 @@ use super::slots::check_class_slots; use super::string_annotation::{ parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, }; -use super::{BoundSuperError, BoundSuperType}; +use super::subclass_of::SubclassOfInner; +use super::{BoundSuperError, BoundSuperType, ClassBase}; /// 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 @@ -763,12 +763,25 @@ impl<'db> TypeInferenceBuilder<'db> { continue; } - // (2) Check for classes that inherit from `@final` classes + // (2) Check for inheritance from plain `Generic`, + // and from classes that inherit from `@final` classes for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() { - // dynamic/unknown bases are never `@final` - let Some(base_class) = base_class.into_class_literal() else { - continue; + let base_class = match base_class { + Type::KnownInstance(KnownInstanceType::Generic) => { + // `Generic` can appear in the MRO of many classes, + // but it is never valid as an explicit base class in user code. + self.context.report_lint_old( + &INVALID_BASE, + &class_node.bases()[i], + format_args!("Cannot inherit from plain `Generic`",), + ); + continue; + } + Type::ClassLiteral(class) => class, + // dynamic/unknown bases are never `@final` + _ => continue, }; + if !base_class.is_final(self.db()) { continue; } @@ -4736,10 +4749,10 @@ impl<'db> TypeInferenceBuilder<'db> { } Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => { match subclass_of.subclass_of() { - ClassBase::Class(class) => { + SubclassOfInner::Class(class) => { !class.instance_member(db, attr).symbol.is_unbound() } - ClassBase::Dynamic(_) => unreachable!( + SubclassOfInner::Dynamic(_) => unreachable!( "Attribute lookup on a dynamic `SubclassOf` type should always return a bound symbol" ), } @@ -4981,8 +4994,10 @@ impl<'db> TypeInferenceBuilder<'db> { | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), (todo @ Type::Dynamic(DynamicType::Todo(_)), _, _) | (_, todo @ Type::Dynamic(DynamicType::Todo(_)), _) => Some(todo), - (todo @ Type::Dynamic(DynamicType::TodoProtocol), _, _) - | (_, todo @ Type::Dynamic(DynamicType::TodoProtocol), _) => Some(todo), + (todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _, _) + | (_, todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _) => Some(todo), + (todo @ Type::Dynamic(DynamicType::SubscriptedGeneric), _, _) + | (_, todo @ Type::Dynamic(DynamicType::SubscriptedGeneric), _) => Some(todo), (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( @@ -6224,7 +6239,10 @@ impl<'db> TypeInferenceBuilder<'db> { Type::IntLiteral(i64::from(bool)), ), (Type::KnownInstance(KnownInstanceType::Protocol), _) => { - Type::Dynamic(DynamicType::TodoProtocol) + Type::Dynamic(DynamicType::SubscriptedProtocol) + } + (Type::KnownInstance(KnownInstanceType::Generic), _) => { + Type::Dynamic(DynamicType::SubscriptedGeneric) } (Type::KnownInstance(known_instance), _) if known_instance.class().is_special_form() => @@ -6331,12 +6349,19 @@ impl<'db> TypeInferenceBuilder<'db> { } } - report_non_subscriptable( - &self.context, - value_node.into(), - value_ty, - "__class_getitem__", - ); + // TODO: properly handle old-style generics; get rid of this temporary hack + if !value_ty.into_class_literal().is_some_and(|class| { + class + .iter_mro(self.db(), None) + .contains(&ClassBase::Dynamic(DynamicType::SubscriptedGeneric)) + }) { + report_non_subscriptable( + &self.context, + value_node.into(), + value_ty, + "__class_getitem__", + ); + } } else { report_non_subscriptable( &self.context, @@ -7508,7 +7533,11 @@ impl<'db> TypeInferenceBuilder<'db> { } KnownInstanceType::Protocol => { self.infer_type_expression(arguments_slice); - Type::Dynamic(DynamicType::TodoProtocol) + Type::Dynamic(DynamicType::SubscriptedProtocol) + } + KnownInstanceType::Generic => { + self.infer_type_expression(arguments_slice); + Type::Dynamic(DynamicType::SubscriptedGeneric) } KnownInstanceType::NoReturn | KnownInstanceType::Never 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 49c1d168fc..52dd82aaa6 100644 --- a/crates/red_knot_python_semantic/src/types/subclass_of.rs +++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs @@ -1,12 +1,12 @@ use crate::symbol::SymbolAndQualifiers; -use super::{ClassBase, Db, KnownClass, MemberLookupPolicy, Type}; +use super::{ClassType, Db, DynamicType, 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)] pub struct SubclassOfType<'db> { // Keep this field private, so that the only way of constructing the struct is through the `from` method. - subclass_of: ClassBase<'db>, + subclass_of: SubclassOfInner<'db>, } impl<'db> SubclassOfType<'db> { @@ -21,11 +21,11 @@ impl<'db> SubclassOfType<'db> { /// /// The eager normalization here means that we do not need to worry elsewhere about distinguishing /// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants. - pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into>) -> Type<'db> { + pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into>) -> Type<'db> { let subclass_of = subclass_of.into(); match subclass_of { - ClassBase::Dynamic(_) => Type::SubclassOf(Self { subclass_of }), - ClassBase::Class(class) => { + SubclassOfInner::Dynamic(_) => Type::SubclassOf(Self { subclass_of }), + SubclassOfInner::Class(class) => { if class.is_final(db) { Type::from(class) } else if class.is_object(db) { @@ -40,19 +40,19 @@ impl<'db> SubclassOfType<'db> { /// Return a [`Type`] instance representing the type `type[Unknown]`. pub(crate) const fn subclass_of_unknown() -> Type<'db> { Type::SubclassOf(SubclassOfType { - subclass_of: ClassBase::unknown(), + subclass_of: SubclassOfInner::unknown(), }) } /// Return a [`Type`] instance representing the type `type[Any]`. pub(crate) const fn subclass_of_any() -> Type<'db> { Type::SubclassOf(SubclassOfType { - subclass_of: ClassBase::any(), + subclass_of: SubclassOfInner::Dynamic(DynamicType::Any), }) } - /// Return the inner [`ClassBase`] value wrapped by this `SubclassOfType`. - pub(crate) const fn subclass_of(self) -> ClassBase<'db> { + /// Return the inner [`SubclassOfInner`] value wrapped by this `SubclassOfType`. + pub(crate) const fn subclass_of(self) -> SubclassOfInner<'db> { self.subclass_of } @@ -77,17 +77,17 @@ impl<'db> SubclassOfType<'db> { /// Return `true` if `self` is a subtype of `other`. /// - /// This can only return `true` if `self.subclass_of` is a [`ClassBase::Class`] variant; + /// This can only return `true` if `self.subclass_of` is a [`SubclassOfInner::Class`] variant; /// only fully static types participate in subtyping. pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool { match (self.subclass_of, other.subclass_of) { // Non-fully-static types do not participate in subtyping - (ClassBase::Dynamic(_), _) | (_, ClassBase::Dynamic(_)) => false, + (SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => false, // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, // and `type[int]` describes all possible runtime subclasses of the class `int`. // The first set is a subset of the second set, because `bool` is itself a subclass of `int`. - (ClassBase::Class(self_class), ClassBase::Class(other_class)) => { + (SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => { // N.B. The subclass relation is fully static self_class.is_subclass_of(db, other_class) } @@ -96,8 +96,73 @@ impl<'db> SubclassOfType<'db> { pub(crate) fn to_instance(self) -> Type<'db> { match self.subclass_of { - ClassBase::Class(class) => Type::instance(class), - ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), + SubclassOfInner::Class(class) => Type::instance(class), + SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), + } + } +} + +/// An enumeration of the different kinds of `type[]` types that a [`SubclassOfType`] can represent: +/// +/// 1. A "subclass of a class": `type[C]` for any class object `C` +/// 2. A "subclass of a dynamic type": `type[Any]`, `type[Unknown]` and `type[@Todo]` +/// +/// In the long term, we may want to implement . +/// Doing this would allow us to get rid of this enum, +/// since `type[Any]` would be represented as `type & Any` +/// rather than using the [`Type::SubclassOf`] variant at all; +/// [`SubclassOfType`] would then be a simple wrapper around [`ClassType`]. +/// +/// Note that this enum is similar to the [`super::ClassBase`] enum, +/// but does not include the `ClassBase::Protocol` and `ClassBase::Generic` variants +/// (`type[Protocol]` and `type[Generic]` are not valid types). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub(crate) enum SubclassOfInner<'db> { + Class(ClassType<'db>), + Dynamic(DynamicType), +} + +impl<'db> SubclassOfInner<'db> { + pub(crate) const fn unknown() -> Self { + Self::Dynamic(DynamicType::Unknown) + } + + pub(crate) const fn is_dynamic(self) -> bool { + matches!(self, Self::Dynamic(_)) + } + + pub(crate) const fn into_class(self) -> Option> { + match self { + Self::Class(class) => Some(class), + Self::Dynamic(_) => None, + } + } + + pub(crate) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + match ty { + Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), + Type::ClassLiteral(literal) => Some(if literal.is_known(db, KnownClass::Any) { + Self::Dynamic(DynamicType::Any) + } else { + Self::Class(literal.default_specialization(db)) + }), + Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), + _ => None, + } + } +} + +impl<'db> From> for SubclassOfInner<'db> { + fn from(value: ClassType<'db>) -> Self { + SubclassOfInner::Class(value) + } +} + +impl<'db> From> for Type<'db> { + fn from(value: SubclassOfInner<'db>) -> Self { + match value { + SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic), + SubclassOfInner::Class(class) => class.into(), } } } diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index e3b341fee6..8f69687465 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -3,8 +3,8 @@ use std::cmp::Ordering; use crate::db::Db; use super::{ - class_base::ClassBase, DynamicType, InstanceType, KnownInstanceType, SuperOwnerKind, TodoType, - Type, + class_base::ClassBase, subclass_of::SubclassOfInner, DynamicType, InstanceType, + KnownInstanceType, SuperOwnerKind, TodoType, Type, }; /// Return an [`Ordering`] that describes the canonical order in which two types should appear @@ -109,10 +109,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::SubclassOf(left), Type::SubclassOf(right)) => { match (left.subclass_of(), right.subclass_of()) { - (ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(&right), - (ClassBase::Class(_), _) => Ordering::Less, - (_, ClassBase::Class(_)) => Ordering::Greater, - (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { + (SubclassOfInner::Class(left), SubclassOfInner::Class(right)) => left.cmp(&right), + (SubclassOfInner::Class(_), _) => Ordering::Less, + (_, SubclassOfInner::Class(_)) => Ordering::Greater, + (SubclassOfInner::Dynamic(left), SubclassOfInner::Dynamic(right)) => { dynamic_elements_ordering(left, right) } } @@ -143,6 +143,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(right), (ClassBase::Class(_), _) => Ordering::Less, (_, ClassBase::Class(_)) => Ordering::Greater, + (ClassBase::Protocol, _) => Ordering::Less, + (_, ClassBase::Protocol) => Ordering::Greater, + (ClassBase::Generic, _) => Ordering::Less, + (_, ClassBase::Generic) => Ordering::Greater, (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { dynamic_elements_ordering(*left, *right) } @@ -230,6 +234,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (KnownInstanceType::OrderedDict, _) => Ordering::Less, (_, KnownInstanceType::OrderedDict) => Ordering::Greater, + (KnownInstanceType::Generic, _) => Ordering::Less, + (_, KnownInstanceType::Generic) => Ordering::Greater, + (KnownInstanceType::Protocol, _) => Ordering::Less, (_, KnownInstanceType::Protocol) => Ordering::Greater, @@ -364,7 +371,10 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering #[cfg(not(debug_assertions))] (DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal, - (DynamicType::TodoProtocol, _) => Ordering::Less, - (_, DynamicType::TodoProtocol) => Ordering::Greater, + (DynamicType::SubscriptedGeneric, _) => Ordering::Less, + (_, DynamicType::SubscriptedGeneric) => Ordering::Greater, + + (DynamicType::SubscriptedProtocol, _) => Ordering::Less, + (_, DynamicType::SubscriptedProtocol) => Ordering::Greater, } }