From 1f689bf2ab237cadce427dbc747c04501b160cd2 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 29 Jan 2026 19:13:04 +0000 Subject: [PATCH] [ty] Emit an error if a TypeVarTuple is used to subscript `Generic` or `Protocol` without being unpacked --- .../annotations/unsupported_special_types.md | 7 ++++ .../src/types/infer/builder.rs | 35 +++++++++++++++++-- .../ty_python_semantic/src/types/subscript.rs | 10 ++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md index 120ff9e25f896..78741eb05603f 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_types.md @@ -45,3 +45,10 @@ class Baz[*Ts]: ... # TODO: false positive z: Baz[int, str, bytes] # error: [not-subscriptable] ``` + +And we also provide some basic validation in some cases: + +```py +# error: [invalid-generic-class] "`TypeVarTuple` must be unpacked with `*` or `Unpack[]` when used as an argument to `Generic`" +class Spam(Generic[Ts]): ... +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 4afcb73f28b17..f34f8d1ba6bb5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -15252,6 +15252,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { NotYetSupported, /// A duplicate typevar was provided. DuplicateTypevar(&'db str), + /// A `TypeVarTuple` was provided but not unpacked. + TypeVarTupleMustBeUnpacked, } impl<'db> LegacyGenericContextError<'db> { @@ -15259,7 +15261,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match self { LegacyGenericContextError::InvalidArgument(_) | LegacyGenericContextError::VariadicTupleArguments - | LegacyGenericContextError::DuplicateTypevar(_) => Type::unknown(), + | LegacyGenericContextError::DuplicateTypevar(_) + | LegacyGenericContextError::TypeVarTupleMustBeUnpacked => Type::unknown(), LegacyGenericContextError::NotYetSupported => { todo_type!("ParamSpecs and TypeVarTuples") } @@ -15301,6 +15304,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { typevar.name(db), )); } + } else if let Type::NominalInstance(instance) = argument_ty + && instance.has_known_class(db, KnownClass::TypeVarTuple) + { + return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked); } else if any_over_type( db, argument_ty, @@ -15353,7 +15360,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }, )) } - Err(error) => Ok(error.into_type()), + Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked) => { + Err(SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::TypeVarTupleNotUnpacked { + origin: LegacyGenericOrigin::Generic, + }, + )) + } + Err( + error @ (LegacyGenericContextError::NotYetSupported + | LegacyGenericContextError::VariadicTupleArguments), + ) => Ok(error.into_type()), } } Type::SpecialForm(SpecialFormType::Protocol) => { @@ -15379,7 +15397,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }, )) } - Err(error) => Ok(error.into_type()), + Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked) => { + Err(SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::TypeVarTupleNotUnpacked { + origin: LegacyGenericOrigin::Protocol, + }, + )) + } + Err( + error @ (LegacyGenericContextError::NotYetSupported + | LegacyGenericContextError::VariadicTupleArguments), + ) => Ok(error.into_type()), } } Type::SpecialForm(SpecialFormType::Concatenate) => { diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs index d6d28434bc4e8..9151c694c63ba 100644 --- a/crates/ty_python_semantic/src/types/subscript.rs +++ b/crates/ty_python_semantic/src/types/subscript.rs @@ -147,6 +147,8 @@ pub(crate) enum SubscriptErrorKind<'db> { origin: LegacyGenericOrigin, typevar_name: &'db str, }, + /// A `TypeVarTuple` was provided to `Generic` or `Protocol` without being unpacked. + TypeVarTupleNotUnpacked { origin: LegacyGenericOrigin }, } impl<'db> SubscriptError<'db> { @@ -334,6 +336,14 @@ impl<'db> SubscriptErrorKind<'db> { )); } } + Self::TypeVarTupleNotUnpacked { origin } => { + if let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, subscript) { + builder.into_diagnostic(format_args!( + "`TypeVarTuple` must be unpacked with `*` or `Unpack[]` when \ + used as an argument to `{origin}`", + )); + } + } } }