diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md index 55cf73ace80a0..e75b2c78efa28 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md @@ -680,3 +680,74 @@ reveal_type(test(fn0)) # revealed: tuple[()] reveal_type(test(fn1)) # revealed: tuple[str] reveal_type(test(fn2)) # revealed: tuple[str, bytes] ``` + +## Missing unpack + +A legacy type variable tuple must also be unpacked. In a tuple annotation, it recovers as +`*tuple[Unknown, ...]`, so `tuple[Ts]` becomes `tuple[Unknown, ...]`, rather than the single-element +`tuple[Unknown]`. This avoids a cascading assignment error when the value is assigned to a correctly +unpacked tuple annotation. + +```py +from typing import Generic, TypeVarTuple + +Ts = TypeVarTuple("Ts") + +# error: [invalid-generic-class] "`TypeVarTuple` must be unpacked with `*` or `Unpack[]` when used as an argument to `Generic`" +class Container(Generic[Ts]): + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + def __init__(self, values: tuple[Ts]) -> None: + reveal_type(values) # revealed: tuple[Unknown, ...] + self.values: tuple[*Ts] = values +``` + +`typing.Tuple` uses the same recovery as the built-in `tuple`. + +```py +from typing import Tuple + +# error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +def legacy_tuple(values: Tuple[Ts]) -> None: + reveal_type(values) # revealed: tuple[Unknown, ...] +``` + +## Missing unpack in implicit tuple aliases + +Tuple specializations used to define implicit type aliases also recover bare type variable tuples as +`*tuple[Unknown, ...]`. This applies to both `tuple` and `typing.Tuple`. + +```py +from typing import Tuple, TypeVarTuple + +Ts = TypeVarTuple("Ts") + +BuiltinAlias = tuple[Ts] # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +LegacyAlias = Tuple[Ts] # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + +reveal_type(BuiltinAlias) # revealed: +reveal_type(LegacyAlias) # revealed: + +def aliases(builtin: BuiltinAlias, legacy: LegacyAlias) -> None: + reveal_type(builtin) # revealed: tuple[Unknown, ...] + reveal_type(legacy) # revealed: tuple[Unknown, ...] +``` + +## Missing unpack in a union-valued tuple element + +In a homogeneous tuple annotation, a name that may refer to a bare type variable tuple or a valid +element type preserves the valid alternative and recovers the bare pack to `Unknown`. + +```py +from typing import TypeVarTuple + +Ts = TypeVarTuple("Ts") + +def condition() -> bool: + return True + +Element = Ts if condition() else int + +# error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +def homogeneous_union(values: tuple[Element, ...]) -> None: + reveal_type(values) # revealed: tuple[Unknown | int, ...] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md index 4fae525b2b4ea..b1e0f1eca525a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md @@ -1718,6 +1718,8 @@ type Alias[*Ts1, *Ts2] = tuple[*Ts1] | tuple[*Ts2] ### Must always be unpacked +A type variable tuple represents zero or more types, so it cannot be used as a single type. + ```py def invalid[*Ts](x: Ts) -> None: ... # error: [invalid-type-form] def invalid_args[*Ts](*args: Ts) -> None: ... # error: [invalid-type-form] @@ -1726,10 +1728,98 @@ class InvalidTupleElement[*Ts]: # error: [invalid-type-form] "Bare TypeVarTuple `Ts` is not valid in this context in a type expression" values: tuple[Ts] +reveal_type(InvalidTupleElement[int, str]().values) # revealed: tuple[Unknown, ...] + def valid[*Ts](x: tuple[*Ts]) -> tuple[*Ts]: return x ``` +A bare type variable tuple in a tuple annotation recovers as `*tuple[Unknown, ...]`, preserving any +fixed elements before and after it. Treating the bare pack as one `Unknown` element would +incorrectly impose a fixed length. + +```py +# error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +def mixed[*Ts](values: tuple[int, Ts, str]) -> None: + reveal_type(values) # revealed: tuple[int, *tuple[Unknown, ...], str] +``` + +### Missing unpack in a homogeneous tuple + +Adding an ellipsis does not make a bare type variable tuple a valid element type. The invalid +specialization recovers to `tuple[Unknown, ...]`. + +```py +# error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +def homogeneous[*Ts](values: tuple[Ts, ...]) -> None: + reveal_type(values) # revealed: tuple[Unknown, ...] +``` + +### Missing unpack inside another type + +Recovery only affects the bare pack's position in its tuple. An enclosing tuple or `type[]` +annotation keeps its structure. An ordinary tuple with an `Unknown` element keeps its fixed length. + +```py +from ty_extensions._internal import Unknown + +def nested[*Ts]( + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + values: tuple[tuple[Ts]], + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + cls: type[tuple[Ts]], + fixed: tuple[Unknown], +) -> None: + reveal_type(values) # revealed: tuple[tuple[Unknown, ...]] + reveal_type(cls) # revealed: type[tuple[Unknown, ...]] + reveal_type(fixed) # revealed: tuple[Unknown] +``` + +### Missing unpack in quoted annotations + +Quoting the whole tuple annotation or just the bare type variable tuple does not change the +diagnostic or the fallback type. + +```py +def quoted[*Ts]( + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + whole: "tuple[Ts]", + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + element: tuple["Ts"], +) -> None: + reveal_type(whole) # revealed: tuple[Unknown, ...] + reveal_type(element) # revealed: tuple[Unknown, ...] +``` + +### Other errors alongside a missing unpack + +Recovering from a missing unpack does not prevent us from reporting independent errors in the +remaining tuple elements. + +```py +# error: [invalid-type-form] "Bare TypeVarTuple `Ts`" +# error: [unresolved-reference] "Name `Missing` used when not defined" +def invalid_sibling[*Ts](values: tuple[Ts, Missing]) -> None: + reveal_type(values) # revealed: tuple[*tuple[Unknown, ...], Unknown] +``` + +### Missing unpack alongside other variadic elements + +A bare type variable tuple alongside a valid variadic unpack or another bare pack reports only the +missing-unpack errors, without a cascading multiple-unpack error. + +```py +def other_variadic[*Ts]( + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + before: tuple[Ts, *tuple[int, ...]], + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + after: tuple[*tuple[int, ...], Ts], + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + # error: [invalid-type-form] "Bare TypeVarTuple `Ts`" + repeated: tuple[Ts, Ts], +) -> None: ... +``` + ### Invalid unpack operand Only tuple types and type variable tuples can be unpacked in a type expression. diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index a9eec2fa1e1c4..5cc8c5b73d7e1 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -86,6 +86,9 @@ bitflags::bitflags! { /// The operand of an `Unpack[...]` expression is neither a tuple nor a `TypeVarTuple`. const INVALID_UNPACK = 1 << 1; + + /// The expression refers to a `TypeVarTuple` without unpacking it. + const INVALID_BARE_TYPE_VAR_TUPLE = 1 << 2; } } 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 f4c9b0a17c145..9aef62a253cf8 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 @@ -108,7 +108,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { fn infer_name_or_attribute<'db>( ty: Type<'db>, annotation: &ast::Expr, - builder: &TypeInferenceBuilder<'db, '_>, + builder: &mut TypeInferenceBuilder<'db, '_>, pep_613_policy: PEP613Policy, ) -> AnnotationExpressionInference<'db> { let special_case = match ty { 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 7b043787e483b..57270829449d1 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 @@ -17,15 +17,15 @@ use crate::types::infer::{InferenceFlags, TypeExpressionFlags}; use crate::types::signatures::{ConcatenateTail, 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::tuple::{TupleSpec, TupleSpecBuilder, TupleType}; use ty_python_core::scope::ScopeKind; use crate::types::{ BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, - IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, LiteralValueTypeKind, - Parameter, Parameters, SpecialFormType, SubclassOfType, Type, TypeContext, TypeFormType, - TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, UnionType, any_over_type, - todo_type, + IntersectionType, InvalidTypeExpression, KnownClass, KnownInstanceType, LintDiagnosticGuard, + LiteralValueTypeKind, Parameter, Parameters, SpecialFormType, SubclassOfType, Type, + TypeContext, TypeFormType, TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, + UnionType, any_over_type, todo_type, }; use crate::{FxOrderSet, add_inferred_python_version_hint_to_diagnostic}; @@ -99,7 +99,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } pub(super) fn infer_name_or_attribute_type_expression( - &self, + &mut self, ty: Type<'db>, annotation: &ast::Expr, ) -> Type<'db> { @@ -121,6 +121,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.inference_flags(), ) .unwrap_or_else(|error| { + if error.invalid_expressions.iter().any(|invalid| { + matches!(invalid, InvalidTypeExpression::InvalidBareTypeVarTuple(_)) + }) { + self.store_type_expression_flags( + annotation, + TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE, + ); + } error.into_fallback_type(&self.context, annotation, self.inference_flags()) }); self.check_for_unbound_type_variable(annotation, result_ty) @@ -1062,6 +1070,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// This method assumes that a type has already been inferred and stored for the `value` /// of the subscript passed in. + /// + /// Recovers a bare `TypeVarTuple` as `*tuple[Unknown, ...]`, preserving surrounding elements. + /// An enclosing `tuple[tuple[Ts]]` still has exactly one element. pub(super) fn infer_tuple_type_expression( &mut self, tuple: &ast::ExprSubscript, @@ -1175,6 +1186,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else { // TODO: emit a diagnostic } + } else if self + .type_expression_flags(element) + .contains(TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE) + { + // Do not count recovery as another explicit unpack. + element_types = + element_types.concat(db, env, &TupleSpec::homogeneous(Type::unknown())); } else { element_types.push(element_ty); } @@ -1200,7 +1218,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); } self.store_expression_type(single_element, Type::unknown()); - return TupleType::heterogeneous(db, env, std::iter::once(Type::unknown())); + return TupleType::heterogeneous(db, env, [Type::unknown()]); } let previously_in_valid_unpack_context = self .context @@ -1211,6 +1229,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { InferenceFlags::IN_VALID_UNPACK_CONTEXT, previously_in_valid_unpack_context, ); + if self + .type_expression_flags(single_element) + .contains(TypeExpressionFlags::INVALID_BARE_TYPE_VAR_TUPLE) + { + return TupleType::homogeneous(db, env, Type::unknown()); + } let single_element_is_unpack = matches!(single_element, ast::Expr::Starred(_)) || matches!( single_element, @@ -1226,16 +1250,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } else if let Type::TypeVar(typevar) = single_element_ty && typevar.is_typevartuple(self.db()) { - return TupleType::new( - db, - env, - &TupleSpecBuilder::with_capacity(0) - .concat_variadic_typevar(db, env, typevar) - .build(), - ); + return TupleType::unpacked_typevartuple(db, env, typevar); } } - TupleType::heterogeneous(db, env, std::iter::once(single_element_ty)) + TupleType::heterogeneous(db, env, [single_element_ty]) } } }