From 8935c47fd0990fbf065d16f784ac9356c9d5bc60 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Oct 2025 10:18:16 -0400 Subject: [PATCH 1/2] fix: error if `each` block has `key` but no `as` clause --- .../docs/98-reference/.generated/compile-errors.md | 8 +++++++- packages/svelte/messages/compile-errors/template.md | 6 +++++- packages/svelte/src/compiler/errors.js | 13 +++++++++++-- .../compiler/phases/2-analyze/visitors/EachBlock.js | 4 ++++ .../_config.js | 2 +- .../_config.js | 2 +- .../samples/each-key-without-as/_config.js | 8 ++++++++ .../samples/each-key-without-as/main.svelte | 7 +++++++ 8 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index b9c44163c906..c5703c636b70 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -199,7 +199,7 @@ Cyclical dependency detected: %cycle% ### const_tag_invalid_reference ``` -The `{@const %name% = ...}` declaration is not available in this snippet +The `{@const %name% = ...}` declaration is not available in this snippet ``` The following is an error: @@ -453,6 +453,12 @@ This turned out to be buggy and unpredictable, particularly when working with de {/each} ``` +### each_key_without_as + +``` +An `{#each ...}` block without an `as` clause cannot have a key +``` + ### effect_invalid_placement ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index dc26a027677c..ac95bfe4a703 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -126,7 +126,7 @@ ## const_tag_invalid_reference -> The `{@const %name% = ...}` declaration is not available in this snippet +> The `{@const %name% = ...}` declaration is not available in this snippet The following is an error: @@ -179,6 +179,10 @@ The same applies to components: > `%type%` name cannot be empty +## each_key_without_as + +> An `{#each ...}` block without an `as` clause cannot have a key + ## element_invalid_closing_tag > `` attempted to close an element that was not open diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 44fc641ee52c..5e3968215f1e 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -986,13 +986,13 @@ export function const_tag_invalid_placement(node) { } /** - * The `{@const %name% = ...}` declaration is not available in this snippet + * The `{@const %name% = ...}` declaration is not available in this snippet * @param {null | number | NodeLike} node * @param {string} name * @returns {never} */ export function const_tag_invalid_reference(node, name) { - e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet \nhttps://svelte.dev/e/const_tag_invalid_reference`); + e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet\nhttps://svelte.dev/e/const_tag_invalid_reference`); } /** @@ -1023,6 +1023,15 @@ export function directive_missing_name(node, type) { e(node, 'directive_missing_name', `\`${type}\` name cannot be empty\nhttps://svelte.dev/e/directive_missing_name`); } +/** + * An `{#each ...}` block without an `as` clause cannot have a key + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function each_key_without_as(node) { + e(node, 'each_key_without_as', `An \`{#each ...}\` block without an \`as\` clause cannot have a key\nhttps://svelte.dev/e/each_key_without_as`); +} + /** * `` attempted to close an element that was not open * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index e6a83921b1f1..d3eb58053ea8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -28,6 +28,10 @@ export function EachBlock(node, context) { node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index; } + if (node.metadata.keyed && !node.context) { + e.each_key_without_as(node); + } + // evaluate expression in parent scope context.visit(node.expression, { ...context.state, diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js index 74242781804d..be9d5a483f3d 100644 --- a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js @@ -4,7 +4,7 @@ export default test({ async: true, error: { code: 'const_tag_invalid_reference', - message: 'The `{@const foo = ...}` declaration is not available in this snippet ', + message: 'The `{@const foo = ...}` declaration is not available in this snippet', position: [376, 379] } }); diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js index 7ff71a61f93f..5132bd93b754 100644 --- a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js @@ -4,7 +4,7 @@ export default test({ async: true, error: { code: 'const_tag_invalid_reference', - message: 'The `{@const foo = ...}` declaration is not available in this snippet ', + message: 'The `{@const foo = ...}` declaration is not available in this snippet', position: [298, 301] } }); diff --git a/packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js new file mode 100644 index 000000000000..923fe0c0ac1a --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'each_key_without_as', + message: 'An `{#each ...}` block without an `as` clause cannot have a key' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte new file mode 100644 index 000000000000..794740de8f37 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte @@ -0,0 +1,7 @@ + + +{#each items, i (items[i].id)} +

{items[i].id}

+{/each} From 51bc88ea9ce8ddb02eae787c8b72d978d0015f2b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Oct 2025 10:18:37 -0400 Subject: [PATCH 2/2] changeset --- .changeset/thirty-rules-dance.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thirty-rules-dance.md diff --git a/.changeset/thirty-rules-dance.md b/.changeset/thirty-rules-dance.md new file mode 100644 index 000000000000..7fcf8d63d0ad --- /dev/null +++ b/.changeset/thirty-rules-dance.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: error if `each` block has `key` but no `as` clause