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`
pull/18673/head
subotac 4 weeks ago committed by GitHub
parent a5ea12ee93
commit 384ee73aa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve short-circuiting for logical assignments to private state fields

@ -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
);
}
}
}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<p>truthy||value</p>`
});

@ -0,0 +1,24 @@
<script>
class Values {
#or = $state.raw('truthy');
#and = $state.raw('');
#nullish = $state.raw('value');
get or() {
return (this.#or ||= 'assigned');
}
get and() {
return (this.#and &&= 'assigned');
}
get nullish() {
return (this.#nullish ??= 'assigned');
}
}
const values = new Values();
const result = $derived([values.or, values.and, values.nullish]);
</script>
<p>{result.join('|')}</p>
Loading…
Cancel
Save