diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 6e036febf3..a7d4a9180a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -189,30 +189,12 @@ function get_delegated_event(event_name, handler, context) { return non_hoistable; } - const is_runes = context.state.analysis.runes; - - // Validate that we don't have any each bindings that are mutated. - if (is_runes && binding !== null && binding.kind === 'each') { - for (const { path } of binding.references) { - const last = path.at(-1); - - // If the last reference node is an update or assigment expression to the binding - // then we know the binding was mutated directly rather than part of another node path. - if ( - last != null && - (last.type === 'AssignmentExpression' || last.type === 'UpdateExpression') - ) { - error(last, 'invalid-each-mutation'); - } - } - } - if ( binding !== null && // Bail-out if the the binding is a rest param (binding.declaration_kind === 'rest_param' || // Bail-out if we reference anything from the EachBlock (for now) that mutates in non-runes mode, - (((!is_runes && binding.kind === 'each') || + (((!context.state.analysis.runes && binding.kind === 'each') || // or any normal not reactive bindings that are mutated. binding.kind === 'normal' || // or any reactive imports (those are rewritten) (can only happen in legacy mode) diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 6a1aaf0fec..cbd8a354bb 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -334,6 +334,9 @@ function is_tag_valid_with_parent(tag, parent_tag) { * @type {import('zimmerframe').Visitors} */ const validation = { + AssignmentExpression(node, context) { + validate_assignment(node, node.left, context.state); + }, BindDirective(node, context) { validate_no_const_assignment(node, node.expression, context.state.scope, true); @@ -655,6 +658,9 @@ const validation = { error(child, 'invalid-title-content'); } }, + UpdateExpression(node, context) { + validate_assignment(node, node.argument, context.state); + }, ExpressionTag(node, context) { if (!node.parent) return; if (context.state.parent_element) { @@ -904,24 +910,28 @@ function validate_no_const_assignment(node, argument, scope, is_binding) { function validate_assignment(node, argument, state) { validate_no_const_assignment(node, argument, state.scope, false); - let left = /** @type {import('estree').Expression | import('estree').Super} */ (argument); - - if (left.type === 'Identifier') { - const binding = state.scope.get(left.name); + if (state.analysis.runes && argument.type === 'Identifier') { + const binding = state.scope.get(argument.name); if (binding?.kind === 'derived') { error(node, 'invalid-derived-assignment'); } + + if (binding?.kind === 'each') { + error(node, 'invalid-each-mutation'); + } } + let object = /** @type {import('estree').Expression | import('estree').Super} */ (argument); + /** @type {import('estree').Expression | import('estree').PrivateIdentifier | null} */ let property = null; - while (left.type === 'MemberExpression') { - property = left.property; - left = left.object; + while (object.type === 'MemberExpression') { + property = object.property; + object = object.object; } - if (left.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') { + if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') { if (state.private_derived_state.includes(property.name)) { error(node, 'invalid-derived-assignment'); } @@ -929,14 +939,6 @@ function validate_assignment(node, argument, state) { } export const validation_runes = merge(validation, a11y_validators, { - AssignmentExpression(node, { state, path }) { - const parent = path.at(-1); - if (parent && parent.type === 'ConstTag') return; - validate_assignment(node, node.left, state); - }, - UpdateExpression(node, { state }) { - validate_assignment(node, node.argument, state); - }, LabeledStatement(node, { path }) { if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return; error(node, 'invalid-legacy-reactive-statement');