[ty] Add support for `Optional` and `Annotated` in implicit type aliases (#21321)

## Summary

Add support for `Optional` and `Annotated` in implicit type aliases

part of https://github.com/astral-sh/ty/issues/221

## Typing conformance changes

New expected diagnostics.

## Ecosystem

A lot of true positives, some known limitations unrelated to this PR.

## Test Plan

New Markdown tests
This commit is contained in:
David Peter 2025-11-10 10:24:38 +01:00 committed by GitHub
parent 3fa609929f
commit 238f151371
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 343 additions and 125 deletions

View File

@ -76,8 +76,7 @@ from ty_extensions import reveal_mro
class C(Annotated[int, "foo"]): ... class C(Annotated[int, "foo"]): ...
# TODO: Should be `(<class 'C'>, <class 'int'>, <class 'object'>)` reveal_mro(C) # revealed: (<class 'C'>, <class 'int'>, <class 'object'>)
reveal_mro(C) # revealed: (<class 'C'>, @Todo(Inference of subscript on special form), <class 'object'>)
``` ```
### Not parameterized ### Not parameterized

View File

@ -33,7 +33,7 @@ g(None)
We also support unions in type aliases: We also support unions in type aliases:
```py ```py
from typing_extensions import Any, Never, Literal from typing_extensions import Any, Never, Literal, LiteralString, Tuple, Annotated, Optional
from ty_extensions import Unknown from ty_extensions import Unknown
IntOrStr = int | str IntOrStr = int | str
@ -56,6 +56,14 @@ UnknownOrInt = Unknown | int
IntOrUnknown = int | Unknown IntOrUnknown = int | Unknown
StrOrZero = str | Literal[0] StrOrZero = str | Literal[0]
ZeroOrStr = Literal[0] | str 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(IntOrStr) # revealed: types.UnionType
reveal_type(IntOrStrOrBytes1) # 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(IntOrUnknown) # revealed: types.UnionType
reveal_type(StrOrZero) # revealed: types.UnionType reveal_type(StrOrZero) # revealed: types.UnionType
reveal_type(ZeroOrStr) # 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 _( def _(
int_or_str: IntOrStr, int_or_str: IntOrStr,
@ -99,6 +115,14 @@ def _(
int_or_unknown: IntOrUnknown, int_or_unknown: IntOrUnknown,
str_or_zero: StrOrZero, str_or_zero: StrOrZero,
zero_or_str: ZeroOrStr, 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) # revealed: int | str
reveal_type(int_or_str_or_bytes1) # revealed: int | str | bytes 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(int_or_unknown) # revealed: int | Unknown
reveal_type(str_or_zero) # revealed: str | Literal[0] reveal_type(str_or_zero) # revealed: str | Literal[0]
reveal_type(zero_or_str) # revealed: Literal[0] | str 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 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 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? ## Stringified annotations?
From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html): From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html):

View File

@ -63,7 +63,7 @@ error[invalid-argument-type]: Invalid second argument to `isinstance`
10 | # error: [invalid-argument-type] 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: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects
info: Elements `typing.Literal` and `<class 'list[int]'>` in the union are not class objects info: Elements `<typing.Literal special form>` and `<class 'list[int]'>` in the union are not class objects
info: rule `invalid-argument-type` is enabled by default info: rule `invalid-argument-type` is enabled by default
``` ```

View File

@ -6462,7 +6462,12 @@ impl<'db> Type<'db> {
} }
Ok(builder.build()) 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 { 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 /// A single instance of `types.UnionType`, which stores the left- and
/// right-hand sides of a PEP 604 union. /// right-hand sides of a PEP 604 union.
UnionType(TypeList<'db>), UnionType(InternedTypes<'db>),
/// A single instance of `typing.Literal` /// 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>( 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); visitor.visit_type(db, default_ty);
} }
} }
KnownInstanceType::UnionType(list) | KnownInstanceType::Literal(list) => { KnownInstanceType::UnionType(list) => {
for element in list.elements(db) { for element in list.elements(db) {
visitor.visit_type(db, *element); 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::ConstraintSet(set)
} }
Self::UnionType(list) => Self::UnionType(list.normalized_impl(db, visitor)), 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::ConstraintSet(_) => KnownClass::ConstraintSet,
Self::UnionType(_) => KnownClass::UnionType, Self::UnionType(_) => KnownClass::UnionType,
Self::Literal(_) => KnownClass::GenericAlias, Self::Literal(_) => KnownClass::GenericAlias,
Self::Annotated(_) => KnownClass::GenericAlias,
} }
} }
@ -7848,7 +7861,10 @@ impl<'db> KnownInstanceType<'db> {
) )
} }
KnownInstanceType::UnionType(_) => f.write_str("types.UnionType"), KnownInstanceType::UnionType(_) => f.write_str("types.UnionType"),
KnownInstanceType::Literal(_) => f.write_str("typing.Literal"), KnownInstanceType::Literal(_) => f.write_str("<typing.Literal special form>"),
KnownInstanceType::Annotated(_) => {
f.write_str("<typing.Annotated special form>")
}
} }
} }
} }
@ -8045,7 +8061,7 @@ impl<'db> InvalidTypeExpressionError<'db> {
fn into_fallback_type( fn into_fallback_type(
self, self,
context: &InferContext, context: &InferContext,
node: &ast::Expr, node: &impl Ranged,
is_reachable: bool, is_reachable: bool,
) -> Type<'db> { ) -> Type<'db> {
let InvalidTypeExpressionError { let InvalidTypeExpressionError {
@ -8984,29 +9000,25 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
/// # Ordering /// # Ordering
/// Ordering is based on the context's salsa-assigned id and not on its values. /// 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. /// 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)] #[derive(PartialOrd, Ord)]
pub struct TypeList<'db> { pub struct InternedTypes<'db> {
#[returns(deref)] #[returns(deref)]
elements: Box<[Type<'db>]>, 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( pub(crate) fn from_elements(
db: &'db dyn Db, db: &'db dyn Db,
elements: impl IntoIterator<Item = Type<'db>>, elements: impl IntoIterator<Item = Type<'db>>,
) -> TypeList<'db> { ) -> InternedTypes<'db> {
TypeList::new(db, elements.into_iter().collect::<Box<[_]>>()) InternedTypes::new(db, elements.into_iter().collect::<Box<[_]>>())
}
pub(crate) fn singleton(db: &'db dyn Db, element: Type<'db>) -> TypeList<'db> {
TypeList::from_elements(db, [element])
} }
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
TypeList::new( InternedTypes::new(
db, db,
self.elements(db) self.elements(db)
.iter() .iter()
@ -9014,10 +9026,24 @@ impl<'db> TypeList<'db> {
.collect::<Box<[_]>>(), .collect::<Box<[_]>>(),
) )
} }
}
/// Turn this list of types `[T1, T2, ...]` into a union type `T1 | T2 | ...`. /// A salsa-interned `Type`
pub(crate) fn to_union(self, db: &'db dyn Db) -> Type<'db> { ///
UnionType::from_elements(db, self.elements(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, 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))
} }
} }

View File

@ -170,6 +170,7 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::ConstraintSet(_) | KnownInstanceType::ConstraintSet(_)
| KnownInstanceType::UnionType(_) | KnownInstanceType::UnionType(_)
| KnownInstanceType::Literal(_) => None, | KnownInstanceType::Literal(_) => None,
KnownInstanceType::Annotated(ty) => Self::try_from_type(db, ty.inner(db), subclass),
}, },
Type::SpecialForm(special_form) => match special_form { Type::SpecialForm(special_form) => match special_form {

View File

@ -69,12 +69,12 @@ use crate::types::diagnostic::{
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, 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_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_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
report_instance_layout_conflict, report_invalid_assignment, report_instance_layout_conflict, report_invalid_arguments_to_annotated,
report_invalid_attribute_assignment, report_invalid_exception_caught, report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_exception_cause, report_invalid_exception_raised, report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_exception_raised, report_invalid_generator_function_return_type,
report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base,
report_invalid_type_checking_constant, report_invalid_return_type, report_invalid_type_checking_constant,
report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
report_possibly_missing_attribute, report_possibly_unresolved_reference, report_possibly_missing_attribute, report_possibly_unresolved_reference,
report_rebound_typevar, report_slice_step_size_zero, 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::visitor::any_over_type;
use crate::types::{ use crate::types::{
CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams, CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, DynamicType, InternedType, InternedTypes, IntersectionBuilder, IntersectionType, KnownClass,
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm, KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter,
Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type, ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness,
TypeAliasType, TypeAndQualifiers, TypeContext, TypeList, TypeQualifiers, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
binding_type, todo_type, binding_type, todo_type,
@ -8755,14 +8755,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| Type::GenericAlias(..) | Type::GenericAlias(..)
| Type::SpecialForm(_) | Type::SpecialForm(_)
| Type::KnownInstance( | Type::KnownInstance(
KnownInstanceType::UnionType(_) | KnownInstanceType::Literal(_), KnownInstanceType::UnionType(_)
| KnownInstanceType::Literal(_)
| KnownInstanceType::Annotated(_),
), ),
Type::ClassLiteral(..) Type::ClassLiteral(..)
| Type::SubclassOf(..) | Type::SubclassOf(..)
| Type::GenericAlias(..) | Type::GenericAlias(..)
| Type::SpecialForm(_) | Type::SpecialForm(_)
| Type::KnownInstance( | Type::KnownInstance(
KnownInstanceType::UnionType(_) | KnownInstanceType::Literal(_), KnownInstanceType::UnionType(_)
| KnownInstanceType::Literal(_)
| KnownInstanceType::Annotated(_),
), ),
ast::Operator::BitOr, ast::Operator::BitOr,
) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 => { ) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 => {
@ -8770,7 +8774,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Some(left_ty) Some(left_ty)
} else { } else {
Some(Type::KnownInstance(KnownInstanceType::UnionType( 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) => && instance.has_known_class(self.db(), KnownClass::NoneType) =>
{ {
Some(Type::KnownInstance(KnownInstanceType::UnionType( 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> { 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 { let ast::ExprSubscript {
range: _, range: _,
node_index: _, node_index: _,
value, value: _,
slice, slice,
ctx, ctx,
} = subscript; } = subscript;
let value_ty = self.infer_expression(value, TypeContext::default());
let mut constraint_keys = vec![]; let mut constraint_keys = vec![];
// If `value` is a valid reference, we attempt type narrowing by assignment. // If `value` is a valid reference, we attempt type narrowing by assignment.
@ -9896,13 +9909,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::from(tuple.to_class_type(db)) Type::from(tuple.to_class_type(db))
}; };
match value_ty {
Type::ClassLiteral(class) => {
// HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the // 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. // 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 // 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 // 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_ // updating all of the subscript logic below to use custom callables for all of the _other_
// special cases, too. // special cases, too.
if let Type::ClassLiteral(class) = value_ty {
if class.is_tuple(self.db()) { if class.is_tuple(self.db()) {
return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice));
} }
@ -9915,10 +9929,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
); );
} }
} }
if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty { Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => {
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()) { if let Some(generic_context) = type_alias.generic_context(self.db()) {
return self.infer_explicit_type_alias_specialization( return self.infer_explicit_type_alias_specialization(
subscript, subscript,
@ -9928,10 +9939,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
); );
} }
} }
if value_ty == Type::SpecialForm(SpecialFormType::Literal) { Type::SpecialForm(SpecialFormType::Tuple) => {
return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice));
}
Type::SpecialForm(SpecialFormType::Literal) => {
match self.infer_literal_parameter_type(slice) { match self.infer_literal_parameter_type(slice) {
Ok(result) => { Ok(result) => {
return Type::KnownInstance(KnownInstanceType::Literal(TypeList::singleton( return Type::KnownInstance(KnownInstanceType::Literal(InternedType::new(
self.db(), self.db(),
result, result,
))); )));
@ -9951,6 +9965,62 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
} }
} }
} }
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()); let slice_ty = self.infer_expression(slice, TypeContext::default());
let result_ty = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); let result_ty = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx);

View File

@ -4,7 +4,7 @@ use ruff_python_ast as ast;
use super::{DeferredExpressionState, TypeInferenceBuilder}; use super::{DeferredExpressionState, TypeInferenceBuilder};
use crate::types::diagnostic::{ use crate::types::diagnostic::{
self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, 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::signatures::Signature;
use crate::types::string_annotation::parse_string_annotation; 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) { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!( builder.into_diagnostic(format_args!(
"`{ty}` is not a generic class", "`{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() Type::unknown()
} }
KnownInstanceType::Annotated(_) => {
self.infer_type_expression(slice);
todo_type!("Generic specialization of typing.Annotated")
}
}, },
Type::Dynamic(DynamicType::Todo(_)) => { Type::Dynamic(DynamicType::Todo(_)) => {
self.infer_type_expression(slice); self.infer_type_expression(slice);
@ -900,7 +904,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
ty ty
} }
fn infer_parameterized_special_form_type_expression( pub(crate) fn infer_parameterized_special_form_type_expression(
&mut self, &mut self,
subscript: &ast::ExprSubscript, subscript: &ast::ExprSubscript,
special_form: SpecialFormType, special_form: SpecialFormType,
@ -909,36 +913,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let arguments_slice = &*subscript.slice; let arguments_slice = &*subscript.slice;
match special_form { match special_form {
SpecialFormType::Annotated => { SpecialFormType::Annotated => {
let ast::Expr::Tuple(ast::ExprTuple { let ty = self
elts: arguments, .. .infer_subscript_load_impl(
}) = arguments_slice Type::SpecialForm(SpecialFormType::Annotated),
else { subscript,
report_invalid_arguments_to_annotated(&self.context, subscript); )
.in_type_expression(db, self.scope(), None)
// `Annotated[]` with less than two arguments is an error at runtime. .unwrap_or_else(|err| err.into_fallback_type(&self.context, subscript, true));
// 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);
self.store_expression_type(arguments_slice, ty); self.store_expression_type(arguments_slice, ty);
ty ty
} }
@ -1453,8 +1434,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
return Ok(value_ty); return Ok(value_ty);
} }
} }
Type::KnownInstance(KnownInstanceType::Literal(list)) => { Type::KnownInstance(KnownInstanceType::Literal(ty)) => {
return Ok(list.to_union(self.db())); return Ok(ty.inner(self.db()));
} }
// `Literal[SomeEnum.Member]` // `Literal[SomeEnum.Member]`
Type::EnumLiteral(_) => { Type::EnumLiteral(_) => {