Skip to content

Commit 11da03d

Browse files
committed
feat(prefer-svelte-reactivity): added rule implementation
1 parent 80e583d commit 11da03d

File tree

13 files changed

+328
-0
lines changed

13 files changed

+328
-0
lines changed

.changeset/rich-colts-nail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
feat: added the `prefer-svelte-reactivity` rule

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
273273
| [svelte/no-store-async](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-store-async/) | disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features | :star: |
274274
| [svelte/no-top-level-browser-globals](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-top-level-browser-globals/) | disallow using top-level browser global variables | |
275275
| [svelte/no-unknown-style-directive-property](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unknown-style-directive-property/) | disallow unknown `style:property` | :star: |
276+
| [svelte/prefer-svelte-reactivity](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-svelte-reactivity/) | disallow using mutable instances of built-in classes where a reactive alternative is provided by svelte/reactivity | :star: |
276277
| [svelte/require-store-callbacks-use-set-param](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-callbacks-use-set-param/) | store callbacks must use `set` param | :bulb: |
277278
| [svelte/require-store-reactive-access](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-store-reactive-access/) | disallow to use of the store itself as an operand. Need to use $ prefix or get function. | :star::wrench: |
278279
| [svelte/valid-compile](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-compile/) | disallow warnings when compiling. | |

docs/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
3030
| [svelte/no-store-async](./rules/no-store-async.md) | disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features | :star: |
3131
| [svelte/no-top-level-browser-globals](./rules/no-top-level-browser-globals.md) | disallow using top-level browser global variables | |
3232
| [svelte/no-unknown-style-directive-property](./rules/no-unknown-style-directive-property.md) | disallow unknown `style:property` | :star: |
33+
| [svelte/prefer-svelte-reactivity](./rules/prefer-svelte-reactivity.md) | disallow using mutable instances of built-in classes where a reactive alternative is provided by svelte/reactivity | :star: |
3334
| [svelte/require-store-callbacks-use-set-param](./rules/require-store-callbacks-use-set-param.md) | store callbacks must use `set` param | :bulb: |
3435
| [svelte/require-store-reactive-access](./rules/require-store-reactive-access.md) | disallow to use of the store itself as an operand. Need to use $ prefix or get function. | :star::wrench: |
3536
| [svelte/valid-compile](./rules/valid-compile.md) | disallow warnings when compiling. | |

packages/eslint-plugin-svelte/src/configs/flat/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const config: Linter.Config[] = [
3737
'svelte/no-unused-svelte-ignore': 'error',
3838
'svelte/no-useless-children-snippet': 'error',
3939
'svelte/no-useless-mustaches': 'error',
40+
'svelte/prefer-svelte-reactivity': 'error',
4041
'svelte/prefer-writable-derived': 'error',
4142
'svelte/require-each-key': 'error',
4243
'svelte/require-event-dispatcher-types': 'error',

packages/eslint-plugin-svelte/src/rule-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,11 @@ export interface RuleOptions {
316316
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/
317317
*/
318318
'svelte/prefer-style-directive'?: Linter.RuleEntry<[]>
319+
/**
320+
* disallow using mutable instances of built-in classes where a reactive alternative is provided by svelte/reactivity
321+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-svelte-reactivity/
322+
*/
323+
'svelte/prefer-svelte-reactivity'?: Linter.RuleEntry<[]>
319324
/**
320325
* Prefer using writable $derived instead of $state and $effect
321326
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import { ReferenceTracker } from '@eslint-community/eslint-utils';
2+
import { createRule } from '../utils/index.js';
3+
import type { TSESTree } from '@typescript-eslint/types';
4+
import { findVariable, isIn } from '../utils/ast-utils.js';
5+
import { getSvelteContext } from '../utils/svelte-context.js';
6+
7+
export default createRule('prefer-svelte-reactivity', {
8+
meta: {
9+
docs: {
10+
description:
11+
'disallow using mutable instances of built-in classes where a reactive alternative is provided by svelte/reactivity',
12+
category: 'Possible Errors',
13+
recommended: true
14+
},
15+
schema: [],
16+
messages: {
17+
mutableDateUsed:
18+
'Found a mutable instance of the built-in Date class. Use SvelteDate instead.',
19+
mutableMapUsed: 'Found a mutable instance of the built-in Map class. Use SvelteMap instead.',
20+
mutableSetUsed: 'Found a mutable instance of the built-in Set class. Use SvelteSet instead.',
21+
mutableURLUsed: 'Found a mutable instance of the built-in URL class. Use SvelteURL instead.',
22+
mutableURLSearchParamsUsed:
23+
'Found a mutable instance of the built-in URLSearchParams class. Use SvelteURLSearchParams instead.'
24+
},
25+
type: 'problem',
26+
conditions: [
27+
{
28+
svelteVersions: ['5'],
29+
svelteFileTypes: ['.svelte', '.svelte.[js|ts]']
30+
}
31+
]
32+
},
33+
create(context) {
34+
const exportedVars: TSESTree.Node[] = [];
35+
return {
36+
...(getSvelteContext(context)?.svelteFileType === '.svelte.[js|ts]' && {
37+
ExportNamedDeclaration(node) {
38+
if (node.declaration !== null) {
39+
exportedVars.push(node.declaration);
40+
}
41+
for (const specifier of node.specifiers) {
42+
if (specifier.local.type !== 'Identifier') {
43+
continue;
44+
}
45+
const defs = findVariable(context, specifier.local)?.defs ?? [];
46+
for (const def of defs) {
47+
exportedVars.push(def.node);
48+
}
49+
}
50+
},
51+
ExportDefaultDeclaration(node) {
52+
if (node.declaration.type === 'Identifier') {
53+
const defs = findVariable(context, node.declaration)?.defs ?? [];
54+
for (const def of defs) {
55+
exportedVars.push(def.node);
56+
}
57+
} else {
58+
exportedVars.push(node.declaration);
59+
}
60+
}
61+
}),
62+
'Program:exit'() {
63+
const referenceTracker = new ReferenceTracker(context.sourceCode.scopeManager.globalScope!);
64+
for (const { node, path } of referenceTracker.iterateGlobalReferences({
65+
Date: {
66+
[ReferenceTracker.CONSTRUCT]: true
67+
},
68+
Map: {
69+
[ReferenceTracker.CONSTRUCT]: true
70+
},
71+
Set: {
72+
[ReferenceTracker.CONSTRUCT]: true
73+
},
74+
URL: {
75+
[ReferenceTracker.CONSTRUCT]: true
76+
},
77+
URLSearchParams: {
78+
[ReferenceTracker.CONSTRUCT]: true
79+
}
80+
})) {
81+
const messageId =
82+
path[0] === 'Date'
83+
? 'mutableDateUsed'
84+
: path[0] === 'Map'
85+
? 'mutableMapUsed'
86+
: path[0] === 'Set'
87+
? 'mutableSetUsed'
88+
: path[0] === 'URL'
89+
? 'mutableURLUsed'
90+
: 'mutableURLSearchParamsUsed';
91+
for (const exportedVar of exportedVars) {
92+
if (isIn(node, exportedVar)) {
93+
context.report({
94+
messageId,
95+
node
96+
});
97+
}
98+
}
99+
if (path[0] === 'Date' && isDateMutable(referenceTracker, node as TSESTree.Expression)) {
100+
context.report({
101+
messageId: 'mutableDateUsed',
102+
node
103+
});
104+
}
105+
if (path[0] === 'Map' && isMapMutable(referenceTracker, node as TSESTree.Expression)) {
106+
context.report({
107+
messageId: 'mutableMapUsed',
108+
node
109+
});
110+
}
111+
if (path[0] === 'Set' && isSetMutable(referenceTracker, node as TSESTree.Expression)) {
112+
context.report({
113+
messageId: 'mutableSetUsed',
114+
node
115+
});
116+
}
117+
if (path[0] === 'URL' && isURLMutable(referenceTracker, node as TSESTree.Expression)) {
118+
context.report({
119+
messageId: 'mutableURLUsed',
120+
node
121+
});
122+
}
123+
if (
124+
path[0] === 'URLSearchParams' &&
125+
isURLSearchParamsMutable(referenceTracker, node as TSESTree.Expression)
126+
) {
127+
context.report({
128+
messageId: 'mutableURLSearchParamsUsed',
129+
node
130+
});
131+
}
132+
}
133+
}
134+
};
135+
}
136+
});
137+
138+
function isDateMutable(referenceTracker: ReferenceTracker, ctorNode: TSESTree.Expression): boolean {
139+
return !referenceTracker
140+
.iteratePropertyReferences(ctorNode, {
141+
setDate: {
142+
[ReferenceTracker.CALL]: true
143+
},
144+
setFullYear: {
145+
[ReferenceTracker.CALL]: true
146+
},
147+
setHours: {
148+
[ReferenceTracker.CALL]: true
149+
},
150+
setMilliseconds: {
151+
[ReferenceTracker.CALL]: true
152+
},
153+
setMinutes: {
154+
[ReferenceTracker.CALL]: true
155+
},
156+
setMonth: {
157+
[ReferenceTracker.CALL]: true
158+
},
159+
setSeconds: {
160+
[ReferenceTracker.CALL]: true
161+
},
162+
setTime: {
163+
[ReferenceTracker.CALL]: true
164+
},
165+
setUTCDate: {
166+
[ReferenceTracker.CALL]: true
167+
},
168+
setUTCFullYear: {
169+
[ReferenceTracker.CALL]: true
170+
},
171+
setUTCHours: {
172+
[ReferenceTracker.CALL]: true
173+
},
174+
setUTCMilliseconds: {
175+
[ReferenceTracker.CALL]: true
176+
},
177+
setUTCMinutes: {
178+
[ReferenceTracker.CALL]: true
179+
},
180+
setUTCMonth: {
181+
[ReferenceTracker.CALL]: true
182+
},
183+
setUTCSeconds: {
184+
[ReferenceTracker.CALL]: true
185+
},
186+
setYear: {
187+
[ReferenceTracker.CALL]: true
188+
}
189+
})
190+
.next().done;
191+
}
192+
193+
function isMapMutable(referenceTracker: ReferenceTracker, ctorNode: TSESTree.Expression): boolean {
194+
return !referenceTracker
195+
.iteratePropertyReferences(ctorNode, {
196+
clear: {
197+
[ReferenceTracker.CALL]: true
198+
},
199+
delete: {
200+
[ReferenceTracker.CALL]: true
201+
},
202+
set: {
203+
[ReferenceTracker.CALL]: true
204+
}
205+
})
206+
.next().done;
207+
}
208+
209+
function isSetMutable(referenceTracker: ReferenceTracker, ctorNode: TSESTree.Expression): boolean {
210+
return !referenceTracker
211+
.iteratePropertyReferences(ctorNode, {
212+
add: {
213+
[ReferenceTracker.CALL]: true
214+
},
215+
clear: {
216+
[ReferenceTracker.CALL]: true
217+
},
218+
delete: {
219+
[ReferenceTracker.CALL]: true
220+
}
221+
})
222+
.next().done;
223+
}
224+
225+
function isURLMutable(referenceTracker: ReferenceTracker, ctorNode: TSESTree.Expression): boolean {
226+
for (const { node } of referenceTracker.iteratePropertyReferences(ctorNode, {
227+
hash: {
228+
[ReferenceTracker.READ]: true
229+
},
230+
host: {
231+
[ReferenceTracker.READ]: true
232+
},
233+
hostname: {
234+
[ReferenceTracker.READ]: true
235+
},
236+
href: {
237+
[ReferenceTracker.READ]: true
238+
},
239+
password: {
240+
[ReferenceTracker.READ]: true
241+
},
242+
pathname: {
243+
[ReferenceTracker.READ]: true
244+
},
245+
port: {
246+
[ReferenceTracker.READ]: true
247+
},
248+
protocol: {
249+
[ReferenceTracker.READ]: true
250+
},
251+
search: {
252+
[ReferenceTracker.READ]: true
253+
},
254+
username: {
255+
[ReferenceTracker.READ]: true
256+
}
257+
})) {
258+
if (node.parent.type === 'AssignmentExpression') {
259+
return true;
260+
}
261+
}
262+
return false;
263+
}
264+
265+
function isURLSearchParamsMutable(
266+
referenceTracker: ReferenceTracker,
267+
ctorNode: TSESTree.Expression
268+
): boolean {
269+
return !referenceTracker
270+
.iteratePropertyReferences(ctorNode, {
271+
append: {
272+
[ReferenceTracker.CALL]: true
273+
},
274+
delete: {
275+
[ReferenceTracker.CALL]: true
276+
},
277+
set: {
278+
[ReferenceTracker.CALL]: true
279+
},
280+
sort: {
281+
[ReferenceTracker.CALL]: true
282+
}
283+
})
284+
.next().done;
285+
}

packages/eslint-plugin-svelte/src/utils/ast-utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,3 +722,16 @@ export function findVariableForReplacement(
722722

723723
return { hasConflict: false, variable };
724724
}
725+
726+
/**
727+
* Check if one node is contained withing the subtree of another node.
728+
*/
729+
export function isIn(needle: TSESTree.Node, haystack: TSESTree.Node): boolean {
730+
if (needle === haystack) {
731+
return true;
732+
}
733+
if (needle.parent === undefined || needle.parent === null) {
734+
return false;
735+
}
736+
return isIn(needle.parent, haystack);
737+
}

packages/eslint-plugin-svelte/src/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import preferClassDirective from '../rules/prefer-class-directive.js';
6262
import preferConst from '../rules/prefer-const.js';
6363
import preferDestructuredStoreProps from '../rules/prefer-destructured-store-props.js';
6464
import preferStyleDirective from '../rules/prefer-style-directive.js';
65+
import preferSvelteReactivity from '../rules/prefer-svelte-reactivity.js';
6566
import preferWritableDerived from '../rules/prefer-writable-derived.js';
6667
import requireEachKey from '../rules/require-each-key.js';
6768
import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js';
@@ -141,6 +142,7 @@ export const rules = [
141142
preferConst,
142143
preferDestructuredStoreProps,
143144
preferStyleDirective,
145+
preferSvelteReactivity,
144146
preferWritableDerived,
145147
requireEachKey,
146148
requireEventDispatcherTypes,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0"
3+
}

0 commit comments

Comments
 (0)