diff --git a/.changeset/long-humans-repair.md b/.changeset/long-humans-repair.md
new file mode 100644
index 0000000000..e295d41ba6
--- /dev/null
+++ b/.changeset/long-humans-repair.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+breaking: prevent unparenthesized sequence expressions in attributes
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index ade5e580ae..318225be95 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -282,7 +282,9 @@ const attributes = {
},
'invalid-let-directive-placement': () => 'let directive at invalid position',
'invalid-style-directive-modifier': () =>
- `Invalid 'style:' modifier. Valid modifiers are: 'important'`
+ `Invalid 'style:' modifier. Valid modifiers are: 'important'`,
+ 'invalid-sequence-expression': () =>
+ `Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses`
};
/** @satisfies {Errors} */
diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js
index 509bc8714d..547901ea7b 100644
--- a/packages/svelte/src/compiler/index.js
+++ b/packages/svelte/src/compiler/index.js
@@ -41,7 +41,7 @@ export function compile(source, options) {
};
}
- const analysis = analyze_component(parsed, combined_options);
+ const analysis = analyze_component(parsed, source, combined_options);
const result = transform_component(analysis, source, combined_options);
return result;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index d185a9c21a..5773a562ee 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -258,10 +258,11 @@ export function analyze_module(ast, options) {
/**
* @param {import('#compiler').Root} root
+ * @param {string} source
* @param {import('#compiler').ValidatedCompileOptions} options
* @returns {import('../types.js').ComponentAnalysis}
*/
-export function analyze_component(root, options) {
+export function analyze_component(root, source, options) {
const scope_root = new ScopeRoot();
const module = js(root.module, scope_root, false, null);
@@ -396,7 +397,8 @@ export function analyze_component(root, options) {
})
: '',
keyframes: []
- }
+ },
+ source
};
if (!options.customElement && root.options?.customElement) {
diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js
index 74ed1b1005..69d879d021 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/validation.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js
@@ -50,6 +50,18 @@ function validate_component(node, context) {
}
if (attribute.type === 'Attribute') {
+ if (context.state.analysis.runes && is_expression_attribute(attribute)) {
+ const expression = attribute.value[0].expression;
+ if (expression.type === 'SequenceExpression') {
+ let i = /** @type {number} */ (expression.start);
+ while (--i > 0) {
+ const char = context.state.analysis.source[i];
+ if (char === '(') break; // parenthesized sequence expressions are ok
+ if (char === '{') error(expression, 'invalid-sequence-expression');
+ }
+ }
+ }
+
validate_attribute_name(attribute, context);
if (attribute.name === 'slot') {
@@ -81,12 +93,21 @@ function validate_element(node, context) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
+ const is_expression = is_expression_attribute(attribute);
+
+ if (context.state.analysis.runes && is_expression) {
+ const expression = attribute.value[0].expression;
+ if (expression.type === 'SequenceExpression') {
+ error(expression, 'invalid-sequence-expression');
+ }
+ }
+
if (regex_illegal_attribute_character.test(attribute.name)) {
error(attribute, 'invalid-attribute-name', attribute.name);
}
if (attribute.name.startsWith('on') && attribute.name.length > 2) {
- if (!is_expression_attribute(attribute)) {
+ if (!is_expression) {
error(attribute, 'invalid-event-attribute-value');
}
diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts
index 2c663b1704..390ea49a46 100644
--- a/packages/svelte/src/compiler/phases/types.d.ts
+++ b/packages/svelte/src/compiler/phases/types.d.ts
@@ -73,6 +73,7 @@ export interface ComponentAnalysis extends Analysis {
hash: string;
keyframes: string[];
};
+ source: string;
}
declare module 'estree' {
diff --git a/packages/svelte/tests/compiler-errors/samples/attribute-sequence-expression/_config.js b/packages/svelte/tests/compiler-errors/samples/attribute-sequence-expression/_config.js
new file mode 100644
index 0000000000..79ddec267b
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/attribute-sequence-expression/_config.js
@@ -0,0 +1,10 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'invalid-sequence-expression',
+ message:
+ 'Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses',
+ position: [163, 170]
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/attribute-sequence-expression/main.svelte b/packages/svelte/tests/compiler-errors/samples/attribute-sequence-expression/main.svelte
new file mode 100644
index 0000000000..1c65426078
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/attribute-sequence-expression/main.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+