Skip to content

Commit 49448ae

Browse files
committed
feat: add no-unnecessary-state-wrap rule
1 parent 20a2f32 commit 49448ae

21 files changed

+587
-23
lines changed

Diff for: .changeset/popular-needles-remain.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
feat: add `no-unnecessary-state-wrap` rule

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ These rules relate to better ways of doing things to help you avoid problems:
361361
| [svelte/no-reactive-functions](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-functions/) | it's not necessary to define functions in reactive statements | :star::bulb: |
362362
| [svelte/no-reactive-literals](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-literals/) | don't assign literal values in reactive statements | :star::bulb: |
363363
| [svelte/no-svelte-internal](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-svelte-internal/) | svelte/internal will be removed in Svelte 6. | :star: |
364+
| [svelte/no-unnecessary-state-wrap](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unnecessary-state-wrap/) | Disallow unnecessary $state wrapping of reactive classes | :star::wrench::bulb: |
364365
| [svelte/no-unused-class-name](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/) | disallow the use of a class in the template without a corresponding style | |
365366
| [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
366367
| [svelte/no-useless-children-snippet](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-children-snippet/) | disallow explicit children snippet where it's not needed | :star: |

Diff for: docs/rules.md

+24-23
Original file line numberDiff line numberDiff line change
@@ -46,29 +46,30 @@ These rules relate to security vulnerabilities in Svelte code:
4646

4747
These rules relate to better ways of doing things to help you avoid problems:
4848

49-
| Rule ID | Description | |
50-
| :--------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :------------- |
51-
| [svelte/block-lang](./rules/block-lang.md) | disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks. | :bulb: |
52-
| [svelte/button-has-type](./rules/button-has-type.md) | disallow usage of button without an explicit type attribute | |
53-
| [svelte/no-at-debug-tags](./rules/no-at-debug-tags.md) | disallow the use of `{@debug}` | :star: |
54-
| [svelte/no-ignored-unsubscribe](./rules/no-ignored-unsubscribe.md) | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores. | |
55-
| [svelte/no-immutable-reactive-statements](./rules/no-immutable-reactive-statements.md) | disallow reactive statements that don't reference reactive values. | :star: |
56-
| [svelte/no-inline-styles](./rules/no-inline-styles.md) | disallow attributes and directives that produce inline styles | |
57-
| [svelte/no-inspect](./rules/no-inspect.md) | Warns against the use of `$inspect` directive | :star: |
58-
| [svelte/no-reactive-functions](./rules/no-reactive-functions.md) | it's not necessary to define functions in reactive statements | :star::bulb: |
59-
| [svelte/no-reactive-literals](./rules/no-reactive-literals.md) | don't assign literal values in reactive statements | :star::bulb: |
60-
| [svelte/no-svelte-internal](./rules/no-svelte-internal.md) | svelte/internal will be removed in Svelte 6. | :star: |
61-
| [svelte/no-unused-class-name](./rules/no-unused-class-name.md) | disallow the use of a class in the template without a corresponding style | |
62-
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
63-
| [svelte/no-useless-children-snippet](./rules/no-useless-children-snippet.md) | disallow explicit children snippet where it's not needed | :star: |
64-
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :star::wrench: |
65-
| [svelte/prefer-const](./rules/prefer-const.md) | Require `const` declarations for variables that are never reassigned after declared | :wrench: |
66-
| [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
67-
| [svelte/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | :star: |
68-
| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | :star: |
69-
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |
70-
| [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store | :star: |
71-
| [svelte/valid-each-key](./rules/valid-each-key.md) | enforce keys to use variables defined in the `{#each}` block | :star: |
49+
| Rule ID | Description | |
50+
| :--------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :------------------- |
51+
| [svelte/block-lang](./rules/block-lang.md) | disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks. | :bulb: |
52+
| [svelte/button-has-type](./rules/button-has-type.md) | disallow usage of button without an explicit type attribute | |
53+
| [svelte/no-at-debug-tags](./rules/no-at-debug-tags.md) | disallow the use of `{@debug}` | :star: |
54+
| [svelte/no-ignored-unsubscribe](./rules/no-ignored-unsubscribe.md) | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores. | |
55+
| [svelte/no-immutable-reactive-statements](./rules/no-immutable-reactive-statements.md) | disallow reactive statements that don't reference reactive values. | :star: |
56+
| [svelte/no-inline-styles](./rules/no-inline-styles.md) | disallow attributes and directives that produce inline styles | |
57+
| [svelte/no-inspect](./rules/no-inspect.md) | Warns against the use of `$inspect` directive | :star: |
58+
| [svelte/no-reactive-functions](./rules/no-reactive-functions.md) | it's not necessary to define functions in reactive statements | :star::bulb: |
59+
| [svelte/no-reactive-literals](./rules/no-reactive-literals.md) | don't assign literal values in reactive statements | :star::bulb: |
60+
| [svelte/no-svelte-internal](./rules/no-svelte-internal.md) | svelte/internal will be removed in Svelte 6. | :star: |
61+
| [svelte/no-unnecessary-state-wrap](./rules/no-unnecessary-state-wrap.md) | Disallow unnecessary $state wrapping of reactive classes | :star::wrench::bulb: |
62+
| [svelte/no-unused-class-name](./rules/no-unused-class-name.md) | disallow the use of a class in the template without a corresponding style | |
63+
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
64+
| [svelte/no-useless-children-snippet](./rules/no-useless-children-snippet.md) | disallow explicit children snippet where it's not needed | :star: |
65+
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :star::wrench: |
66+
| [svelte/prefer-const](./rules/prefer-const.md) | Require `const` declarations for variables that are never reassigned after declared | :wrench: |
67+
| [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
68+
| [svelte/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | :star: |
69+
| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | :star: |
70+
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |
71+
| [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store | :star: |
72+
| [svelte/valid-each-key](./rules/valid-each-key.md) | enforce keys to use variables defined in the `{#each}` block | :star: |
7273

7374
## Stylistic Issues
7475

Diff for: docs/rules/no-unnecessary-state-wrap.md

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
pageClass: 'rule-details'
3+
sidebarDepth: 0
4+
title: 'svelte/no-unnecessary-state-wrap'
5+
description: 'Disallow unnecessary $state wrapping of reactive classes'
6+
---
7+
8+
# svelte/no-unnecessary-state-wrap
9+
10+
> Disallow unnecessary $state wrapping of reactive classes
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
- :gear: This rule is included in `"plugin:svelte/recommended"`.
14+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
15+
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
16+
17+
## :book: Rule Details
18+
19+
In Svelte 5, several built-in classes from `svelte/reactivity` are already reactive by default:
20+
21+
- `SvelteSet`
22+
- `SvelteMap`
23+
- `SvelteURL`
24+
- `SvelteURLSearchParams`
25+
- `SvelteDate`
26+
- `MediaQuery`
27+
28+
Therefore, wrapping them with `$state` is unnecessary and can lead to confusion.
29+
30+
<!--eslint-skip-->
31+
32+
```svelte
33+
<script>
34+
/* eslint svelte/no-unnecessary-state-wrap: "error" */
35+
36+
// ✓ GOOD
37+
const set = new SvelteSet();
38+
const map = new SvelteMap();
39+
const url = new SvelteURL('https://example.com');
40+
const params = new SvelteURLSearchParams('key=value');
41+
const date = new SvelteDate();
42+
const mediaQuery = new MediaQuery('(min-width: 800px)');
43+
44+
// ✗ BAD
45+
const set = $state(new SvelteSet());
46+
const map = $state(new SvelteMap());
47+
const url = $state(new SvelteURL('https://example.com'));
48+
const params = $state(new SvelteURLSearchParams('key=value'));
49+
const date = $state(new SvelteDate());
50+
const mediaQuery = $state(new MediaQuery('(min-width: 800px)'));
51+
</script>
52+
```
53+
54+
## :wrench: Options
55+
56+
```json
57+
{
58+
"svelte/no-unnecessary-state-wrap": [
59+
"error",
60+
{
61+
"additionalReactiveClasses": []
62+
}
63+
]
64+
}
65+
```
66+
67+
- `additionalReactiveClasses` ... An array of class names that should also be considered reactive. This is useful when you have custom classes that are inherently reactive. Default is `[]`.
68+
69+
### Examples with Options
70+
71+
#### `additionalReactiveClasses`
72+
73+
```svelte
74+
<script>
75+
/* eslint svelte/no-unnecessary-state-wrap: ["error", { "additionalReactiveClasses": ["MyReactiveClass"] }] */
76+
77+
// ✓ GOOD
78+
const myState = new MyReactiveClass();
79+
80+
// ✗ BAD
81+
const myState = $state(new MyReactiveClass());
82+
</script>
83+
```
84+
85+
## :mag: Implementation
86+
87+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts)
88+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-unnecessary-state-wrap.ts)

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

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const config: Linter.Config[] = [
3232
'svelte/no-store-async': 'error',
3333
'svelte/no-svelte-internal': 'error',
3434
'svelte/no-unknown-style-directive-property': 'error',
35+
'svelte/no-unnecessary-state-wrap': 'error',
3536
'svelte/no-unused-svelte-ignore': 'error',
3637
'svelte/no-useless-children-snippet': 'error',
3738
'svelte/no-useless-mustaches': 'error',

Diff for: packages/eslint-plugin-svelte/src/rule-types.ts

+9
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,11 @@ export interface RuleOptions {
256256
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unknown-style-directive-property/
257257
*/
258258
'svelte/no-unknown-style-directive-property'?: Linter.RuleEntry<SvelteNoUnknownStyleDirectiveProperty>
259+
/**
260+
* Disallow unnecessary $state wrapping of reactive classes
261+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unnecessary-state-wrap/
262+
*/
263+
'svelte/no-unnecessary-state-wrap'?: Linter.RuleEntry<SvelteNoUnnecessaryStateWrap>
259264
/**
260265
* disallow the use of a class in the template without a corresponding style
261266
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/
@@ -508,6 +513,10 @@ type SvelteNoUnknownStyleDirectiveProperty = []|[{
508513
ignoreProperties?: [string, ...(string)[]]
509514
ignorePrefixed?: boolean
510515
}]
516+
// ----- svelte/no-unnecessary-state-wrap -----
517+
type SvelteNoUnnecessaryStateWrap = []|[{
518+
additionalReactiveClasses?: string[]
519+
}]
511520
// ----- svelte/no-unused-class-name -----
512521
type SvelteNoUnusedClassName = []|[{
513522
allowedClassNames?: string[]

0 commit comments

Comments
 (0)