From 384ee73aa27b41beb214fe5f3e4cc4339b489ddd Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Wed, 19 Aug 2026 05:18:47 +0300 Subject: [PATCH] fix: preserve logical assignments to private state fields (#18594) Fixes #18592. Logical assignments to private `$state` fields were compiled as unconditional setter calls. This preserves JavaScript short-circuiting by evaluating the setter only when `||=`, `&&=`, or `??=` should assign. The regression sample covers the no-assignment branch for all three operators, alongside the existing assignment-path coverage. ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` --- .changeset/tidy-ravens-assign.md | 5 ++++ .../client/visitors/AssignmentExpression.js | 19 ++++++++++++--- .../_config.js | 5 ++++ .../main.svelte | 24 +++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 .changeset/tidy-ravens-assign.md create mode 100644 packages/svelte/tests/runtime-runes/samples/class-private-fields-logical-assignment/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-private-fields-logical-assignment/main.svelte diff --git a/.changeset/tidy-ravens-assign.md b/.changeset/tidy-ravens-assign.md new file mode 100644 index 0000000000..312adde9b5 --- /dev/null +++ b/.changeset/tidy-ravens-assign.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve short-circuiting for logical assignments to private state fields diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index e66e3408e2..7ab148f0a3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */ +/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, LogicalOperator, Pattern } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types.js' */ import * as b from '#compiler/builders'; @@ -79,8 +79,13 @@ function build_assignment(operator, left, right, context) { // special case — assignment to private state field if (left.property.type === 'PrivateIdentifier') { + const logical_operator = ['||=', '&&=', '??='].includes(operator) + ? /** @type {LogicalOperator} */ (operator.slice(0, -1)) + : null; let value = /** @type {Expression} */ ( - context.visit(build_assignment_value(operator, left, right)) + context.visit( + logical_operator === null ? build_assignment_value(operator, left, right) : right + ) ); const needs_proxy = @@ -88,7 +93,15 @@ function build_assignment(operator, left, right, context) { is_non_coercive_operator(operator) && should_proxy(value, context.state.scope); - return b.call('$.set', left, value, needs_proxy && b.true); + const assignment = b.call('$.set', left, value, needs_proxy && b.true); + + return logical_operator === null + ? assignment + : b.logical( + logical_operator, + /** @type {Expression} */ (context.visit(left)), + assignment + ); } } } diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-fields-logical-assignment/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-fields-logical-assignment/_config.js new file mode 100644 index 0000000000..d919c18396 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-logical-assignment/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `
truthy||value
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-fields-logical-assignment/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-private-fields-logical-assignment/main.svelte new file mode 100644 index 0000000000..cc74150b35 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-logical-assignment/main.svelte @@ -0,0 +1,24 @@ + + +{result.join('|')}