From 40e296656e57c816deaecebfda506d644af62f22 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 1 Apr 2026 09:28:04 -0400 Subject: [PATCH] fix: generalize lang="ts" hint to all template expression contexts Instead of only hinting on snippet params, detect TypeScript syntax across all expression sites: acorn_error catches colon annotations that make acorn fail, read_expression catches as/satisfies/!/: that acorn silently skips over. --- .../src/compiler/phases/1-parse/index.js | 12 +++++-- .../phases/1-parse/read/expression.js | 35 +++++++++++++++---- .../src/compiler/phases/1-parse/state/tag.js | 6 +--- .../ts-annotation-no-lang-as/_config.js | 9 +++++ .../ts-annotation-no-lang-as/main.svelte | 1 + .../_config.js | 9 +++++ .../main.svelte | 1 + .../ts-annotation-no-lang-colon/_config.js | 9 +++++ .../ts-annotation-no-lang-colon/main.svelte | 1 + .../ts-annotation-no-lang-non-null/_config.js | 9 +++++ .../main.svelte | 1 + .../_config.js | 9 +++++ .../main.svelte | 1 + 13 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-as/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-as/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-attribute/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-attribute/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-colon/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-colon/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-non-null/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-non-null/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-satisfies/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-satisfies/main.svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 5c2426fac6..c37ffc3c8b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -177,11 +177,17 @@ export class Parser { /** * @param {any} err - * @param {string} [suffix] * @returns {never} */ - acorn_error(err, suffix = '') { - e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, '') + suffix); + acorn_error(err) { + const message = err.message.replace(regex_position_indicator, ''); + + const hint = + !this.ts && typeof err.pos === 'number' && this.template[err.pos] === ':' + ? ` (did you forget to add \`lang="ts"\`?)` + : ''; + + e.js_parse_error(err.pos, message + hint); } /** diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js index 5d21f85792..afc74f8557 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js @@ -33,14 +33,19 @@ export function get_loose_identifier(parser, opening_token) { * @returns {Expression} */ export default function read_expression(parser, opening_token, disallow_loose) { + /** @type {Expression | undefined} */ + let node; + try { let comment_index = parser.root.comments.length; - const node = parse_expression_at( - parser.template, - parser.root.comments, - parser.ts, - parser.index + node = /** @type {Expression} */ ( + parse_expression_at( + parser.template, + parser.root.comments, + parser.ts, + parser.index + ) ); let num_parens = 0; @@ -76,8 +81,6 @@ export default function read_expression(parser, opening_token, disallow_loose) { } parser.index = index; - - return /** @type {Expression} */ (node); } catch (err) { // If we are in an each loop we need the error to be thrown in cases like // `as { y = z }` so we still throw and handle the error there @@ -90,4 +93,22 @@ export default function read_expression(parser, opening_token, disallow_loose) { parser.acorn_error(err); } + + if (!parser.ts) { + let j = parser.index; + while (j < parser.template.length && regex_whitespace.test(parser.template[j])) j++; + + const remaining = parser.template.slice(j); + + if ( + remaining[0] === ':' || + /^satisfies[\s(]/.test(remaining) || + (remaining[0] === '!' && remaining[1] !== '=') || + (/^as[\s(]/.test(remaining) && !disallow_loose) + ) { + e.js_parse_error(j, `Unexpected token (did you forget to add \`lang="ts"\`?)`); + } + } + + return node; } diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 255e190619..396512a228 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -403,11 +403,7 @@ function open(parser) { ) ); } catch (/** @type {any} */ err) { - const suffix = - !parser.ts && parser.template[err.pos] === ':' - ? ` (did you forget to add \`lang="ts"\`?)` - : ''; - parser.acorn_error(err, suffix); + parser.acorn_error(err); } } else { function_expression = { params: [] }; diff --git a/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-as/_config.js b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-as/_config.js new file mode 100644 index 0000000000..a6eca38f13 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-as/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'js_parse_error', + message: 'Unexpected token (did you forget to add `lang="ts"`?)', + position: [3, 3] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-as/main.svelte b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-as/main.svelte new file mode 100644 index 0000000000..b04bd39332 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-as/main.svelte @@ -0,0 +1 @@ +{x as number} \ No newline at end of file diff --git a/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-attribute/_config.js b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-attribute/_config.js new file mode 100644 index 0000000000..0ae7494b60 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-attribute/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'js_parse_error', + message: 'Unexpected token (did you forget to add `lang="ts"`?)', + position: [14, 14] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-attribute/main.svelte b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-attribute/main.svelte new file mode 100644 index 0000000000..86351fb664 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-attribute/main.svelte @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-colon/_config.js b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-colon/_config.js new file mode 100644 index 0000000000..fe9f0c2e62 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-colon/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'js_parse_error', + message: 'Unexpected token (did you forget to add `lang="ts"`?)', + position: [2, 2] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-colon/main.svelte b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-colon/main.svelte new file mode 100644 index 0000000000..70c0f4c13b --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-colon/main.svelte @@ -0,0 +1 @@ +{x: number} \ No newline at end of file diff --git a/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-non-null/_config.js b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-non-null/_config.js new file mode 100644 index 0000000000..fe9f0c2e62 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-non-null/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'js_parse_error', + message: 'Unexpected token (did you forget to add `lang="ts"`?)', + position: [2, 2] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-non-null/main.svelte b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-non-null/main.svelte new file mode 100644 index 0000000000..6c2f391078 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-non-null/main.svelte @@ -0,0 +1 @@ +{x!} \ No newline at end of file diff --git a/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-satisfies/_config.js b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-satisfies/_config.js new file mode 100644 index 0000000000..a6eca38f13 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-satisfies/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'js_parse_error', + message: 'Unexpected token (did you forget to add `lang="ts"`?)', + position: [3, 3] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-satisfies/main.svelte b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-satisfies/main.svelte new file mode 100644 index 0000000000..4417664603 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/ts-annotation-no-lang-satisfies/main.svelte @@ -0,0 +1 @@ +{x satisfies Foo} \ No newline at end of file