Skip to content

Commit d9866a1

Browse files
authored
fix: false positives for bind: with member in svelte/no-immutable-reactive-statements rule (#585)
1 parent 9545f67 commit d9866a1

13 files changed

+138
-25
lines changed

.changeset/kind-baboons-judge.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-svelte": patch
3+
---
4+
5+
fix: false positives for `bind:` with member in `svelte/no-immutable-reactive-statements` rule

src/rules/html-quotes.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ export default createRule('html-quotes', {
192192
return;
193193
}
194194
if (
195-
node.key.range[0] <= node.expression.range![0] &&
196-
node.expression.range![1] <= node.key.range[1]
195+
node.key.range[0] <= node.expression.range[0] &&
196+
node.expression.range[1] <= node.key.range[1]
197197
) {
198198
// shorthand
199199
return;

src/rules/no-immutable-reactive-statements.ts

+38-19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AST } from 'svelte-eslint-parser';
22
import { createRule } from '../utils';
33
import type { Scope, Variable, Reference, Definition } from '@typescript-eslint/scope-manager';
44
import type { TSESTree } from '@typescript-eslint/types';
5+
import { findVariable, iterateIdentifiers } from '../utils/ast-utils';
56

67
export default createRule('no-immutable-reactive-statements', {
78
meta: {
@@ -104,31 +105,49 @@ export default createRule('no-immutable-reactive-statements', {
104105
) {
105106
return true;
106107
}
107-
if (isMutableMember(reference.identifier)) {
108+
if (hasWriteMember(reference.identifier)) {
108109
return true;
109110
}
110111
}
111112
return false;
113+
}
112114

113-
function isMutableMember(
114-
expr: TSESTree.Identifier | TSESTree.JSXIdentifier | TSESTree.MemberExpression
115-
): boolean {
116-
if (expr.type === 'JSXIdentifier') return false;
117-
const parent = expr.parent;
118-
if (parent.type === 'AssignmentExpression') {
119-
return parent.left === expr;
120-
}
121-
if (parent.type === 'UpdateExpression') {
122-
return parent.argument === expr;
123-
}
124-
if (parent.type === 'UnaryExpression') {
125-
return parent.operator === 'delete' && parent.argument === expr;
126-
}
127-
if (parent.type === 'MemberExpression') {
128-
return parent.object === expr && isMutableMember(parent);
129-
}
130-
return false;
115+
/** Checks whether the given expression has writing to a member or not. */
116+
function hasWriteMember(
117+
expr: TSESTree.Identifier | TSESTree.JSXIdentifier | TSESTree.MemberExpression
118+
): boolean {
119+
if (expr.type === 'JSXIdentifier') return false;
120+
const parent = expr.parent as TSESTree.Node | AST.SvelteNode;
121+
if (parent.type === 'AssignmentExpression') {
122+
return parent.left === expr;
123+
}
124+
if (parent.type === 'UpdateExpression') {
125+
return parent.argument === expr;
126+
}
127+
if (parent.type === 'UnaryExpression') {
128+
return parent.operator === 'delete' && parent.argument === expr;
129+
}
130+
if (parent.type === 'MemberExpression') {
131+
return parent.object === expr && hasWriteMember(parent);
131132
}
133+
if (parent.type === 'SvelteDirective') {
134+
return parent.kind === 'Binding' && parent.expression === expr;
135+
}
136+
if (parent.type === 'SvelteEachBlock') {
137+
return parent.expression === expr && hasWriteReference(parent.context);
138+
}
139+
140+
return false;
141+
}
142+
143+
/** Checks whether the given pattern has writing or not. */
144+
function hasWriteReference(pattern: TSESTree.DestructuringPattern): boolean {
145+
for (const id of iterateIdentifiers(pattern)) {
146+
const variable = findVariable(context, id);
147+
if (variable && hasWrite(variable)) return true;
148+
}
149+
150+
return false;
132151
}
133152

134153
/**

src/utils/ast-utils.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,40 @@ export function findVariable(context: RuleContext, node: TSESTree.Identifier): V
230230
// Remove the $ and search for the variable again, as it may be a store access variable.
231231
return eslintUtils.findVariable(initialScope, node.name.slice(1));
232232
}
233+
/**
234+
* Iterate the identifiers of a given pattern node.
235+
*/
236+
export function* iterateIdentifiers(
237+
node: TSESTree.DestructuringPattern
238+
): Iterable<TSESTree.Identifier> {
239+
const buffer = [node];
240+
let pattern: TSESTree.DestructuringPattern | undefined;
241+
while ((pattern = buffer.shift())) {
242+
if (pattern.type === 'Identifier') {
243+
yield pattern;
244+
} else if (pattern.type === 'ArrayPattern') {
245+
for (const element of pattern.elements) {
246+
if (element) {
247+
buffer.push(element);
248+
}
249+
}
250+
} else if (pattern.type === 'ObjectPattern') {
251+
for (const property of pattern.properties) {
252+
if (property.type === 'Property') {
253+
buffer.push(property.value as TSESTree.DestructuringPattern);
254+
} else if (property.type === 'RestElement') {
255+
buffer.push(property);
256+
}
257+
}
258+
} else if (pattern.type === 'AssignmentPattern') {
259+
buffer.push(pattern.left);
260+
} else if (pattern.type === 'RestElement') {
261+
buffer.push(pattern.argument);
262+
} else if (pattern.type === 'MemberExpression') {
263+
// noop
264+
}
265+
}
266+
}
233267

234268
/**
235269
* Gets the scope for the current node
@@ -367,8 +401,8 @@ export function getMustacheTokens(
367401
return null;
368402
}
369403
if (
370-
node.key.range[0] <= node.expression.range![0] &&
371-
node.expression.range![1] <= node.key.range[1]
404+
node.key.range[0] <= node.expression.range[0] &&
405+
node.expression.range[1] <= node.key.range[1]
372406
) {
373407
// shorthand
374408
return null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
const values = ['a', 'b'];
3+
$: first = values[0];
4+
</script>
5+
6+
{first}
7+
<input bind:value={values[0]} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
const values = ['a', 'b'];
3+
$: first = values[0];
4+
</script>
5+
6+
{first}
7+
{#each values as value}
8+
<input bind:value />
9+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
const values = [['a'], ['b']];
3+
$: first = values[0][0];
4+
</script>
5+
6+
{first}
7+
{#each values as a}
8+
{#each a as value}
9+
<input bind:value />
10+
{/each}
11+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
const values = ['a', 'b'];
3+
$: first = values[0];
4+
</script>
5+
6+
{first}
7+
{values}
8+
{#each values as value}
9+
<button on:click={() => (value = 'foo')}>Foo</button>
10+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
const values = [{ a: 'a' }, { a: 'b' }];
3+
$: first = values[0].a;
4+
</script>
5+
6+
{first}
7+
{#each values as { a }}
8+
<input bind:value={a} />
9+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
const values = [['a'], ['b']];
3+
$: first = values[0][0];
4+
</script>
5+
6+
{first}
7+
{#each values as [value]}
8+
<input bind:value />
9+
{/each}

tools/update-types-for-node.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export type Node = TSESTree.Node
3737
export type Program = TSESTree.Program
3838
export type Expression = TSESTree.Expression
3939
export type Statement = TSESTree.Statement
40-
export type Pattern = TSESTree.Pattern`
40+
export type Pattern = TSESTree.DestructuringPattern`
4141
];
4242
const typesForNodeCode = [
4343
`/*

typings/estree/index.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type Node = TSESTree.Node;
1111
export type Program = TSESTree.Program;
1212
export type Expression = TSESTree.Expression;
1313
export type Statement = TSESTree.Statement;
14-
export type Pattern = TSESTree.Pattern;
14+
export type Pattern = TSESTree.DestructuringPattern;
1515
export type AccessorProperty = TSESTree.AccessorProperty;
1616
export type ArrayExpression = TSESTree.ArrayExpression;
1717
export type ArrayPattern = TSESTree.ArrayPattern;

0 commit comments

Comments
 (0)