Skip to content

JSDoc type-cast parentheses applied to function parameters produce invalid output ((row) = fallback) — regression in 2.3.3 #181

Description

@thebjorn

Since 2.3.3 (fix: preserve parantheses of jsdoc type casts, 3cbb677), the cast-parenthesization can fire on the left side of an AssignmentPattern in a function parameter list, where the parentheses are not valid syntax:

const row_template = ($$anchor, /** @type {any} */ (row) = $.noop) => {

(row) = $.noop is not a valid parameter binding, so the printed module fails to parse. 2.3.2 prints the same input as /** @type {any} */ row = $.noop, which is valid.

Related: #164 reported what looks like the same root regression for variable declarators (let /** @type {…} */ (computed);), fixed via #165/#167 — but the function-parameter case here still reproduces on 2.3.6.

Real-world impact

Svelte 5 resolves esrap ^2.x, so any Svelte 5 app with JSDoc-typed snippet parameters ({#snippet row(/** @type {any} */ row)}) now compiles to syntactically invalid JS with svelte ≥ 5.56.9 + esrap ≥ 2.3.3. vite build fails with parse errors that get attributed to whichever plugin re-parses the output first (in our case rolldown's builtin:vite-dynamic-import-vars, with the misleading message "Expected a semicolon or an implicit semicolon after a statement"). Dev mode and unit tests pass; only production builds break, so it surfaced as a Vercel deploy failure on a green CI.

Reproduction (via svelte 5, which drives esrap with loc-anchored comments)

package.json

{
    "type": "module",
    "dependencies": { "svelte": "5.56.9" },
    "overrides": { "esrap": "2.3.6" }
}

Repro.svelte (minimized from a real component; every remaining line is load-bearing — removing any of them makes the output valid again)

<script>
    const handle_delete = async (item) => {
        try {
        } catch (e) {
        }
    }
</script>
<div>
    <div>
    </div>
    {#if ok}
        <div>
            <Form enhance={enhance_create}>
            </Form>
        </div>
    {/if}
</div>
{#snippet row_template(/** @type {any} */ row, )}
    {#if x}
        <td >
            <Btn onclick={() => handle_delete(row)}></Btn>
        </td>
    {/if}
{/snippet}

repro.mjs

import { compile } from 'svelte/compiler'
import vm from 'node:vm'
import fs from 'node:fs'

const out = compile(fs.readFileSync('Repro.svelte', 'utf8'), {
    generate: 'client',
    filename: 'Repro.svelte',
    experimental: { async: true }
})
console.log(out.js.code.split('\n').slice(9, 12).join('\n'))
try {
    new vm.SourceTextModule(out.js.code)
    console.log('>>> output is valid JS')
} catch (e) {
    console.log('>>> output is INVALID JS:', e.message)
}

Run with node --experimental-vm-modules repro.mjs:

export default function Repro($$anchor) {
	const row_template = ($$anchor, /** @type {any} */ (row) = $.noop) => {
		var fragment = $.comment();
>>> output is INVALID JS: Invalid destructuring assignment target

Flipping only the esrap override:

esrap result
2.3.2 valid
2.3.3 invalid
2.3.6 (latest) invalid

Notes

  • The comments in question are the user's /** @type {any} */ annotations on snippet parameters in the .svelte source; svelte re-emits them through esrap's comments option, and the cast-paren preservation from 2.3.3 wraps the re-anchored target even when it is a binding position.
  • A secondary observation while minimizing: re-anchored comments can also land on unrelated nodes as nested casts, e.g. a {@render} call printed as /** @type {any} */ (/** @type {number} */ (row))($$anchor, …) — valid JS, but the comments have migrated from the snippet's parameter list to a call site.
  • Happy to test a fix against the original (unminimized) component.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions