From 5b8db1be35555fee3b938f9d7eab93f2aee48b7e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:01:39 +0200 Subject: [PATCH] fix: error at compile time on duplicate snippet/declaration tag definitions (#18351) --- .changeset/clever-poems-shave.md | 5 ++++ .../2-analyze/visitors/DeclarationTag.js | 11 ++++++++ .../phases/2-analyze/visitors/SnippetBlock.js | 25 +++++++++++-------- .../errors.json | 14 +++++++++++ .../input.svelte | 5 ++++ .../snippet-already-declared/errors.json | 14 +++++++++++ .../snippet-already-declared/input.svelte | 5 ++++ 7 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 .changeset/clever-poems-shave.md create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-already-declared/errors.json create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-already-declared/input.svelte create mode 100644 packages/svelte/tests/validator/samples/snippet-already-declared/errors.json create mode 100644 packages/svelte/tests/validator/samples/snippet-already-declared/input.svelte diff --git a/.changeset/clever-poems-shave.md b/.changeset/clever-poems-shave.md new file mode 100644 index 0000000000..252c86b905 --- /dev/null +++ b/.changeset/clever-poems-shave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: error at compile time on duplicate snippet/declaration tag definitions diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js index b5e8c931f1..52e9480838 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js @@ -2,6 +2,7 @@ /** @import { Context } from '../types' */ import * as b from '#compiler/builders'; import * as e from '../../../errors.js'; +import { extract_identifiers } from '../../../utils/ast.js'; /** * @param {AST.DeclarationTag} node @@ -12,6 +13,16 @@ export function DeclarationTag(node, context) { e.declaration_tag_no_legacy_mode(node); } + const is_top_level = context.path.length === 1 && context.path[0].type === 'Fragment'; + if (is_top_level) { + const duplicate = node.declaration.declarations + .flatMap((declaration) => extract_identifiers(declaration.id)) + .find((id) => context.state.analysis.instance.scope.declarations.has(id.name)); + if (duplicate) { + e.declaration_duplicate(duplicate, duplicate.name); + } + } + context.visit(node.declaration, { ...context.state, in_declaration_tag: true, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js index c0350f8e6f..7618716ae5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js @@ -25,19 +25,24 @@ export function SnippetBlock(node, context) { context.next({ ...context.state, parent_element: null }); - const can_hoist = - context.path.length === 1 && - context.path[0].type === 'Fragment' && - can_hoist_snippet(context.state.scope, context.state.scopes); + const is_top_level = context.path.length === 1 && context.path[0].type === 'Fragment'; - const name = node.expression.name; + if (is_top_level) { + const name = node.expression.name; - if (can_hoist) { - const binding = /** @type {Binding} */ (context.state.scope.get(name)); - context.state.analysis.module.scope.declarations.set(name, binding); - } + if (context.state.analysis.instance.scope.declarations.has(name)) { + e.declaration_duplicate(node.expression, name); + } + + node.metadata.can_hoist = + is_top_level && can_hoist_snippet(context.state.scope, context.state.scopes); - node.metadata.can_hoist = can_hoist; + if (node.metadata.can_hoist) { + const name = node.expression.name; + const binding = /** @type {Binding} */ (context.state.scope.get(name)); + context.state.analysis.module.scope.declarations.set(name, binding); + } + } const { path } = context; const parent = path.at(-2); diff --git a/packages/svelte/tests/validator/samples/declaration-tag-already-declared/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-already-declared/errors.json new file mode 100644 index 0000000000..723b060d19 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-already-declared/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_duplicate", + "message": "`foo` has already been declared", + "start": { + "line": 5, + "column": 5 + }, + "end": { + "line": 5, + "column": 8 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-already-declared/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-already-declared/input.svelte new file mode 100644 index 0000000000..15372811fb --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-already-declared/input.svelte @@ -0,0 +1,5 @@ + + +{let foo = 'baz'} diff --git a/packages/svelte/tests/validator/samples/snippet-already-declared/errors.json b/packages/svelte/tests/validator/samples/snippet-already-declared/errors.json new file mode 100644 index 0000000000..bac71c2c81 --- /dev/null +++ b/packages/svelte/tests/validator/samples/snippet-already-declared/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_duplicate", + "message": "`foo` has already been declared", + "start": { + "line": 5, + "column": 10 + }, + "end": { + "line": 5, + "column": 13 + } + } +] diff --git a/packages/svelte/tests/validator/samples/snippet-already-declared/input.svelte b/packages/svelte/tests/validator/samples/snippet-already-declared/input.svelte new file mode 100644 index 0000000000..5745c2f8f9 --- /dev/null +++ b/packages/svelte/tests/validator/samples/snippet-already-declared/input.svelte @@ -0,0 +1,5 @@ + + +{#snippet foo()}baz{/snippet}