Skip to content

Commit 31a96e6

Browse files
committed
feat(prefer-svelte-reactivity): added rule implementation
1 parent e586fae commit 31a96e6

File tree

7 files changed

+258
-0
lines changed

7 files changed

+258
-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 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 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 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: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { ReferenceTracker } from '@eslint-community/eslint-utils';
2+
import { createRule } from '../utils/index.js';
3+
import type { Expression } from 'estree';
4+
5+
export default createRule('prefer-svelte-reactivity', {
6+
meta: {
7+
docs: {
8+
description:
9+
'disallow using built-in classes where a reactive alternative is provided by svelte/reactivity',
10+
category: 'Possible Errors',
11+
recommended: true
12+
},
13+
schema: [],
14+
messages: {
15+
mutableDateUsed:
16+
'Found a mutable instance of the built-in Date class. Use SvelteDate instead.',
17+
mutableMapUsed: 'Found a mutable instance of the built-in Map class. Use SvelteMap instead.',
18+
mutableSetUsed: 'Found a mutable instance of the built-in Set class. Use SvelteSet instead.',
19+
mutableURLUsed: 'Found a mutable instance of the built-in URL class. Use SvelteURL instead.',
20+
mutableURLSearchParamsUsed:
21+
'Found a mutable instance of the built-in URLSearchParams class. Use SvelteURLSearchParams instead.'
22+
},
23+
type: 'problem', // 'problem', or 'layout',
24+
conditions: [
25+
{
26+
svelteVersions: ['5'],
27+
svelteFileTypes: ['.svelte', '.svelte.[js|ts]']
28+
}
29+
]
30+
},
31+
create(context) {
32+
return {
33+
Program() {
34+
const referenceTracker = new ReferenceTracker(context.sourceCode.scopeManager.globalScope!);
35+
for (const { node, path } of referenceTracker.iterateGlobalReferences({
36+
Date: {
37+
[ReferenceTracker.CONSTRUCT]: true
38+
},
39+
Map: {
40+
[ReferenceTracker.CONSTRUCT]: true
41+
},
42+
Set: {
43+
[ReferenceTracker.CONSTRUCT]: true
44+
},
45+
URL: {
46+
[ReferenceTracker.CONSTRUCT]: true
47+
},
48+
URLSearchParams: {
49+
[ReferenceTracker.CONSTRUCT]: true
50+
}
51+
})) {
52+
if (path[0] === 'Date' && isDateMutable(referenceTracker, node)) {
53+
context.report({
54+
messageId: 'mutableDateUsed',
55+
node
56+
});
57+
}
58+
if (path[0] === 'Map' && isMapMutable(referenceTracker, node)) {
59+
context.report({
60+
messageId: 'mutableMapUsed',
61+
node
62+
});
63+
}
64+
if (path[0] === 'Set' && isSetMutable(referenceTracker, node)) {
65+
context.report({
66+
messageId: 'mutableSetUsed',
67+
node
68+
});
69+
}
70+
if (path[0] === 'URL' && isURLMutable(referenceTracker, node)) {
71+
context.report({
72+
messageId: 'mutableURLUsed',
73+
node
74+
});
75+
}
76+
if (path[0] === 'URLSearchParams' && isURLSearchParamsMutable(referenceTracker, node)) {
77+
context.report({
78+
messageId: 'mutableURLSearchParamsUsed',
79+
node
80+
});
81+
}
82+
}
83+
}
84+
};
85+
}
86+
});
87+
88+
function isDateMutable(referenceTracker: ReferenceTracker, ctorNode: Expression): boolean {
89+
return (
90+
Array.from(
91+
referenceTracker.iteratePropertyReferences(ctorNode, {
92+
setDate: {
93+
[ReferenceTracker.CALL]: true
94+
},
95+
setFullYear: {
96+
[ReferenceTracker.CALL]: true
97+
},
98+
setHours: {
99+
[ReferenceTracker.CALL]: true
100+
},
101+
setMilliseconds: {
102+
[ReferenceTracker.CALL]: true
103+
},
104+
setMinutes: {
105+
[ReferenceTracker.CALL]: true
106+
},
107+
setMonth: {
108+
[ReferenceTracker.CALL]: true
109+
},
110+
setSeconds: {
111+
[ReferenceTracker.CALL]: true
112+
},
113+
setTime: {
114+
[ReferenceTracker.CALL]: true
115+
},
116+
setUTCDate: {
117+
[ReferenceTracker.CALL]: true
118+
},
119+
setUTCFullYear: {
120+
[ReferenceTracker.CALL]: true
121+
},
122+
setUTCHours: {
123+
[ReferenceTracker.CALL]: true
124+
},
125+
setUTCMilliseconds: {
126+
[ReferenceTracker.CALL]: true
127+
},
128+
setUTCMinutes: {
129+
[ReferenceTracker.CALL]: true
130+
},
131+
setUTCMonth: {
132+
[ReferenceTracker.CALL]: true
133+
},
134+
setUTCSeconds: {
135+
[ReferenceTracker.CALL]: true
136+
},
137+
setYear: {
138+
[ReferenceTracker.CALL]: true
139+
}
140+
})
141+
).length > 0
142+
);
143+
}
144+
145+
function isMapMutable(referenceTracker: ReferenceTracker, ctorNode: Expression): boolean {
146+
return (
147+
Array.from(
148+
referenceTracker.iteratePropertyReferences(ctorNode, {
149+
clear: {
150+
[ReferenceTracker.CALL]: true
151+
},
152+
delete: {
153+
[ReferenceTracker.CALL]: true
154+
},
155+
set: {
156+
[ReferenceTracker.CALL]: true
157+
}
158+
})
159+
).length > 0
160+
);
161+
}
162+
163+
function isSetMutable(referenceTracker: ReferenceTracker, ctorNode: Expression): boolean {
164+
return (
165+
Array.from(
166+
referenceTracker.iteratePropertyReferences(ctorNode, {
167+
add: {
168+
[ReferenceTracker.CALL]: true
169+
},
170+
clear: {
171+
[ReferenceTracker.CALL]: true
172+
},
173+
delete: {
174+
[ReferenceTracker.CALL]: true
175+
}
176+
})
177+
).length > 0
178+
);
179+
}
180+
181+
function isURLMutable(referenceTracker: ReferenceTracker, ctorNode: Expression): boolean {
182+
for (const { node } of referenceTracker.iteratePropertyReferences(ctorNode, {
183+
hash: {
184+
[ReferenceTracker.READ]: true
185+
},
186+
host: {
187+
[ReferenceTracker.READ]: true
188+
},
189+
hostname: {
190+
[ReferenceTracker.READ]: true
191+
},
192+
href: {
193+
[ReferenceTracker.READ]: true
194+
},
195+
password: {
196+
[ReferenceTracker.READ]: true
197+
},
198+
pathname: {
199+
[ReferenceTracker.READ]: true
200+
},
201+
port: {
202+
[ReferenceTracker.READ]: true
203+
},
204+
protocol: {
205+
[ReferenceTracker.READ]: true
206+
},
207+
search: {
208+
[ReferenceTracker.READ]: true
209+
},
210+
username: {
211+
[ReferenceTracker.READ]: true
212+
}
213+
})) {
214+
if (node.parent.type === 'AssignmentExpression') {
215+
return true;
216+
}
217+
}
218+
return false;
219+
}
220+
221+
function isURLSearchParamsMutable(
222+
referenceTracker: ReferenceTracker,
223+
ctorNode: Expression
224+
): boolean {
225+
return (
226+
Array.from(
227+
referenceTracker.iteratePropertyReferences(ctorNode, {
228+
append: {
229+
[ReferenceTracker.CALL]: true
230+
},
231+
delete: {
232+
[ReferenceTracker.CALL]: true
233+
},
234+
set: {
235+
[ReferenceTracker.CALL]: true
236+
},
237+
sort: {
238+
[ReferenceTracker.CALL]: true
239+
}
240+
})
241+
).length > 0
242+
);
243+
}

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,

0 commit comments

Comments
 (0)