diff --git a/.changeset/declaration-tag-division.md b/.changeset/declaration-tag-division.md new file mode 100644 index 0000000000..de265f0ab1 --- /dev/null +++ b/.changeset/declaration-tag-division.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: parse declaration tag contents more robustly diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index d338f9ffb8..fb60c228c5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -5,7 +5,6 @@ import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; import { tsPlugin } from '@sveltejs/acorn-typescript'; import * as e from '../../errors.js'; -import { find_matching_bracket } from './utils/bracket.js'; const JSParser = acorn.Parser; const TSParser = JSParser.extend(tsPlugin()); @@ -106,38 +105,26 @@ export function parse_expression_at(parser, source, index) { * @returns {Statement} */ export function parse_statement_at(parser, source, index) { - const acorn = parser.ts ? TSParser : JSParser; - let end = find_matching_bracket(source, index, '{'); - if (end === undefined) e.unexpected_eof(source.length); - - while (source[end - 1] === ';') { - end -= 1; - } - - const padded_source = `${' '.repeat(index)}${source.slice(index, end)}`; - const { onComment, add_comments } = get_comment_handlers( - padded_source, - parser.root.comments, - index - ); + // cast to `any`: acorn's Parser constructor and parseStatement/nextToken aren't in its public types + const acorn = /** @type {any} */ (parser.ts ? TSParser : JSParser); + const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index); try { - const ast = acorn.parse(padded_source, { - onComment, - sourceType: 'module', - ecmaVersion: 16, - locations: true - }); - - add_comments(ast); - - const statement = /** @type {Statement} */ ( - /** @type {unknown} */ (/** @type {Program} */ (ast).body[0]) + // This is like parseExpressionAt but for statements + const p = new acorn( + { onComment, sourceType: 'module', ecmaVersion: 16, locations: true }, + source, + index ); - statement.end = Math.min(/** @type {number} */ (statement.end), end); + p.nextToken(); + const statement = /** @type {Statement} */ (p.parseStatement(null, true, Object.create(null))); + add_comments(/** @type {acorn.Node} */ (statement)); return statement; - } catch (e) { - handle_parse_error(e); + } catch (err) { + // A statement that runs to the end of the source (e.g. an unterminated declaration tag) + // is an EOF, not a stray token; preserve the friendlier `unexpected_eof` diagnostic. + if (/** @type {any} */ (err).pos === source.length) e.unexpected_eof(source.length); + handle_parse_error(err); } } diff --git a/packages/svelte/tests/print/samples/declaration-tag-division/input.svelte b/packages/svelte/tests/print/samples/declaration-tag-division/input.svelte new file mode 100644 index 0000000000..0e20727748 --- /dev/null +++ b/packages/svelte/tests/print/samples/declaration-tag-division/input.svelte @@ -0,0 +1,20 @@ + + +{#if visible} + {const half = total / 2} + {let derived = $derived(total / 4)} + {const member = width / height} + {const call = Math.max(total, 1) / 2} + {const string_then_division = 'ab' / divisor} + {const typed: number = total / 2} + {const { fallback = total / 2 } = options} + {const regex = /[}]/} +

{half} {derived} {member} {call} {string_then_division} {typed} {fallback} {regex}

+{/if} diff --git a/packages/svelte/tests/print/samples/declaration-tag-division/output.svelte b/packages/svelte/tests/print/samples/declaration-tag-division/output.svelte new file mode 100644 index 0000000000..0e20727748 --- /dev/null +++ b/packages/svelte/tests/print/samples/declaration-tag-division/output.svelte @@ -0,0 +1,20 @@ + + +{#if visible} + {const half = total / 2} + {let derived = $derived(total / 4)} + {const member = width / height} + {const call = Math.max(total, 1) / 2} + {const string_then_division = 'ab' / divisor} + {const typed: number = total / 2} + {const { fallback = total / 2 } = options} + {const regex = /[}]/} +

{half} {derived} {member} {call} {string_then_division} {typed} {fallback} {regex}

+{/if}