fix(parser): validate TS named tuple rest elements (#26419)

## Problem

Named tuple rest elements bypass the parser's existing checks for optional elements after an array rest and for multiple array rests. For example, both of these declarations currently parse successfully even though TypeScript rejects them:

```ts
type OptionalAfterRest = [...a: string[], b?: number];
type RestAfterRest = [...a: string[], ...b: number[]];
```

The equivalent unnamed forms already produce TS1266 and TS1265, respectively. Mixed named and unnamed array rests have the same gap when either rest is named.

## Cause and change

`parse_tuple_type` inspects `TSRestType.type_annotation` to decide whether an element is an array rest. For a named element, that annotation is a `TSNamedTupleMember`, and the actual type is stored in its `element_type`. Matching directly on the outer annotation therefore skips the array check and fails to track the rest element.

Unwrap the named member with `as_ts_type()` before applying the existing classification. Named and unnamed elements then share the same validation and existing diagnostics:

- An optional element after an array rest reports TS1266.
- A second array rest reports TS1265, including mixed named and unnamed forms.

The diagnostic spans continue to use the complete tuple elements, including their labels. The change borrows the underlying type without changing the AST representation or introducing allocations.

## Preserved behavior

A rest marker alone does not make a spread an unbounded array. Fixed-tuple and generic variadic spreads continue to be accepted, including:

```ts
type FixedBeforeOptional = [...a: [string], b?: number];
type RestBeforeFixed = [...a: string[], ...b: [number]];
type VariadicBeforeOptional<T extends unknown[]> = [...a: T, b?: number];
type RestBeforeVariadic<T extends unknown[]> = [...a: string[], ...b: T];
type MultipleVariadic<T extends unknown[], U extends unknown[]> = [...a: T, ...b: U];
```

This change preserves the existing type-reference classification; it does not add type resolution or change how shadowed `Array` references are handled.
This commit is contained in:
camc314
2026-09-07 17:43:20 +00:00
parent 5cd6da4464
commit d61e3bf887
9 changed files with 68 additions and 14 deletions
+5 -1
View File
@@ -979,7 +979,11 @@ impl<'a, C: Config> ParserImpl<'a, C> {
// example of invalid code:
// type C<T extends unknown[]> = [...string[], ...T[]];
if let TSTupleElement::TSRestType(rest) = &tuple
&& match &rest.type_annotation {
&& let Some(rest_type) = (match &rest.type_annotation {
TSType::TSNamedTupleMember(named) => named.element_type.as_ts_type(),
ty => Some(ty),
})
&& match rest_type {
TSType::TSArrayType(_) => true,
// Check for `Array<...>` type
TSType::TSTypeReference(ts_ref) => match &ts_ref.type_name {
@@ -0,0 +1,4 @@
type OptionalAfterRest = [...a: string[], b?: number];
type RestAfterRest = [...a: string[], ...b: number[]];
type NamedRestAfterUnnamedRest = [...string[], ...b: number[]];
type UnnamedRestAfterNamedRest = [...a: string[], ...number[]];
@@ -0,0 +1,8 @@
type FixedBeforeOptional = [...a: [string], b?: number];
type FixedBeforeRest = [...a: [string], ...b: number[]];
type RestBeforeFixed = [...a: string[], ...b: [number]];
type RestBeforeRequired = [...a: string[], b: number];
type VariadicBeforeOptional<T extends unknown[]> = [...a: T, b?: number];
type VariadicBeforeRest<T extends unknown[]> = [...a: T, ...b: string[]];
type RestBeforeVariadic<T extends unknown[]> = [...a: string[], ...b: T];
type MultipleVariadic<T extends unknown[], U extends unknown[]> = [...a: T, ...b: U];
+2 -2
View File
@@ -1,3 +1,3 @@
codegen_misc Summary:
AST Parsed : 89/89 (100.00%)
Positive Passed: 89/89 (100.00%)
AST Parsed : 90/90 (100.00%)
Positive Passed: 90/90 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
formatter_misc Summary:
AST Parsed : 89/89 (100.00%)
Positive Passed: 89/89 (100.00%)
AST Parsed : 90/90 (100.00%)
Positive Passed: 90/90 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
lexer_misc Summary:
AST Parsed : 278/278 (100.00%)
Positive Passed: 278/278 (100.00%)
AST Parsed : 280/280 (100.00%)
Positive Passed: 280/280 (100.00%)
+41 -3
View File
@@ -1,7 +1,7 @@
parser_misc Summary:
AST Parsed : 89/89 (100.00%)
Positive Passed: 89/89 (100.00%)
Negative Passed: 189/189 (100.00%)
AST Parsed : 90/90 (100.00%)
Positive Passed: 90/90 (100.00%)
Negative Passed: 190/190 (100.00%)
× Cannot assign to 'arguments' in strict mode
╭─[misc/fail/arguments-eval-ambient.ts:2:13]
@@ -4594,6 +4594,44 @@ Negative Passed: 189/189 (100.00%)
╰────
help: Try inserting a semicolon here
× TS(1266): An optional element cannot follow a rest element.
╭─[misc/fail/tuple-named-rest.ts:1:27]
1 │ type OptionalAfterRest = [...a: string[], b?: number];
· ───────┬────── ─────┬────
· │ ╰── Optional element here
· ╰── Rest element seen here
2 │ type RestAfterRest = [...a: string[], ...b: number[]];
╰────
× TS(1265): A rest element cannot follow another rest element.
╭─[misc/fail/tuple-named-rest.ts:2:23]
1 │ type OptionalAfterRest = [...a: string[], b?: number];
2 │ type RestAfterRest = [...a: string[], ...b: number[]];
· ───────┬────── ───────┬──────
· │ ╰── Second rest element here
· ╰── First seen here
3 │ type NamedRestAfterUnnamedRest = [...string[], ...b: number[]];
╰────
× TS(1265): A rest element cannot follow another rest element.
╭─[misc/fail/tuple-named-rest.ts:3:35]
2 │ type RestAfterRest = [...a: string[], ...b: number[]];
3 │ type NamedRestAfterUnnamedRest = [...string[], ...b: number[]];
· ─────┬───── ───────┬──────
· │ ╰── Second rest element here
· ╰── First seen here
4 │ type UnnamedRestAfterNamedRest = [...a: string[], ...number[]];
╰────
× TS(1265): A rest element cannot follow another rest element.
╭─[misc/fail/tuple-named-rest.ts:4:35]
3 │ type NamedRestAfterUnnamedRest = [...string[], ...b: number[]];
4 │ type UnnamedRestAfterNamedRest = [...a: string[], ...number[]];
· ───────┬────── ─────┬─────
· │ ╰── Second rest element here
· ╰── First seen here
╰────
× TS(8011): Type arguments can only be used in TypeScript files.
╭─[misc/fail/type-arguments-in-js.js:1:18]
1 │ class A extends B<C> {}
+2 -2
View File
@@ -1,6 +1,6 @@
semantic_misc Summary:
AST Parsed : 89/89 (100.00%)
Positive Passed: 85/89 (95.51%)
AST Parsed : 90/90 (100.00%)
Positive Passed: 86/90 (95.56%)
semantic Error: tasks/coverage/misc/pass/declare-let-private.ts
Bindings mismatch:
after transform: ScopeId(0): ["private"]
@@ -1,3 +1,3 @@
transformer_misc Summary:
AST Parsed : 89/89 (100.00%)
Positive Passed: 89/89 (100.00%)
AST Parsed : 90/90 (100.00%)
Positive Passed: 90/90 (100.00%)