Skip to content

Commit f6badb1

Browse files
committed
fix: properly analyze group expressions
fixes #9947 for real closes #10379
1 parent 74f5140 commit f6badb1

File tree

6 files changed

+75
-9
lines changed

6 files changed

+75
-9
lines changed

.changeset/forty-suns-smile.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: properly analyze group expressions

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,7 @@ const common_visitors = {
10141014
// entries as keys.
10151015
i = context.path.length;
10161016
const each_blocks = [];
1017-
const expression_ids = extract_all_identifiers_from_expression(node.expression);
1017+
const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression);
10181018
let ids = expression_ids;
10191019
while (i--) {
10201020
const parent = context.path[i];
@@ -1027,7 +1027,7 @@ const common_visitors = {
10271027
}
10281028
each_blocks.push(parent);
10291029
ids = ids.filter((id) => !references.includes(id));
1030-
ids.push(...extract_all_identifiers_from_expression(parent.expression));
1030+
ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]);
10311031
}
10321032
}
10331033
}
@@ -1038,8 +1038,8 @@ const common_visitors = {
10381038
// but this is a limitation of the current static analysis we do; it also never worked in Svelte 4)
10391039
const bindings = expression_ids.map((id) => context.state.scope.get(id.name));
10401040
let group_name;
1041-
outer: for (const [b, group] of context.state.analysis.binding_groups) {
1042-
if (b.length !== bindings.length) continue;
1041+
outer: for (const [[key, b], group] of context.state.analysis.binding_groups) {
1042+
if (b.length !== bindings.length || key !== keypath) continue;
10431043
for (let i = 0; i < bindings.length; i++) {
10441044
if (bindings[i] !== b[i]) continue outer;
10451045
}
@@ -1048,7 +1048,7 @@ const common_visitors = {
10481048

10491049
if (!group_name) {
10501050
group_name = context.state.scope.root.unique('binding_group');
1051-
context.state.analysis.binding_groups.set(bindings, group_name);
1051+
context.state.analysis.binding_groups.set([keypath, bindings], group_name);
10521052
}
10531053

10541054
node.metadata = {

packages/svelte/src/compiler/phases/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export interface ComponentAnalysis extends Analysis {
6868
inject_styles: boolean;
6969
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
7070
/** Identifiers that make up the `bind:group` expression -> internal group binding name */
71-
binding_groups: Map<Array<Binding | null>, Identifier>;
71+
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
7272
slot_names: Set<string>;
7373
}
7474

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,15 @@ export function extract_identifiers(param, nodes = []) {
9696
}
9797

9898
/**
99-
* Extracts all identifiers from an expression.
99+
* Extracts all identifiers and a stringified keypath from an expression.
100100
* @param {import('estree').Expression} expr
101-
* @returns {import('estree').Identifier[]}
101+
* @returns {[keypath: string, ids: import('estree').Identifier[]]}
102102
*/
103103
export function extract_all_identifiers_from_expression(expr) {
104104
/** @type {import('estree').Identifier[]} */
105105
let nodes = [];
106+
/** @type {string[]} */
107+
let keypath = [];
106108

107109
walk(
108110
expr,
@@ -113,11 +115,30 @@ export function extract_all_identifiers_from_expression(expr) {
113115
if (parent?.type !== 'MemberExpression' || parent.property !== node || parent.computed) {
114116
nodes.push(node);
115117
}
118+
119+
if (parent?.type === 'MemberExpression' && parent.computed && parent.property === node) {
120+
keypath.push(`[${node.name}]`);
121+
} else {
122+
keypath.push(node.name);
123+
}
124+
},
125+
Literal(node, { path }) {
126+
const value = typeof node.value === 'string' ? `"${node.value}"` : String(node.value);
127+
const parent = path.at(-1);
128+
if (parent?.type === 'MemberExpression' && parent.computed && parent.property === node) {
129+
keypath.push(`[${value}]`);
130+
} else {
131+
keypath.push(value);
132+
}
133+
},
134+
ThisExpression(_, { next }) {
135+
keypath.push('this');
136+
next();
116137
}
117138
}
118139
);
119140

120-
return nodes;
141+
return [keypath.join('.'), nodes];
121142
}
122143

123144
/**
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
async test({ assert, target }) {
5+
const checkboxes = /** @type {NodeListOf<HTMLInputElement>} */ (
6+
target.querySelectorAll('input[type="checkbox"]')
7+
);
8+
9+
assert.isFalse(checkboxes[0].checked);
10+
assert.isTrue(checkboxes[1].checked);
11+
assert.isFalse(checkboxes[2].checked);
12+
13+
await checkboxes[1].click();
14+
15+
const noChecked = target.querySelector('#output')?.innerHTML;
16+
assert.equal(noChecked, '');
17+
18+
await checkboxes[1].click();
19+
20+
const oneChecked = target.querySelector('#output')?.innerHTML;
21+
assert.equal(oneChecked, 'Mint choc chip');
22+
}
23+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script lang="ts">
2+
import { writable } from 'svelte/store';
3+
let menu = ['Cookies and cream', 'Mint choc chip', 'Raspberry ripple'];
4+
let order = writable({flavours: ['Mint choc chip'], scoops: 1 });
5+
</script>
6+
7+
<form method="POST">
8+
<input type="radio" bind:group={$order.scoops} name="scoops" value={1} /> One scoop
9+
<input type="radio" bind:group={$order.scoops} name="scoops" value={2} /> Two scoops
10+
<input type="radio" bind:group={$order.scoops} name="scoops" value={3} /> Three scoops
11+
12+
{#each menu as flavour}
13+
<input type="checkbox" bind:group={$order.flavours} name="flavours" value={flavour} /> {flavour}
14+
{/each}
15+
</form>
16+
17+
<div id="output">{$order.flavours.join('+')}</div>

0 commit comments

Comments
 (0)