Skip to content

Commit a7bd557

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

27 files changed

+682
-23
lines changed
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: add `no-unnecessary-state-wrap` rule

README.md

Lines changed: 1 addition & 0 deletions
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: |

docs/rules.md

Lines changed: 24 additions & 23 deletions
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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
"allowExplicitWrap": false,
62+
"additionalReactiveClasses": []
63+
}
64+
]
65+
}
66+
```
67+
68+
- `allowExplicitWrap` ... If `true`, allows explicit `$state` wrapping even when it's not necessary. This might be useful in cases where you want to maintain consistency with other state declarations. Default is `false`.
69+
- `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 `[]`.
70+
71+
### Examples with Options
72+
73+
#### `allowExplicitWrap: true`
74+
75+
```svelte
76+
<script>
77+
/* eslint svelte/no-unnecessary-state-wrap: ["error", { "allowExplicitWrap": true }] */
78+
79+
// ✓ GOOD: Explicit wrapping is allowed
80+
const set = $state(new SvelteSet());
81+
const map = $state(new SvelteMap());
82+
83+
// ✓ GOOD: Direct usage is also valid
84+
const set2 = new SvelteSet();
85+
const map2 = new SvelteMap();
86+
</script>
87+
```
88+
89+
#### `additionalReactiveClasses`
90+
91+
```svelte
92+
<script>
93+
/* eslint svelte/no-unnecessary-state-wrap: ["error", { "additionalReactiveClasses": ["MyReactiveClass"] }] */
94+
95+
// ✓ GOOD
96+
const myState = new MyReactiveClass();
97+
98+
// ✗ BAD
99+
const myState = $state(new MyReactiveClass());
100+
</script>
101+
```
102+
103+
## :mag: Implementation
104+
105+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts)
106+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-unnecessary-state-wrap.ts)

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

Lines changed: 1 addition & 0 deletions
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',

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

Lines changed: 10 additions & 0 deletions
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,11 @@ type SvelteNoUnknownStyleDirectiveProperty = []|[{
508513
ignoreProperties?: [string, ...(string)[]]
509514
ignorePrefixed?: boolean
510515
}]
516+
// ----- svelte/no-unnecessary-state-wrap -----
517+
type SvelteNoUnnecessaryStateWrap = []|[{
518+
allowExplicitWrap?: boolean
519+
additionalReactiveClasses?: string[]
520+
}]
511521
// ----- svelte/no-unused-class-name -----
512522
type SvelteNoUnusedClassName = []|[{
513523
allowedClassNames?: string[]

0 commit comments

Comments
 (0)