diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md index 445c7a05da3aa..0496dbb4ce402 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md @@ -38,8 +38,12 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form] ```py from typing_extensions import LiteralString -a: LiteralString[str] # error: [invalid-type-form] -b: LiteralString["foo"] # error: [invalid-type-form] +# error: [invalid-type-form] +a: LiteralString[str] + +# error: [invalid-type-form] +# error: [unresolved-reference] "Name `foo` used when not defined" +b: LiteralString["foo"] ``` ### As a base class 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 3d76ece35d281..1c7665d84d197 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 @@ -89,9 +89,12 @@ python-version = "3.12" Some of these are not subscriptable: ```py -from typing_extensions import Self, TypeAlias +from typing_extensions import Self, TypeAlias, TypeVar -X: TypeAlias[T] = int # error: [invalid-type-form] +T = TypeVar("T") + +# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter" +X: TypeAlias[T] = int class Foo[T]: # error: [invalid-type-form] "Special form `typing.Self` expected no type parameter" diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md index 7826fbf92d1bd..fcc61160f284d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md @@ -11,8 +11,6 @@ from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict X: Final = 42 Y: Final[int] = 42 -# TODO: `TypedDict` is actually valid as a base -# error: [invalid-base] class Bar(TypedDict): x: Required[int] y: NotRequired[str] diff --git a/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md b/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md new file mode 100644 index 0000000000000..22d43260ba758 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/typed_dict.md @@ -0,0 +1,24 @@ +# `TypedDict` + +We do not support `TypedDict`s yet. This test mainly exists to make sure that we do not emit any +errors for the definition of a `TypedDict`. + +```py +from typing_extensions import TypedDict, Required + +class Person(TypedDict): + name: str + age: int | None + +# TODO: This should not be an error: +# error: [invalid-assignment] +alice: Person = {"name": "Alice", "age": 30} + +# Alternative syntax +Message = TypedDict("Message", {"id": Required[int], "content": str}, total=False) + +msg = Message(id=1, content="Hello") + +# No errors for yet-unsupported features (`closed`): +OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True) +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 070adc5aabae3..9a86e1a6413d8 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -3890,6 +3890,28 @@ impl<'db> Type<'db> { } }, + Type::KnownInstance(KnownInstanceType::TypedDict) => { + Signatures::single(CallableSignature::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("typename"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("fields"))) + .with_annotated_type(KnownClass::Dict.to_instance(db)) + .with_default_type(Type::any()), + Parameter::keyword_only(Name::new_static("total")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + // Future compatibility, in case new keyword arguments will be added: + Parameter::keyword_variadic(Name::new_static("kwargs")) + .with_annotated_type(Type::any()), + ]), + None, + ), + )) + } + Type::GenericAlias(_) => { // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` @@ -4471,6 +4493,7 @@ impl<'db> Type<'db> { KnownInstanceType::TypingSelf => Ok(todo_type!("Support for `typing.Self`")), KnownInstanceType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")), + KnownInstanceType::TypedDict => Ok(todo_type!("Support for `typing.TypedDict`")), KnownInstanceType::Protocol => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol], diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 397c2aaa118e2..8f5fb666589fd 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -19,9 +19,9 @@ use crate::types::diagnostic::{ use crate::types::generics::{Specialization, SpecializationBuilder}; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ - BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, KnownClass, - KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, TupleType, - UnionType, WrapperDescriptorKind, + todo_type, BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators, + KnownClass, KnownFunction, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, + TupleType, UnionType, WrapperDescriptorKind, }; use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic}; use ruff_python_ast as ast; @@ -772,6 +772,10 @@ impl<'db> Bindings<'db> { _ => {} }, + Type::KnownInstance(KnownInstanceType::TypedDict) => { + overload.set_return_type(todo_type!("TypedDict")); + } + // Not a special case _ => {} } 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 395ff5268da16..91c132fc3d49e 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -169,6 +169,9 @@ impl<'db> ClassBase<'db> { KnownInstanceType::OrderedDict => { Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db)) } + KnownInstanceType::TypedDict => { + Self::try_from_type(db, KnownClass::Dict.to_class_literal(db)) + } KnownInstanceType::Callable => { Self::try_from_type(db, todo_type!("Support for Callable as a base class")) } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 29d69e5099e8b..e9448d83b0969 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -7710,6 +7710,8 @@ impl<'db> TypeInferenceBuilder<'db> { | KnownInstanceType::Any | KnownInstanceType::AlwaysTruthy | KnownInstanceType::AlwaysFalsy => { + self.infer_type_expression(arguments_slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", @@ -7720,7 +7722,10 @@ impl<'db> TypeInferenceBuilder<'db> { } KnownInstanceType::TypingSelf | KnownInstanceType::TypeAlias + | KnownInstanceType::TypedDict | KnownInstanceType::Unknown => { + self.infer_type_expression(arguments_slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Special form `{}` expected no type parameter", @@ -7730,6 +7735,8 @@ impl<'db> TypeInferenceBuilder<'db> { Type::unknown() } KnownInstanceType::LiteralString => { + self.infer_type_expression(arguments_slice); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let mut diag = builder.into_diagnostic(format_args!( "Type `{}` expected no type parameter", diff --git a/crates/red_knot_python_semantic/src/types/known_instance.rs b/crates/red_knot_python_semantic/src/types/known_instance.rs index 27ed7a533a143..01316c1c5698b 100644 --- a/crates/red_knot_python_semantic/src/types/known_instance.rs +++ b/crates/red_knot_python_semantic/src/types/known_instance.rs @@ -95,6 +95,7 @@ pub enum KnownInstanceType<'db> { NotRequired, TypeAlias, TypeGuard, + TypedDict, TypeIs, ReadOnly, // TODO: fill this enum out with more special forms, etc. @@ -125,6 +126,7 @@ impl<'db> KnownInstanceType<'db> { | Self::NotRequired | Self::TypeAlias | Self::TypeGuard + | Self::TypedDict | Self::TypeIs | Self::List | Self::Dict @@ -172,6 +174,7 @@ impl<'db> KnownInstanceType<'db> { Self::NotRequired => "typing.NotRequired", Self::TypeAlias => "typing.TypeAlias", Self::TypeGuard => "typing.TypeGuard", + Self::TypedDict => "typing.TypedDict", Self::TypeIs => "typing.TypeIs", Self::List => "typing.List", Self::Dict => "typing.Dict", @@ -220,6 +223,7 @@ impl<'db> KnownInstanceType<'db> { Self::NotRequired => KnownClass::SpecialForm, Self::TypeAlias => KnownClass::SpecialForm, Self::TypeGuard => KnownClass::SpecialForm, + Self::TypedDict => KnownClass::SpecialForm, Self::TypeIs => KnownClass::SpecialForm, Self::ReadOnly => KnownClass::SpecialForm, Self::List => KnownClass::StdlibAlias, @@ -293,6 +297,7 @@ impl<'db> KnownInstanceType<'db> { "Required" => Self::Required, "TypeAlias" => Self::TypeAlias, "TypeGuard" => Self::TypeGuard, + "TypedDict" => Self::TypedDict, "TypeIs" => Self::TypeIs, "ReadOnly" => Self::ReadOnly, "Concatenate" => Self::Concatenate, @@ -350,6 +355,7 @@ impl<'db> KnownInstanceType<'db> { | Self::NotRequired | Self::TypeAlias | Self::TypeGuard + | Self::TypedDict | Self::TypeIs | Self::ReadOnly | Self::TypeAliasType(_) 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 8037611f5109f..64d9bb726a78a 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -221,6 +221,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (KnownInstanceType::TypeGuard, _) => Ordering::Less, (_, KnownInstanceType::TypeGuard) => Ordering::Greater, + (KnownInstanceType::TypedDict, _) => Ordering::Less, + (_, KnownInstanceType::TypedDict) => Ordering::Greater, + (KnownInstanceType::List, _) => Ordering::Less, (_, KnownInstanceType::List) => Ordering::Greater,