diff --git a/Cargo.lock b/Cargo.lock index 52c0aaeefa..deb4027e6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3441,7 +3441,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" version = "0.23.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=86ca4a9d70e97dd5107e6111a09647dd10ff7535#86ca4a9d70e97dd5107e6111a09647dd10ff7535" +source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50" dependencies = [ "boxcar", "compact_str", @@ -3465,12 +3465,12 @@ dependencies = [ [[package]] name = "salsa-macro-rules" version = "0.23.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=86ca4a9d70e97dd5107e6111a09647dd10ff7535#86ca4a9d70e97dd5107e6111a09647dd10ff7535" +source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50" [[package]] name = "salsa-macros" version = "0.23.0" -source = "git+https://github.com/salsa-rs/salsa.git?rev=86ca4a9d70e97dd5107e6111a09647dd10ff7535#86ca4a9d70e97dd5107e6111a09647dd10ff7535" +source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 12dd3b015f..8d6726a9a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,7 +141,7 @@ regex-automata = { version = "0.4.9" } rustc-hash = { version = "2.0.0" } rustc-stable-hash = { version = "0.1.2" } # When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml` -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "86ca4a9d70e97dd5107e6111a09647dd10ff7535", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d66fe331d546216132ace503512b94d5c68d2c50", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md index 5b628c7afa..d02317fc15 100644 --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -208,7 +208,28 @@ static_assert(has_member(Answer, "YES")) static_assert(has_member(Answer, "__members__")) ``` -### Unions +### TypedDicts + +```py +from ty_extensions import has_member, static_assert +from typing import TypedDict + +class Person(TypedDict): + name: str + age: int | None + +static_assert(not has_member(Person, "name")) +static_assert(not has_member(Person, "age")) + +static_assert(has_member(Person, "__total__")) +static_assert(has_member(Person, "__required_keys__")) + +def _(person: Person): + static_assert(not has_member(person, "name")) + static_assert(not has_member(person, "age")) + + static_assert(has_member(person, "keys")) +``` For unions, `ide_support::all_members` only returns members that are available on all elements of the union. diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md index 5e5febf3a2..310b59602b 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -205,3 +205,54 @@ reveal_type(bool(AmbiguousEnum2.YES)) # revealed: bool reveal_type(bool(CustomLenEnum.NO)) # revealed: bool reveal_type(bool(CustomLenEnum.YES)) # revealed: bool ``` + +## TypedDict + +It may be feasible to infer `Literal[True]` for some `TypedDict` types, if `{}` can definitely be +excluded as a possible value. We currently do not attempt to do this. + +If `{}` is the *only* possible value, we could infer `Literal[False]`. This might only be possible +if something like is accepted, a `TypedDict` has no defined +items, and `closed=True` is used. + +```py +from typing_extensions import TypedDict, Literal, NotRequired + +class Normal(TypedDict): + a: str + b: int + +def _(n: Normal) -> None: + # Could be `Literal[True]` + reveal_type(bool(n)) # revealed: bool + +class OnlyFalsyItems(TypedDict): + wrong: Literal[False] + +def _(n: OnlyFalsyItems) -> None: + # Could be `Literal[True]` (it does not matter if all items are falsy) + reveal_type(bool(n)) # revealed: bool + +class Empty(TypedDict): + pass + +def _(e: Empty) -> None: + # This should be `bool`. `Literal[False]` would be wrong, as `Empty` can be subclassed. + reveal_type(bool(e)) # revealed: bool + +class AllKeysOptional(TypedDict, total=False): + a: str + b: int + +def _(a: AllKeysOptional) -> None: + # This should be `bool`. `Literal[True]` would be wrong as `{}` is a valid value. + reveal_type(bool(a)) # revealed: bool + +class AllKeysNotRequired(TypedDict): + a: NotRequired[str] + b: NotRequired[int] + +def _(a: AllKeysNotRequired) -> None: + # This should be `bool`. `Literal[True]` would be wrong as `{}` is a valid value. + reveal_type(bool(a)) # revealed: bool +``` diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 04cdb025d4..dff4124977 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -60,6 +60,8 @@ Assignments to keys are also validated: ```py # TODO: this should be an error alice["name"] = None +# TODO: this should be an error +bob["name"] = None ``` Assignments to non-existing keys are disallowed: @@ -67,6 +69,8 @@ Assignments to non-existing keys are disallowed: ```py # TODO: this should be an error alice["extra"] = True +# TODO: this should be an error +bob["extra"] = True ``` ## Structural assignability @@ -123,7 +127,7 @@ dangerous(alice) reveal_type(alice["name"]) # revealed: Unknown ``` -## Types of keys and values +## Methods on `TypedDict` ```py from typing import TypedDict @@ -133,8 +137,13 @@ class Person(TypedDict): age: int | None def _(p: Person) -> None: - reveal_type(p.keys()) # revealed: @Todo(Support for `TypedDict`) - reveal_type(p.values()) # revealed: @Todo(Support for `TypedDict`) + reveal_type(p.keys()) # revealed: dict_keys[str, object] + reveal_type(p.values()) # revealed: dict_values[str, object] + + reveal_type(p.setdefault("name", "Alice")) # revealed: @Todo(Support for `TypedDict`) + + reveal_type(p.get("name")) # revealed: @Todo(Support for `TypedDict`) + reveal_type(p.get("name", "Unknown")) # revealed: @Todo(Support for `TypedDict`) ``` ## Unlike normal classes @@ -149,11 +158,16 @@ class Person(TypedDict): name: str age: int | None -# TODO: this should be an error +# error: [unresolved-attribute] "Type `` has no attribute `name`" Person.name -# TODO: this should be an error -Person(name="Alice", age=30).name +def _(P: type[Person]): + # error: [unresolved-attribute] "Type `type[Person]` has no attribute `name`" + P.name + +def _(p: Person) -> None: + # error: [unresolved-attribute] "Type `Person` has no attribute `name`" + p.name ``` ## Special properties @@ -167,9 +181,29 @@ class Person(TypedDict): name: str age: int | None -reveal_type(Person.__total__) # revealed: @Todo(Support for `TypedDict`) -reveal_type(Person.__required_keys__) # revealed: @Todo(Support for `TypedDict`) -reveal_type(Person.__optional_keys__) # revealed: @Todo(Support for `TypedDict`) +reveal_type(Person.__total__) # revealed: bool +reveal_type(Person.__required_keys__) # revealed: frozenset[str] +reveal_type(Person.__optional_keys__) # revealed: frozenset[str] +``` + +These attributes can not be accessed on inhabitants: + +```py +def _(person: Person) -> None: + # TODO: these should be errors + person.__total__ + person.__required_keys__ + person.__optional_keys__ +``` + +Also, they can not be accessed on `type(person)`, as that would be `dict` at runtime: + +```py +def _(t_person: type[Person]) -> None: + # TODO: these should be errors + t_person.__total__ + t_person.__required_keys__ + t_person.__optional_keys__ ``` ## Subclassing @@ -272,6 +306,9 @@ msg = Message(id=1, content="Hello") OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True) reveal_type(Message.__required_keys__) # revealed: @Todo(Support for `TypedDict`) + +# TODO: this should be an error +msg.content ``` [`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 8816e1bf87..38add1570e 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -223,7 +223,8 @@ impl<'db> Completion<'db> { Type::NominalInstance(_) | Type::PropertyInstance(_) | Type::Tuple(_) - | Type::BoundSuper(_) => CompletionKind::Struct, + | Type::BoundSuper(_) + | Type::TypedDict(_) => CompletionKind::Struct, Type::IntLiteral(_) | Type::BooleanLiteral(_) | Type::TypeIs(_) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index bf2bc151fc..86c14e0827 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -606,6 +606,8 @@ pub enum Type<'db> { BoundSuper(BoundSuperType<'db>), /// A subtype of `bool` that allows narrowing in both positive and negative cases. TypeIs(TypeIsType<'db>), + /// A type that represents an inhabitant of a `TypedDict`. + TypedDict(TypedDictType<'db>), } #[salsa::tracked] @@ -780,6 +782,10 @@ impl<'db> Type<'db> { Type::TypeIs(type_is) => { type_is.with_type(db, type_is.return_type(db).materialize(db, variance)) } + Type::TypedDict(_) => { + // TODO: Materialization of gradual TypedDicts + *self + } } } @@ -1080,6 +1086,12 @@ impl<'db> Type<'db> { // Always normalize single-member enums to their class instance (`Literal[Single.VALUE]` => `Single`) enum_literal.enum_class_instance(db) } + + Type::TypedDict(_) => { + // TODO: Normalize TypedDicts + self + } + Type::LiteralString | Type::AlwaysFalsy | Type::AlwaysTruthy @@ -1128,7 +1140,8 @@ impl<'db> Type<'db> { | Type::AlwaysTruthy | Type::PropertyInstance(_) // might inherit `Any`, but subtyping is still reflexive - | Type::ClassLiteral(_) => true, + | Type::ClassLiteral(_) + => true, Type::Dynamic(_) | Type::NominalInstance(_) | Type::ProtocolInstance(_) @@ -1140,7 +1153,8 @@ impl<'db> Type<'db> { | Type::Tuple(_) | Type::TypeVar(_) | Type::BoundSuper(_) - | Type::TypeIs(_) => false, + | Type::TypeIs(_) + | Type::TypedDict(_) => false, } } @@ -1203,7 +1217,8 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::BytesLiteral(_) | Type::Tuple(_) - | Type::TypeIs(_) => None, + | Type::TypeIs(_) + | Type::TypedDict(_) => None, // TODO Type::MethodWrapper(_) @@ -1297,6 +1312,11 @@ impl<'db> Type<'db> { field.default_type(db).has_relation_to(db, right, relation) } + (Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => { + // TODO: Implement assignability and subtyping for TypedDict + relation.is_assignability() + } + // In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied: // 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`. // TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`. @@ -1761,6 +1781,11 @@ impl<'db> Type<'db> { (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => false, + (Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => { + // TODO: Implement disjointness for TypedDict + false + } + // A typevar is never disjoint from itself, since all occurrences of the typevar must // be specialized to the same type. (This is an important difference between typevars // and `Any`!) Different typevars might be disjoint, depending on their bounds and @@ -2374,6 +2399,7 @@ impl<'db> Type<'db> { } Type::AlwaysTruthy | Type::AlwaysFalsy => false, Type::TypeIs(type_is) => type_is.is_bound(db), + Type::TypedDict(_) => false, } } @@ -2462,7 +2488,8 @@ impl<'db> Type<'db> { | Type::Callable(_) | Type::PropertyInstance(_) | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) => false, + | Type::DataclassTransformer(_) + | Type::TypedDict(_) => false, } } @@ -2502,6 +2529,10 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Never => Some(Place::bound(self).into()), + Type::ClassLiteral(class) if class.is_typed_dict(db) => { + Some(class.typed_dict_member(db, None, name, policy)) + } + Type::ClassLiteral(class) => { match (class.known(db), name) { (Some(KnownClass::FunctionType), "__get__") => Some( @@ -2531,6 +2562,10 @@ impl<'db> Type<'db> { } } + Type::GenericAlias(alias) if alias.is_typed_dict(db) => { + Some(alias.origin(db).typed_dict_member(db, None, name, policy)) + } + Type::GenericAlias(alias) => { Some(ClassType::from(*alias).class_member(db, name, policy)) } @@ -2582,7 +2617,8 @@ impl<'db> Type<'db> { | Type::NominalInstance(_) | Type::ProtocolInstance(_) | Type::PropertyInstance(_) - | Type::TypeIs(_) => None, + | Type::TypeIs(_) + | Type::TypedDict(_) => None, } } @@ -2739,6 +2775,8 @@ impl<'db> Type<'db> { Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { Place::Unbound.into() } + + Type::TypedDict(_) => Place::Unbound.into(), } } @@ -3228,7 +3266,8 @@ impl<'db> Type<'db> { | Type::FunctionLiteral(..) | Type::AlwaysTruthy | Type::AlwaysFalsy - | Type::TypeIs(..) => { + | Type::TypeIs(..) + | Type::TypedDict(_) => { let fallback = self.instance_member(db, name_str); let result = self.invoke_descriptor_protocol( @@ -3514,6 +3553,12 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::TypeIs(_) => Truthiness::Ambiguous, + Type::TypedDict(_) => { + // TODO: We could do better here, but it's unclear if this is important. + // See existing `TypedDict`-related tests in `truthiness.md` + Truthiness::Ambiguous + } + Type::FunctionLiteral(_) | Type::BoundMethod(_) | Type::WrapperDescriptor(_) @@ -4552,7 +4597,8 @@ impl<'db> Type<'db> { | Type::Tuple(_) | Type::BoundSuper(_) | Type::ModuleLiteral(_) - | Type::TypeIs(_) => CallableBinding::not_callable(self).into(), + | Type::TypeIs(_) + | Type::TypedDict(_) => CallableBinding::not_callable(self).into(), } } @@ -5167,7 +5213,8 @@ impl<'db> Type<'db> { | Type::BoundSuper(_) | Type::AlwaysTruthy | Type::AlwaysFalsy - | Type::TypeIs(_) => None, + | Type::TypeIs(_) + | Type::TypedDict(_) => None, } } @@ -5206,10 +5253,16 @@ impl<'db> Type<'db> { KnownClass::Float.to_instance(db), ], ), + _ if class.is_typed_dict(db) => { + Type::TypedDict(TypedDictType::new(db, ClassType::NonGeneric(*class))) + } _ => Type::instance(db, class.default_specialization(db)), }; Ok(ty) } + Type::GenericAlias(alias) if alias.is_typed_dict(db) => Ok(Type::TypedDict( + TypedDictType::new(db, ClassType::from(*alias)), + )), Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))), Type::SubclassOf(_) @@ -5235,7 +5288,8 @@ impl<'db> Type<'db> { | Type::BoundSuper(_) | Type::ProtocolInstance(_) | Type::PropertyInstance(_) - | Type::TypeIs(_) => Err(InvalidTypeExpressionError { + | Type::TypeIs(_) + | Type::TypedDict(_) => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec_inline![ InvalidTypeExpression::InvalidType(*self, scope_id) ], @@ -5537,6 +5591,7 @@ impl<'db> Type<'db> { Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), + Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class(db)), } } @@ -5637,6 +5692,10 @@ impl<'db> Type<'db> { Type::GenericAlias(generic.apply_type_mapping(db, type_mapping)) } + Type::TypedDict(typed_dict) => { + Type::TypedDict(typed_dict.apply_type_mapping(db, type_mapping)) + } + Type::SubclassOf(subclass_of) => Type::SubclassOf( subclass_of.apply_type_mapping(db, type_mapping), ), @@ -5796,7 +5855,8 @@ impl<'db> Type<'db> { | Type::EnumLiteral(_) | Type::BoundSuper(_) | Type::SpecialForm(_) - | Type::KnownInstance(_) => {} + | Type::KnownInstance(_) + | Type::TypedDict(_) => {} } } @@ -5911,6 +5971,10 @@ impl<'db> Type<'db> { Protocol::Synthesized(_) => None, }, + Type::TypedDict(typed_dict) => { + Some(TypeDefinition::Class(typed_dict.defining_class(db).definition(db))) + } + Self::Union(_) | Self::Intersection(_) => None, // These types have no definition @@ -6255,9 +6319,6 @@ pub enum DynamicType { /// A special Todo-variant for type aliases declared using `typing.TypeAlias`. /// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions. TodoTypeAlias, - /// A special Todo-variant for classes inheriting from `TypedDict`. - /// A temporary variant to avoid false positives while we wait for full support. - TodoTypedDict, } impl DynamicType { @@ -6289,13 +6350,6 @@ impl std::fmt::Display for DynamicType { f.write_str("@Todo") } } - DynamicType::TodoTypedDict => { - if cfg!(debug_assertions) { - f.write_str("@Todo(Support for `TypedDict`)") - } else { - f.write_str("@Todo") - } - } } } } @@ -8830,6 +8884,40 @@ impl<'db> EnumLiteralType<'db> { } } +/// Type that represents the set of all inhabitants (`dict` instances) that conform to +/// a given `TypedDict` schema. +#[salsa::interned(debug)] +#[derive(PartialOrd, Ord)] +pub struct TypedDictType<'db> { + /// A reference to the class (inheriting from `typing.TypedDict`) that specifies the + /// schema of this `TypedDict`. + defining_class: ClassType<'db>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for TypedDictType<'_> {} + +impl<'db> TypedDictType<'db> { + pub(crate) fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + Self::new( + db, + self.defining_class(db).apply_type_mapping(db, type_mapping), + ) + } +} + +fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + typed_dict: TypedDictType<'db>, + visitor: &mut V, +) { + visitor.visit_type(db, typed_dict.defining_class(db).into()); +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum BoundSuperError<'db> { InvalidPivotClassType { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index ddb84424d5..45e7651b25 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -23,9 +23,9 @@ use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signatu use crate::types::tuple::TupleSpec; use crate::types::{ BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams, - DeprecatedInstance, DynamicType, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, + DeprecatedInstance, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation, TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type, - infer_definition_types, + infer_definition_types, todo_type, }; use crate::{ Db, FxIndexMap, FxOrderSet, Program, @@ -112,6 +112,21 @@ fn try_mro_cycle_initial<'db>( )) } +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_typed_dict_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &bool, + _count: u32, + _self: ClassLiteral<'db>, +) -> salsa::CycleRecoveryAction { + salsa::CycleRecoveryAction::Iterate +} + +#[allow(clippy::unnecessary_wraps)] +fn is_typed_dict_cycle_initial<'db>(_db: &'db dyn Db, _self: ClassLiteral<'db>) -> bool { + false +} + fn try_metaclass_cycle_recover<'db>( _db: &'db dyn Db, _value: &Result<(Type<'db>, Option), MetaclassError<'db>>, @@ -140,6 +155,8 @@ pub(crate) enum CodeGeneratorKind { DataclassLike, /// Classes inheriting from `typing.NamedTuple` NamedTuple, + /// Classes inheriting from `typing.TypedDict` + TypedDict, } impl CodeGeneratorKind { @@ -148,6 +165,8 @@ impl CodeGeneratorKind { Some(CodeGeneratorKind::DataclassLike) } else if CodeGeneratorKind::NamedTuple.matches(db, class) { Some(CodeGeneratorKind::NamedTuple) + } else if CodeGeneratorKind::TypedDict.matches(db, class) { + Some(CodeGeneratorKind::TypedDict) } else { None } @@ -165,6 +184,7 @@ impl CodeGeneratorKind { base.into_class_literal() .is_some_and(|c| c.is_known(db, KnownClass::NamedTuple)) }), + Self::TypedDict => class.is_typed_dict(db), } } } @@ -238,6 +258,10 @@ impl<'db> GenericAlias<'db> { // look in `self.tuple`. self.specialization(db).find_legacy_typevars(db, typevars); } + + pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool { + self.origin(db).is_typed_dict(db) + } } impl<'db> From> for Type<'db> { @@ -423,15 +447,6 @@ impl<'db> ClassType<'db> { other: Self, relation: TypeRelation, ) -> bool { - // TODO: remove this branch once we have proper support for TypedDicts. - if self.is_known(db, KnownClass::Dict) - && other - .iter_mro(db) - .any(|b| matches!(b, ClassBase::Dynamic(DynamicType::TodoTypedDict))) - { - return true; - } - self.iter_mro(db).any(|base| { match base { ClassBase::Dynamic(_) => match relation { @@ -455,6 +470,11 @@ impl<'db> ClassType<'db> { (ClassType::Generic(_), ClassType::NonGeneric(_)) | (ClassType::NonGeneric(_), ClassType::Generic(_)) => false, }, + + ClassBase::TypedDict => { + // TODO: Implement subclassing and assignability for TypedDicts. + true + } } }) } @@ -873,6 +893,11 @@ impl<'db> ClassType<'db> { /// See [`Type::instance_member`] for more details. pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { let (class_literal, specialization) = self.class_literal(db); + + if class_literal.is_typed_dict(db) { + return Place::Unbound.into(); + } + class_literal .instance_member(db, specialization, name) .map_type(|ty| ty.apply_optional_specialization(db, specialization)) @@ -1457,6 +1482,22 @@ impl<'db> ClassLiteral<'db> { .contains(&ClassBase::Class(other)) } + /// Return `true` if this class constitutes a typed dict specification (inherits from + /// `typing.TypedDict`, either directly or indirectly). + #[salsa::tracked( + cycle_fn=is_typed_dict_cycle_recover, + cycle_initial=is_typed_dict_cycle_initial, + heap_size=get_size2::heap_size + )] + pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool { + if let Some(known) = self.known(db) { + return known.is_typed_dict_subclass(); + } + + self.iter_mro(db, None) + .any(|base| matches!(base, ClassBase::TypedDict)) + } + /// Return the explicit `metaclass` of this class, if one is defined. /// /// ## Note @@ -1692,6 +1733,12 @@ impl<'db> ClassLiteral<'db> { ) }); } + ClassBase::TypedDict => { + return KnownClass::TypedDictFallback + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy) + .expect("Will return Some() when called on class literal"); + } } if lookup_result.is_ok() { break; @@ -1792,7 +1839,7 @@ impl<'db> ClassLiteral<'db> { /// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or /// a synthesized `__new__` method for a `NamedTuple`. - fn own_synthesized_member( + pub(super) fn own_synthesized_member( self, db: &'db dyn Db, specialization: Option>, @@ -1976,10 +2023,118 @@ impl<'db> ClassLiteral<'db> { } None } + (CodeGeneratorKind::TypedDict, "__setitem__") => { + // TODO: synthesize a set of overloads with precise types + let signature = Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))), + Parameter::positional_only(Some(Name::new_static("value"))), + ]), + Some(Type::none(db)), + ); + + Some(CallableType::function_like(db, signature)) + } + (CodeGeneratorKind::TypedDict, "__getitem__") => { + // TODO: synthesize a set of overloads with precise types + let signature = Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))), + ]), + Some(todo_type!("Support for `TypedDict`")), + ); + + Some(CallableType::function_like(db, signature)) + } + (CodeGeneratorKind::TypedDict, "get") => { + // TODO: synthesize a set of overloads with precise types + let signature = Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_default_type(Type::unknown()), + ]), + Some(todo_type!("Support for `TypedDict`")), + ); + + Some(CallableType::function_like(db, signature)) + } + (CodeGeneratorKind::TypedDict, "pop") => { + // TODO: synthesize a set of overloads with precise types. + // Required keys should be forbidden to be popped. + let signature = Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_default_type(Type::unknown()), + ]), + Some(todo_type!("Support for `TypedDict`")), + ); + + Some(CallableType::function_like(db, signature)) + } + (CodeGeneratorKind::TypedDict, "setdefault") => { + // TODO: synthesize a set of overloads with precise types + let signature = Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))), + Parameter::positional_only(Some(Name::new_static("default"))), + ]), + Some(todo_type!("Support for `TypedDict`")), + ); + + Some(CallableType::function_like(db, signature)) + } + (CodeGeneratorKind::TypedDict, "update") => { + // TODO: synthesize a set of overloads with precise types + let signature = Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::variadic(Name::new_static("args")), + Parameter::keyword_variadic(Name::new_static("kwargs")), + ]), + Some(Type::none(db)), + ); + + Some(CallableType::function_like(db, signature)) + } _ => None, } } + /// Member lookup for classes that inherit from `typing.TypedDict`. + /// + /// This is implemented as a separate method because the item definitions on a `TypedDict`-based + /// class are *not* accessible as class members. Instead, this mostly defers to `TypedDictFallback`, + /// unless `name` corresponds to one of the specialized synthetic members like `__getitem__`. + pub(crate) fn typed_dict_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + if let Some(member) = self.own_synthesized_member(db, specialization, name) { + Place::bound(member).into() + } else { + KnownClass::TypedDictFallback + .to_class_literal(db) + .find_name_in_mro_with_policy(db, name, policy) + .expect("`find_name_in_mro_with_policy` will return `Some()` when called on class literal") + } + } + /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. /// /// See [`ClassLiteral::own_fields`] for more details. @@ -2110,6 +2265,10 @@ impl<'db> ClassLiteral<'db> { specialization: Option>, name: &str, ) -> PlaceAndQualifiers<'db> { + if self.is_typed_dict(db) { + return Place::Unbound.into(); + } + let mut union = UnionBuilder::new(db); let mut union_qualifiers = TypeQualifiers::empty(); @@ -2147,6 +2306,9 @@ impl<'db> ClassLiteral<'db> { union = union.add(ty); } } + ClassBase::TypedDict => { + return Place::bound(todo_type!("Support for `TypedDict`")).into(); + } } } @@ -2862,6 +3024,7 @@ pub enum KnownClass { InitVar, // _typeshed._type_checker_internals NamedTupleFallback, + TypedDictFallback, } impl KnownClass { @@ -2958,7 +3121,8 @@ impl KnownClass { | Self::Field | Self::KwOnly | Self::InitVar - | Self::NamedTupleFallback => Truthiness::Ambiguous, + | Self::NamedTupleFallback + | Self::TypedDictFallback => Truthiness::Ambiguous, } } @@ -3040,6 +3204,7 @@ impl KnownClass { | Self::SupportsIndex | Self::NamedTuple | Self::NamedTupleFallback + | Self::TypedDictFallback | Self::Counter | Self::DefaultDict | Self::OrderedDict @@ -3123,7 +3288,85 @@ impl KnownClass { | KnownClass::Field | KnownClass::KwOnly | KnownClass::InitVar - | KnownClass::NamedTupleFallback => false, + | KnownClass::NamedTupleFallback + | KnownClass::TypedDictFallback => false, + } + } + + /// Return `true` if this class is a (true) subclass of `typing.TypedDict`. + pub(crate) const fn is_typed_dict_subclass(self) -> bool { + match self { + KnownClass::Bool + | KnownClass::Object + | KnownClass::Bytes + | KnownClass::Bytearray + | KnownClass::Type + | KnownClass::Int + | KnownClass::Float + | KnownClass::Complex + | KnownClass::Str + | KnownClass::List + | KnownClass::Tuple + | KnownClass::Set + | KnownClass::FrozenSet + | KnownClass::Dict + | KnownClass::Slice + | KnownClass::Property + | KnownClass::BaseException + | KnownClass::Exception + | KnownClass::BaseExceptionGroup + | KnownClass::ExceptionGroup + | KnownClass::Staticmethod + | KnownClass::Classmethod + | KnownClass::Awaitable + | KnownClass::Generator + | KnownClass::Deprecated + | KnownClass::Super + | KnownClass::Enum + | KnownClass::EnumType + | KnownClass::Auto + | KnownClass::Member + | KnownClass::Nonmember + | KnownClass::ABCMeta + | KnownClass::GenericAlias + | KnownClass::ModuleType + | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType + | KnownClass::UnionType + | KnownClass::GeneratorType + | KnownClass::AsyncGeneratorType + | KnownClass::CoroutineType + | KnownClass::NoneType + | KnownClass::Any + | KnownClass::StdlibAlias + | KnownClass::SpecialForm + | KnownClass::TypeVar + | KnownClass::ParamSpec + | KnownClass::ParamSpecArgs + | KnownClass::ParamSpecKwargs + | KnownClass::TypeVarTuple + | KnownClass::TypeAliasType + | KnownClass::NoDefaultType + | KnownClass::NamedTuple + | KnownClass::NewType + | KnownClass::SupportsIndex + | KnownClass::Iterable + | KnownClass::Iterator + | KnownClass::ChainMap + | KnownClass::Counter + | KnownClass::DefaultDict + | KnownClass::Deque + | KnownClass::OrderedDict + | KnownClass::VersionInfo + | KnownClass::EllipsisType + | KnownClass::NotImplementedType + | KnownClass::Field + | KnownClass::KwOnly + | KnownClass::InitVar + | KnownClass::NamedTupleFallback + | KnownClass::TypedDictFallback => false, } } @@ -3211,7 +3454,8 @@ impl KnownClass { | Self::Field | Self::KwOnly | Self::InitVar - | Self::NamedTupleFallback => false, + | Self::NamedTupleFallback + | Self::TypedDictFallback => false, } } @@ -3307,6 +3551,7 @@ impl KnownClass { Self::KwOnly => "KW_ONLY", Self::InitVar => "InitVar", Self::NamedTupleFallback => "NamedTupleFallback", + Self::TypedDictFallback => "TypedDictFallback", } } @@ -3563,7 +3808,7 @@ impl KnownClass { | Self::Deque | Self::OrderedDict => KnownModule::Collections, Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses, - Self::NamedTupleFallback => KnownModule::TypeCheckerInternals, + Self::NamedTupleFallback | Self::TypedDictFallback => KnownModule::TypeCheckerInternals, } } @@ -3640,7 +3885,8 @@ impl KnownClass { | Self::InitVar | Self::Iterable | Self::Iterator - | Self::NamedTupleFallback => false, + | Self::NamedTupleFallback + | Self::TypedDictFallback => false, } } @@ -3719,7 +3965,8 @@ impl KnownClass { | Self::InitVar | Self::Iterable | Self::Iterator - | Self::NamedTupleFallback => false, + | Self::NamedTupleFallback + | Self::TypedDictFallback => false, } } @@ -3809,6 +4056,7 @@ impl KnownClass { "KW_ONLY" => Self::KwOnly, "InitVar" => Self::InitVar, "NamedTupleFallback" => Self::NamedTupleFallback, + "TypedDictFallback" => Self::TypedDictFallback, _ => return None, }; @@ -3873,6 +4121,7 @@ impl KnownClass { | Self::KwOnly | Self::InitVar | Self::NamedTupleFallback + | Self::TypedDictFallback | Self::Awaitable | Self::Generator => module == self.canonical_module(db), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 8a359e3a15..9cbe28464a 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -24,6 +24,7 @@ pub enum ClassBase<'db> { /// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`, /// `Protocol[T]`, or bare `Protocol`. Generic, + TypedDict, } impl<'db> ClassBase<'db> { @@ -39,7 +40,7 @@ impl<'db> ClassBase<'db> { match self { Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()), Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)), - Self::Protocol | Self::Generic => self, + Self::Protocol | Self::Generic | Self::TypedDict => self, } } @@ -51,11 +52,11 @@ impl<'db> ClassBase<'db> { ClassBase::Dynamic( DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec - | DynamicType::TodoTypeAlias - | DynamicType::TodoTypedDict, + | DynamicType::TodoTypeAlias, ) => "@Todo", ClassBase::Protocol => "Protocol", ClassBase::Generic => "Generic", + ClassBase::TypedDict => "TypedDict", } } @@ -158,7 +159,8 @@ impl<'db> ClassBase<'db> { | Type::ProtocolInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy - | Type::TypeIs(_) => None, + | Type::TypeIs(_) + | Type::TypedDict(_) => None, Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic), @@ -234,7 +236,7 @@ impl<'db> ClassBase<'db> { SpecialFormType::OrderedDict => { Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db)) } - SpecialFormType::TypedDict => Some(Self::Dynamic(DynamicType::TodoTypedDict)), + SpecialFormType::TypedDict => Some(Self::TypedDict), SpecialFormType::Callable => { Self::try_from_type(db, todo_type!("Support for Callable as a base class")) } @@ -245,14 +247,14 @@ impl<'db> ClassBase<'db> { pub(super) fn into_class(self) -> Option> { match self { Self::Class(class) => Some(class), - Self::Dynamic(_) | Self::Generic | Self::Protocol => None, + Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => None, } } fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { match self { Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)), - Self::Dynamic(_) | Self::Generic | Self::Protocol => self, + Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => self, } } @@ -276,7 +278,10 @@ impl<'db> ClassBase<'db> { .try_mro(db, specialization) .is_err_and(MroError::is_cycle) } - ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::Protocol => false, + ClassBase::Dynamic(_) + | ClassBase::Generic + | ClassBase::Protocol + | ClassBase::TypedDict => false, } } @@ -288,7 +293,9 @@ impl<'db> ClassBase<'db> { ) -> impl Iterator> { match self { ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic), - ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self), + ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::TypedDict => { + ClassBaseMroIterator::length_2(db, self) + } ClassBase::Class(class) => { ClassBaseMroIterator::from_class(db, class, additional_specialization) } @@ -309,6 +316,7 @@ impl<'db> From> for Type<'db> { ClassBase::Class(class) => class.into(), ClassBase::Protocol => Type::SpecialForm(SpecialFormType::Protocol), ClassBase::Generic => Type::SpecialForm(SpecialFormType::Generic), + ClassBase::TypedDict => Type::SpecialForm(SpecialFormType::TypedDict), } } } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 57cd9f139b..3e325fcca9 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -234,6 +234,9 @@ impl Display for DisplayRepresentation<'_> { } f.write_str("]") } + Type::TypedDict(typed_dict) => { + f.write_str(typed_dict.defining_class(self.db).name(self.db)) + } } } } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 9e4225836a..eaa2f1053a 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -975,7 +975,8 @@ fn is_instance_truthiness<'db>( | Type::TypeIs(..) | Type::Callable(..) | Type::Dynamic(..) - | Type::Never => { + | Type::Never + | Type::TypedDict(_) => { // We could probably try to infer more precise types in some of these cases, but it's unclear // if it's worth the effort. Truthiness::Ambiguous diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index d04fda4150..ba6d458c60 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -98,6 +98,14 @@ impl<'db> AllMembers<'db> { self.extend_with_instance_members(db, class_literal); } + Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => { + self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + } + + Type::GenericAlias(generic_alias) if generic_alias.is_typed_dict(db) => { + self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + } + Type::ClassLiteral(class_literal) => { self.extend_with_class_members(db, ty, class_literal); @@ -155,6 +163,14 @@ impl<'db> AllMembers<'db> { _ => {} }, + Type::TypedDict(_) => { + if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) { + self.extend_with_class_members(db, ty, class_literal); + } + + self.extend_with_type(db, KnownClass::TypedDictFallback.to_instance(db)); + } + Type::ModuleLiteral(literal) => { self.extend_with_type(db, KnownClass::ModuleType.to_instance(db)); let module = literal.module(db); diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index adc79dfe3b..1bda721b7a 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -3906,7 +3906,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::TypeVar(..) | Type::AlwaysTruthy | Type::AlwaysFalsy - | Type::TypeIs(_) => { + | Type::TypeIs(_) + | Type::TypedDict(_) => { // First, try to call the `__setattr__` dunder method. If this is present/defined, overrides // assigning the attributed by the normal mechanism. let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy( @@ -7149,7 +7150,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypeVar(_) - | Type::TypeIs(_), + | Type::TypeIs(_) + | Type::TypedDict(_), ) => { let unary_dunder_method = match op { ast::UnaryOp::Invert => "__invert__", @@ -7274,8 +7276,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { todo @ Type::Dynamic( DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec - | DynamicType::TodoTypeAlias - | DynamicType::TodoTypedDict, + | DynamicType::TodoTypeAlias, ), _, _, @@ -7285,8 +7286,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { todo @ Type::Dynamic( DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec - | DynamicType::TodoTypeAlias - | DynamicType::TodoTypedDict, + | DynamicType::TodoTypeAlias, ), _, ) => Some(todo), @@ -7474,7 +7474,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypeVar(_) - | Type::TypeIs(_), + | Type::TypeIs(_) + | Type::TypedDict(_), Type::FunctionLiteral(_) | Type::BooleanLiteral(_) | Type::Callable(..) @@ -7503,7 +7504,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::Tuple(_) | Type::BoundSuper(_) | Type::TypeVar(_) - | Type::TypeIs(_), + | Type::TypeIs(_) + | Type::TypedDict(_), op, ) => { // We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 632a513b7b..db62ed988e 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -269,7 +269,10 @@ impl<'db> Mro<'db> { continue; } match base { - ClassBase::Class(_) | ClassBase::Generic | ClassBase::Protocol => { + ClassBase::Class(_) + | ClassBase::Generic + | ClassBase::Protocol + | ClassBase::TypedDict => { errors.push(DuplicateBaseError { duplicate_base: base, first_index: *first_index, diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 50fcba8783..2147318b3e 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -256,7 +256,8 @@ impl ClassInfoConstraintFunction { | Type::KnownInstance(_) | Type::TypeIs(_) | Type::WrapperDescriptor(_) - | Type::DataclassTransformer(_) => None, + | Type::DataclassTransformer(_) + | Type::TypedDict(_) => None, } } } diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 58b62af789..23f9c61625 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -166,6 +166,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (ClassBase::Generic, _) => Ordering::Less, (_, ClassBase::Generic) => Ordering::Greater, + (ClassBase::TypedDict, _) => Ordering::Less, + (_, ClassBase::TypedDict) => Ordering::Greater, + (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { dynamic_elements_ordering(left, right) } @@ -234,6 +237,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( unreachable!("Two equal, normalized intersections should share the same Salsa ID") } + + (Type::TypedDict(left), Type::TypedDict(right)) => left.cmp(right), + (Type::TypedDict(_), _) => Ordering::Less, + (_, Type::TypedDict(_)) => Ordering::Greater, } } @@ -257,9 +264,6 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering (DynamicType::TodoTypeAlias, _) => Ordering::Less, (_, DynamicType::TodoTypeAlias) => Ordering::Greater, - - (DynamicType::TodoTypedDict, _) => Ordering::Less, - (_, DynamicType::TodoTypedDict) => Ordering::Greater, } } diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index 3c9bb00a14..a63a1e98d3 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -4,7 +4,7 @@ use crate::{ BoundMethodType, BoundSuperType, CallableType, GenericAlias, IntersectionType, KnownInstanceType, MethodWrapperKind, NominalInstanceType, PropertyInstanceType, ProtocolInstanceType, SubclassOfType, Type, TypeAliasType, TypeIsType, TypeVarInstance, - UnionType, + TypedDictType, UnionType, class::walk_generic_alias, function::{FunctionType, walk_function_type}, instance::{walk_nominal_instance_type, walk_protocol_instance_type}, @@ -12,7 +12,8 @@ use crate::{ tuple::{TupleType, walk_tuple_type}, walk_bound_method_type, walk_bound_super_type, walk_callable_type, walk_intersection_type, walk_known_instance_type, walk_method_wrapper_type, walk_property_instance_type, - walk_type_alias_type, walk_type_var_type, walk_typeis_type, walk_union, + walk_type_alias_type, walk_type_var_type, walk_typed_dict_type, walk_typeis_type, + walk_union, }, }; @@ -107,6 +108,10 @@ pub(crate) trait TypeVisitor<'db> { fn visit_type_alias_type(&mut self, db: &'db dyn Db, type_alias: TypeAliasType<'db>) { walk_type_alias_type(db, type_alias, self); } + + fn visit_typed_dict_type(&mut self, db: &'db dyn Db, typed_dict: TypedDictType<'db>) { + walk_typed_dict_type(db, typed_dict, self); + } } /// Enumeration of types that may contain other types, such as unions, intersections, and generics. @@ -128,6 +133,7 @@ enum NonAtomicType<'db> { TypeIs(TypeIsType<'db>), TypeVar(TypeVarInstance<'db>), ProtocolInstance(ProtocolInstanceType<'db>), + TypedDict(TypedDictType<'db>), } enum TypeKind<'db> { @@ -190,6 +196,9 @@ impl<'db> From> for TypeKind<'db> { } Type::TypeVar(type_var) => TypeKind::NonAtomic(NonAtomicType::TypeVar(type_var)), Type::TypeIs(type_is) => TypeKind::NonAtomic(NonAtomicType::TypeIs(type_is)), + Type::TypedDict(typed_dict) => { + TypeKind::NonAtomic(NonAtomicType::TypedDict(typed_dict)) + } } } } @@ -226,6 +235,7 @@ fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( NonAtomicType::ProtocolInstance(protocol) => { visitor.visit_protocol_instance_type(db, protocol); } + NonAtomicType::TypedDict(typed_dict) => visitor.visit_typed_dict_type(db, typed_dict), } } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index b2d71742b7..b71a962eae 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -30,7 +30,7 @@ ty_python_semantic = { path = "../crates/ty_python_semantic" } ty_vendored = { path = "../crates/ty_vendored" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "86ca4a9d70e97dd5107e6111a09647dd10ff7535", default-features = false, features = [ +salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d66fe331d546216132ace503512b94d5c68d2c50", default-features = false, features = [ "compact_str", "macros", "salsa_unstable",