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 292b15ec32ace..66fc452d8ac79 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -1443,7 +1443,7 @@ from typing import List, Dict # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" InvalidList = List[1] -# error: [invalid-type-form] "`typing.List` requires exactly one argument" +# error: [invalid-type-form] "`typing.List` requires exactly 1 argument, got 2" ListTooManyArgs = List[int, str] # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" @@ -1452,10 +1452,10 @@ InvalidDict1 = Dict[1, str] # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" InvalidDict2 = Dict[str, 2] -# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 1" +# error: [invalid-type-form] "`typing.Dict` requires exactly 2 arguments, got 1" DictTooFewArgs = Dict[str] -# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 3" +# error: [invalid-type-form] "`typing.Dict` requires exactly 2 arguments, got 3" DictTooManyArgs = Dict[str, int, float] def _( @@ -1470,7 +1470,7 @@ def _( reveal_type(list_too_many_args) # revealed: list[Unknown] reveal_type(invalid_dict1) # revealed: dict[Unknown, str] reveal_type(invalid_dict2) # revealed: dict[str, Unknown] - reveal_type(dict_too_few_args) # revealed: dict[str, Unknown] + reveal_type(dict_too_few_args) # revealed: dict[Unknown, Unknown] reveal_type(dict_too_many_args) # revealed: dict[Unknown, Unknown] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index ad901d4042c28..8b1bb49e30d2d 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -478,9 +478,14 @@ bad4: TypeAlias = Final # error: [invalid-type-form] bad5: TypeAlias = Required[int] # error: [invalid-type-form] bad6: TypeAlias = NotRequired[int] # error: [invalid-type-form] bad7: TypeAlias = ReadOnly[int] # error: [invalid-type-form] -bad8: TypeAlias = Unpack[tuple[int, ...]] # error: [invalid-type-form] bad9: TypeAlias = InitVar[int] # error: [invalid-type-form] bad10: TypeAlias = InitVar # error: [invalid-type-form] + +# TODO: this should cause us to emit an error (`Unpack` is not valid at the +# top level in this context), but for different reasons to the above cases: +# `Unpack` is not a type qualifier, and so the error message in our diagnostic +# shouldn't say that it is. +differently_bad: TypeAlias = Unpack[tuple[int, ...]] ``` [type expression]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index a4d17e190a163..50fe94eb97ba0 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1,5 +1,4 @@ use compact_str::ToCompactString; -use infer::nearest_enclosing_class; use itertools::{Either, Itertools}; use ruff_diagnostics::{Edit, Fix}; use rustc_hash::FxHashMap; @@ -63,8 +62,7 @@ use crate::types::function::{ FunctionType, KnownFunction, }; use crate::types::generics::{ - ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, typing_self, - walk_generic_context, + ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, walk_generic_context, }; pub(crate) use crate::types::generics::{GenericContext, SpecializationBuilder}; use crate::types::mro::{Mro, MroIterator, StaticMroError}; @@ -75,6 +73,7 @@ pub(crate) use crate::types::narrow::{ use crate::types::newtype::NewType; pub(crate) use crate::types::signatures::{Parameter, Parameters}; use crate::types::signatures::{ParameterForm, walk_signature}; +use crate::types::special_form::TypeQualifier; use crate::types::tuple::{Tuple, TupleSpec, TupleSpecBuilder}; use crate::types::typed_dict::TypedDictField; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; @@ -5991,134 +5990,9 @@ impl<'db> Type<'db> { KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)), }, - Type::SpecialForm(special_form) => match special_form { - SpecialFormType::Never | SpecialFormType::NoReturn => Ok(Type::Never), - SpecialFormType::LiteralString => Ok(Type::literal_string()), - SpecialFormType::Any => Ok(Type::any()), - SpecialFormType::Unknown => Ok(Type::unknown()), - SpecialFormType::AlwaysTruthy => Ok(Type::AlwaysTruthy), - SpecialFormType::AlwaysFalsy => Ok(Type::AlwaysFalsy), - - // We treat `typing.Type` exactly the same as `builtins.type`: - SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)), - SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())), - - // Legacy `typing` aliases - SpecialFormType::List => Ok(KnownClass::List.to_instance(db)), - SpecialFormType::Dict => Ok(KnownClass::Dict.to_instance(db)), - SpecialFormType::Set => Ok(KnownClass::Set.to_instance(db)), - SpecialFormType::FrozenSet => Ok(KnownClass::FrozenSet.to_instance(db)), - SpecialFormType::ChainMap => Ok(KnownClass::ChainMap.to_instance(db)), - SpecialFormType::Counter => Ok(KnownClass::Counter.to_instance(db)), - SpecialFormType::DefaultDict => Ok(KnownClass::DefaultDict.to_instance(db)), - SpecialFormType::Deque => Ok(KnownClass::Deque.to_instance(db)), - SpecialFormType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)), - - // TODO: Use an opt-in rule for a bare `Callable` - SpecialFormType::Callable => Ok(Type::Callable(CallableType::unknown(db))), - - // Special case: `NamedTuple` in a type expression is understood to describe the type - // `tuple[object, ...] & `. - // This isn't very principled (since at runtime, `NamedTuple` is just a function), - // but it appears to be what users often expect, and it improves compatibility with - // other type checkers such as mypy. - // See conversation in https://github.com/astral-sh/ruff/pull/19915. - SpecialFormType::NamedTuple => Ok(IntersectionType::from_elements( - db, - [ - Type::homogeneous_tuple(db, Type::object()), - KnownClass::NamedTupleLike.to_instance(db), - ], - )), - SpecialFormType::TypingSelf => { - let index = semantic_index(db, scope_id.file(db)); - let Some(class) = nearest_enclosing_class(db, index, scope_id) else { - return Err(InvalidTypeExpressionError { - fallback_type: Type::unknown(), - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::InvalidType(*self, scope_id) - ], - }); - }; - - Ok( - typing_self(db, scope_id, typevar_binding_context, class.into()) - .map(Type::TypeVar) - .unwrap_or(*self), - ) - } - // We ensure that `typing.TypeAlias` used in the expected position (annotating an - // annotated assignment statement) doesn't reach here. Using it in any other type - // expression is an error. - SpecialFormType::TypeAlias => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![InvalidTypeExpression::TypeAlias], - fallback_type: Type::unknown(), - }), - SpecialFormType::TypedDict => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![InvalidTypeExpression::TypedDict], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Literal - | SpecialFormType::Union - | SpecialFormType::Intersection => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::RequiresArguments(*special_form) - ], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Protocol => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![InvalidTypeExpression::Protocol], - fallback_type: Type::unknown(), - }), - SpecialFormType::Generic => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![InvalidTypeExpression::Generic], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Optional - | SpecialFormType::Not - | SpecialFormType::Top - | SpecialFormType::Bottom - | SpecialFormType::TypeOf - | SpecialFormType::TypeIs - | SpecialFormType::TypeGuard - | SpecialFormType::Unpack - | SpecialFormType::CallableTypeOf => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::RequiresOneArgument(*special_form) - ], - fallback_type: Type::unknown(), - }), - - SpecialFormType::Annotated | SpecialFormType::Concatenate => { - Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::RequiresTwoArguments(*special_form) - ], - fallback_type: Type::unknown(), - }) - } - - SpecialFormType::ClassVar | SpecialFormType::Final => { - Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::TypeQualifier(*special_form) - ], - fallback_type: Type::unknown(), - }) - } - - SpecialFormType::ReadOnly - | SpecialFormType::NotRequired - | SpecialFormType::Required => Err(InvalidTypeExpressionError { - invalid_expressions: smallvec_inline![ - InvalidTypeExpression::TypeQualifierRequiresOneArgument(*special_form) - ], - fallback_type: Type::unknown(), - }), - }, + Type::SpecialForm(special_form) => { + special_form.in_type_expression(db, scope_id, typevar_binding_context) + } Type::Union(union) => { let mut builder = UnionBuilder::new(db); @@ -7956,9 +7830,10 @@ impl<'db> TypeAndQualifiers<'db> { self.origin } - /// Insert/add an additional type qualifier. - pub(crate) fn add_qualifier(&mut self, qualifier: TypeQualifiers) { + /// Return `self` with an additional qualifier added to the set of qualifiers. + pub(crate) fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self { self.qualifiers |= qualifier; + self } /// Return the set of type qualifiers. @@ -8043,10 +7918,10 @@ enum InvalidTypeExpression<'db> { TypeAlias, /// Type qualifiers are always invalid in *type expressions*, /// but these ones are okay with 0 arguments in *annotation expressions* - TypeQualifier(SpecialFormType), + TypeQualifier(TypeQualifier), /// Type qualifiers that are invalid in type expressions, /// and which would require exactly one argument even if they appeared in an annotation expression - TypeQualifierRequiresOneArgument(SpecialFormType), + TypeQualifierRequiresOneArgument(TypeQualifier), /// Some types are always invalid in type expressions InvalidType(Type<'db>, ScopeId<'db>), } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 5e12ec91c9e4f..a8f1ea4722fe0 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -1,14 +1,13 @@ use crate::types::class::CodeGeneratorKind; use crate::types::generics::{ApplySpecialization, Specialization}; use crate::types::mro::MroIterator; -use crate::{Db, DisplaySettings}; - use crate::types::tuple::TupleType; use crate::types::{ ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, SpecialFormType, StaticMroError, Type, TypeContext, TypeMapping, todo_type, }; +use crate::{Db, DisplaySettings}; /// Enumeration of the possible kinds of types we allow in class bases. /// @@ -213,23 +212,20 @@ impl<'db> ClassBase<'db> { }, Type::SpecialForm(special_form) => match special_form { + SpecialFormType::TypeQualifier(_) => None, + SpecialFormType::Annotated | SpecialFormType::Literal | SpecialFormType::LiteralString | SpecialFormType::Union | SpecialFormType::NoReturn | SpecialFormType::Never - | SpecialFormType::Final - | SpecialFormType::NotRequired | SpecialFormType::TypeGuard | SpecialFormType::TypeIs | SpecialFormType::TypingSelf | SpecialFormType::Unpack - | SpecialFormType::ClassVar | SpecialFormType::Concatenate - | SpecialFormType::Required | SpecialFormType::TypeAlias - | SpecialFormType::ReadOnly | SpecialFormType::Optional | SpecialFormType::Not | SpecialFormType::Top @@ -242,9 +238,9 @@ impl<'db> ClassBase<'db> { SpecialFormType::Any => Some(Self::Dynamic(DynamicType::Any)), SpecialFormType::Unknown => Some(Self::unknown()), - SpecialFormType::Protocol => Some(Self::Protocol), SpecialFormType::Generic => Some(Self::Generic), + SpecialFormType::TypedDict => Some(Self::TypedDict), SpecialFormType::NamedTuple => { let class = subclass?.as_static()?; @@ -261,41 +257,19 @@ impl<'db> ClassBase<'db> { ) } - // TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO - SpecialFormType::Dict => { - Self::try_from_type(db, KnownClass::Dict.to_class_literal(db), subclass) - } - SpecialFormType::List => { - Self::try_from_type(db, KnownClass::List.to_class_literal(db), subclass) - } + // TODO: Classes inheriting from `typing.Type` also have `Generic` in their MRO SpecialFormType::Type => { Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) } + SpecialFormType::Tuple => { Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db), subclass) } - SpecialFormType::Set => { - Self::try_from_type(db, KnownClass::Set.to_class_literal(db), subclass) - } - SpecialFormType::FrozenSet => { - Self::try_from_type(db, KnownClass::FrozenSet.to_class_literal(db), subclass) - } - SpecialFormType::ChainMap => { - Self::try_from_type(db, KnownClass::ChainMap.to_class_literal(db), subclass) - } - SpecialFormType::Counter => { - Self::try_from_type(db, KnownClass::Counter.to_class_literal(db), subclass) - } - SpecialFormType::DefaultDict => { - Self::try_from_type(db, KnownClass::DefaultDict.to_class_literal(db), subclass) - } - SpecialFormType::Deque => { - Self::try_from_type(db, KnownClass::Deque.to_class_literal(db), subclass) - } - SpecialFormType::OrderedDict => { - Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db), subclass) + + SpecialFormType::LegacyStdlibAlias(alias) => { + Self::try_from_type(db, alias.aliased_class().to_class_literal(db), subclass) } - SpecialFormType::TypedDict => Some(Self::TypedDict), + SpecialFormType::Callable => Self::try_from_type( db, todo_type!("Support for Callable as a base class"), diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 6a828626ef47e..f708fd62e7ff9 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -3926,7 +3926,7 @@ pub(crate) fn report_invalid_arguments_to_annotated( pub(crate) fn report_invalid_argument_number_to_special_form( context: &InferContext, subscript: &ast::ExprSubscript, - special_form: SpecialFormType, + special_form: impl Into, received_arguments: usize, expected_arguments: u8, ) { @@ -3939,6 +3939,7 @@ pub(crate) fn report_invalid_argument_number_to_special_form( builder.into_diagnostic(format_args!( "Special form `{special_form}` expected exactly {expected_arguments} {noun}, \ got {received_arguments}", + special_form = special_form.into(), )); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f05127b4dc1f4..818ebc240fd55 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -119,6 +119,7 @@ use crate::types::generics::{ use crate::types::infer::nearest_enclosing_function; use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind}; use crate::types::newtype::NewType; +use crate::types::special_form::AliasSpec; use crate::types::subclass_of::SubclassOfInner; use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleSpecBuilder, TupleType}; @@ -9160,11 +9161,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if is_pep_613_type_alias { let is_valid_special_form = |ty: Type<'db>| match ty { - Type::SpecialForm(special_form) => special_form.is_valid_in_type_expression(), - Type::ClassLiteral(literal) - if literal.is_known(self.db(), KnownClass::InitVar) => - { - false + Type::SpecialForm(SpecialFormType::TypeQualifier(_)) => false, + Type::ClassLiteral(literal) => { + !literal.is_known(self.db(), KnownClass::InitVar) } _ => true, }; @@ -15933,11 +15932,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } } - Type::SpecialForm(SpecialFormType::Tuple) => { - return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(subscript)); - } - Type::SpecialForm(SpecialFormType::Literal) => { - match self.infer_literal_parameter_type(slice) { + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::Tuple => { + return tuple_generic_alias( + self.db(), + self.infer_tuple_type_expression(subscript), + ); + } + SpecialFormType::Literal => match self.infer_literal_parameter_type(slice) { Ok(result) => { return Type::KnownInstance(KnownInstanceType::Literal(InternedType::new( self.db(), @@ -15957,255 +15959,213 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } 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()); - }; + }, + SpecialFormType::Annotated => { + let ast::Expr::Tuple(ast::ExprTuple { + elts: ref arguments, + .. + }) = **slice + else { + report_invalid_arguments_to_annotated(&self.context, subscript); - if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); - } + return self.infer_expression(slice, TypeContext::default()); + }; - let [type_expr, metadata @ ..] = &arguments[..] else { - for argument in arguments { - self.infer_expression(argument, TypeContext::default()); + if arguments.len() < 2 { + report_invalid_arguments_to_annotated(&self.context, subscript); } - self.store_expression_type(slice, Type::unknown()); - return Type::unknown(); - }; - for element in metadata { - self.infer_expression(element, TypeContext::default()); - } + 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(); + }; - let ty = self.infer_type_expression(type_expr); + for element in metadata { + self.infer_expression(element, TypeContext::default()); + } - return Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( - self.db(), - ty, - ))); - } - Type::SpecialForm(SpecialFormType::Optional) => { - let db = self.db(); + let ty = self.infer_type_expression(type_expr); - if matches!(**slice, ast::Expr::Tuple(_)) - && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( - "`typing.Optional` requires exactly one argument" - )); + return Type::KnownInstance(KnownInstanceType::Annotated(InternedType::new( + self.db(), + ty, + ))); } + SpecialFormType::Optional => { + let db = self.db(); - let ty = self.infer_type_expression(slice); + if matches!(**slice, ast::Expr::Tuple(_)) + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( + "`typing.Optional` requires exactly one argument" + )); + } - // `Optional[None]` is equivalent to `None`: - if ty.is_none(db) { - return ty; + let ty = self.infer_type_expression(slice); + + // `Optional[None]` is equivalent to `None`: + if ty.is_none(db) { + return ty; + } + + return Type::KnownInstance(KnownInstanceType::UnionType( + UnionTypeInstance::new( + db, + None, + Ok(UnionType::from_elements(db, [ty, Type::none(db)])), + ), + )); } + SpecialFormType::Union => { + let db = self.db(); - return Type::KnownInstance(KnownInstanceType::UnionType(UnionTypeInstance::new( - db, - None, - Ok(UnionType::from_elements(db, [ty, Type::none(db)])), - ))); - } - Type::SpecialForm(SpecialFormType::Union) => { - let db = self.db(); + match **slice { + ast::Expr::Tuple(ref tuple) => { + let mut elements = tuple + .elts + .iter() + .map(|elt| self.infer_type_expression(elt)) + .peekable(); - match **slice { - ast::Expr::Tuple(ref tuple) => { - let mut elements = tuple - .elts - .iter() - .map(|elt| self.infer_type_expression(elt)) - .peekable(); + let is_empty = elements.peek().is_none(); + let union_type = Type::KnownInstance(KnownInstanceType::UnionType( + UnionTypeInstance::new( + db, + None, + Ok(UnionType::from_elements(db, elements)), + ), + )); - let is_empty = elements.peek().is_none(); - let union_type = Type::KnownInstance(KnownInstanceType::UnionType( - UnionTypeInstance::new( - db, - None, - Ok(UnionType::from_elements(db, elements)), - ), - )); + if is_empty + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic( + "`typing.Union` requires at least one type argument", + ); + } - if is_empty - && let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic( - "`typing.Union` requires at least one type argument", - ); + return union_type; + } + _ => { + return self.infer_expression(slice, TypeContext::default()); } - - return union_type; - } - _ => { - return self.infer_expression(slice, TypeContext::default()); } } - } - Type::SpecialForm(SpecialFormType::Type) => { - // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` - let argument_ty = self.infer_type_expression(slice); - return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( - InternedType::new(self.db(), argument_ty), - )); - } - Type::SpecialForm(SpecialFormType::Callable) => { - let arguments = if let ast::Expr::Tuple(tuple) = &*subscript.slice { - &*tuple.elts - } else { - std::slice::from_ref(&*subscript.slice) - }; + SpecialFormType::Type => { + // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` + let argument_ty = self.infer_type_expression(slice); + return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + InternedType::new(self.db(), argument_ty), + )); + } + SpecialFormType::Callable => { + let arguments = if let ast::Expr::Tuple(tuple) = &*subscript.slice { + &*tuple.elts + } else { + std::slice::from_ref(&*subscript.slice) + }; - // TODO: Remove this once we support Concatenate properly. This is necessary - // to avoid a lot of false positives downstream, because we can't represent the typevar- - // specialized `Callable` types yet. - let num_arguments = arguments.len(); - if num_arguments == 2 { - let first_arg = &arguments[0]; - let second_arg = &arguments[1]; - - if first_arg.is_subscript_expr() { - let first_arg_ty = self.infer_expression(first_arg, TypeContext::default()); - if let Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) = - first_arg_ty - { - let mut variables = generic_context - .variables(self.db()) - .collect::>(); + // TODO: Remove this once we support Concatenate properly. This is necessary + // to avoid a lot of false positives downstream, because we can't represent the typevar- + // specialized `Callable` types yet. + let num_arguments = arguments.len(); + if num_arguments == 2 { + let first_arg = &arguments[0]; + let second_arg = &arguments[1]; + + if first_arg.is_subscript_expr() { + let first_arg_ty = + self.infer_expression(first_arg, TypeContext::default()); + if let Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) = + first_arg_ty + { + let mut variables = generic_context + .variables(self.db()) + .collect::>(); - let return_ty = - self.infer_expression(second_arg, TypeContext::default()); - return_ty.bind_and_find_all_legacy_typevars( - self.db(), - self.typevar_binding_context, - &mut variables, - ); + let return_ty = + self.infer_expression(second_arg, TypeContext::default()); + return_ty.bind_and_find_all_legacy_typevars( + self.db(), + self.typevar_binding_context, + &mut variables, + ); - let generic_context = - GenericContext::from_typevar_instances(self.db(), variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); - } + let generic_context = + GenericContext::from_typevar_instances(self.db(), variables); + return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); + } - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic(format_args!( "The first argument to `Callable` must be either a list of types, \ ParamSpec, Concatenate, or `...`", )); + } + return Type::KnownInstance(KnownInstanceType::Callable( + CallableType::unknown(self.db()), + )); } - return Type::KnownInstance(KnownInstanceType::Callable( - CallableType::unknown(self.db()), - )); } - } - let callable = self - .infer_callable_type(subscript) - .as_callable() - .expect("always returns Type::Callable"); + let callable = self + .infer_callable_type(subscript) + .as_callable() + .expect("always returns Type::Callable"); - return Type::KnownInstance(KnownInstanceType::Callable(callable)); - } - // `typing` special forms with a single generic argument - Type::SpecialForm( - special_form @ (SpecialFormType::List - | SpecialFormType::Set - | SpecialFormType::FrozenSet - | SpecialFormType::Counter - | SpecialFormType::Deque), - ) => { - let slice_ty = self.infer_type_expression(slice); + return Type::KnownInstance(KnownInstanceType::Callable(callable)); + } + SpecialFormType::LegacyStdlibAlias(alias) => { + let AliasSpec { + class, + expected_argument_number, + } = alias.alias_spec(); - let element_ty = if matches!(**slice, ast::Expr::Tuple(_)) { - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - builder.into_diagnostic(format_args!( - "`typing.{}` requires exactly one argument", - special_form.name() - )); - } - Type::unknown() - } else { - slice_ty - }; + let args = if let ast::Expr::Tuple(t) = &**slice { + &*t.elts + } else { + std::slice::from_ref(&**slice) + }; - let class = special_form - .aliased_stdlib_class() - .expect("A known stdlib class is available"); - - return class - .to_specialized_class_type(self.db(), &[element_ty]) - .map(Type::from) - .unwrap_or_else(Type::unknown); - } - // `typing` special forms with two generic arguments - Type::SpecialForm( - special_form @ (SpecialFormType::Dict - | SpecialFormType::ChainMap - | SpecialFormType::DefaultDict - | SpecialFormType::OrderedDict), - ) => { - let (first_ty, second_ty) = if let ast::Expr::Tuple(ast::ExprTuple { - elts: ref arguments, - .. - }) = **slice - { - if arguments.len() != 2 - && let Some(builder) = + if args.len() != expected_argument_number { + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic(format_args!( - "`typing.{}` requires exactly two arguments, got {}", - special_form.name(), - arguments.len() - )); - } - - if let [first_expr, second_expr] = &arguments[..] { - let first_ty = self.infer_type_expression(first_expr); - let second_ty = self.infer_type_expression(second_expr); - - (first_ty, second_ty) - } else { - for argument in arguments { - self.infer_type_expression(argument); + { + let noun = if expected_argument_number == 1 { + "argument" + } else { + "arguments" + }; + builder.into_diagnostic(format_args!( + "`typing.{name}` requires exactly \ + {expected_argument_number} {noun}, got {got}", + name = special_form.name(), + got = args.len() + )); } - - (Type::unknown(), Type::unknown()) } - } else { - let first_ty = self.infer_type_expression(slice); - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - builder.into_diagnostic(format_args!( - "`typing.{}` requires exactly two arguments, got 1", - special_form.name() - )); - } - - (first_ty, Type::unknown()) - }; + let arg_types: Vec<_> = args + .iter() + .map(|arg| self.infer_type_expression(arg)) + .collect(); - let class = special_form - .aliased_stdlib_class() - .expect("Stdlib class available"); + return class + .to_specialized_class_type(self.db(), arg_types) + .map(Type::from) + .unwrap_or_else(Type::unknown); + } + _ => {} + }, - return class - .to_specialized_class_type(self.db(), &[first_ty, second_ty]) - .map(Type::from) - .unwrap_or_else(Type::unknown); - } Type::KnownInstance( KnownInstanceType::UnionType(_) | KnownInstanceType::Annotated(_) diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index e57e43f056409..7a245bd545f33 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -10,7 +10,8 @@ use crate::types::string_annotation::{ BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation, }; use crate::types::{ - KnownClass, SpecialFormType, Type, TypeAndQualifiers, TypeContext, TypeQualifiers, todo_type, + KnownClass, SpecialFormType, Type, TypeAndQualifiers, TypeContext, TypeQualifier, + TypeQualifiers, todo_type, }; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -89,37 +90,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder: &TypeInferenceBuilder<'db, '_>, pep_613_policy: PEP613Policy, ) -> TypeAndQualifiers<'db> { - match ty { - Type::SpecialForm(SpecialFormType::ClassVar) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::CLASS_VAR, - ), - Type::SpecialForm(SpecialFormType::Final) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::FINAL, - ), - Type::SpecialForm(SpecialFormType::Required) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::REQUIRED, - ), - Type::SpecialForm(SpecialFormType::NotRequired) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::NOT_REQUIRED, - ), - Type::SpecialForm(SpecialFormType::ReadOnly) => TypeAndQualifiers::new( - Type::unknown(), - TypeOrigin::Declared, - TypeQualifiers::READ_ONLY, - ), - Type::SpecialForm(SpecialFormType::TypeAlias) - if pep_613_policy == PEP613Policy::Allowed => - { - TypeAndQualifiers::declared(ty) - } + let special_case = match ty { + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::TypeQualifier(qualifier) => Some(TypeAndQualifiers::new( + Type::unknown(), + TypeOrigin::Declared, + TypeQualifiers::from(qualifier), + )), + SpecialFormType::TypeAlias if pep_613_policy == PEP613Policy::Allowed => { + Some(TypeAndQualifiers::declared(ty)) + } + _ => None, + }, // Conditional import of `typing.TypeAlias` or `typing_extensions.TypeAlias` on a // Python version where the former doesn't exist. Type::Union(union) @@ -131,7 +113,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) }) => { - TypeAndQualifiers::declared(Type::SpecialForm(SpecialFormType::TypeAlias)) + Some(TypeAndQualifiers::declared(Type::SpecialForm( + SpecialFormType::TypeAlias, + ))) } Type::ClassLiteral(class) if class.is_known(builder.db(), KnownClass::InitVar) => { if let Some(builder) = @@ -140,13 +124,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder .into_diagnostic("`InitVar` may not be used without a type argument"); } - TypeAndQualifiers::new( + Some(TypeAndQualifiers::new( Type::unknown(), TypeOrigin::Declared, TypeQualifiers::INIT_VAR, - ) + )) } - _ => TypeAndQualifiers::declared( + _ => None, + }; + + special_case.unwrap_or_else(|| { + TypeAndQualifiers::declared( ty.default_specialize(builder.db()) .in_type_expression( builder.db(), @@ -160,8 +148,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.is_reachable(annotation), ) }), - ), - } + ) + }) } // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression @@ -228,135 +216,129 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let slice = &**slice; match value_ty { - Type::SpecialForm(SpecialFormType::Annotated) => { - // This branch is similar to the corresponding branch in `infer_parameterized_special_form_type_expression`, but - // `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly - // differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`). - if let ast::Expr::Tuple(ast::ExprTuple { - elts: arguments, .. - }) = slice - { - if arguments.len() < 2 { - report_invalid_arguments_to_annotated(&self.context, subscript); - } - - if let [inner_annotation, metadata @ ..] = &arguments[..] { - for element in metadata { - self.infer_expression(element, TypeContext::default()); + Type::SpecialForm(special_form) => match special_form { + SpecialFormType::Annotated => { + // This branch is similar to the corresponding branch in + // `infer_parameterized_special_form_type_expression`, but + // `Annotated[…]` can appear both in annotation expressions and in + // type expressions, and needs to be handled slightly + // differently in each case (calling either `infer_type_expression_*` + // or `infer_annotation_expression_*`). + if let ast::Expr::Tuple(ast::ExprTuple { + elts: arguments, .. + }) = slice + { + if arguments.len() < 2 { + report_invalid_arguments_to_annotated(&self.context, subscript); } - let inner_annotation_ty = self.infer_annotation_expression_impl( - inner_annotation, - PEP613Policy::Disallowed, - ); + if let [inner_annotation, metadata @ ..] = &arguments[..] { + for element in metadata { + self.infer_expression(element, TypeContext::default()); + } - self.store_expression_type(slice, inner_annotation_ty.inner_type()); - inner_annotation_ty - } else { - for argument in arguments { - self.infer_expression(argument, TypeContext::default()); + let inner_annotation_ty = self + .infer_annotation_expression_impl( + inner_annotation, + PEP613Policy::Disallowed, + ); + + self.store_expression_type( + slice, + inner_annotation_ty.inner_type(), + ); + inner_annotation_ty + } else { + for argument in arguments { + self.infer_expression(argument, TypeContext::default()); + } + self.store_expression_type(slice, Type::unknown()); + TypeAndQualifiers::declared(Type::unknown()) } - self.store_expression_type(slice, Type::unknown()); - TypeAndQualifiers::declared(Type::unknown()) + } else { + report_invalid_arguments_to_annotated(&self.context, subscript); + self.infer_annotation_expression_impl( + slice, + PEP613Policy::Disallowed, + ) } - } else { - report_invalid_arguments_to_annotated(&self.context, subscript); - self.infer_annotation_expression_impl(slice, PEP613Policy::Disallowed) } - } - Type::SpecialForm( - type_qualifier @ (SpecialFormType::ClassVar - | SpecialFormType::Final - | SpecialFormType::Required - | SpecialFormType::NotRequired - | SpecialFormType::ReadOnly), - ) => { - let arguments = if let ast::Expr::Tuple(tuple) = slice { - &*tuple.elts - } else { - std::slice::from_ref(slice) - }; - let type_and_qualifiers = if let [argument] = arguments { - let mut type_and_qualifiers = self.infer_annotation_expression_impl( - argument, - PEP613Policy::Disallowed, - ); - - // Emit a diagnostic if ClassVar and Final are combined in a class that is - // not a dataclass, since Final already implies the semantics of ClassVar. - let classvar_and_final = match type_qualifier { - SpecialFormType::Final => type_and_qualifiers - .qualifiers - .contains(TypeQualifiers::CLASS_VAR), - SpecialFormType::ClassVar => type_and_qualifiers - .qualifiers - .contains(TypeQualifiers::FINAL), - _ => false, + SpecialFormType::TypeQualifier(qualifier) => { + let arguments = if let ast::Expr::Tuple(tuple) = slice { + &*tuple.elts + } else { + std::slice::from_ref(slice) }; - if classvar_and_final - && nearest_enclosing_class(self.db(), self.index, self.scope()) - .is_none_or(|class| !class.is_dataclass_like(self.db())) - && let Some(builder) = self - .context - .report_lint(&REDUNDANT_FINAL_CLASSVAR, subscript) - { - builder.into_diagnostic(format_args!( - "`Combining `ClassVar` and `Final` is redundant" - )); - } + let type_and_qualifiers = if let [argument] = arguments { + let type_and_qualifiers = self.infer_annotation_expression_impl( + argument, + PEP613Policy::Disallowed, + ); + + // Emit a diagnostic if ClassVar and Final are combined in a class that is + // not a dataclass, since Final already implies the semantics of ClassVar. + let classvar_and_final = match qualifier { + TypeQualifier::Final => type_and_qualifiers + .qualifiers + .contains(TypeQualifiers::CLASS_VAR), + TypeQualifier::ClassVar => type_and_qualifiers + .qualifiers + .contains(TypeQualifiers::FINAL), + _ => false, + }; + if classvar_and_final + && nearest_enclosing_class(self.db(), self.index, self.scope()) + .is_none_or(|class| !class.is_dataclass_like(self.db())) + && let Some(builder) = self + .context + .report_lint(&REDUNDANT_FINAL_CLASSVAR, subscript) + { + builder.into_diagnostic(format_args!( + "`Combining `ClassVar` and `Final` is redundant" + )); + } - match type_qualifier { - SpecialFormType::ClassVar => { - type_and_qualifiers.add_qualifier(TypeQualifiers::CLASS_VAR); - if type_and_qualifiers + if qualifier == TypeQualifier::ClassVar + && type_and_qualifiers .inner_type() .has_non_self_typevar(self.db()) - && let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - builder.into_diagnostic( - "`ClassVar` cannot contain type variables", - ); - } - } - SpecialFormType::Final => { - type_and_qualifiers.add_qualifier(TypeQualifiers::FINAL); + && let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + builder.into_diagnostic( + "`ClassVar` cannot contain type variables", + ); } - SpecialFormType::Required => { - type_and_qualifiers.add_qualifier(TypeQualifiers::REQUIRED); - } - SpecialFormType::NotRequired => { - type_and_qualifiers.add_qualifier(TypeQualifiers::NOT_REQUIRED); + type_and_qualifiers.with_qualifier(TypeQualifiers::from(qualifier)) + } else { + for element in arguments { + self.infer_annotation_expression_impl( + element, + PEP613Policy::Disallowed, + ); } - SpecialFormType::ReadOnly => { - type_and_qualifiers.add_qualifier(TypeQualifiers::READ_ONLY); + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, subscript) + { + let num_arguments = arguments.len(); + builder.into_diagnostic(format_args!( + "Type qualifier `{qualifier}` expected exactly 1 \ + argument, got {num_arguments}", + )); } - _ => unreachable!(), + TypeAndQualifiers::declared(Type::unknown()) + }; + if slice.is_tuple_expr() { + self.store_expression_type(slice, type_and_qualifiers.inner_type()); } type_and_qualifiers - } else { - for element in arguments { - self.infer_annotation_expression_impl( - element, - PEP613Policy::Disallowed, - ); - } - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, subscript) - { - let num_arguments = arguments.len(); - builder.into_diagnostic(format_args!( - "Type qualifier `{type_qualifier}` expected exactly 1 argument, \ - got {num_arguments}", - )); - } - TypeAndQualifiers::declared(Type::unknown()) - }; - if slice.is_tuple_expr() { - self.store_expression_type(slice, type_and_qualifiers.inner_type()); } - type_and_qualifiers - } + _ => TypeAndQualifiers::declared( + self.infer_subscript_type_expression_no_store( + subscript, slice, value_ty, + ), + ), + }, Type::ClassLiteral(class) if class.is_known(self.db(), KnownClass::InitVar) => { let arguments = if let ast::Expr::Tuple(tuple) = slice { &*tuple.elts @@ -364,12 +346,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { std::slice::from_ref(slice) }; let type_and_qualifiers = if let [argument] = arguments { - let mut type_and_qualifiers = self.infer_annotation_expression_impl( + self.infer_annotation_expression_impl( argument, PEP613Policy::Disallowed, - ); - type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR); - type_and_qualifiers + ) + .with_qualifier(TypeQualifiers::INIT_VAR) } else { for element in arguments { self.infer_annotation_expression_impl( 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 f41fce6f6fc9e..b07ae6dd88bcf 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 @@ -11,6 +11,7 @@ use crate::types::diagnostic::{ use crate::types::generics::bind_typevar; use crate::types::infer::builder::InnerExpressionInferenceState; use crate::types::signatures::Signature; +use crate::types::special_form::{AliasSpec, LegacyStdlibAlias}; use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::{ @@ -1213,9 +1214,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { fn infer_parameterized_legacy_typing_alias( &mut self, subscript_node: &ast::ExprSubscript, - expected_arg_count: usize, - alias: SpecialFormType, - class: KnownClass, + alias: LegacyStdlibAlias, ) -> Type<'db> { let arguments = &*subscript_node.slice; let args = if let ast::Expr::Tuple(t) = arguments { @@ -1223,15 +1222,21 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { std::slice::from_ref(arguments) }; - if args.len() != expected_arg_count { + + let AliasSpec { + class, + expected_argument_number, + } = alias.alias_spec(); + + if args.len() != expected_argument_number { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript_node) { - let noun = if expected_arg_count == 1 { + let noun = if expected_argument_number == 1 { "argument" } else { "arguments" }; builder.into_diagnostic(format_args!( - "Legacy alias `{alias}` expected exactly {expected_arg_count} {noun}, \ + "Legacy alias `{alias}` expected exactly {expected_argument_number} {noun}, \ got {}", args.len() )); @@ -1301,7 +1306,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { callable_type } - pub(crate) fn infer_parameterized_special_form_type_expression( + fn infer_parameterized_special_form_type_expression( &mut self, subscript: &ast::ExprSubscript, special_form: SpecialFormType, @@ -1532,70 +1537,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } callable_type } - - SpecialFormType::ChainMap => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::ChainMap, - KnownClass::ChainMap, - ), - SpecialFormType::OrderedDict => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::OrderedDict, - KnownClass::OrderedDict, - ), - SpecialFormType::Dict => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::Dict, - KnownClass::Dict, - ), - SpecialFormType::List => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::List, - KnownClass::List, - ), - SpecialFormType::DefaultDict => self.infer_parameterized_legacy_typing_alias( - subscript, - 2, - SpecialFormType::DefaultDict, - KnownClass::DefaultDict, - ), - SpecialFormType::Counter => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::Counter, - KnownClass::Counter, - ), - SpecialFormType::Set => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::Set, - KnownClass::Set, - ), - SpecialFormType::FrozenSet => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::FrozenSet, - KnownClass::FrozenSet, - ), - SpecialFormType::Deque => self.infer_parameterized_legacy_typing_alias( - subscript, - 1, - SpecialFormType::Deque, - KnownClass::Deque, - ), - - SpecialFormType::ClassVar - | SpecialFormType::Final - | SpecialFormType::Required - | SpecialFormType::NotRequired - | SpecialFormType::ReadOnly => { + SpecialFormType::LegacyStdlibAlias(alias) => { + self.infer_parameterized_legacy_typing_alias(subscript, alias) + } + SpecialFormType::TypeQualifier(qualifier) => { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { let diag = builder.into_diagnostic(format_args!( - "Type qualifier `{special_form}` is not allowed in type expressions \ + "Type qualifier `{qualifier}` is not allowed in type expressions \ (only in annotation expressions)", )); diagnostic::add_type_expression_reference_link(diag); @@ -1632,9 +1580,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - let diag = builder.into_diagnostic(format_args!( + let diag = builder.into_diagnostic( "Special form `typing.TypeGuard` expected exactly one type parameter", - )); + ); diagnostic::add_type_expression_reference_link(diag); } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 31f8b7fdffdfd..dc98dbf579534 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -228,17 +228,24 @@ impl ClassInfoConstraintFunction { ) } - // We don't have a good meta-type for `Callable`s right now, - // so only apply `isinstance()` narrowing, not `issubclass()` - Type::SpecialForm(SpecialFormType::Callable) - if self == ClassInfoConstraintFunction::IsInstance => - { - Some(Type::Callable(CallableType::unknown(db)).top_materialization(db)) - } + Type::SpecialForm(form) => match form { + SpecialFormType::LegacyStdlibAlias(alias) => { + self.generate_constraint(db, alias.aliased_class().to_class_literal(db)) + } + SpecialFormType::Tuple => { + self.generate_constraint(db, KnownClass::Tuple.to_class_literal(db)) + } + SpecialFormType::Type => { + self.generate_constraint(db, KnownClass::Type.to_class_literal(db)) + } + + // We don't have a good meta-type for `Callable`s right now, + // so only apply `isinstance()` narrowing, not `issubclass()` + SpecialFormType::Callable => (self == ClassInfoConstraintFunction::IsInstance) + .then(|| Type::Callable(CallableType::unknown(db)).top_materialization(db)), - Type::SpecialForm(special_form) => special_form - .aliased_stdlib_class() - .and_then(|class| self.generate_constraint(db, class.to_class_literal(db))), + _ => None, + }, Type::AlwaysFalsy | Type::AlwaysTruthy diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 7d2d43f97dcdf..b5c89466069e4 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -4,32 +4,55 @@ use super::{ClassType, Type, class::KnownClass}; use crate::db::Db; use crate::semantic_index::place::ScopedPlaceId; -use crate::semantic_index::{FileScopeId, place_table, use_def_map}; -use crate::types::TypeDefinition; +use crate::semantic_index::{ + FileScopeId, definition::Definition, place_table, scope::ScopeId, semantic_index, use_def_map, +}; +use crate::types::{ + CallableType, IntersectionBuilder, InvalidTypeExpression, InvalidTypeExpressionError, + TypeDefinition, TypeQualifiers, generics::typing_self, infer::nearest_enclosing_class, +}; use ruff_db::files::File; -use std::str::FromStr; +use strum_macros::EnumString; use ty_module_resolver::{KnownModule, file_to_module, resolve_module_confident}; /// Enumeration of specific runtime symbols that are special enough /// that they can each be considered to inhabit a unique type. /// +/// The enum uses a nested structure: variants that fall into well-defined subcategories +/// (legacy stdlib aliases and type qualifiers) are represented as nested enums, +/// while other special forms that each require unique handling remain as direct variants. +/// /// # Ordering /// /// Ordering is stable and should be the same between runs. -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - Hash, - salsa::Update, - PartialOrd, - Ord, - strum_macros::EnumString, - get_size2::GetSize, -)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] pub enum SpecialFormType { + /// Special forms that are simple aliases to classes elsewhere in the standard library. + LegacyStdlibAlias(LegacyStdlibAlias), + + /// Special forms that are type qualifiers + TypeQualifier(TypeQualifier), + + /// The special form `typing.Tuple`. + /// + /// While this is technically an alias to `builtins.tuple`, it requires special handling + /// for type-expression parsing. + Tuple, + + /// The special form `typing.Type`. + /// + /// While this is technically an alias to `builtins.type`, it requires special handling + /// for type-expression parsing. + Type, + + /// The special form `Callable`. + /// + /// While `typing.Callable` aliases `collections.abc.Callable`, we view both objects + /// as inhabiting the same special form type internally. Moreover, `Callable` requires + /// special handling for both type-expression parsing and `isinstance`/`issubclass` + /// narrowing. + Callable, + Any, /// The symbol `typing.Annotated` (which can also be found as `typing_extensions.Annotated`) Annotated, @@ -45,28 +68,6 @@ pub enum SpecialFormType { NoReturn, /// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`) Never, - /// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`) - Tuple, - /// The symbol `typing.List` (which can also be found as `typing_extensions.List`) - List, - /// The symbol `typing.Dict` (which can also be found as `typing_extensions.Dict`) - Dict, - /// The symbol `typing.Set` (which can also be found as `typing_extensions.Set`) - Set, - /// The symbol `typing.FrozenSet` (which can also be found as `typing_extensions.FrozenSet`) - FrozenSet, - /// The symbol `typing.ChainMap` (which can also be found as `typing_extensions.ChainMap`) - ChainMap, - /// The symbol `typing.Counter` (which can also be found as `typing_extensions.Counter`) - Counter, - /// The symbol `typing.DefaultDict` (which can also be found as `typing_extensions.DefaultDict`) - DefaultDict, - /// The symbol `typing.Deque` (which can also be found as `typing_extensions.Deque`) - Deque, - /// The symbol `typing.OrderedDict` (which can also be found as `typing_extensions.OrderedDict`) - OrderedDict, - /// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`) - Type, /// The symbol `ty_extensions.Unknown` Unknown, /// The symbol `ty_extensions.AlwaysTruthy` @@ -85,24 +86,12 @@ pub enum SpecialFormType { Top, /// The symbol `ty_extensions.Bottom` Bottom, - /// The symbol `typing.Callable` - /// (which can also be found as `typing_extensions.Callable` or as `collections.abc.Callable`) - Callable, /// The symbol `typing.Self` (which can also be found as `typing_extensions.Self`) - #[strum(serialize = "Self")] TypingSelf, - /// The symbol `typing.Final` (which can also be found as `typing_extensions.Final`) - Final, - /// The symbol `typing.ClassVar` (which can also be found as `typing_extensions.ClassVar`) - ClassVar, /// The symbol `typing.Concatenate` (which can also be found as `typing_extensions.Concatenate`) Concatenate, /// The symbol `typing.Unpack` (which can also be found as `typing_extensions.Unpack`) Unpack, - /// The symbol `typing.Required` (which can also be found as `typing_extensions.Required`) - Required, - /// The symbol `typing.NotRequired` (which can also be found as `typing_extensions.NotRequired`) - NotRequired, /// The symbol `typing.TypeAlias` (which can also be found as `typing_extensions.TypeAlias`) TypeAlias, /// The symbol `typing.TypeGuard` (which can also be found as `typing_extensions.TypeGuard`) @@ -111,8 +100,6 @@ pub enum SpecialFormType { TypedDict, /// The symbol `typing.TypeIs` (which can also be found as `typing_extensions.TypeIs`) TypeIs, - /// The symbol `typing.ReadOnly` (which can also be found as `typing_extensions.ReadOnly`) - ReadOnly, /// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`) /// @@ -146,13 +133,9 @@ impl SpecialFormType { | Self::Tuple | Self::Type | Self::TypingSelf - | Self::Final - | Self::ClassVar | Self::Callable | Self::Concatenate | Self::Unpack - | Self::Required - | Self::NotRequired | Self::TypeAlias | Self::TypeGuard | Self::TypedDict @@ -163,7 +146,7 @@ impl SpecialFormType { | Self::Bottom | Self::Intersection | Self::CallableTypeOf - | Self::ReadOnly => KnownClass::SpecialForm, + | Self::TypeQualifier(_) => KnownClass::SpecialForm, // Typeshed says it's an instance of `_SpecialForm`, // but then we wouldn't recognise things like `issubclass(`X, Protocol)` @@ -172,15 +155,7 @@ impl SpecialFormType { Self::Generic | Self::Any => KnownClass::Type, - Self::List - | Self::Dict - | Self::DefaultDict - | Self::Set - | Self::FrozenSet - | Self::Counter - | Self::Deque - | Self::ChainMap - | Self::OrderedDict => KnownClass::StdlibAlias, + Self::LegacyStdlibAlias(_) => KnownClass::StdlibAlias, Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy => KnownClass::Object, @@ -207,28 +182,196 @@ impl SpecialFormType { file: File, symbol_name: &str, ) -> Option { - let candidate = Self::from_str(symbol_name).ok()?; + let candidate = Self::from_name(symbol_name)?; candidate .check_module(file_to_module(db, file)?.known(db)?) .then_some(candidate) } + /// Parse a `SpecialFormType` from its runtime symbol name. + fn from_name(name: &str) -> Option { + /// An enum that maps 1:1 with `SpecialFormType`, but which holds no associated data + /// (and therefore can have `EnumString` derived on it). + /// This is much more robust than having a manual `from_string` method that matches + /// on string literals, because experience has shown it's very easy to forget to + /// update such a method when adding new variants. + #[derive(EnumString)] + enum SpecialFormTypeBuilder { + Tuple, + Type, + Callable, + Any, + Annotated, + Literal, + LiteralString, + Optional, + Union, + NoReturn, + Never, + Unknown, + AlwaysTruthy, + AlwaysFalsy, + Not, + Intersection, + TypeOf, + CallableTypeOf, + Top, + Bottom, + #[strum(serialize = "Self")] + TypingSelf, + Concatenate, + Unpack, + TypeAlias, + TypeGuard, + TypedDict, + TypeIs, + Protocol, + Generic, + NamedTuple, + List, + Dict, + FrozenSet, + Set, + ChainMap, + Counter, + DefaultDict, + Deque, + OrderedDict, + Final, + ClassVar, + ReadOnly, + Required, + NotRequired, + } + + // This implementation exists purely to enforce that every variant of `SpecialFormType` + // is included in the `SpecialFormTypeBuilder` enum + #[cfg(test)] + impl From for SpecialFormTypeBuilder { + fn from(value: SpecialFormType) -> Self { + match value { + SpecialFormType::AlwaysFalsy => Self::AlwaysFalsy, + SpecialFormType::AlwaysTruthy => Self::AlwaysTruthy, + SpecialFormType::Annotated => Self::Annotated, + SpecialFormType::Callable => Self::Callable, + SpecialFormType::CallableTypeOf => Self::CallableTypeOf, + SpecialFormType::Concatenate => Self::Concatenate, + SpecialFormType::Intersection => Self::Intersection, + SpecialFormType::Literal => Self::Literal, + SpecialFormType::LiteralString => Self::LiteralString, + SpecialFormType::Never => Self::Never, + SpecialFormType::NoReturn => Self::NoReturn, + SpecialFormType::Not => Self::Not, + SpecialFormType::Optional => Self::Optional, + SpecialFormType::Protocol => Self::Protocol, + SpecialFormType::Type => Self::Type, + SpecialFormType::TypeAlias => Self::TypeAlias, + SpecialFormType::TypeGuard => Self::TypeGuard, + SpecialFormType::TypeIs => Self::TypeIs, + SpecialFormType::TypingSelf => Self::TypingSelf, + SpecialFormType::Union => Self::Union, + SpecialFormType::Unknown => Self::Unknown, + SpecialFormType::Generic => Self::Generic, + SpecialFormType::NamedTuple => Self::NamedTuple, + SpecialFormType::Any => Self::Any, + SpecialFormType::Bottom => Self::Bottom, + SpecialFormType::Top => Self::Top, + SpecialFormType::Unpack => Self::Unpack, + SpecialFormType::Tuple => Self::Tuple, + SpecialFormType::TypedDict => Self::TypedDict, + SpecialFormType::TypeOf => Self::TypeOf, + SpecialFormType::LegacyStdlibAlias(alias) => match alias { + LegacyStdlibAlias::List => Self::List, + LegacyStdlibAlias::Dict => Self::Dict, + LegacyStdlibAlias::Set => Self::Set, + LegacyStdlibAlias::FrozenSet => Self::FrozenSet, + LegacyStdlibAlias::ChainMap => Self::ChainMap, + LegacyStdlibAlias::Counter => Self::Counter, + LegacyStdlibAlias::DefaultDict => Self::DefaultDict, + LegacyStdlibAlias::Deque => Self::Deque, + LegacyStdlibAlias::OrderedDict => Self::OrderedDict, + }, + SpecialFormType::TypeQualifier(qualifier) => match qualifier { + TypeQualifier::Final => Self::Final, + TypeQualifier::ClassVar => Self::ClassVar, + TypeQualifier::ReadOnly => Self::ReadOnly, + TypeQualifier::Required => Self::Required, + TypeQualifier::NotRequired => Self::NotRequired, + }, + } + } + } + + SpecialFormTypeBuilder::try_from(name) + .ok() + .map(|form| match form { + SpecialFormTypeBuilder::AlwaysFalsy => Self::AlwaysFalsy, + SpecialFormTypeBuilder::AlwaysTruthy => Self::AlwaysTruthy, + SpecialFormTypeBuilder::Annotated => Self::Annotated, + SpecialFormTypeBuilder::Callable => Self::Callable, + SpecialFormTypeBuilder::CallableTypeOf => Self::CallableTypeOf, + SpecialFormTypeBuilder::Concatenate => Self::Concatenate, + SpecialFormTypeBuilder::Intersection => Self::Intersection, + SpecialFormTypeBuilder::Literal => Self::Literal, + SpecialFormTypeBuilder::LiteralString => Self::LiteralString, + SpecialFormTypeBuilder::Never => Self::Never, + SpecialFormTypeBuilder::NoReturn => Self::NoReturn, + SpecialFormTypeBuilder::Not => Self::Not, + SpecialFormTypeBuilder::Optional => Self::Optional, + SpecialFormTypeBuilder::Protocol => Self::Protocol, + SpecialFormTypeBuilder::Type => Self::Type, + SpecialFormTypeBuilder::TypeAlias => Self::TypeAlias, + SpecialFormTypeBuilder::TypeGuard => Self::TypeGuard, + SpecialFormTypeBuilder::TypeIs => Self::TypeIs, + SpecialFormTypeBuilder::TypingSelf => Self::TypingSelf, + SpecialFormTypeBuilder::Union => Self::Union, + SpecialFormTypeBuilder::Unknown => Self::Unknown, + SpecialFormTypeBuilder::Generic => Self::Generic, + SpecialFormTypeBuilder::NamedTuple => Self::NamedTuple, + SpecialFormTypeBuilder::Any => Self::Any, + SpecialFormTypeBuilder::Bottom => Self::Bottom, + SpecialFormTypeBuilder::Top => Self::Top, + SpecialFormTypeBuilder::Unpack => Self::Unpack, + SpecialFormTypeBuilder::Tuple => Self::Tuple, + SpecialFormTypeBuilder::TypedDict => Self::TypedDict, + SpecialFormTypeBuilder::TypeOf => Self::TypeOf, + SpecialFormTypeBuilder::List => Self::LegacyStdlibAlias(LegacyStdlibAlias::List), + SpecialFormTypeBuilder::Dict => Self::LegacyStdlibAlias(LegacyStdlibAlias::Dict), + SpecialFormTypeBuilder::Set => Self::LegacyStdlibAlias(LegacyStdlibAlias::Set), + SpecialFormTypeBuilder::FrozenSet => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet) + } + SpecialFormTypeBuilder::ChainMap => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap) + } + SpecialFormTypeBuilder::Counter => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::Counter) + } + SpecialFormTypeBuilder::DefaultDict => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict) + } + SpecialFormTypeBuilder::Deque => Self::LegacyStdlibAlias(LegacyStdlibAlias::Deque), + SpecialFormTypeBuilder::OrderedDict => { + Self::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict) + } + SpecialFormTypeBuilder::Final => Self::TypeQualifier(TypeQualifier::Final), + SpecialFormTypeBuilder::ClassVar => Self::TypeQualifier(TypeQualifier::ClassVar), + SpecialFormTypeBuilder::ReadOnly => Self::TypeQualifier(TypeQualifier::ReadOnly), + SpecialFormTypeBuilder::Required => Self::TypeQualifier(TypeQualifier::Required), + SpecialFormTypeBuilder::NotRequired => { + Self::TypeQualifier(TypeQualifier::NotRequired) + } + }) + } + /// Return `true` if `module` is a module from which this `SpecialFormType` variant can validly originate. /// /// Most variants can only exist in one module, which is the same as `self.class().canonical_module(db)`. /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. pub(super) fn check_module(self, module: KnownModule) -> bool { match self { - Self::ClassVar - | Self::Deque - | Self::List - | Self::Dict - | Self::DefaultDict - | Self::Set - | Self::FrozenSet - | Self::Counter - | Self::ChainMap - | Self::OrderedDict + Self::TypeQualifier(TypeQualifier::ClassVar) + | Self::LegacyStdlibAlias(_) | Self::Optional | Self::Union | Self::NoReturn @@ -241,11 +384,14 @@ impl SpecialFormType { | Self::Literal | Self::LiteralString | Self::Never - | Self::Final + | Self::TypeQualifier( + TypeQualifier::Final + | TypeQualifier::Required + | TypeQualifier::NotRequired + | TypeQualifier::ReadOnly, + ) | Self::Concatenate | Self::Unpack - | Self::Required - | Self::NotRequired | Self::TypeAlias | Self::TypeGuard | Self::TypedDict @@ -253,8 +399,7 @@ impl SpecialFormType { | Self::TypingSelf | Self::Protocol | Self::NamedTuple - | Self::Any - | Self::ReadOnly => { + | Self::Any => { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } @@ -281,16 +426,30 @@ impl SpecialFormType { match self { // TypedDict can be called as a constructor to create TypedDict types Self::TypedDict + // Collection constructors are callable // TODO actually implement support for calling them - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::NamedTuple - | Self::OrderedDict => true, + | Self::LegacyStdlibAlias( + LegacyStdlibAlias::ChainMap + | LegacyStdlibAlias::Counter + | LegacyStdlibAlias::DefaultDict + | LegacyStdlibAlias::Deque + | LegacyStdlibAlias::OrderedDict + ) + | Self::NamedTuple => true, + + // Unlike the aliases to `collections` classes, + // the aliases to builtin classes are *not* callable... + Self::LegacyStdlibAlias( + LegacyStdlibAlias::List + | LegacyStdlibAlias::Dict + | LegacyStdlibAlias::Set + | LegacyStdlibAlias::FrozenSet + ) + | Self::Tuple + | Self::Type => false, - // All other special forms are not callable + // All other special forms are also not callable Self::Annotated | Self::Literal | Self::LiteralString @@ -298,12 +457,6 @@ impl SpecialFormType { | Self::Union | Self::NoReturn | Self::Never - | Self::Tuple - | Self::List - | Self::Dict - | Self::Set - | Self::FrozenSet - | Self::Type | Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy @@ -315,144 +468,24 @@ impl SpecialFormType { | Self::CallableTypeOf | Self::Callable | Self::TypingSelf - | Self::Final - | Self::ClassVar + | Self::TypeQualifier(_) | Self::Concatenate | Self::Unpack - | Self::Required - | Self::NotRequired | Self::TypeAlias | Self::TypeGuard | Self::TypeIs - | Self::ReadOnly | Self::Protocol | Self::Any | Self::Generic => false, } } - /// Return `true` if this special form type is valid in a type-expression context (and not - /// just in an *annotation* expression context). See the following section of the typing - /// specification for more details: - /// - pub(super) const fn is_valid_in_type_expression(self) -> bool { - match self { - Self::ClassVar - | Self::Final - | Self::Required - | Self::NotRequired - | SpecialFormType::ReadOnly - | SpecialFormType::Unpack - | SpecialFormType::TypeAlias => false, - Self::Annotated - | SpecialFormType::Any - | SpecialFormType::Literal - | SpecialFormType::LiteralString - | SpecialFormType::Optional - | SpecialFormType::Union - | SpecialFormType::NoReturn - | SpecialFormType::Never - | SpecialFormType::Tuple - | SpecialFormType::List - | SpecialFormType::Dict - | SpecialFormType::Set - | SpecialFormType::FrozenSet - | SpecialFormType::ChainMap - | SpecialFormType::Counter - | SpecialFormType::DefaultDict - | SpecialFormType::Deque - | SpecialFormType::OrderedDict - | SpecialFormType::Type - | SpecialFormType::Unknown - | SpecialFormType::AlwaysTruthy - | SpecialFormType::AlwaysFalsy - | SpecialFormType::Not - | SpecialFormType::Intersection - | SpecialFormType::TypeOf - | SpecialFormType::CallableTypeOf - | SpecialFormType::Top - | SpecialFormType::Bottom - | SpecialFormType::Callable - | SpecialFormType::TypingSelf - | SpecialFormType::Concatenate - | SpecialFormType::TypeGuard - | SpecialFormType::TypedDict - | SpecialFormType::TypeIs - | SpecialFormType::Protocol - | SpecialFormType::Generic - | SpecialFormType::NamedTuple => true, - } - } - - /// Return `Some(KnownClass)` if this special form is an alias - /// to a standard library class. - pub(super) const fn aliased_stdlib_class(self) -> Option { - match self { - Self::List => Some(KnownClass::List), - Self::Dict => Some(KnownClass::Dict), - Self::Set => Some(KnownClass::Set), - Self::FrozenSet => Some(KnownClass::FrozenSet), - Self::ChainMap => Some(KnownClass::ChainMap), - Self::Counter => Some(KnownClass::Counter), - Self::DefaultDict => Some(KnownClass::DefaultDict), - Self::Deque => Some(KnownClass::Deque), - Self::OrderedDict => Some(KnownClass::OrderedDict), - Self::Tuple => Some(KnownClass::Tuple), - Self::Type => Some(KnownClass::Type), - - Self::AlwaysFalsy - | Self::AlwaysTruthy - | Self::Annotated - | Self::Bottom - | Self::CallableTypeOf - | Self::ClassVar - | Self::Concatenate - | Self::Final - | Self::Intersection - | Self::Literal - | Self::LiteralString - | Self::Never - | Self::NoReturn - | Self::Not - | Self::ReadOnly - | Self::Required - | Self::TypeAlias - | Self::TypeGuard - | Self::NamedTuple - | Self::NotRequired - | Self::Optional - | Self::Top - | Self::TypeIs - | Self::TypedDict - | Self::TypingSelf - | Self::Union - | Self::Unknown - | Self::TypeOf - | Self::Any - // `typing.Callable` is an alias to `collections.abc.Callable`, - // but they're both the same `SpecialFormType` in our model, - // and neither is a class in typeshed (even though the `collections.abc` one is at runtime) - | Self::Callable - | Self::Protocol - | Self::Generic - | Self::Unpack => None, - } - } - /// Return `true` if this special form is valid as the second argument /// to `issubclass()` and `isinstance()` calls. pub(super) const fn is_valid_isinstance_target(self) -> bool { match self { Self::Callable - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::FrozenSet - | Self::Dict - | Self::List - | Self::OrderedDict - | Self::Set + | Self::LegacyStdlibAlias(_) | Self::Tuple | Self::Type | Self::Protocol @@ -463,21 +496,17 @@ impl SpecialFormType { | Self::Annotated | Self::Bottom | Self::CallableTypeOf - | Self::ClassVar + | Self::TypeQualifier(_) | Self::Concatenate - | Self::Final | Self::Intersection | Self::Literal | Self::LiteralString | Self::Never | Self::NoReturn | Self::Not - | Self::ReadOnly - | Self::Required | Self::TypeAlias | Self::TypeGuard | Self::NamedTuple - | Self::NotRequired | Self::Optional | Self::Top | Self::TypeIs @@ -505,27 +534,27 @@ impl SpecialFormType { SpecialFormType::Tuple => "Tuple", SpecialFormType::Type => "Type", SpecialFormType::TypingSelf => "Self", - SpecialFormType::Final => "Final", - SpecialFormType::ClassVar => "ClassVar", + SpecialFormType::TypeQualifier(TypeQualifier::Final) => "Final", + SpecialFormType::TypeQualifier(TypeQualifier::ClassVar) => "ClassVar", SpecialFormType::Callable => "Callable", SpecialFormType::Concatenate => "Concatenate", SpecialFormType::Unpack => "Unpack", - SpecialFormType::Required => "Required", - SpecialFormType::NotRequired => "NotRequired", + SpecialFormType::TypeQualifier(TypeQualifier::Required) => "Required", + SpecialFormType::TypeQualifier(TypeQualifier::NotRequired) => "NotRequired", SpecialFormType::TypeAlias => "TypeAlias", SpecialFormType::TypeGuard => "TypeGuard", SpecialFormType::TypedDict => "TypedDict", SpecialFormType::TypeIs => "TypeIs", - SpecialFormType::List => "List", - SpecialFormType::Dict => "Dict", - SpecialFormType::DefaultDict => "DefaultDict", - SpecialFormType::Set => "Set", - SpecialFormType::FrozenSet => "FrozenSet", - SpecialFormType::Counter => "Counter", - SpecialFormType::Deque => "Deque", - SpecialFormType::ChainMap => "ChainMap", - SpecialFormType::OrderedDict => "OrderedDict", - SpecialFormType::ReadOnly => "ReadOnly", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::List) => "List", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Dict) => "Dict", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::DefaultDict) => "DefaultDict", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Set) => "Set", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::FrozenSet) => "FrozenSet", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Counter) => "Counter", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::Deque) => "Deque", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::ChainMap) => "ChainMap", + SpecialFormType::LegacyStdlibAlias(LegacyStdlibAlias::OrderedDict) => "OrderedDict", + SpecialFormType::TypeQualifier(TypeQualifier::ReadOnly) => "ReadOnly", SpecialFormType::Unknown => "Unknown", SpecialFormType::AlwaysTruthy => "AlwaysTruthy", SpecialFormType::AlwaysFalsy => "AlwaysFalsy", @@ -555,30 +584,20 @@ impl SpecialFormType { | SpecialFormType::Tuple | SpecialFormType::Type | SpecialFormType::TypingSelf - | SpecialFormType::Final - | SpecialFormType::ClassVar + | SpecialFormType::TypeQualifier(_) | SpecialFormType::Callable | SpecialFormType::Concatenate | SpecialFormType::Unpack - | SpecialFormType::Required - | SpecialFormType::NotRequired | SpecialFormType::TypeAlias | SpecialFormType::TypeGuard | SpecialFormType::TypedDict | SpecialFormType::TypeIs - | SpecialFormType::ReadOnly | SpecialFormType::Protocol | SpecialFormType::Generic | SpecialFormType::NamedTuple - | SpecialFormType::List - | SpecialFormType::Dict - | SpecialFormType::DefaultDict - | SpecialFormType::Set - | SpecialFormType::FrozenSet - | SpecialFormType::Counter - | SpecialFormType::Deque - | SpecialFormType::ChainMap - | SpecialFormType::OrderedDict => &[KnownModule::Typing, KnownModule::TypingExtensions], + | SpecialFormType::LegacyStdlibAlias(_) => { + &[KnownModule::Typing, KnownModule::TypingExtensions] + } SpecialFormType::Unknown | SpecialFormType::AlwaysTruthy @@ -608,6 +627,128 @@ impl SpecialFormType { }) .map(TypeDefinition::SpecialForm) } + + /// Interpret this special form as an unparameterized type in a type-expression context. + /// + /// This is called for the "misc" special forms that are not aliases, type qualifiers, + /// `Tuple`, `Type`, or `Callable` (those are handled by their respective call sites). + pub(super) fn in_type_expression<'db>( + self, + db: &'db dyn Db, + scope_id: ScopeId<'db>, + typevar_binding_context: Option>, + ) -> Result, InvalidTypeExpressionError<'db>> { + match self { + Self::Never | Self::NoReturn => Ok(Type::Never), + Self::LiteralString => Ok(Type::literal_string()), + Self::Any => Ok(Type::any()), + Self::Unknown => Ok(Type::unknown()), + Self::AlwaysTruthy => Ok(Type::AlwaysTruthy), + Self::AlwaysFalsy => Ok(Type::AlwaysFalsy), + + // Special case: `NamedTuple` in a type expression is understood to describe the type + // `tuple[object, ...] & `. + // This isn't very principled (since at runtime, `NamedTuple` is just a function), + // but it appears to be what users often expect, and it improves compatibility with + // other type checkers such as mypy. + // See conversation in https://github.com/astral-sh/ruff/pull/19915. + Self::NamedTuple => Ok(IntersectionBuilder::new(db) + .positive_elements([ + Type::homogeneous_tuple(db, Type::object()), + KnownClass::NamedTupleLike.to_instance(db), + ]) + .build()), + + Self::TypingSelf => { + let index = semantic_index(db, scope_id.file(db)); + let Some(class) = nearest_enclosing_class(db, index, scope_id) else { + return Err(InvalidTypeExpressionError { + fallback_type: Type::unknown(), + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::InvalidType(Type::SpecialForm(self), scope_id) + ], + }); + }; + + Ok( + typing_self(db, scope_id, typevar_binding_context, class.into()) + .map(Type::TypeVar) + .unwrap_or(Type::SpecialForm(self)), + ) + } + // We ensure that `typing.TypeAlias` used in the expected position (annotating an + // annotated assignment statement) doesn't reach here. Using it in any other type + // expression is an error. + Self::TypeAlias => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::TypeAlias], + fallback_type: Type::unknown(), + }), + Self::TypedDict => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::TypedDict], + fallback_type: Type::unknown(), + }), + + Self::Literal | Self::Union | Self::Intersection => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::RequiresArguments(self) + ], + fallback_type: Type::unknown(), + }), + + Self::Protocol => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Protocol], + fallback_type: Type::unknown(), + }), + Self::Generic => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Generic], + fallback_type: Type::unknown(), + }), + + Self::Optional + | Self::Not + | Self::Top + | Self::Bottom + | Self::TypeOf + | Self::TypeIs + | Self::TypeGuard + | Self::Unpack + | Self::CallableTypeOf => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::RequiresOneArgument(self) + ], + fallback_type: Type::unknown(), + }), + + Self::Annotated | Self::Concatenate => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::RequiresTwoArguments(self) + ], + fallback_type: Type::unknown(), + }), + + // We treat `typing.Type` exactly the same as `builtins.type`: + SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)), + SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())), + SpecialFormType::Callable => Ok(Type::Callable(CallableType::unknown(db))), + SpecialFormType::LegacyStdlibAlias(alias) => Ok(alias.aliased_class().to_instance(db)), + SpecialFormType::TypeQualifier(qualifier) => { + let err = match qualifier { + TypeQualifier::Final | TypeQualifier::ClassVar => { + InvalidTypeExpression::TypeQualifier(qualifier) + } + TypeQualifier::ReadOnly + | TypeQualifier::NotRequired + | TypeQualifier::Required => { + InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) + } + }; + Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![err], + fallback_type: Type::unknown(), + }) + } + } + } } impl std::fmt::Display for SpecialFormType { @@ -620,3 +761,93 @@ impl std::fmt::Display for SpecialFormType { ) } } + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] +pub enum LegacyStdlibAlias { + List, + Dict, + Set, + FrozenSet, + ChainMap, + Counter, + DefaultDict, + Deque, + OrderedDict, +} + +impl LegacyStdlibAlias { + pub(super) const fn alias_spec(self) -> AliasSpec { + let (class, expected_argument_number) = match self { + LegacyStdlibAlias::List => (KnownClass::List, 1), + LegacyStdlibAlias::Dict => (KnownClass::Dict, 2), + LegacyStdlibAlias::Set => (KnownClass::Set, 1), + LegacyStdlibAlias::FrozenSet => (KnownClass::FrozenSet, 1), + LegacyStdlibAlias::ChainMap => (KnownClass::ChainMap, 2), + LegacyStdlibAlias::Counter => (KnownClass::Counter, 1), + LegacyStdlibAlias::DefaultDict => (KnownClass::DefaultDict, 2), + LegacyStdlibAlias::Deque => (KnownClass::Deque, 1), + LegacyStdlibAlias::OrderedDict => (KnownClass::OrderedDict, 2), + }; + + AliasSpec { + class, + expected_argument_number, + } + } + + pub(super) const fn aliased_class(self) -> KnownClass { + self.alias_spec().class + } +} + +impl From for SpecialFormType { + fn from(value: LegacyStdlibAlias) -> Self { + SpecialFormType::LegacyStdlibAlias(value) + } +} + +impl std::fmt::Display for LegacyStdlibAlias { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + SpecialFormType::from(*self).fmt(f) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] +pub enum TypeQualifier { + ReadOnly, + Final, + ClassVar, + Required, + NotRequired, +} + +impl From for SpecialFormType { + fn from(value: TypeQualifier) -> Self { + SpecialFormType::TypeQualifier(value) + } +} + +impl From for TypeQualifiers { + fn from(value: TypeQualifier) -> Self { + match value { + TypeQualifier::ReadOnly => TypeQualifiers::READ_ONLY, + TypeQualifier::Final => TypeQualifiers::FINAL, + TypeQualifier::ClassVar => TypeQualifiers::CLASS_VAR, + TypeQualifier::Required => TypeQualifiers::REQUIRED, + TypeQualifier::NotRequired => TypeQualifiers::NOT_REQUIRED, + } + } +} + +impl std::fmt::Display for TypeQualifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + SpecialFormType::from(*self).fmt(f) + } +} + +/// Information regarding the [`KnownClass`] a [`LegacyStdlibAlias`] refers to. +#[derive(Debug)] +pub(super) struct AliasSpec { + pub(super) class: KnownClass, + pub(super) expected_argument_number: usize, +}