diff --git a/.changeset/wicked-readers-knock.md b/.changeset/wicked-readers-knock.md new file mode 100644 index 0000000000..4ec417402f --- /dev/null +++ b/.changeset/wicked-readers-knock.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: treat property accesses of literals as pure diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index 82356ea619..2dec4361c8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, Expression, Identifier, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ +/** @import { AssignmentExpression, Expression, Literal, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { AnalysisState, Context } from '../../types' */ /** @import { Scope } from '../../../scope' */ @@ -176,21 +176,42 @@ export function is_safe_identifier(expression, scope) { } /** - * @param {Expression | Super} node + * @param {Expression | Literal | Super} node * @param {Context} context * @returns {boolean} */ export function is_pure(node, context) { + if (node.type === 'Literal') { + return true; + } + if (node.type === 'CallExpression') { + if (!is_pure(node.callee, context)) { + return false; + } + for (let arg of node.arguments) { + if (!is_pure(arg.type === 'SpreadElement' ? arg.argument : arg, context)) { + return false; + } + } + return true; + } if (node.type !== 'Identifier' && node.type !== 'MemberExpression') { return false; } - const left = object(node); + /** @type {Expression | Super | null} */ + let left = node; + while (left.type === 'MemberExpression') { + left = left.object; + } + if (!left) return false; if (left.type === 'Identifier') { const binding = context.state.scope.get(left.name); if (binding === null) return true; // globals are assumed to be safe + } else if (is_pure(left, context)) { + return true; } // TODO add more cases (safe Svelte imports, etc) diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-expressions-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/inline-expressions-2/_config.js new file mode 100644 index 0000000000..1869540b2c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/inline-expressions-2/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + html: ` +

Without text expression: 7.36

+

With text expression: 7.36

+

With text expression and function call: 7.36

+

With text expression and property access: 4

+

Hello name!

+

4

` +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-expressions-2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/inline-expressions-2/main.svelte new file mode 100644 index 0000000000..78a325dcc4 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/inline-expressions-2/main.svelte @@ -0,0 +1,6 @@ +

Without text expression: 7.36

+

With text expression: {7.36}

+

With text expression and function call: {(7.36).toLocaleString()}

+

With text expression and property access: {"test".length}

+

Hello {('name').toUpperCase().toLowerCase()}!

+

{"test".length}