From bcb6b72af2b1a4854f148ed64763c7d80f0a258b Mon Sep 17 00:00:00 2001 From: 7nik Date: Wed, 4 Jun 2025 14:32:46 +0300 Subject: [PATCH] bail out pure expressions --- .../client/visitors/shared/utils.js | 52 ++++++++++++++++++- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../samples/each-index-non-null/index.svelte | 2 +- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 36a3707e74..d3b5b09bc4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */ +/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; @@ -361,6 +361,54 @@ export function validate_mutation(node, context, expression) { ); } +/** + * Checks whether the expression contains assignments, function calls, or member accesses + * @param {Expression|Pattern} expression + * @returns {boolean} + */ +function is_pure_expression(expression) { + // It's supposed that values do not have custom @@toPrimitive() or toString(), + // which may be implicitly called in expressions like `a + b`, `a & b`, `+a`, `str: ${a}` + switch (expression.type) { + case "ArrayExpression": return expression.elements.every((element) => element == null || (element.type === "SpreadElement" ? false : is_pure_expression(element))); + case "BinaryExpression": return expression.left.type !== "PrivateIdentifier" && is_pure_expression(expression.left) && is_pure_expression(expression.right); + case "ConditionalExpression": return is_pure_expression(expression.test) && is_pure_expression(expression.consequent) && is_pure_expression(expression.alternate); + case "Identifier": return true; + case "Literal": return true; + case "LogicalExpression": return is_pure_expression(expression.left) && is_pure_expression(expression.right); + case "MetaProperty": return true; // new.target + case "ObjectExpression": return expression.properties.every((property) => + property.type !== "SpreadElement" + && property.key.type !== "PrivateIdentifier" + && is_pure_expression(property.key) + && is_pure_expression(property.value) + ); + case "SequenceExpression": return expression.expressions.every(is_pure_expression); + case "TemplateLiteral": return expression.expressions.every(is_pure_expression); + case "ThisExpression": return true; + case "UnaryExpression": return is_pure_expression(expression.argument); + case "YieldExpression": return expression.argument == null || is_pure_expression(expression.argument); + + case "ArrayPattern": + case "ArrowFunctionExpression": + case "AssignmentExpression": + case "AssignmentPattern": + case "AwaitExpression": + case "CallExpression": + case "ChainExpression": + case "ClassExpression": + case "FunctionExpression": + case "ImportExpression": + case "MemberExpression": + case "NewExpression": + case "ObjectPattern": + case "RestElement": + case "TaggedTemplateExpression": + case "UpdateExpression": + return false; + } +} + /** * Serializes an expression with reactivity like in Svelte 4 * @param {Expression} expression @@ -370,7 +418,7 @@ export function build_legacy_expression(expression, context) { // To recreate Svelte 4 behaviour, we track the dependencies // the compiler can 'see', but we untrack the effect itself const serialized_expression = /** @type {Expression} */ (context.visit(expression)); - if (expression.type === "Identifier") return serialized_expression; + if (is_pure_expression(expression)) return serialized_expression; /** @type {Expression[]} */ const sequence = []; diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js index 804a7c26f1..b6cc2b0d76 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -8,7 +8,7 @@ export default function Each_index_non_null($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => { + $.each(node, 0, () => [,,,,,], $.index, ($$anchor, $$item, i) => { var p = root_1(); p.textContent = `index: ${i}`; diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js index 3431e36833..afa9f32e76 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Each_index_non_null($$payload) { - const each_array = $.ensure_array_like(Array(10)); + const each_array = $.ensure_array_like([,,,,,]); $$payload.out += ``; diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte index 03bfc9e372..3407f9ab59 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte @@ -1,3 +1,3 @@ -{#each Array(10), i} +{#each [,,,,,], i}

index: {i}

{/each}