Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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: <class 'tuple[Unknown, ...]'>
reveal_type(LegacyAlias) # revealed: <class 'tuple[Unknown, ...]'>

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, ...]
```
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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> {
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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])
}
}
}
Expand Down
Loading