diff --git a/.changeset/popular-needles-remain.md b/.changeset/popular-needles-remain.md
new file mode 100644
index 000000000..9fc497018
--- /dev/null
+++ b/.changeset/popular-needles-remain.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+feat: add `no-unnecessary-state-wrap` rule
diff --git a/README.md b/README.md
index a09728778..3315bd3ba 100644
--- a/README.md
+++ b/README.md
@@ -302,6 +302,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [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: |
| [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: |
| [svelte/no-svelte-internal](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-svelte-internal/) | svelte/internal will be removed in Svelte 6. | :star: |
+| [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::bulb: |
| [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 | |
| [svelte/no-unused-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-props/) | Warns about defined Props properties that are unused | :star: |
| [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
diff --git a/docs/rules.md b/docs/rules.md
index ac26d5e41..58d896b10 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -59,6 +59,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [svelte/no-reactive-functions](./rules/no-reactive-functions.md) | it's not necessary to define functions in reactive statements | :star::bulb: |
| [svelte/no-reactive-literals](./rules/no-reactive-literals.md) | don't assign literal values in reactive statements | :star::bulb: |
| [svelte/no-svelte-internal](./rules/no-svelte-internal.md) | svelte/internal will be removed in Svelte 6. | :star: |
+| [svelte/no-unnecessary-state-wrap](./rules/no-unnecessary-state-wrap.md) | Disallow unnecessary $state wrapping of reactive classes | :star::bulb: |
| [svelte/no-unused-class-name](./rules/no-unused-class-name.md) | disallow the use of a class in the template without a corresponding style | |
| [svelte/no-unused-props](./rules/no-unused-props.md) | Warns about defined Props properties that are unused | :star: |
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
diff --git a/docs/rules/no-unnecessary-state-wrap.md b/docs/rules/no-unnecessary-state-wrap.md
new file mode 100644
index 000000000..8e10ae3ec
--- /dev/null
+++ b/docs/rules/no-unnecessary-state-wrap.md
@@ -0,0 +1,115 @@
+---
+pageClass: 'rule-details'
+sidebarDepth: 0
+title: 'svelte/no-unnecessary-state-wrap'
+description: 'Disallow unnecessary $state wrapping of reactive classes'
+---
+
+# svelte/no-unnecessary-state-wrap
+
+> Disallow unnecessary $state wrapping of reactive classes
+
+- :exclamation: **_This rule has not been released yet._**
+- :gear: This rule is included in `"plugin:svelte/recommended"`.
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+
+## :book: Rule Details
+
+In Svelte 5, several built-in classes from `svelte/reactivity` are already reactive by default:
+
+- `SvelteSet`
+- `SvelteMap`
+- `SvelteURL`
+- `SvelteURLSearchParams`
+- `SvelteDate`
+- `MediaQuery`
+
+Therefore, wrapping them with `$state` is unnecessary and can lead to confusion.
+
+
+
+```svelte
+
+```
+
+## :wrench: Options
+
+```json
+{
+ "svelte/no-unnecessary-state-wrap": [
+ "error",
+ {
+ "additionalReactiveClasses": [],
+ "allowReassign": false
+ }
+ ]
+}
+```
+
+- `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 `[]`.
+- `allowReassign` ... If `true`, allows `$state` wrapping of reactive classes when the variable is reassigned. Default is `false`.
+
+### Examples with Options
+
+#### `additionalReactiveClasses`
+
+```svelte
+
+```
+
+#### `allowReassign`
+
+```svelte
+
+```
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-unnecessary-state-wrap.ts)
diff --git a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts
index 458226621..b1b2d51ad 100644
--- a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts
+++ b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts
@@ -32,6 +32,7 @@ const config: Linter.Config[] = [
'svelte/no-store-async': 'error',
'svelte/no-svelte-internal': 'error',
'svelte/no-unknown-style-directive-property': 'error',
+ 'svelte/no-unnecessary-state-wrap': 'error',
'svelte/no-unused-props': 'error',
'svelte/no-unused-svelte-ignore': 'error',
'svelte/no-useless-children-snippet': 'error',
diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts
index f0bc2b25f..86e6d65b8 100644
--- a/packages/eslint-plugin-svelte/src/rule-types.ts
+++ b/packages/eslint-plugin-svelte/src/rule-types.ts
@@ -256,6 +256,11 @@ export interface RuleOptions {
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unknown-style-directive-property/
*/
'svelte/no-unknown-style-directive-property'?: Linter.RuleEntry
+ /**
+ * Disallow unnecessary $state wrapping of reactive classes
+ * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unnecessary-state-wrap/
+ */
+ 'svelte/no-unnecessary-state-wrap'?: Linter.RuleEntry
/**
* disallow the use of a class in the template without a corresponding style
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/
@@ -518,6 +523,11 @@ type SvelteNoUnknownStyleDirectiveProperty = []|[{
ignoreProperties?: [string, ...(string)[]]
ignorePrefixed?: boolean
}]
+// ----- svelte/no-unnecessary-state-wrap -----
+type SvelteNoUnnecessaryStateWrap = []|[{
+ additionalReactiveClasses?: string[]
+ allowReassign?: boolean
+}]
// ----- svelte/no-unused-class-name -----
type SvelteNoUnusedClassName = []|[{
allowedClassNames?: string[]
diff --git a/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts b/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts
new file mode 100644
index 000000000..6c1ff5ef7
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts
@@ -0,0 +1,156 @@
+import { createRule } from '../utils/index.js';
+import { getSourceCode } from '../utils/compat.js';
+import { ReferenceTracker } from '@eslint-community/eslint-utils';
+import type { TSESTree } from '@typescript-eslint/types';
+
+const REACTIVE_CLASSES = [
+ 'SvelteSet',
+ 'SvelteMap',
+ 'SvelteURL',
+ 'SvelteURLSearchParams',
+ 'SvelteDate',
+ 'MediaQuery'
+];
+
+export default createRule('no-unnecessary-state-wrap', {
+ meta: {
+ docs: {
+ description: 'Disallow unnecessary $state wrapping of reactive classes',
+ category: 'Best Practices',
+ recommended: true
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ additionalReactiveClasses: {
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ uniqueItems: true
+ },
+ allowReassign: {
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ unnecessaryStateWrap: '{{className}} is already reactive, $state wrapping is unnecessary.',
+ suggestRemoveStateWrap: 'Remove unnecessary $state wrapping'
+ },
+ type: 'suggestion',
+ hasSuggestions: true,
+ conditions: [
+ {
+ svelteVersions: ['5'],
+ runes: [true, 'undetermined']
+ }
+ ]
+ },
+ create(context) {
+ const options = context.options[0] ?? {};
+ const additionalReactiveClasses = options.additionalReactiveClasses ?? [];
+ const allowReassign = options.allowReassign ?? false;
+
+ const referenceTracker = new ReferenceTracker(getSourceCode(context).scopeManager.globalScope!);
+ const traceMap: Record> = {};
+ for (const reactiveClass of REACTIVE_CLASSES) {
+ traceMap[reactiveClass] = {
+ [ReferenceTracker.CALL]: true,
+ [ReferenceTracker.CONSTRUCT]: true
+ };
+ }
+
+ // Track all reactive class imports and their aliases
+ const references = referenceTracker.iterateEsmReferences({
+ 'svelte/reactivity': {
+ [ReferenceTracker.ESM]: true,
+ ...traceMap
+ }
+ });
+
+ const referenceNodeAndNames = Array.from(references).map(({ node, path }) => {
+ return {
+ node,
+ name: path[path.length - 1]
+ };
+ });
+
+ function isReassigned(identifier: TSESTree.Identifier): boolean {
+ const variable = getSourceCode(context).scopeManager.getDeclaredVariables(
+ identifier.parent
+ )[0];
+ return variable.references.some((ref) => {
+ return ref.isWrite() && ref.identifier !== identifier;
+ });
+ }
+
+ function reportUnnecessaryStateWrap(
+ stateNode: TSESTree.Node,
+ targetNode: TSESTree.Node,
+ className: string,
+ identifier?: TSESTree.Identifier
+ ) {
+ if (allowReassign && identifier && isReassigned(identifier)) {
+ return;
+ }
+
+ context.report({
+ node: targetNode,
+ messageId: 'unnecessaryStateWrap',
+ data: {
+ className
+ },
+ suggest: [
+ {
+ messageId: 'suggestRemoveStateWrap',
+ fix(fixer) {
+ return fixer.replaceText(stateNode, getSourceCode(context).getText(targetNode));
+ }
+ }
+ ]
+ });
+ }
+
+ return {
+ CallExpression(node: TSESTree.CallExpression) {
+ if (node.callee.type !== 'Identifier' || node.callee.name !== '$state') {
+ return;
+ }
+
+ for (const arg of node.arguments) {
+ if (
+ (arg.type === 'NewExpression' || arg.type === 'CallExpression') &&
+ arg.callee.type === 'Identifier'
+ ) {
+ const name = arg.callee.name;
+ if (additionalReactiveClasses.includes(name)) {
+ const parent = node.parent;
+ if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
+ reportUnnecessaryStateWrap(node, arg, name, parent.id);
+ }
+ }
+ }
+ }
+ },
+
+ 'Program:exit': () => {
+ for (const { node, name } of referenceNodeAndNames) {
+ if (
+ node.parent?.type === 'CallExpression' &&
+ node.parent.callee.type === 'Identifier' &&
+ node.parent.callee.name === '$state'
+ ) {
+ const parent = node.parent.parent;
+ if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
+ reportUnnecessaryStateWrap(node.parent, node, name, parent.id);
+ }
+ }
+ }
+ }
+ };
+ }
+});
diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts
index 2242164fb..3151d17f1 100644
--- a/packages/eslint-plugin-svelte/src/utils/rules.ts
+++ b/packages/eslint-plugin-svelte/src/utils/rules.ts
@@ -50,6 +50,7 @@ import noSvelteInternal from '../rules/no-svelte-internal.js';
import noTargetBlank from '../rules/no-target-blank.js';
import noTrailingSpaces from '../rules/no-trailing-spaces.js';
import noUnknownStyleDirectiveProperty from '../rules/no-unknown-style-directive-property.js';
+import noUnnecessaryStateWrap from '../rules/no-unnecessary-state-wrap.js';
import noUnusedClassName from '../rules/no-unused-class-name.js';
import noUnusedProps from '../rules/no-unused-props.js';
import noUnusedSvelteIgnore from '../rules/no-unused-svelte-ignore.js';
@@ -124,6 +125,7 @@ export const rules = [
noTargetBlank,
noTrailingSpaces,
noUnknownStyleDirectiveProperty,
+ noUnnecessaryStateWrap,
noUnusedClassName,
noUnusedProps,
noUnusedSvelteIgnore,
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/_requirements.json
new file mode 100644
index 000000000..498661308
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/_requirements.json
@@ -0,0 +1,3 @@
+{
+ "svelte": ">=5.0.0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/additional-class-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/additional-class-config.json
new file mode 100644
index 000000000..8bde8d11e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/additional-class-config.json
@@ -0,0 +1,7 @@
+{
+ "options": [
+ {
+ "additionalReactiveClasses": ["CustomReactiveClass1", "CustomReactiveClass2"]
+ }
+ ]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/additional-class-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/additional-class-errors.yaml
new file mode 100644
index 000000000..30518c2f7
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/additional-class-errors.yaml
@@ -0,0 +1,34 @@
+- message: CustomReactiveClass1 is already reactive, $state wrapping is unnecessary.
+ line: 5
+ column: 25
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: |
+
+- message: CustomReactiveClass2 is already reactive, $state wrapping is unnecessary.
+ line: 6
+ column: 25
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: |
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/additional-class-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/additional-class-input.svelte
new file mode 100644
index 000000000..29aef1979
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/additional-class-input.svelte
@@ -0,0 +1,10 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/allow-reassign-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/allow-reassign-config.json
new file mode 100644
index 000000000..e68fc5b00
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/allow-reassign-config.json
@@ -0,0 +1,7 @@
+{
+ "options": [
+ {
+ "allowReassign": true
+ }
+ ]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/allow-reassign-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/allow-reassign-errors.yaml
new file mode 100644
index 000000000..536b5f9c6
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/allow-reassign-errors.yaml
@@ -0,0 +1,30 @@
+- message: SvelteSet is already reactive, $state wrapping is unnecessary.
+ line: 6
+ column: 21
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: |
+
+- message: SvelteMap is already reactive, $state wrapping is unnecessary.
+ line: 7
+ column: 19
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: |
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/allow-reassign-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/allow-reassign-input.svelte
new file mode 100644
index 000000000..85a1b156b
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/allow-reassign-input.svelte
@@ -0,0 +1,8 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/basic-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/basic-errors.yaml
new file mode 100644
index 000000000..4c2f5b9bd
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/basic-errors.yaml
@@ -0,0 +1,174 @@
+- message: SvelteSet is already reactive, $state wrapping is unnecessary.
+ line: 12
+ column: 21
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: |
+
+- message: SvelteMap is already reactive, $state wrapping is unnecessary.
+ line: 13
+ column: 21
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: |
+
+- message: SvelteURL is already reactive, $state wrapping is unnecessary.
+ line: 14
+ column: 21
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: |
+
+- message: SvelteURLSearchParams is already reactive, $state wrapping is unnecessary.
+ line: 15
+ column: 24
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: |
+
+- message: SvelteDate is already reactive, $state wrapping is unnecessary.
+ line: 16
+ column: 22
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: |
+
+- message: MediaQuery is already reactive, $state wrapping is unnecessary.
+ line: 17
+ column: 28
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: |
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/basic-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/basic-input.svelte
new file mode 100644
index 000000000..5af31b886
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/basic-input.svelte
@@ -0,0 +1,22 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/import-alias-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/import-alias-errors.yaml
new file mode 100644
index 000000000..ecde6627d
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/import-alias-errors.yaml
@@ -0,0 +1,28 @@
+- message: SvelteSet is already reactive, $state wrapping is unnecessary.
+ line: 5
+ column: 21
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: >
+
+- message: SvelteMap is already reactive, $state wrapping is unnecessary.
+ line: 6
+ column: 21
+ suggestions:
+ - desc: Remove unnecessary $state wrapping
+ messageId: suggestRemoveStateWrap
+ output: >
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/import-alias-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/import-alias-input.svelte
new file mode 100644
index 000000000..b9ff44b60
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/invalid/import-alias-input.svelte
@@ -0,0 +1,7 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/_requirements.json
new file mode 100644
index 000000000..498661308
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/_requirements.json
@@ -0,0 +1,3 @@
+{
+ "svelte": ">=5.0.0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/additional-class-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/additional-class-config.json
new file mode 100644
index 000000000..8bde8d11e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/additional-class-config.json
@@ -0,0 +1,7 @@
+{
+ "options": [
+ {
+ "additionalReactiveClasses": ["CustomReactiveClass1", "CustomReactiveClass2"]
+ }
+ ]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/additional-class-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/additional-class-input.svelte
new file mode 100644
index 000000000..ac036cb9c
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/additional-class-input.svelte
@@ -0,0 +1,9 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-config.json
new file mode 100644
index 000000000..e68fc5b00
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-config.json
@@ -0,0 +1,7 @@
+{
+ "options": [
+ {
+ "allowReassign": true
+ }
+ ]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-input.svelte
new file mode 100644
index 000000000..0f882919d
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-input.svelte
@@ -0,0 +1,10 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/basic-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/basic-input.svelte
new file mode 100644
index 000000000..77798ee8e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/basic-input.svelte
@@ -0,0 +1,22 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/src/rules/no-unnecessary-state-wrap.ts b/packages/eslint-plugin-svelte/tests/src/rules/no-unnecessary-state-wrap.ts
new file mode 100644
index 000000000..a4d8a3952
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/src/rules/no-unnecessary-state-wrap.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from '../../utils/eslint-compat.js';
+import rule from '../../../src/rules/no-unnecessary-state-wrap.js';
+import { loadTestCases } from '../../utils/utils.js';
+
+const tester = new RuleTester({
+ languageOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+});
+
+tester.run('no-unnecessary-state-wrap', rule as any, loadTestCases('no-unnecessary-state-wrap'));