Skip to content

Commit a8dc96e

Browse files
fix: detect mutations within assignments expressions (alternative approach) (#12429)
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 --------- Co-authored-by: Simon Holthausen <[email protected]>
1 parent efe3b39 commit a8dc96e

File tree

6 files changed

+71
-34
lines changed

6 files changed

+71
-34
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: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -772,8 +772,7 @@ const legacy_scope_tweaker = {
772772
}
773773

774774
if (
775-
binding !== null &&
776-
binding.kind === 'normal' &&
775+
binding?.kind === 'normal' &&
777776
((binding.scope === state.instance_scope && binding.declaration_kind !== 'function') ||
778777
binding.declaration_kind === 'import')
779778
) {
@@ -798,22 +797,22 @@ const legacy_scope_tweaker = {
798797
parent.left === binding.node
799798
) {
800799
binding.kind = 'derived';
801-
} else {
802-
let idx = -1;
803-
let ancestor = path.at(idx);
804-
while (ancestor) {
805-
if (ancestor.type === 'EachBlock') {
806-
// Ensures that the array is reactive when only its entries are mutated
807-
// TODO: this doesn't seem correct. We should be checking at the points where
808-
// the identifier (the each expression) is used in a way that makes it reactive.
809-
// This just forces the collection identifier to always be reactive even if it's
810-
// not.
811-
if (ancestor.expression === (idx === -1 ? node : path.at(idx + 1))) {
800+
}
801+
} else if (binding?.kind === 'each' && binding.mutated) {
802+
// Ensure that the array is marked as reactive even when only its entries are mutated
803+
let i = path.length;
804+
while (i--) {
805+
const ancestor = path[i];
806+
if (
807+
ancestor.type === 'EachBlock' &&
808+
state.analysis.template.scopes.get(ancestor)?.declarations.get(node.name) === binding
809+
) {
810+
for (const binding of ancestor.metadata.references) {
811+
if (binding.kind === 'normal') {
812812
binding.kind = 'state';
813-
break;
814813
}
815814
}
816-
ancestor = path.at(--idx);
815+
break;
817816
}
818817
}
819818
}

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { walk } from 'zimmerframe';
33
import { is_element_node } from './nodes.js';
44
import * as b from '../utils/builders.js';
55
import * as e from '../errors.js';
6-
import { extract_identifiers, extract_identifiers_from_destructuring } from '../utils/ast.js';
6+
import {
7+
extract_identifiers,
8+
extract_identifiers_from_destructuring,
9+
object,
10+
unwrap_pattern
11+
} from '../utils/ast.js';
712
import { JsKeywords, Runes } from './constants.js';
813

914
export class Scope {
@@ -713,11 +718,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
713718
const binding = scope.get(/** @type {import('estree').Identifier} */ (object).name);
714719
if (binding) binding.mutated = true;
715720
} else {
716-
extract_identifiers(node).forEach((identifier) => {
717-
const binding = scope.get(identifier.name);
718-
if (binding && identifier !== binding.node) {
721+
unwrap_pattern(node).forEach((node) => {
722+
let id = node.type === 'Identifier' ? node : object(node);
723+
if (id === null) return;
724+
725+
const binding = scope.get(id.name);
726+
if (binding && id !== binding.node) {
719727
binding.mutated = true;
720-
binding.reassigned = true;
728+
binding.reassigned = node.type === 'Identifier';
721729
}
722730
});
723731
}

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

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,47 +54,62 @@ export function is_event_attribute(attribute) {
5454
}
5555

5656
/**
57-
* Extracts all identifiers from a pattern.
58-
* @param {ESTree.Pattern} param
59-
* @param {ESTree.Identifier[]} [nodes]
60-
* @returns {ESTree.Identifier[]}
57+
* Extracts all identifiers and member expressions from a pattern.
58+
* @param {ESTree.Pattern} pattern
59+
* @param {Array<ESTree.Identifier | ESTree.MemberExpression>} [nodes]
60+
* @returns {Array<ESTree.Identifier | ESTree.MemberExpression>}
6161
*/
62-
export function extract_identifiers(param, nodes = []) {
63-
switch (param.type) {
62+
export function unwrap_pattern(pattern, nodes = []) {
63+
switch (pattern.type) {
6464
case 'Identifier':
65-
nodes.push(param);
65+
nodes.push(pattern);
66+
break;
67+
68+
case 'MemberExpression':
69+
// member expressions can be part of an assignment pattern, but not a binding pattern
70+
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#binding_and_assignment
71+
nodes.push(pattern);
6672
break;
6773

6874
case 'ObjectPattern':
69-
for (const prop of param.properties) {
75+
for (const prop of pattern.properties) {
7076
if (prop.type === 'RestElement') {
71-
extract_identifiers(prop.argument, nodes);
77+
unwrap_pattern(prop.argument, nodes);
7278
} else {
73-
extract_identifiers(prop.value, nodes);
79+
unwrap_pattern(prop.value, nodes);
7480
}
7581
}
7682

7783
break;
7884

7985
case 'ArrayPattern':
80-
for (const element of param.elements) {
81-
if (element) extract_identifiers(element, nodes);
86+
for (const element of pattern.elements) {
87+
if (element) unwrap_pattern(element, nodes);
8288
}
8389

8490
break;
8591

8692
case 'RestElement':
87-
extract_identifiers(param.argument, nodes);
93+
unwrap_pattern(pattern.argument, nodes);
8894
break;
8995

9096
case 'AssignmentPattern':
91-
extract_identifiers(param.left, nodes);
97+
unwrap_pattern(pattern.left, nodes);
9298
break;
9399
}
94100

95101
return nodes;
96102
}
97103

104+
/**
105+
* Extracts all identifiers from a pattern.
106+
* @param {ESTree.Pattern} pattern
107+
* @returns {ESTree.Identifier[]}
108+
*/
109+
export function extract_identifiers(pattern) {
110+
return unwrap_pattern(pattern, []).filter((node) => node.type === 'Identifier');
111+
}
112+
98113
/**
99114
* Extracts all identifiers and a stringified keypath from an expression.
100115
* @param {ESTree.Expression} expr
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)