Skip to content

fix(core): read a type assertion in a for head, collect a rest parameter's default and a parameter after an arrow function's rest parameter, reject ? and type annotations outside parameters, and end an async rest parameter after its annotation (#723, #724, #725, #726, #727) - #777

Merged
leonidaz merged 4 commits into
mainfrom
fix/core-arrow-parameter-follow-ups
Sep 26, 2026

Conversation

@leonidaz

@leonidaz leonidaz commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Closes #723
Closes #724
Closes #725
Closes #726
Closes #727

These are the follow-ups from #728: arrow function parameters, rest parameters, and for heads that TSRX read differently from TypeScript's TSX parser. #723, #724, and #725 are caused in @sveltejs/acorn-typescript, and each fix is a narrow override in TSRXPlugin marked UPSTREAM(…). #726 and #727 are checker-level mistakes (#415) where acorn is right about JavaScript: collect and loose mode now record TypeScript's error and keep parsing, and a strict parse throws it. Some inputs that parsed now throw, because TypeScript rejects them (see Behavior change).

The bugs and the fixes

#723: a type assertion in a for…in or for…of head

for ((a as T) of x), for ([a as T] of x), for ({ a: b! } of x), and for ((a!) in x) failed with Unexpected type cast in parameter position. in every mode. acorn converts a loop's head with toAssignable(init, false), and acorn-typescript raises that error when isBinding is false. Its flag means the opposite of @babel/parser's isLHS, where the code comes from (sveltejs/acorn-typescript#148).

  • toAssignable converts a type assertion's expression without the error when isBinding is false, and keeps the assertion as acorn-typescript does for an assignment target (with preserveTypeScriptWrapper, or in place for a nested one).
  • TSRX's parseForStatement passes preserveTypeScriptWrapper, so the loop's left side keeps the assertion and the parentheses around it (preserveParens), as an assignment target's does. acorn ignores toAssignable's result there; TSRX's copy uses it.
  • [Bug]: tsrx-core: A type assertion in an arrow function's parameters parses with no error, and the output keeps it #706's check in checkLValPattern, which raises the error for an assertion in a binding, is unchanged, so (a as T) => a still fails.

#724: a ? or a type annotation outside an arrow function's parameters

acorn-typescript's parseParenItem reads a ? and a type annotation after each item of a parenthesized expression and of every list that parseExprList reads (call and new arguments, array literals, decorator arguments), and parseExprList reads a type annotation after a spread. They're meant for an arrow function's parameters. Where the list wasn't one, the item kept them: (x: number) crashed the compile (Not implemented: TSTypeCastExpression), and f(x?) and f(...x: number[]) were printed back (sveltejs/acorn-typescript#149).

  • parseParenAndDistinguishExpression and parseExprList note the list being read (#expressionList), and whether it can be an arrow function's parameters: a parenthesized list that can be an arrow function (canBeArrow), or the arguments of async (…). No other list can.
  • parseParenItem and parseSpread pass what they read to #checkParameterSyntax. In a list that can't be an arrow function's parameters, it raises @babel/parser's error there: Did not expect a type annotation here. at the annotation's :, or Unexpected token at the ?. Otherwise it notes the first one, and the list raises it when no arrow function follows (parseParenAndDistinguishExpression, #parseAsyncArguments).
  • The note is undone with a speculative parse that acorn-typescript abandons (parseEffects.willSet).
  • [Bug]: tsrx-core: An async arrow function's optional rest parameter fails to parse in every mode, where TypeScript reports TS1047 from its checker #702's ? after a spread among the arguments of async (…) goes through the same check, so #readingAsyncArguments and the question field are gone. Its behavior is the same.
export const t = f(
	a,
	b?: string,
);

This throws Unexpected token (3:2) at the ?, where TypeScript reports TS1109 at the :.

#725: an async arrow function's rest parameter's range

The rest parameter of async (...a: number[]) => a ended before its type annotation, since acorn-typescript's parseExprList sets it without moving the spread's end (sveltejs/acorn-typescript#150). tsrx-prettier-plugin places comments by these ranges, and moved the comment in async (...a /* c */: number[]) => a after the annotation. parseSpread now reads the annotation among the arguments of async (…) itself, before parseExprList does, and moves the spread's end past it, and past a ? (#702), as a rest parameter's is elsewhere. The formatter keeps the comment where Prettier does: const f = async (...a /* c */ : number[]) => a;.

#726: a rest parameter's default (TS1048)

function f(...a = []) {}, (...a = []) => a, async (...a = []) => a, and type F = (...a = []) => void failed in every mode. TypeScript's parser reads the default, and its checker reports TS1048 A rest parameter cannot have an initializer. at the parameter's name.

tsrx-prettier-plugin refuses it (REJECTED_MISTAKES), since the tree doesn't keep the default. Prettier's typescript parser drops it (function f(...a) {}).

#727: a parameter after an arrow function's rest parameter (TS1014)

(...a, b) => 1 threw Comma is not permitted after the rest element in collect mode, which records it for a function, a signature, and an async arrow function. acorn's parseParenAndDistinguishExpression ends a parenthesized list at its rest element and raises the error at the comma.

  • #collectCheckerLevelError, at that comma, calls #readArrowParametersPastRestElement. In a list that can be an arrow function's parameters, at its top level, where the rest of the list reads as parameters and => follows (#readsAsArrowParameters, the lookahead that #startsArrowParameterProperty uses), it records the error at the comma and reads the items after it, as #parseBindingListPastRestElement does for a function's. parseParenArrowList adds them to the arrow function's parameters and checks their pattern errors. Otherwise the error is thrown, as before.
  • #parseArrowParameterProperty raised it after a rest element with modifiers, (x, public ...a, b) => b, in both kinds of list. It records it now, as #parseRestParameterProperty does for a function's, and the list goes on.

Behavior change

TypeScript's parser rejects each of these (decision 23 in #596). They parsed in every mode, and now throw in every mode (#724):

Input Error
(x: number), (a, b: T), f(x: number), f?.(x: number), f<T>(x: number), new F(x: number), [x: number], ([...x: number[]]), ({ a: (b: number) }), @dec(x: number) class A {}, ([x: number]) => 1, async(x: number), f(...x: number[]), async(...x: number[]) Did not expect a type annotation here. at the : (TypeScript: TS1005 '=>' expected. or ',' expected.)
(x?: number), (x, y?), f(x?), async(x?), ([x?]) => 1 Unexpected token at the ? (TypeScript: TS1005 or TS1109)

Inputs that threw and parse now: #723's for heads in every mode, and #726's and #727's in collect and loose mode. Inputs that threw and throw a different error now:

@tsrx/prettier-plugin parses strictly, so it throws for all of the #724 inputs too.

Overrides

Override Issue Upstream What it does
toAssignable, with parseForStatement #723 sveltejs/acorn-typescript#148 Converts a type assertion without the error when isBinding is false, and keeps it.
parseParenItem, parseSpread, #checkParameterSyntax, from parseParenAndDistinguishExpression, parseExprList, and #parseAsyncArguments #724 sveltejs/acorn-typescript#149 Rejects a ? or a type annotation where the list isn't an arrow function's parameters.
parseSpread #725 sveltejs/acorn-typescript#150 Reads a spread's type annotation among the arguments of async (…) and moves its end.
parseBindingListItem, parseParenItem, parseSpread, parseSubscriptAsyncArrow, #readRestParameterDefault, parseFunctionBody, tsParseBindingListForSignature #726 (acorn is right; sveltejs/acorn-typescript#89 for checker diagnostics) Records TS1048 when collecting and leaves the default out.
#collectCheckerLevelError, #readArrowParametersPastRestElement, parseParenArrowList, #parseArrowParameterProperty #727 (acorn is right) Records TS1014 at the comma when collecting and keeps the parameters.

I checked sveltejs/acorn-typescript#110's head: it doesn't touch toAssignable, parseParenItem, parseExprList, or parseParenAndDistinguishExpression, so it fixes none of these, as the upstream issues say.

The formatters

tsrx-prettier-plugin parses with collect: true:

prettier-known-failures.json is unchanged (empty).

Language server

The language server compiles with loose: true. I ran the new collect-mode parses through compile_to_volar_mappings for all five targets: (...a, b) => [a, b], (x, public ...a, b) => 1, function h(...a = []) {}, (...a: number[] = []) => a, async (...a = []) => a, async (...a: number[] = [1]) => a, type H = (...a = []) => void, interface I { m(...a = []): void }, #723's for heads, and both kinds inside a template's setup. None fails. The virtual code keeps a parameter after a rest parameter ((...a, b) => [a, b]), so TypeScript reports its own TS1014 there too, following #558's convention. It leaves out a rest parameter's default, as the tree does, so only TSRX reports TS1048. A strict compile prints #723's heads without the parentheses (for (a as string of x)), which TypeScript accepts.

Downstream

No node changes shape for code that parses today. #725 moves the end of an async arrow function's rest parameter to where a rest parameter's end already is without async, with the same fields. Everything else is code that threw and parses now, or parsed and throws now, with no other AST change. So I didn't check Ripple or Octane.

Verification

Parse comparison with main (9c2f24f8; e63c530a, merged in, changes no parser file). 19,898 sources, 18,924 distinct:

  • the 70 tracked .tsrx files (59 distinct);
  • the 1,548 distinct inputs and 1,841 distinct outputs of the 2,040 Prettier snapshot cases under packages/tsrx-prettier-plugin/tests/prettier/ (readSnapshotEntries/readFormatCase);
  • 15,748 distinct string and template literals from the test files, this PR's included.

Each was parsed strict, with { collect: true, errors: [], comments: [], preserveParens: true }, and with { loose: true }, before and after. That's 59,694 parses in four worker threads, each killed if a parse took over 15 s. None hung. I compared every AST, comment list, and error list, or the thrown message and position.

85 sources changed. No .tsrx file and no Prettier input or output changed. 72 are this PR's new test strings. The other 13 are existing test strings:

By mode:

I also ran signature parameter lists, strict and collecting, before and after, since tsParseBindingListForSignature reads them through parseBindingList in a strict parse now: optional patterns and rest parameters in function types, method, call, and construct signatures, ambient functions, and parameters after a rest parameter. None changed.

I timed parsing the .tsrx files and Prettier sources in alternating processes before and after. The machine was under heavy load (load average about 30), and the difference was within run-to-run noise.

Tests.

Local runs. These pass:

  • vitest tsrx-utils, tsrx-analyze, prettier-plugin, tsrx-prettier-plugin, tsrx-react, tsrx-preact, tsrx-solid, tsrx-vue, and tsrx-hono (--maxWorkers=4), prettier-plugin and tsrx-prettier-plugin again after merging main.
  • pnpm typecheck. tsrx-tsc isn't linked after --ignore-scripts, so I built @tsrx/typescript-plugin and ran vite-plugin-hono's typecheck:tests through packages/typescript-plugin/dist/tsc.js, then the steps after it.
  • pnpm format (no changes outside this PR's files), pnpm format:check after merging main, and pnpm changeset:check.

A patch changeset for @tsrx/core is included. tsrx-prettier-plugin is private.

Findings

Filed here, each with a separate cause and not fixed in this PR:

Same causes as the issues here, and not changed:

🤖 Generated with Claude Code


Note

Medium Risk
Changes core parser behavior for common TypeScript edge cases; some inputs that previously parsed now throw, which can affect format-on-save and strict tooling paths.

Overview
This PR tightens @tsrx/core parsing so arrow parameters, rest parameters, and for…in / for…of heads match TypeScript more closely, with matching Prettier plugin rejections and tests.

Now parses and keeps in the AST (collect/loose): type assertions in loop heads (for ((a as T) of x)); async rest parameters whose source range includes ? and type annotations (fixes comment placement in the formatter); arrow functions with parameters after a rest ((...a, b) => …), with TS1014 recorded at the comma.

Checker-level mistakes in collect/loose: rest-parameter defaults (...a = []) raise A rest parameter cannot have an initializer., read past the = but omit the default from the tree (like typescript-estree); signatures still get TS2371 where applicable. tsrx-prettier-plugin treats that message like other rejected mistakes.

Stricter errors (aligned with TypeScript): optional markers and type annotations in non-parameter lists—parentheses, call/new/array/decorator arguments without =>—now fail with Did not expect a type annotation here. or Unexpected token instead of crashing or printing invalid code.

Implementation is mostly TSRXPlugin overrides (toAssignable, parseParenItem, parseSpread, #readRestParameterDefault, #readArrowParametersPastRestElement, #checkParameterSyntax, etc.) plus expanded parser and Prettier tests.

Reviewed by Cursor Bugbot for commit 5608aa4. Bugbot is set up for automated code reviews on this repo. Configure here.

leonidaz and others added 2 commits September 26, 2026 03:31
…meter's default and a parameter after an arrow function's rest parameter, reject `?` and type annotations outside parameters, and end an async rest parameter after its annotation (#723, #724, #725, #726, #727)

- #723: a type assertion in a `for…in` or `for…of` head parses and is kept,
  as in an assignment target (`toAssignable`, sveltejs/acorn-typescript#148).
- #724: a `?` or a type annotation after an item of a parenthesized
  expression or an expression list, or a type annotation after a spread,
  fails where the list isn't an arrow function's parameters
  (`parseParenItem`, `parseSpread`, sveltejs/acorn-typescript#149).
- #725: an async arrow function's rest parameter covers its `?` and type
  annotation (`parseSpread`, sveltejs/acorn-typescript#150).
- #726: a rest parameter's default records TS1048 when collecting, and the
  tree leaves it out, as typescript-estree does; tsrx-prettier-plugin
  refuses it.
- #727: a parameter after an arrow function's rest parameter records TS1014
  when collecting, and the arrow function keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue. You can view the agent here.

Reviewed by Cursor Bugbot for commit 0d91fc9. Configure here.

Comment thread packages/tsrx/src/plugin.js Resolved
@leonidaz
leonidaz merged commit fcc3dc8 into main Sep 26, 2026
8 checks passed
@leonidaz
leonidaz deleted the fix/core-arrow-parameter-follow-ups branch September 26, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment