Skip to content

Commit dbc9e24

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

File tree

7 files changed

+261
-0
lines changed

7 files changed

+261
-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: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { ReferenceTracker } from '@eslint-community/eslint-utils';
2+
import { createRule } from '../utils/index.js';
3+
import type { TSESTree } from '@typescript-eslint/types';
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 as TSESTree.Expression)) {
53+
context.report({
54+
messageId: 'mutableDateUsed',
55+
node
56+
});
57+
}
58+
if (path[0] === 'Map' && isMapMutable(referenceTracker, node as TSESTree.Expression)) {
59+
context.report({
60+
messageId: 'mutableMapUsed',
61+
node
62+
});
63+
}
64+
if (path[0] === 'Set' && isSetMutable(referenceTracker, node as TSESTree.Expression)) {
65+
context.report({
66+
messageId: 'mutableSetUsed',
67+
node
68+
});
69+
}
70+
if (path[0] === 'URL' && isURLMutable(referenceTracker, node as TSESTree.Expression)) {
71+
context.report({
72+
messageId: 'mutableURLUsed',
73+
node
74+
});
75+
}
76+
if (
77+
path[0] === 'URLSearchParams' &&
78+
isURLSearchParamsMutable(referenceTracker, node as TSESTree.Expression)
79+
) {
80+
context.report({
81+
messageId: 'mutableURLSearchParamsUsed',
82+
node
83+
});
84+
}
85+
}
86+
}
87+
};
88+
}
89+
});
90+
91+
function isDateMutable(referenceTracker: ReferenceTracker, ctorNode: TSESTree.Expression): boolean {
92+
return (
93+
Array.from(
94+
referenceTracker.iteratePropertyReferences(ctorNode, {
95+
setDate: {
96+
[ReferenceTracker.CALL]: true
97+
},
98+
setFullYear: {
99+
[ReferenceTracker.CALL]: true
100+
},
101+
setHours: {
102+
[ReferenceTracker.CALL]: true
103+
},
104+
setMilliseconds: {
105+
[ReferenceTracker.CALL]: true
106+
},
107+
setMinutes: {
108+
[ReferenceTracker.CALL]: true
109+
},
110+
setMonth: {
111+
[ReferenceTracker.CALL]: true
112+
},
113+
setSeconds: {
114+
[ReferenceTracker.CALL]: true
115+
},
116+
setTime: {
117+
[ReferenceTracker.CALL]: true
118+
},
119+
setUTCDate: {
120+
[ReferenceTracker.CALL]: true
121+
},
122+
setUTCFullYear: {
123+
[ReferenceTracker.CALL]: true
124+
},
125+
setUTCHours: {
126+
[ReferenceTracker.CALL]: true
127+
},
128+
setUTCMilliseconds: {
129+
[ReferenceTracker.CALL]: true
130+
},
131+
setUTCMinutes: {
132+
[ReferenceTracker.CALL]: true
133+
},
134+
setUTCMonth: {
135+
[ReferenceTracker.CALL]: true
136+
},
137+
setUTCSeconds: {
138+
[ReferenceTracker.CALL]: true
139+
},
140+
setYear: {
141+
[ReferenceTracker.CALL]: true
142+
}
143+
})
144+
).length > 0
145+
);
146+
}
147+
148+
function isMapMutable(referenceTracker: ReferenceTracker, ctorNode: TSESTree.Expression): boolean {
149+
return (
150+
Array.from(
151+
referenceTracker.iteratePropertyReferences(ctorNode, {
152+
clear: {
153+
[ReferenceTracker.CALL]: true
154+
},
155+
delete: {
156+
[ReferenceTracker.CALL]: true
157+
},
158+
set: {
159+
[ReferenceTracker.CALL]: true
160+
}
161+
})
162+
).length > 0
163+
);
164+
}
165+
166+
function isSetMutable(referenceTracker: ReferenceTracker, ctorNode: TSESTree.Expression): boolean {
167+
return (
168+
Array.from(
169+
referenceTracker.iteratePropertyReferences(ctorNode, {
170+
add: {
171+
[ReferenceTracker.CALL]: true
172+
},
173+
clear: {
174+
[ReferenceTracker.CALL]: true
175+
},
176+
delete: {
177+
[ReferenceTracker.CALL]: true
178+
}
179+
})
180+
).length > 0
181+
);
182+
}
183+
184+
function isURLMutable(referenceTracker: ReferenceTracker, ctorNode: TSESTree.Expression): boolean {
185+
for (const { node } of referenceTracker.iteratePropertyReferences(ctorNode, {
186+
hash: {
187+
[ReferenceTracker.READ]: true
188+
},
189+
host: {
190+
[ReferenceTracker.READ]: true
191+
},
192+
hostname: {
193+
[ReferenceTracker.READ]: true
194+
},
195+
href: {
196+
[ReferenceTracker.READ]: true
197+
},
198+
password: {
199+
[ReferenceTracker.READ]: true
200+
},
201+
pathname: {
202+
[ReferenceTracker.READ]: true
203+
},
204+
port: {
205+
[ReferenceTracker.READ]: true
206+
},
207+
protocol: {
208+
[ReferenceTracker.READ]: true
209+
},
210+
search: {
211+
[ReferenceTracker.READ]: true
212+
},
213+
username: {
214+
[ReferenceTracker.READ]: true
215+
}
216+
})) {
217+
if (node.parent.type === 'AssignmentExpression') {
218+
return true;
219+
}
220+
}
221+
return false;
222+
}
223+
224+
function isURLSearchParamsMutable(
225+
referenceTracker: ReferenceTracker,
226+
ctorNode: TSESTree.Expression
227+
): boolean {
228+
return (
229+
Array.from(
230+
referenceTracker.iteratePropertyReferences(ctorNode, {
231+
append: {
232+
[ReferenceTracker.CALL]: true
233+
},
234+
delete: {
235+
[ReferenceTracker.CALL]: true
236+
},
237+
set: {
238+
[ReferenceTracker.CALL]: true
239+
},
240+
sort: {
241+
[ReferenceTracker.CALL]: true
242+
}
243+
})
244+
).length > 0
245+
);
246+
}

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)