Skip to content

Commit 3c19e50

Browse files
committed
fix: detect mutations within assignments expressions
This enhances our "variable was mutated" detection to also recognize that `foo` in `[foo[1]] = [..]` was mutated. This allows us to be more granular about detecting mutations to each expressions in legacy mode, which fixes #12169
1 parent 36a6a6b commit 3c19e50

File tree

6 files changed

+49
-24
lines changed

6 files changed

+49
-24
lines changed

.changeset/thirty-dogs-whisper.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: detect mutations within assignment expressions

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -769,8 +769,7 @@ const legacy_scope_tweaker = {
769769
}
770770

771771
if (
772-
binding !== null &&
773-
binding.kind === 'normal' &&
772+
binding?.kind === 'normal' &&
774773
((binding.scope === state.instance_scope && binding.declaration_kind !== 'function') ||
775774
binding.declaration_kind === 'import')
776775
) {
@@ -795,23 +794,24 @@ const legacy_scope_tweaker = {
795794
parent.left === binding.node
796795
) {
797796
binding.kind = 'derived';
798-
} else {
799-
let idx = -1;
800-
let ancestor = path.at(idx);
801-
while (ancestor) {
802-
if (ancestor.type === 'EachBlock') {
803-
// Ensures that the array is reactive when only its entries are mutated
804-
// TODO: this doesn't seem correct. We should be checking at the points where
805-
// the identifier (the each expression) is used in a way that makes it reactive.
806-
// This just forces the collection identifier to always be reactive even if it's
807-
// not.
808-
if (ancestor.expression === (idx === -1 ? node : path.at(idx + 1))) {
809-
binding.kind = 'state';
810-
break;
797+
}
798+
} else if (binding?.kind === 'each' && binding.mutated) {
799+
// Ensure that the array is marked as reactive even when only its entries are mutated
800+
let idx = -1;
801+
let ancestor = path.at(idx);
802+
while (ancestor) {
803+
if (
804+
ancestor.type === 'EachBlock' &&
805+
state.analysis.template.scopes.get(ancestor)?.declarations.get(node.name) === binding
806+
) {
807+
for (const reference of ancestor.metadata.references) {
808+
if (reference.kind === 'normal') {
809+
reference.kind = 'state';
811810
}
812811
}
813-
ancestor = path.at(--idx);
812+
break;
814813
}
814+
ancestor = path.at(--idx);
815815
}
816816
}
817817
}

packages/svelte/src/compiler/phases/scope.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
713713
const binding = scope.get(/** @type {import('estree').Identifier} */ (object).name);
714714
if (binding) binding.mutated = true;
715715
} else {
716-
extract_identifiers(node).forEach((identifier) => {
716+
extract_identifiers(node, [], true).forEach((identifier) => {
717717
const binding = scope.get(identifier.name);
718718
if (binding && identifier !== binding.node) {
719719
binding.mutated = true;

packages/svelte/src/compiler/utils/ast.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,41 +54,51 @@ export function is_event_attribute(attribute) {
5454
}
5555

5656
/**
57-
* Extracts all identifiers from a pattern.
57+
* Extracts all identifiers from a pattern. By default only those which can be newly declared as part of an assignment expression are included.
5858
* @param {ESTree.Pattern} param
5959
* @param {ESTree.Identifier[]} [nodes]
60+
* @param {boolean} [include_member_expressions] If `true`, will also include member expressions which can be important in a case like `[a, b] = [c, d]`
6061
* @returns {ESTree.Identifier[]}
6162
*/
62-
export function extract_identifiers(param, nodes = []) {
63+
export function extract_identifiers(param, nodes = [], include_member_expressions = false) {
6364
switch (param.type) {
6465
case 'Identifier':
6566
nodes.push(param);
6667
break;
6768

69+
case 'MemberExpression':
70+
if (
71+
include_member_expressions &&
72+
(param.object.type === 'Identifier' || param.object.type === 'MemberExpression')
73+
) {
74+
extract_identifiers(param.object, nodes, include_member_expressions);
75+
}
76+
break;
77+
6878
case 'ObjectPattern':
6979
for (const prop of param.properties) {
7080
if (prop.type === 'RestElement') {
71-
extract_identifiers(prop.argument, nodes);
81+
extract_identifiers(prop.argument, nodes, include_member_expressions);
7282
} else {
73-
extract_identifiers(prop.value, nodes);
83+
extract_identifiers(prop.value, nodes, include_member_expressions);
7484
}
7585
}
7686

7787
break;
7888

7989
case 'ArrayPattern':
8090
for (const element of param.elements) {
81-
if (element) extract_identifiers(element, nodes);
91+
if (element) extract_identifiers(element, nodes, include_member_expressions);
8292
}
8393

8494
break;
8595

8696
case 'RestElement':
87-
extract_identifiers(param.argument, nodes);
97+
extract_identifiers(param.argument, nodes, include_member_expressions);
8898
break;
8999

90100
case 'AssignmentPattern':
91-
extract_identifiers(param.left, nodes);
101+
extract_identifiers(param.left, nodes, include_member_expressions);
92102
break;
93103
}
94104

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
const { foo } = x();
3+
</script>
4+
5+
{#each foo as f}{/each}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
const { foo } = x();
3+
</script>
4+
5+
{#each foo as f}{/each}

0 commit comments

Comments
 (0)