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}