diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md index 5089804119..7c0ba50f1a 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md @@ -76,8 +76,7 @@ from ty_extensions import reveal_mro class C(Annotated[int, "foo"]): ... -# TODO: Should be `(, , )` -reveal_mro(C) # revealed: (, @Todo(Inference of subscript on special form), ) +reveal_mro(C) # revealed: (, , ) ``` ### Not parameterized diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 504921c317..690225d9b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -33,7 +33,7 @@ g(None) We also support unions in type aliases: ```py -from typing_extensions import Any, Never, Literal +from typing_extensions import Any, Never, Literal, LiteralString, Tuple, Annotated, Optional from ty_extensions import Unknown IntOrStr = int | str @@ -56,6 +56,14 @@ UnknownOrInt = Unknown | int IntOrUnknown = int | Unknown StrOrZero = str | Literal[0] ZeroOrStr = Literal[0] | str +LiteralStringOrInt = LiteralString | int +IntOrLiteralString = int | LiteralString +NoneOrTuple = None | Tuple[int, str] +TupleOrNone = Tuple[int, str] | None +IntOrAnnotated = int | Annotated[str, "meta"] +AnnotatedOrInt = Annotated[str, "meta"] | int +IntOrOptional = int | Optional[str] +OptionalOrInt = Optional[str] | int reveal_type(IntOrStr) # revealed: types.UnionType reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType @@ -77,6 +85,14 @@ reveal_type(UnknownOrInt) # revealed: types.UnionType reveal_type(IntOrUnknown) # revealed: types.UnionType reveal_type(StrOrZero) # revealed: types.UnionType reveal_type(ZeroOrStr) # revealed: types.UnionType +reveal_type(IntOrLiteralString) # revealed: types.UnionType +reveal_type(LiteralStringOrInt) # revealed: types.UnionType +reveal_type(NoneOrTuple) # revealed: types.UnionType +reveal_type(TupleOrNone) # revealed: types.UnionType +reveal_type(IntOrAnnotated) # revealed: types.UnionType +reveal_type(AnnotatedOrInt) # revealed: types.UnionType +reveal_type(IntOrOptional) # revealed: types.UnionType +reveal_type(OptionalOrInt) # revealed: types.UnionType def _( int_or_str: IntOrStr, @@ -99,6 +115,14 @@ def _( int_or_unknown: IntOrUnknown, str_or_zero: StrOrZero, zero_or_str: ZeroOrStr, + literal_string_or_int: LiteralStringOrInt, + int_or_literal_string: IntOrLiteralString, + none_or_tuple: NoneOrTuple, + tuple_or_none: TupleOrNone, + int_or_annotated: IntOrAnnotated, + annotated_or_int: AnnotatedOrInt, + int_or_optional: IntOrOptional, + optional_or_int: OptionalOrInt, ): reveal_type(int_or_str) # revealed: int | str reveal_type(int_or_str_or_bytes1) # revealed: int | str | bytes @@ -120,6 +144,14 @@ def _( reveal_type(int_or_unknown) # revealed: int | Unknown reveal_type(str_or_zero) # revealed: str | Literal[0] reveal_type(zero_or_str) # revealed: Literal[0] | str + reveal_type(literal_string_or_int) # revealed: LiteralString | int + reveal_type(int_or_literal_string) # revealed: int | LiteralString + reveal_type(none_or_tuple) # revealed: None | tuple[int, str] + reveal_type(tuple_or_none) # revealed: tuple[int, str] | None + reveal_type(int_or_annotated) # revealed: int | str + reveal_type(annotated_or_int) # revealed: str | int + reveal_type(int_or_optional) # revealed: int | str | None + reveal_type(optional_or_int) # revealed: str | None | int ``` If a type is unioned with itself in a value expression, the result is just that type. No @@ -325,6 +357,115 @@ def _(weird: IntLiteral1[int]): reveal_type(weird) # revealed: Unknown ``` +## `Annotated` + +Basic usage: + +```py +from typing import Annotated + +MyAnnotatedInt = Annotated[int, "some metadata", 1, 2, 3] + +def _(annotated_int: MyAnnotatedInt): + reveal_type(annotated_int) # revealed: int +``` + +Usage with generics: + +```py +from typing import TypeVar + +T = TypeVar("T") + +Deprecated = Annotated[T, "deprecated attribute"] + +class C: + old: Deprecated[int] + +# TODO: Should be `int` +reveal_type(C().old) # revealed: @Todo(Generic specialization of typing.Annotated) +``` + +If the metadata argument is missing, we emit an error (because this code fails at runtime), but +still use the first element as the type, when used in annotations: + +```py +# error: [invalid-type-form] "Special form `typing.Annotated` expected at least 2 arguments (one type and at least one metadata element)" +WronglyAnnotatedInt = Annotated[int] + +def _(wrongly_annotated_int: WronglyAnnotatedInt): + reveal_type(wrongly_annotated_int) # revealed: int +``` + +## `Optional` + +Starting with Python 3.14, `Optional[int]` creates an instance of `typing.Union`, which is an alias +for `types.UnionType`. We only support this new behavior and do not attempt to model the details of +the pre-3.14 behavior: + +```py +from typing import Optional + +MyOptionalInt = Optional[int] + +reveal_type(MyOptionalInt) # revealed: types.UnionType + +def _(optional_int: MyOptionalInt): + reveal_type(optional_int) # revealed: int | None +``` + +A special case is `Optional[None]`, which is equivalent to `None`: + +```py +JustNone = Optional[None] + +reveal_type(JustNone) # revealed: None + +def _(just_none: JustNone): + reveal_type(just_none) # revealed: None +``` + +Invalid uses: + +```py +# error: [invalid-type-form] "`typing.Optional` requires exactly one argument" +Optional[int, str] +``` + +## `LiteralString`, `NoReturn`, `Never` + +```py +from typing_extensions import LiteralString, NoReturn, Never + +MyLiteralString = LiteralString +MyNoReturn = NoReturn +MyNever = Never + +reveal_type(MyLiteralString) # revealed: typing.LiteralString +reveal_type(MyNoReturn) # revealed: typing.NoReturn +reveal_type(MyNever) # revealed: typing.Never + +def _( + ls: MyLiteralString, + nr: MyNoReturn, + nv: MyNever, +): + reveal_type(ls) # revealed: LiteralString + reveal_type(nr) # revealed: Never + reveal_type(nv) # revealed: Never +``` + +## `Tuple` + +```py +from typing import Tuple + +IntAndStr = Tuple[int, str] + +def _(int_and_str: IntAndStr): + reveal_type(int_and_str) # revealed: tuple[int, str] +``` + ## Stringified annotations? From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html): diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins…_-_`classinfo`_is_an_in…_(eeef56c0ef87a30b).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins…_-_`classinfo`_is_an_in…_(eeef56c0ef87a30b).snap index 34383c8fd0..822f9319a1 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins…_-_`classinfo`_is_an_in…_(eeef56c0ef87a30b).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/isinstance.md_-_Narrowing_for_`isins…_-_`classinfo`_is_an_in…_(eeef56c0ef87a30b).snap @@ -63,7 +63,7 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` 10 | # error: [invalid-argument-type] | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects -info: Elements `typing.Literal` and `` in the union are not class objects +info: Elements `` and `` in the union are not class objects info: rule `invalid-argument-type` is enabled by default ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e3b8b8e89e..e69d1fa3a3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6462,7 +6462,12 @@ impl<'db> Type<'db> { } Ok(builder.build()) } - KnownInstanceType::Literal(list) => Ok(list.to_union(db)), + KnownInstanceType::Literal(ty) => Ok(ty.inner(db)), + KnownInstanceType::Annotated(ty) => { + Ok(ty + .inner(db) + .in_type_expression(db, scope_id, typevar_binding_context)?) + } }, Type::SpecialForm(special_form) => match special_form { @@ -7676,10 +7681,13 @@ pub enum KnownInstanceType<'db> { /// A single instance of `types.UnionType`, which stores the left- and /// right-hand sides of a PEP 604 union. - UnionType(TypeList<'db>), + UnionType(InternedTypes<'db>), /// A single instance of `typing.Literal` - Literal(TypeList<'db>), + Literal(InternedType<'db>), + + /// A single instance of `typing.Annotated` + Annotated(InternedType<'db>), } fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -7706,11 +7714,14 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( visitor.visit_type(db, default_ty); } } - KnownInstanceType::UnionType(list) | KnownInstanceType::Literal(list) => { + KnownInstanceType::UnionType(list) => { for element in list.elements(db) { visitor.visit_type(db, *element); } } + KnownInstanceType::Literal(ty) | KnownInstanceType::Annotated(ty) => { + visitor.visit_type(db, ty.inner(db)); + } } } @@ -7748,7 +7759,8 @@ impl<'db> KnownInstanceType<'db> { Self::ConstraintSet(set) } Self::UnionType(list) => Self::UnionType(list.normalized_impl(db, visitor)), - Self::Literal(list) => Self::Literal(list.normalized_impl(db, visitor)), + Self::Literal(ty) => Self::Literal(ty.normalized_impl(db, visitor)), + Self::Annotated(ty) => Self::Annotated(ty.normalized_impl(db, visitor)), } } @@ -7768,6 +7780,7 @@ impl<'db> KnownInstanceType<'db> { Self::ConstraintSet(_) => KnownClass::ConstraintSet, Self::UnionType(_) => KnownClass::UnionType, Self::Literal(_) => KnownClass::GenericAlias, + Self::Annotated(_) => KnownClass::GenericAlias, } } @@ -7848,7 +7861,10 @@ impl<'db> KnownInstanceType<'db> { ) } KnownInstanceType::UnionType(_) => f.write_str("types.UnionType"), - KnownInstanceType::Literal(_) => f.write_str("typing.Literal"), + KnownInstanceType::Literal(_) => f.write_str(""), + KnownInstanceType::Annotated(_) => { + f.write_str("") + } } } } @@ -8045,7 +8061,7 @@ impl<'db> InvalidTypeExpressionError<'db> { fn into_fallback_type( self, context: &InferContext, - node: &ast::Expr, + node: &impl Ranged, is_reachable: bool, ) -> Type<'db> { let InvalidTypeExpressionError { @@ -8984,29 +9000,25 @@ impl<'db> TypeVarBoundOrConstraints<'db> { /// # Ordering /// Ordering is based on the context's salsa-assigned id and not on its values. /// The id may change between runs, or when the context was garbage collected and recreated. -#[salsa::interned(debug)] +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] #[derive(PartialOrd, Ord)] -pub struct TypeList<'db> { +pub struct InternedTypes<'db> { #[returns(deref)] elements: Box<[Type<'db>]>, } -impl get_size2::GetSize for TypeList<'_> {} +impl get_size2::GetSize for InternedTypes<'_> {} -impl<'db> TypeList<'db> { +impl<'db> InternedTypes<'db> { pub(crate) fn from_elements( db: &'db dyn Db, elements: impl IntoIterator>, - ) -> TypeList<'db> { - TypeList::new(db, elements.into_iter().collect::>()) - } - - pub(crate) fn singleton(db: &'db dyn Db, element: Type<'db>) -> TypeList<'db> { - TypeList::from_elements(db, [element]) + ) -> InternedTypes<'db> { + InternedTypes::new(db, elements.into_iter().collect::>()) } pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - TypeList::new( + InternedTypes::new( db, self.elements(db) .iter() @@ -9014,10 +9026,24 @@ impl<'db> TypeList<'db> { .collect::>(), ) } +} - /// Turn this list of types `[T1, T2, ...]` into a union type `T1 | T2 | ...`. - pub(crate) fn to_union(self, db: &'db dyn Db) -> Type<'db> { - UnionType::from_elements(db, self.elements(db)) +/// A salsa-interned `Type` +/// +/// # Ordering +/// Ordering is based on the context's salsa-assigned id and not on its values. +/// The id may change between runs, or when the context was garbage collected and recreated. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] +pub struct InternedType<'db> { + inner: Type<'db>, +} + +impl get_size2::GetSize for InternedType<'_> {} + +impl<'db> InternedType<'db> { + pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { + InternedType::new(db, self.inner(db).normalized_impl(db, visitor)) } } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index caddc88567..87417f8314 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -170,6 +170,7 @@ impl<'db> ClassBase<'db> { | KnownInstanceType::ConstraintSet(_) | KnownInstanceType::UnionType(_) | KnownInstanceType::Literal(_) => None, + KnownInstanceType::Annotated(ty) => Self::try_from_type(db, ty.inner(db), subclass), }, Type::SpecialForm(special_form) => match special_form { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3b1142b89b..37cb64ff0b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -69,12 +69,12 @@ use crate::types::diagnostic::{ hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds, - report_instance_layout_conflict, report_invalid_assignment, - report_invalid_attribute_assignment, report_invalid_exception_caught, - report_invalid_exception_cause, report_invalid_exception_raised, - report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, - report_invalid_or_unsupported_base, report_invalid_return_type, - report_invalid_type_checking_constant, + report_instance_layout_conflict, report_invalid_arguments_to_annotated, + report_invalid_assignment, report_invalid_attribute_assignment, + report_invalid_exception_caught, report_invalid_exception_cause, + report_invalid_exception_raised, report_invalid_generator_function_return_type, + report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base, + report_invalid_return_type, report_invalid_type_checking_constant, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_rebound_typevar, report_slice_step_size_zero, @@ -100,10 +100,10 @@ use crate::types::typed_dict::{ use crate::types::visitor::any_over_type; use crate::types::{ CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams, - DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, - MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm, - Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type, - TypeAliasType, TypeAndQualifiers, TypeContext, TypeList, TypeQualifiers, + DynamicType, InternedType, InternedTypes, IntersectionBuilder, IntersectionType, KnownClass, + KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, + ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, + Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, binding_type, todo_type, @@ -8755,14 +8755,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::GenericAlias(..) | Type::SpecialForm(_) | Type::KnownInstance( - KnownInstanceType::UnionType(_) | KnownInstanceType::Literal(_), + KnownInstanceType::UnionType(_) + | KnownInstanceType::Literal(_) + | KnownInstanceType::Annotated(_), ), Type::ClassLiteral(..) | Type::SubclassOf(..) | Type::GenericAlias(..) | Type::SpecialForm(_) | Type::KnownInstance( - KnownInstanceType::UnionType(_) | KnownInstanceType::Literal(_), + KnownInstanceType::UnionType(_) + | KnownInstanceType::Literal(_) + | KnownInstanceType::Annotated(_), ), ast::Operator::BitOr, ) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 => { @@ -8770,7 +8774,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(left_ty) } else { Some(Type::KnownInstance(KnownInstanceType::UnionType( - TypeList::from_elements(self.db(), [left_ty, right_ty]), + InternedTypes::from_elements(self.db(), [left_ty, right_ty]), ))) } } @@ -8795,7 +8799,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && instance.has_known_class(self.db(), KnownClass::NoneType) => { Some(Type::KnownInstance(KnownInstanceType::UnionType( - TypeList::from_elements(self.db(), [left_ty, right_ty]), + InternedTypes::from_elements(self.db(), [left_ty, right_ty]), ))) } @@ -9863,14 +9867,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { + let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); + self.infer_subscript_load_impl(value_ty, subscript) + } + + fn infer_subscript_load_impl( + &mut self, + value_ty: Type<'db>, + subscript: &ast::ExprSubscript, + ) -> Type<'db> { let ast::ExprSubscript { range: _, node_index: _, - value, + value: _, slice, ctx, } = subscript; - let value_ty = self.infer_expression(value, TypeContext::default()); + let mut constraint_keys = vec![]; // If `value` is a valid reference, we attempt type narrowing by assignment. @@ -9896,60 +9909,117 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::from(tuple.to_class_type(db)) }; - // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the - // subscript inference logic and treat this as an explicit specialization. - // TODO: Move this logic into a custom callable, and update `find_name_in_mro` to return - // this callable as the `__class_getitem__` method on `type`. That probably requires - // updating all of the subscript logic below to use custom callables for all of the _other_ - // special cases, too. - if let Type::ClassLiteral(class) = value_ty { - if class.is_tuple(self.db()) { + match value_ty { + Type::ClassLiteral(class) => { + // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the + // subscript inference logic and treat this as an explicit specialization. + // TODO: Move this logic into a custom callable, and update `find_name_in_mro` to return + // this callable as the `__class_getitem__` method on `type`. That probably requires + // updating all of the subscript logic below to use custom callables for all of the _other_ + // special cases, too. + if class.is_tuple(self.db()) { + return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); + } + if let Some(generic_context) = class.generic_context(self.db()) { + return self.infer_explicit_class_specialization( + subscript, + value_ty, + class, + generic_context, + ); + } + } + Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { + if let Some(generic_context) = type_alias.generic_context(self.db()) { + return self.infer_explicit_type_alias_specialization( + subscript, + value_ty, + type_alias, + generic_context, + ); + } + } + Type::SpecialForm(SpecialFormType::Tuple) => { return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); } - if let Some(generic_context) = class.generic_context(self.db()) { - return self.infer_explicit_class_specialization( - subscript, - value_ty, - class, - generic_context, - ); - } - } - if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty { - return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); - } - if let Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) = value_ty { - if let Some(generic_context) = type_alias.generic_context(self.db()) { - return self.infer_explicit_type_alias_specialization( - subscript, - value_ty, - type_alias, - generic_context, - ); - } - } - if value_ty == Type::SpecialForm(SpecialFormType::Literal) { - match self.infer_literal_parameter_type(slice) { - Ok(result) => { - return Type::KnownInstance(KnownInstanceType::Literal(TypeList::singleton( - self.db(), - result, - ))); - } - Err(nodes) => { - for node in nodes { - let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) - else { - continue; - }; - builder.into_diagnostic( - "Type arguments for `Literal` must be `None`, \ - a literal value (int, bool, str, or bytes), or an enum member", - ); + Type::SpecialForm(SpecialFormType::Literal) => { + match self.infer_literal_parameter_type(slice) { + Ok(result) => { + return Type::KnownInstance(KnownInstanceType::Literal(InternedType::new( + self.db(), + result, + ))); + } + Err(nodes) => { + for node in nodes { + let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, node) + else { + continue; + }; + builder.into_diagnostic( + "Type arguments for `Literal` must be `None`, \ + a literal value (int, bool, str, or bytes), or an enum member", + ); + } + return Type::unknown(); } - return Type::unknown(); } } + Type::SpecialForm(SpecialFormType::Annotated) => { + let ast::Expr::Tuple(ast::ExprTuple { + elts: ref arguments, + .. + }) = **slice + else { + report_invalid_arguments_to_annotated(&self.context, subscript); + + return self.infer_expression(slice, TypeContext::default()); + }; + + if arguments.len() < 2 { + report_invalid_arguments_to_annotated(&self.context, subscript); + } + + let [type_expr, metadata @ ..] = &arguments[..] else { + for argument in arguments { + self.infer_expression(argument, TypeContext::default()); + } + self.store_expression_type(slice, Type::unknown()); + return Type::unknown(); + }; + + for element in metadata { + self.infer_expression(element, TypeContext::default()); + } + + let ty = self.infer_expression(type_expr, TypeContext::default()); + + return Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( + self.db(), + ty, + ))); + } + Type::SpecialForm(SpecialFormType::Optional) => { + if matches!(**slice, ast::Expr::Tuple(_)) { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { + builder.into_diagnostic(format_args!( + "`typing.Optional` requires exactly one argument" + )); + } + } + + let ty = self.infer_expression(slice, TypeContext::default()); + + // `Optional[None]` is equivalent to `None`: + if ty.is_none(self.db()) { + return ty; + } + + return Type::KnownInstance(KnownInstanceType::UnionType( + InternedTypes::from_elements(self.db(), [ty, Type::none(self.db())]), + )); + } + _ => {} } let slice_ty = self.infer_expression(slice, TypeContext::default()); diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index d091487ce7..7694839c15 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -4,7 +4,7 @@ use ruff_python_ast as ast; use super::{DeferredExpressionState, TypeInferenceBuilder}; use crate::types::diagnostic::{ self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, - report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable, + report_invalid_arguments_to_callable, }; use crate::types::signatures::Signature; use crate::types::string_annotation::parse_string_annotation; @@ -819,11 +819,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "`{ty}` is not a generic class", - ty = ty.to_union(self.db()).display(self.db()) + ty = ty.inner(self.db()).display(self.db()) )); } Type::unknown() } + KnownInstanceType::Annotated(_) => { + self.infer_type_expression(slice); + todo_type!("Generic specialization of typing.Annotated") + } }, Type::Dynamic(DynamicType::Todo(_)) => { self.infer_type_expression(slice); @@ -900,7 +904,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ty } - fn infer_parameterized_special_form_type_expression( + pub(crate) fn infer_parameterized_special_form_type_expression( &mut self, subscript: &ast::ExprSubscript, special_form: SpecialFormType, @@ -909,36 +913,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let arguments_slice = &*subscript.slice; match special_form { SpecialFormType::Annotated => { - let ast::Expr::Tuple(ast::ExprTuple { - elts: arguments, .. - }) = arguments_slice - else { - report_invalid_arguments_to_annotated(&self.context, subscript); - - // `Annotated[]` with less than two arguments is an error at runtime. - // However, we still treat `Annotated[T]` as `T` here for the purpose of - // giving better diagnostics later on. - // Pyright also does this. Mypy doesn't; it falls back to `Any` instead. - return self.infer_type_expression(arguments_slice); - }; - - if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); - } - - let [type_expr, metadata @ ..] = &arguments[..] else { - for argument in arguments { - self.infer_expression(argument, TypeContext::default()); - } - self.store_expression_type(arguments_slice, Type::unknown()); - return Type::unknown(); - }; - - for element in metadata { - self.infer_expression(element, TypeContext::default()); - } - - let ty = self.infer_type_expression(type_expr); + let ty = self + .infer_subscript_load_impl( + Type::SpecialForm(SpecialFormType::Annotated), + subscript, + ) + .in_type_expression(db, self.scope(), None) + .unwrap_or_else(|err| err.into_fallback_type(&self.context, subscript, true)); self.store_expression_type(arguments_slice, ty); ty } @@ -1453,8 +1434,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Ok(value_ty); } } - Type::KnownInstance(KnownInstanceType::Literal(list)) => { - return Ok(list.to_union(self.db())); + Type::KnownInstance(KnownInstanceType::Literal(ty)) => { + return Ok(ty.inner(self.db())); } // `Literal[SomeEnum.Member]` Type::EnumLiteral(_) => {