Repository: sveltejs/eslint-plugin-svelte
Pull request #1170 (2025-03-31 08:19:42)

feat: add `prefer-writable-derived` rule
close: https://github.com/sveltejs/eslint-plugin-svelte/issues/1169

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 92c7732968c44478a27faca2b598cbf7c851c05e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/sveltejs/eslint-plugin-svelte/new/feat/prefer-writable-derived?filename=.changeset/sour-ducks-collect.md&value=---%0A%22eslint-plugin-svelte%22%3A%20patch%0A---%0A%0Afeat%3A%20add%20%60prefer-writable-derived%60%20rule%0A)


github-actions[bot]: <!-- posted by pkg.pr.new-comment.mjs -->

## Try the Instant Preview in Online Playground

[ESLint Online Playground](https://eslint-online-playground.netlify.app/?overrideDeps=%7B%22eslint-plugin-svelte%22%3A%22https%3A%2F%2Fpkg.pr.new%2Feslint-plugin-svelte%401170%22%7D#eslint-plugin-svelte%20with%20typescript)

## Install the Instant Preview to Your Local

```
npm i https://pkg.pr.new/eslint-plugin-svelte@1170
```

## Published Instant Preview Packages:

- eslint-plugin-svelte: https://pkg.pr.new/eslint-plugin-svelte@1170

[View Commit](https://github.com/sveltejs/eslint-plugin-svelte/commit/92c7732968c44478a27faca2b598cbf7c851c05e)

Patch:
diff --git a/.changeset/happy-donuts-fold.md b/.changeset/happy-donuts-fold.md
deleted file mode 100644
index 156e2c9..0000000
--- a/.changeset/happy-donuts-fold.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'eslint-plugin-svelte': patch
----
-
-chore: use `context.sourceCode` directly rather than a compatibility helper.
diff --git a/.changeset/petite-impalas-reply.md b/.changeset/petite-impalas-reply.md
deleted file mode 100644
index 0da08bd..0000000
--- a/.changeset/petite-impalas-reply.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'eslint-plugin-svelte': patch
----
-
-chore: use `context.filename` and `context.physicalFilename` instead of compat functions.
diff --git a/.changeset/ready-views-burn.md b/.changeset/ready-views-burn.md
new file mode 100644
index 0000000..a5f11d8
--- /dev/null
+++ b/.changeset/ready-views-burn.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+feat: add `prefer-writable-derived` rule
diff --git a/.changeset/rich-dogs-design.md b/.changeset/rich-dogs-design.md
new file mode 100644
index 0000000..0ef7fbf
--- /dev/null
+++ b/.changeset/rich-dogs-design.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+feat: added the `require-event-prefix` rule
diff --git a/.changeset/tired-bugs-remain.md b/.changeset/tired-bugs-remain.md
deleted file mode 100644
index 1a6b595..0000000
--- a/.changeset/tired-bugs-remain.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'eslint-plugin-svelte': patch
----
-
-chore: switch to `context.cwd` rather than using a compat helper.
diff --git a/.changeset/two-hats-ask.md b/.changeset/two-hats-ask.md
new file mode 100644
index 0000000..8d0295c
--- /dev/null
+++ b/.changeset/two-hats-ask.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+feat(consistent-selector-style): added support for dynamic classes and IDs
diff --git a/README.md b/README.md
index c379a3d..edc472b 100644
--- a/README.md
+++ b/README.md
@@ -310,6 +310,7 @@ These rules relate to better ways of doing things to help you avoid problems:
 | [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :star::wrench: |
 | [svelte/prefer-const](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-const/) | Require `const` declarations for variables that are never reassigned after declared | :wrench: |
 | [svelte/prefer-destructured-store-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
+| [svelte/prefer-writable-derived](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/) | Prefer using writable $derived instead of $state and $effect | :star::bulb: |
 | [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | :star: |
 | [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | :star: |
 | [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized |  |
@@ -337,6 +338,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
 | [svelte/no-spaces-around-equal-signs-in-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-spaces-around-equal-signs-in-attribute/) | disallow spaces around equal signs in attribute | :wrench: |
 | [svelte/prefer-class-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-class-directive/) | require class directives instead of ternary expressions | :wrench: |
 | [svelte/prefer-style-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/) | require style directives instead of style attribute | :wrench: |
+| [svelte/require-event-prefix](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/) | require component event names to start with "on" |  |
 | [svelte/shorthand-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-attribute/) | enforce use of shorthand syntax in attribute | :wrench: |
 | [svelte/shorthand-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-directive/) | enforce use of shorthand syntax in directives | :wrench: |
 | [svelte/sort-attributes](https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-attributes/) | enforce order of attributes | :wrench: |
diff --git a/docs/rules.md b/docs/rules.md
index df2ca5e..75dd5ed 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -67,6 +67,7 @@ These rules relate to better ways of doing things to help you avoid problems:
 | [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md)                           | disallow unnecessary mustache interpolations                                                                                              | :star::wrench: |
 | [svelte/prefer-const](./rules/prefer-const.md)                                           | Require `const` declarations for variables that are never reassigned after declared                                                       | :wrench:       |
 | [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md)     | destructure values from object stores for better change tracking & fewer redraws                                                          | :bulb:         |
+| [svelte/prefer-writable-derived](./rules/prefer-writable-derived.md)                     | Prefer using writable $derived instead of $state and $effect                                                                              | :star::bulb:   |
 | [svelte/require-each-key](./rules/require-each-key.md)                                   | require keyed `{#each}` block                                                                                                             | :star:         |
 | [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md)       | require type parameters for `createEventDispatcher`                                                                                       | :star:         |
 | [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized                                                                                            |                |
@@ -94,6 +95,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
 | [svelte/no-spaces-around-equal-signs-in-attribute](./rules/no-spaces-around-equal-signs-in-attribute.md) | disallow spaces around equal signs in attribute                                    | :wrench: |
 | [svelte/prefer-class-directive](./rules/prefer-class-directive.md)                                       | require class directives instead of ternary expressions                            | :wrench: |
 | [svelte/prefer-style-directive](./rules/prefer-style-directive.md)                                       | require style directives instead of style attribute                                | :wrench: |
+| [svelte/require-event-prefix](./rules/require-event-prefix.md)                                           | require component event names to start with "on"                                   |          |
 | [svelte/shorthand-attribute](./rules/shorthand-attribute.md)                                             | enforce use of shorthand syntax in attribute                                       | :wrench: |
 | [svelte/shorthand-directive](./rules/shorthand-directive.md)                                             | enforce use of shorthand syntax in directives                                      | :wrench: |
 | [svelte/sort-attributes](./rules/sort-attributes.md)                                                     | enforce order of attributes                                                        | :wrench: |
diff --git a/docs/rules/no-at-html-tags.md b/docs/rules/no-at-html-tags.md
index bdec6d9..aba37a4 100644
--- a/docs/rules/no-at-html-tags.md
+++ b/docs/rules/no-at-html-tags.md
@@ -40,7 +40,7 @@ If you are certain the content passed to `{@html}` is sanitized HTML you can dis
 
 ## :books: Further Reading
 
-- [Svelte - Tutorial > 1. Introduction / HTML tags](https://svelte.dev/tutorial/html-tags)
+- [Svelte - Tutorial > Basic Svelte / Introduction / HTML tags](https://svelte.dev/tutorial/svelte/html-tags)
 
 ## :rocket: Version
 
diff --git a/docs/rules/no-unused-props.md b/docs/rules/no-unused-props.md
index fffe386..33f1f08 100644
--- a/docs/rules/no-unused-props.md
+++ b/docs/rules/no-unused-props.md
@@ -163,6 +163,8 @@ Note: Properties of class types are not checked for usage, as they might be used
     "ignoreTypePatterns": [],
     // Patterns to ignore when checking for unused props
     "ignorePropertyPatterns": [],
+    // Whether to allow unused nested properties
+    "allowUnusedNestedProperties": false
   }]
 }
 ```
@@ -170,6 +172,7 @@ Note: Properties of class types are not checked for usage, as they might be used
 - `checkImportedTypes` ... Controls whether to check properties from types defined in external files. Default is `false`, meaning the rule only checks types defined within the component file itself. When set to `true`, the rule will also check properties from imported and extended types.
 - `ignoreTypePatterns` ... Regular expression patterns for type names to exclude from checks. Default is `[]` (no exclusions). Most useful when `checkImportedTypes` is `true`, allowing you to exclude specific imported types (like utility types or third-party types) from being checked.
 - `ignorePropertyPatterns` ... Regular expression patterns for property names to exclude from unused checks. Default is `[]` (no exclusions). Most useful when `checkImportedTypes` is `true`, allowing you to ignore specific properties from external types that shouldn't trigger warnings.
+- `allowUnusedNestedProperties` ... Controls whether to allow unused nested properties. Default is `false`, meaning the rule will report unused properties from nested objects.
 
 Examples:
 
@@ -219,6 +222,21 @@ Examples:
 </script>
 ```
 
+```svelte
+<!-- ✓ Good Example with allowUnusedNestedProperties: true -->
+<script lang="ts">
+  /* eslint svelte/no-unused-props: ["error", { "allowUnusedNestedProperties": true }] */
+  interface Props {
+    user: {
+      name: string;
+      age: number; // Won't be reported as unused
+    };
+  }
+  let { user }: Props = $props();
+  console.log(user.name);
+</script>
+```
+
 ## :gear: Required Configuration
 
 This rule requires `@typescript-eslint/parser` to work. Please refer to the [User Guide](../user-guide.md) for more information.
diff --git a/docs/rules/prefer-class-directive.md b/docs/rules/prefer-class-directive.md
index 5b5c6a1..3eb3ed6 100644
--- a/docs/rules/prefer-class-directive.md
+++ b/docs/rules/prefer-class-directive.md
@@ -62,7 +62,7 @@ You cannot enforce this style by using [prettier-plugin-svelte]. That is, this r
 
 ## :books: Further Reading
 
-- [Svelte - Tutorial > 13. Classes / The class directive](https://svelte.dev/tutorial/classes)
+- [Svelte - Tutorial > Basic Svelte / Classes and styles / The class atribute](https://svelte.dev/tutorial/svelte/classes)
 
 ## :rocket: Version
 
diff --git a/docs/rules/prefer-writable-derived.md b/docs/rules/prefer-writable-derived.md
new file mode 100644
index 0000000..830bec2
--- /dev/null
+++ b/docs/rules/prefer-writable-derived.md
@@ -0,0 +1,60 @@
+---
+pageClass: 'rule-details'
+sidebarDepth: 0
+title: 'svelte/prefer-writable-derived'
+description: 'Prefer using writable $derived instead of $state and $effect'
+---
+
+# svelte/prefer-writable-derived
+
+> Prefer using writable $derived instead of $state and $effect
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
+- :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
+
+This rule reports when you use a combination of `$state` and `$effect` to create a derived value that can be written to. It encourages using the more concise and clearer `$derived` syntax instead.
+
+<!--eslint-skip-->
+
+```svelte
+<script>
+  /* eslint svelte/prefer-writable-derived: "error" */
+  const { initialValue } = $props();
+
+  // ✓ GOOD
+  let value1 = $derived(initialValue);
+
+  // ✗ BAD
+  let value2 = $state(initialValue);
+  $effect(() => {
+    value2 = initialValue;
+  });
+</script>
+```
+
+The rule specifically looks for patterns where:
+
+1. You initialize a variable with `$state()`
+2. You then use `$effect()` or `$effect.pre()` to assign a new value to that same variable
+3. The effect function contains only a single assignment statement
+
+When this pattern is detected, the rule suggests refactoring to use `$derived()` instead, which provides the same functionality in a more concise way.
+
+## :wrench: Options
+
+Nothing.
+
+- This rule has no options.
+
+## :books: Further Reading
+
+- [Svelte Documentation on Reactivity Primitives](https://svelte.dev/docs/svelte-components#script-2-assignments-are-reactive)
+- [Svelte RFC for Reactivity Primitives](https://github.com/sveltejs/rfcs/blob/rfc-better-primitives/text/0000-better-primitives.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts)
diff --git a/docs/rules/require-each-key.md b/docs/rules/require-each-key.md
index cd0069c..0daa7ec 100644
--- a/docs/rules/require-each-key.md
+++ b/docs/rules/require-each-key.md
@@ -44,7 +44,7 @@ Nothing.
 
 ## :books: Further Reading
 
-- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/svelte/keyed-each-blocks)
+- [Svelte - Tutorial > Basic Svelte / Logic / Keyed each blocks](https://svelte.dev/tutorial/svelte/keyed-each-blocks)
 
 ## :rocket: Version
 
diff --git a/docs/rules/require-event-prefix.md b/docs/rules/require-event-prefix.md
new file mode 100644
index 0000000..556509c
--- /dev/null
+++ b/docs/rules/require-event-prefix.md
@@ -0,0 +1,71 @@
+---
+pageClass: 'rule-details'
+sidebarDepth: 0
+title: 'svelte/require-event-prefix'
+description: 'require component event names to start with "on"'
+---
+
+# svelte/require-event-prefix
+
+> require component event names to start with "on"
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
+
+## :book: Rule Details
+
+Starting with Svelte 5, component events are just component props that are functions and so can be called like any function. Events for HTML elements all have their name begin with "on" (e.g. `onclick`). This rule enforces that all component events (i.e. function props) also begin with "on".
+
+<!--eslint-skip-->
+
+```svelte
+<script lang="ts">
+  /* eslint svelte/require-event-prefix: "error" */
+
+  /* ✓ GOOD */
+
+  interface Props {
+    regularProp: string;
+    onclick(): void;
+  }
+
+  let { regularProp, onclick }: Props = $props();
+</script>
+```
+
+```svelte
+<script lang="ts">
+  /* eslint svelte/require-event-prefix: "error" */
+
+  /* ✗ BAD */
+
+  interface Props {
+    click(): void;
+  }
+
+  let { click }: Props = $props();
+</script>
+```
+
+## :wrench: Options
+
+```json
+{
+  "svelte/require-event-prefix": [
+    "error",
+    {
+      "checkAsyncFunctions": false
+    }
+  ]
+}
+```
+
+- `checkAsyncFunctions` ... Whether to also report asychronous function properties. Default `false`.
+
+## :books: Further Reading
+
+- [Svelte docs on events in version 5](https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts)
diff --git a/docs/rules/valid-each-key.md b/docs/rules/valid-each-key.md
index 5e6aa57..2515751 100644
--- a/docs/rules/valid-each-key.md
+++ b/docs/rules/valid-each-key.md
@@ -53,7 +53,7 @@ Nothing.
 
 ## :books: Further Reading
 
-- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/keyed-each-blocks)
+- [Svelte - Tutorial > Basic Svelte / Logic / Keyed each blocks](https://svelte.dev/tutorial/svelte/keyed-each-blocks)
 
 ## :rocket: Version
 
diff --git a/package.json b/package.json
index 254352f..f323d1d 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
     "@typescript-eslint/parser": "^8.16.0",
     "c8": "^10.1.2",
     "env-cmd": "^10.1.0",
-    "eslint": "~9.23.0",
+    "eslint": "~9.25.0",
     "eslint-config-prettier": "^10.0.0",
     "eslint-formatter-friendly": "^7.0.0",
     "eslint-plugin-eslint-plugin": "^6.3.2",
diff --git a/packages/eslint-plugin-svelte/CHANGELOG.md b/packages/eslint-plugin-svelte/CHANGELOG.md
index f0bfe09..9afd3e2 100644
--- a/packages/eslint-plugin-svelte/CHANGELOG.md
+++ b/packages/eslint-plugin-svelte/CHANGELOG.md
@@ -1,5 +1,33 @@
 # eslint-plugin-svelte
 
+## 3.5.1
+
+### Patch Changes
+
+- [#1188](https://github.com/sveltejs/eslint-plugin-svelte/pull/1188) [`9126f1f`](https://github.com/sveltejs/eslint-plugin-svelte/commit/9126f1f017ff990704498570d77fd2d3ff528efe) Thanks [@43081j](https://github.com/43081j)! - Removed eslint-compat-utils from dependencies
+
+## 3.5.0
+
+### Minor Changes
+
+- [#1171](https://github.com/sveltejs/eslint-plugin-svelte/pull/1171) [`842757f`](https://github.com/sveltejs/eslint-plugin-svelte/commit/842757fe45dd2433e801acdce5f66127e2216d7e) Thanks [@baseballyama](https://github.com/baseballyama)! - feat(no-unused-props): add `allowUnusedNestedProperties` option
+
+### Patch Changes
+
+- [#1178](https://github.com/sveltejs/eslint-plugin-svelte/pull/1178) [`7892f4c`](https://github.com/sveltejs/eslint-plugin-svelte/commit/7892f4c56a569ebdf80fa791d39c02754bfebe25) Thanks [@baseballyama](https://github.com/baseballyama)! - fix(no-unused-props): handle alias props name properly
+
+## 3.4.1
+
+### Patch Changes
+
+- [#1173](https://github.com/sveltejs/eslint-plugin-svelte/pull/1173) [`8e53e58`](https://github.com/sveltejs/eslint-plugin-svelte/commit/8e53e58411eba530324a8c223f5a2b00080b0cec) Thanks [@43081j](https://github.com/43081j)! - chore: use `context.sourceCode` directly rather than a compatibility helper.
+
+- [#1177](https://github.com/sveltejs/eslint-plugin-svelte/pull/1177) [`ec22fb4`](https://github.com/sveltejs/eslint-plugin-svelte/commit/ec22fb41d6ecc3a49cde882556bc4df1d7b681a5) Thanks [@43081j](https://github.com/43081j)! - test: use ESLint and Linter directly from eslint in tests
+
+- [#1174](https://github.com/sveltejs/eslint-plugin-svelte/pull/1174) [`b696ef3`](https://github.com/sveltejs/eslint-plugin-svelte/commit/b696ef350846ceb23f7c1078a4e32f8cb8d33681) Thanks [@43081j](https://github.com/43081j)! - chore: use `context.filename` and `context.physicalFilename` instead of compat functions.
+
+- [#1176](https://github.com/sveltejs/eslint-plugin-svelte/pull/1176) [`bafbf5f`](https://github.com/sveltejs/eslint-plugin-svelte/commit/bafbf5f7fc48602f3ede8ec3a7c829e7c4d6e9a4) Thanks [@43081j](https://github.com/43081j)! - chore: switch to `context.cwd` rather than using a compat helper.
+
 ## 3.4.0
 
 ### Minor Changes
diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json
index 0ebd9e9..eb49cb2 100644
--- a/packages/eslint-plugin-svelte/package.json
+++ b/packages/eslint-plugin-svelte/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-svelte",
-  "version": "3.4.0",
+  "version": "3.5.1",
   "description": "ESLint plugin for Svelte using AST",
   "repository": "git+https://github.com/sveltejs/eslint-plugin-svelte.git",
   "homepage": "https://sveltejs.github.io/eslint-plugin-svelte",
@@ -58,9 +58,8 @@
   "dependencies": {
     "@eslint-community/eslint-utils": "^4.4.1",
     "@jridgewell/sourcemap-codec": "^1.5.0",
-    "eslint-compat-utils": "^0.6.4",
     "esutils": "^2.0.3",
-    "known-css-properties": "^0.35.0",
+    "known-css-properties": "^0.36.0",
     "postcss": "^8.4.49",
     "postcss-load-config": "^3.1.4",
     "postcss-safe-parser": "^7.0.0",
diff --git a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts
index b1b2d51..7a08bef 100644
--- a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts
+++ b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts
@@ -37,6 +37,7 @@ const config: Linter.Config[] = [
 			'svelte/no-unused-svelte-ignore': 'error',
 			'svelte/no-useless-children-snippet': 'error',
 			'svelte/no-useless-mustaches': 'error',
+			'svelte/prefer-writable-derived': 'error',
 			'svelte/require-each-key': 'error',
 			'svelte/require-event-dispatcher-types': 'error',
 			'svelte/require-store-reactive-access': 'error',
diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts
index 3c8075c..e36fbda 100644
--- a/packages/eslint-plugin-svelte/src/meta.ts
+++ b/packages/eslint-plugin-svelte/src/meta.ts
@@ -1,5 +1,5 @@
 // IMPORTANT!
 // This file has been automatically generated,
 // in order to update its content execute "pnpm run update"
-export const name = 'eslint-plugin-svelte';
-export const version = '3.4.0';
+export const name = 'eslint-plugin-svelte' as const;
+export const version = '3.5.1' as const;
diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts
index 19e30a3..ac9f2a9 100644
--- a/packages/eslint-plugin-svelte/src/rule-types.ts
+++ b/packages/eslint-plugin-svelte/src/rule-types.ts
@@ -306,6 +306,11 @@ export interface RuleOptions {
    * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/
    */
   'svelte/prefer-style-directive'?: Linter.RuleEntry<[]>
+  /**
+   * Prefer using writable $derived instead of $state and $effect
+   * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/
+   */
+  'svelte/prefer-writable-derived'?: Linter.RuleEntry<[]>
   /**
    * require keyed `{#each}` block
    * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/
@@ -316,6 +321,11 @@ export interface RuleOptions {
    * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/
    */
   'svelte/require-event-dispatcher-types'?: Linter.RuleEntry<[]>
+  /**
+   * require component event names to start with "on"
+   * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-prefix/
+   */
+  'svelte/require-event-prefix'?: Linter.RuleEntry<SvelteRequireEventPrefix>
   /**
    * require style attributes that can be optimized
    * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/
@@ -537,6 +547,7 @@ type SvelteNoUnusedProps = []|[{
   checkImportedTypes?: boolean
   ignoreTypePatterns?: string[]
   ignorePropertyPatterns?: string[]
+  allowUnusedNestedProperties?: boolean
 }]
 // ----- svelte/no-useless-mustaches -----
 type SvelteNoUselessMustaches = []|[{
@@ -553,6 +564,10 @@ type SveltePreferConst = []|[{
   ignoreReadBeforeAssign?: boolean
   excludedRunes?: string[]
 }]
+// ----- svelte/require-event-prefix -----
+type SvelteRequireEventPrefix = []|[{
+  checkAsyncFunctions?: boolean
+}]
 // ----- svelte/shorthand-attribute -----
 type SvelteShorthandAttribute = []|[{
   prefer?: ("always" | "never")
diff --git a/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts b/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts
index 93f2a2c..5581814 100644
--- a/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts
+++ b/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts
@@ -6,9 +6,21 @@ import type {
 	Node as SelectorNode,
 	Tag as SelectorTag
 } from 'postcss-selector-parser';
+import type { SvelteHTMLElement } from 'svelte-eslint-parser/lib/ast';
 import { findClassesInAttribute } from '../utils/ast-utils.js';
+import {
+	extractExpressionPrefixLiteral,
+	extractExpressionSuffixLiteral
+} from '../utils/expression-affixes.js';
 import { createRule } from '../utils/index.js';
 
+interface Selections {
+	exact: Map<string, AST.SvelteHTMLElement[]>;
+	// [prefix, suffix]
+	affixes: Map<[string | null, string | null], AST.SvelteHTMLElement[]>;
+	universalSelector: boolean;
+}
+
 export default createRule('consistent-selector-style', {
 	meta: {
 		docs: {
@@ -62,9 +74,24 @@ export default createRule('consistent-selector-style', {
 		const style = context.options[0]?.style ?? ['type', 'id', 'class'];
 
 		const whitelistedClasses: string[] = [];
-		const classSelections: Map<string, AST.SvelteHTMLElement[]> = new Map();
-		const idSelections: Map<string, AST.SvelteHTMLElement[]> = new Map();
-		const typeSelections: Map<string, AST.SvelteHTMLElement[]> = new Map();
+
+		const selections: {
+			class: Selections;
+			id: Selections;
+			type: Map<string, AST.SvelteHTMLElement[]>;
+		} = {
+			class: {
+				exact: new Map(),
+				affixes: new Map(),
+				universalSelector: false
+			},
+			id: {
+				exact: new Map(),
+				affixes: new Map(),
+				universalSelector: false
+			},
+			type: new Map()
+		};
 
 		/**
 		 * Checks selectors in a given PostCSS node
@@ -109,10 +136,10 @@ export default createRule('consistent-selector-style', {
 		 * Checks a class selector
 		 */
 		function checkClassSelector(node: SelectorClass): void {
-			if (whitelistedClasses.includes(node.value)) {
+			if (selections.class.universalSelector || whitelistedClasses.includes(node.value)) {
 				return;
 			}
-			const selection = classSelections.get(node.value) ?? [];
+			const selection = matchSelection(selections.class, node.value);
 			for (const styleValue of style) {
 				if (styleValue === 'class') {
 					return;
@@ -124,7 +151,7 @@ export default createRule('consistent-selector-style', {
 					});
 					return;
 				}
-				if (styleValue === 'type' && canUseTypeSelector(selection, typeSelections)) {
+				if (styleValue === 'type' && canUseTypeSelector(selection, selections.type)) {
 					context.report({
 						messageId: 'classShouldBeType',
 						loc: styleSelectorNodeLoc(node) as AST.SourceLocation
@@ -138,7 +165,10 @@ export default createRule('consistent-selector-style', {
 		 * Checks an ID selector
 		 */
 		function checkIdSelector(node: SelectorIdentifier): void {
-			const selection = idSelections.get(node.value) ?? [];
+			if (selections.id.universalSelector) {
+				return;
+			}
+			const selection = matchSelection(selections.id, node.value);
 			for (const styleValue of style) {
 				if (styleValue === 'class') {
 					context.report({
@@ -150,7 +180,7 @@ export default createRule('consistent-selector-style', {
 				if (styleValue === 'id') {
 					return;
 				}
-				if (styleValue === 'type' && canUseTypeSelector(selection, typeSelections)) {
+				if (styleValue === 'type' && canUseTypeSelector(selection, selections.type)) {
 					context.report({
 						messageId: 'idShouldBeType',
 						loc: styleSelectorNodeLoc(node) as AST.SourceLocation
@@ -164,7 +194,7 @@ export default createRule('consistent-selector-style', {
 		 * Checks a type selector
 		 */
 		function checkTypeSelector(node: SelectorTag): void {
-			const selection = typeSelections.get(node.value) ?? [];
+			const selection = selections.type.get(node.value) ?? [];
 			for (const styleValue of style) {
 				if (styleValue === 'class') {
 					context.report({
@@ -191,21 +221,39 @@ export default createRule('consistent-selector-style', {
 				if (node.kind !== 'html') {
 					return;
 				}
-				addToArrayMap(typeSelections, node.name.name, node);
-				const classes = node.startTag.attributes.flatMap(findClassesInAttribute);
-				for (const className of classes) {
-					addToArrayMap(classSelections, className, node);
-				}
+				addToArrayMap(selections.type, node.name.name, node);
 				for (const attribute of node.startTag.attributes) {
 					if (attribute.type === 'SvelteDirective' && attribute.kind === 'Class') {
 						whitelistedClasses.push(attribute.key.name.name);
 					}
-					if (attribute.type !== 'SvelteAttribute' || attribute.key.name !== 'id') {
+					for (const className of findClassesInAttribute(attribute)) {
+						addToArrayMap(selections.class.exact, className, node);
+					}
+					if (attribute.type !== 'SvelteAttribute') {
 						continue;
 					}
 					for (const value of attribute.value) {
-						if (value.type === 'SvelteLiteral') {
-							addToArrayMap(idSelections, value.value, node);
+						if (attribute.key.name === 'class' && value.type === 'SvelteMustacheTag') {
+							const prefix = extractExpressionPrefixLiteral(context, value.expression);
+							const suffix = extractExpressionSuffixLiteral(context, value.expression);
+							if (prefix === null && suffix === null) {
+								selections.class.universalSelector = true;
+							} else {
+								addToArrayMap(selections.class.affixes, [prefix, suffix], node);
+							}
+						}
+						if (attribute.key.name === 'id') {
+							if (value.type === 'SvelteLiteral') {
+								addToArrayMap(selections.id.exact, value.value, node);
+							} else if (value.type === 'SvelteMustacheTag') {
+								const prefix = extractExpressionPrefixLiteral(context, value.expression);
+								const suffix = extractExpressionSuffixLiteral(context, value.expression);
+								if (prefix === null && suffix === null) {
+									selections.id.universalSelector = true;
+								} else {
+									addToArrayMap(selections.id.affixes, [prefix, suffix], node);
+								}
+							}
 						}
 					}
 				}
@@ -227,14 +275,27 @@ export default createRule('consistent-selector-style', {
 /**
  * Helper function to add a value to a Map of arrays
  */
-function addToArrayMap(
-	map: Map<string, AST.SvelteHTMLElement[]>,
-	key: string,
+function addToArrayMap<T>(
+	map: Map<T, AST.SvelteHTMLElement[]>,
+	key: T,
 	value: AST.SvelteHTMLElement
 ): void {
 	map.set(key, (map.get(key) ?? []).concat(value));
 }
 
+/**
+ * Finds all nodes in selections that could be matched by key
+ */
+function matchSelection(selections: Selections, key: string): SvelteHTMLElement[] {
+	const selection = selections.exact.get(key) ?? [];
+	selections.affixes.forEach((nodes, [prefix, suffix]) => {
+		if ((prefix === null || key.startsWith(prefix)) && (suffix === null || key.endsWith(suffix))) {
+			selection.push(...nodes);
+		}
+	});
+	return selection;
+}
+
 /**
  * Checks whether a given selection could be obtained using an ID selector
  */
diff --git a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts
index 00f73d0..094f432 100644
--- a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts
+++ b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts
@@ -2,6 +2,7 @@ import type { TSESTree } from '@typescript-eslint/types';
 import { createRule } from '../utils/index.js';
 import { ReferenceTracker } from '@eslint-community/eslint-utils';
 import { findVariable } from '../utils/ast-utils.js';
+import { extractExpressionPrefixVariable } from '../utils/expression-affixes.js';
 import type { RuleContext } from '../types.js';
 import type { SvelteLiteral } from 'svelte-eslint-parser/lib/ast';
 
@@ -221,87 +222,8 @@ function expressionStartsWithBase(
 	url: TSESTree.Expression,
 	basePathNames: Set<TSESTree.Identifier>
 ): boolean {
-	switch (url.type) {
-		case 'BinaryExpression':
-			return binaryExpressionStartsWithBase(context, url, basePathNames);
-		case 'Identifier':
-			return variableStartsWithBase(context, url, basePathNames);
-		case 'MemberExpression':
-			return memberExpressionStartsWithBase(url, basePathNames);
-		case 'TemplateLiteral':
-			return templateLiteralStartsWithBase(context, url, basePathNames);
-		default:
-			return false;
-	}
-}
-
-function binaryExpressionStartsWithBase(
-	context: RuleContext,
-	url: TSESTree.BinaryExpression,
-	basePathNames: Set<TSESTree.Identifier>
-): boolean {
-	return (
-		url.left.type !== 'PrivateIdentifier' &&
-		expressionStartsWithBase(context, url.left, basePathNames)
-	);
-}
-
-function memberExpressionStartsWithBase(
-	url: TSESTree.MemberExpression,
-	basePathNames: Set<TSESTree.Identifier>
-): boolean {
-	return url.property.type === 'Identifier' && basePathNames.has(url.property);
-}
-
-function variableStartsWithBase(
-	context: RuleContext,
-	url: TSESTree.Identifier,
-	basePathNames: Set<TSESTree.Identifier>
-): boolean {
-	if (basePathNames.has(url)) {
-		return true;
-	}
-	const variable = findVariable(context, url);
-	if (
-		variable === null ||
-		variable.identifiers.length !== 1 ||
-		variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
-		variable.identifiers[0].parent.init === null
-	) {
-		return false;
-	}
-	return expressionStartsWithBase(context, variable.identifiers[0].parent.init, basePathNames);
-}
-
-function templateLiteralStartsWithBase(
-	context: RuleContext,
-	url: TSESTree.TemplateLiteral,
-	basePathNames: Set<TSESTree.Identifier>
-): boolean {
-	const startingIdentifier = extractLiteralStartingExpression(url);
-	return (
-		startingIdentifier !== undefined &&
-		expressionStartsWithBase(context, startingIdentifier, basePathNames)
-	);
-}
-
-function extractLiteralStartingExpression(
-	templateLiteral: TSESTree.TemplateLiteral
-): TSESTree.Expression | undefined {
-	const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) =>
-		a.range[0] < b.range[0] ? -1 : 1
-	);
-	for (const part of literalParts) {
-		if (part.type === 'TemplateElement' && part.value.raw === '') {
-			// Skip empty quasi in the begining
-			continue;
-		}
-		if (part.type !== 'TemplateElement') {
-			return part;
-		}
-		return undefined;
-	}
-	return undefined;
+	const prefixVariable = extractExpressionPrefixVariable(context, url);
+	return prefixVariable !== null && basePathNames.has(prefixVariable);
 }
 
 function expressionIsEmpty(url: TSESTree.Expression): boolean {
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
index db54b74..f89f527 100644
--- a/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts
+++ b/packages/eslint-plugin-svelte/src/rules/no-unnecessary-state-wrap.ts
@@ -53,8 +53,12 @@ export default createRule('no-unnecessary-state-wrap', {
 		const options = context.options[0] ?? {};
 		const additionalReactiveClasses = options.additionalReactiveClasses ?? [];
 		const allowReassign = options.allowReassign ?? false;
+		const { globalScope } = context.sourceCode.scopeManager;
+		if (globalScope == null) {
+			return {};
+		}
 
-		const referenceTracker = new ReferenceTracker(context.sourceCode.scopeManager.globalScope!);
+		const referenceTracker = new ReferenceTracker(globalScope);
 		const traceMap: Record<string, Record<string, boolean>> = {};
 		for (const reactiveClass of REACTIVE_CLASSES) {
 			traceMap[reactiveClass] = {
@@ -79,8 +83,10 @@ export default createRule('no-unnecessary-state-wrap', {
 		});
 
 		function isReassigned(identifier: TSESTree.Identifier): boolean {
-			const variable = context.sourceCode.scopeManager.getDeclaredVariables(identifier.parent)[0];
-			return variable.references.some((ref) => {
+			const references = context.sourceCode.scopeManager
+				.getDeclaredVariables(identifier.parent)
+				.flatMap((v) => v.references);
+			return references.some((ref) => {
 				return ref.isWrite() && ref.identifier !== identifier;
 			});
 		}
diff --git a/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts b/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts
index 61b60e9..0ccefce 100644
--- a/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts
+++ b/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts
@@ -6,6 +6,7 @@ import { findVariable } from '../utils/ast-utils.js';
 import { toRegExp } from '../utils/regexp.js';
 
 type PropertyPathArray = string[];
+type DeclaredPropertyNames = Set<{ originalName: string; aliasName: string }>;
 
 let isRemovedWarningShown = false;
 
@@ -37,6 +38,10 @@ export default createRule('no-unused-props', {
 							type: 'string'
 						},
 						default: []
+					},
+					allowUnusedNestedProperties: {
+						type: 'boolean',
+						default: false
 					}
 				},
 				additionalProperties: false
@@ -182,16 +187,27 @@ export default createRule('no-unused-props', {
 			return sourceFile.fileName.includes('node_modules/typescript/lib/');
 		}
 
-		function getUsedPropertyNamesFromPattern(pattern: TSESTree.ObjectPattern): Set<string> {
-			const usedProps = new Set<string>();
+		function getUsedPropertyNamesFromPattern(
+			pattern: TSESTree.ObjectPattern
+		): DeclaredPropertyNames {
+			const usedProps: DeclaredPropertyNames = new Set();
 			for (const prop of pattern.properties) {
-				if (prop.type === 'Property' && prop.key.type === 'Identifier') {
-					usedProps.add(prop.key.name);
+				if (prop.type === 'Property') {
+					if (prop.key.type === 'Identifier') {
+						usedProps.add({ originalName: prop.key.name, aliasName: prop.key.name });
+					} else if (
+						prop.key.type === 'Literal' &&
+						typeof prop.key.value === 'string' &&
+						prop.value.type === 'Identifier'
+					) {
+						usedProps.add({ originalName: prop.key.value, aliasName: prop.value.name });
+					}
 				} else if (prop.type === 'RestElement') {
 					// If there's a rest element, all properties are potentially used
 					return new Set();
 				}
 			}
+
 			return usedProps;
 		}
 
@@ -229,7 +245,7 @@ export default createRule('no-unused-props', {
 		}: {
 			propsType: ts.Type;
 			usedPropertyPaths: string[];
-			declaredPropertyNames: Set<string>;
+			declaredPropertyNames: DeclaredPropertyNames;
 			reportNode: TSESTree.Node;
 			parentPath: string[];
 			checkedPropsTypes: Set<string>;
@@ -287,7 +303,9 @@ export default createRule('no-unused-props', {
 					continue;
 				}
 
-				const isUsedInProps = declaredPropertyNames.has(propName);
+				const isUsedInProps = Array.from(declaredPropertyNames).some((p) => {
+					return p.originalName === propName;
+				});
 
 				if (!isUsedInPath && !isUsedInProps) {
 					reportedPropertyPaths.add(currentPathStr);
@@ -338,11 +356,14 @@ export default createRule('no-unused-props', {
 		 * Returns true if the destructuring pattern includes a rest element,
 		 * which means all remaining properties are potentially used.
 		 */
-		function hasRestElement(usedProps: Set<string>): boolean {
-			return usedProps.size === 0;
+		function hasRestElement(declaredPropertyNames: DeclaredPropertyNames): boolean {
+			return declaredPropertyNames.size === 0;
 		}
 
-		function normalizeUsedPaths(paths: PropertyPathArray[]): PropertyPathArray[] {
+		function normalizeUsedPaths(
+			paths: PropertyPathArray[],
+			allowUnusedNestedProperties: boolean
+		): PropertyPathArray[] {
 			const normalized: PropertyPathArray[] = [];
 			for (const path of paths.sort((a, b) => a.length - b.length)) {
 				if (path.length === 0) continue;
@@ -351,7 +372,11 @@ export default createRule('no-unused-props', {
 				}
 				normalized.push(path);
 			}
-			return normalized;
+			return normalized.map((path) => {
+				// If we allow unused nested properties, only return first level properties
+				if (allowUnusedNestedProperties) return [path[0]];
+				return path;
+			});
 		}
 
 		return {
@@ -370,7 +395,7 @@ export default createRule('no-unused-props', {
 
 				const propsType = typeChecker.getTypeFromTypeNode(tsNode.type);
 				let usedPropertyPathsArray: PropertyPathArray[] = [];
-				let declaredPropertyNames = new Set<string>();
+				let declaredPropertyNames: DeclaredPropertyNames = new Set();
 
 				if (node.id.type === 'ObjectPattern') {
 					declaredPropertyNames = getUsedPropertyNamesFromPattern(node.id);
@@ -396,7 +421,10 @@ export default createRule('no-unused-props', {
 
 				checkUnusedProperties({
 					propsType,
-					usedPropertyPaths: normalizeUsedPaths(usedPropertyPathsArray).map((pathArray) => {
+					usedPropertyPaths: normalizeUsedPaths(
+						usedPropertyPathsArray,
+						options.allowUnusedNestedProperties
+					).map((pathArray) => {
 						return pathArray.join('.');
 					}),
 					declaredPropertyNames,
diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts
new file mode 100644
index 0000000..c7f859d
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts
@@ -0,0 +1,147 @@
+import type { TSESTree } from '@typescript-eslint/types';
+import { createRule } from '../utils/index.js';
+import { getScope } from '../utils/ast-utils.js';
+import { VERSION as SVELTE_VERSION } from 'svelte/compiler';
+import semver from 'semver';
+
+// Writable derived were introduced in Svelte 5.25.0
+const shouldRun = semver.satisfies(SVELTE_VERSION, '>=5.25.0');
+
+type ValidFunctionType = TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression;
+type ValidFunction = ValidFunctionType & {
+	body: TSESTree.BlockStatement;
+};
+
+type ValidAssignmentExpression = TSESTree.AssignmentExpression & {
+	operator: '=';
+	left: TSESTree.Identifier;
+};
+
+type ValidExpressionStatement = TSESTree.ExpressionStatement & {
+	expression: ValidAssignmentExpression;
+};
+
+function isEffectOrEffectPre(node: TSESTree.CallExpression) {
+	if (node.callee.type === 'Identifier') {
+		return node.callee.name === '$effect';
+	}
+	if (node.callee.type === 'MemberExpression') {
+		return (
+			node.callee.object.type === 'Identifier' &&
+			node.callee.object.name === '$effect' &&
+			node.callee.property.type === 'Identifier' &&
+			node.callee.property.name === 'pre'
+		);
+	}
+
+	return false;
+}
+
+function isValidFunctionArgument(argument: TSESTree.Node): argument is ValidFunction {
+	if (
+		(argument.type !== 'FunctionExpression' && argument.type !== 'ArrowFunctionExpression') ||
+		argument.params.length !== 0
+	) {
+		return false;
+	}
+
+	if (argument.body.type !== 'BlockStatement') {
+		return false;
+	}
+
+	return argument.body.body.length === 1;
+}
+
+function isValidAssignment(statement: TSESTree.Statement): statement is ValidExpressionStatement {
+	if (statement.type !== 'ExpressionStatement') return false;
+
+	const { expression } = statement;
+	return (
+		expression.type === 'AssignmentExpression' &&
+		expression.operator === '=' &&
+		expression.left.type === 'Identifier'
+	);
+}
+
+function isStateVariable(init: TSESTree.Expression | null): init is TSESTree.CallExpression {
+	return (
+		init?.type === 'CallExpression' &&
+		init.callee.type === 'Identifier' &&
+		init.callee.name === '$state'
+	);
+}
+
+export default createRule('prefer-writable-derived', {
+	meta: {
+		docs: {
+			description: 'Prefer using writable $derived instead of $state and $effect',
+			category: 'Best Practices',
+			recommended: true
+		},
+		schema: [],
+		messages: {
+			unexpected: 'Prefer using writable $derived instead of $state and $effect',
+			suggestRewrite: 'Rewrite $state and $effect to $derived'
+		},
+		type: 'suggestion',
+		conditions: [
+			{
+				svelteVersions: ['5'],
+				runes: [true, 'undetermined']
+			}
+		],
+		hasSuggestions: true
+	},
+	create(context) {
+		if (!shouldRun) {
+			return {};
+		}
+		return {
+			CallExpression: (node: TSESTree.CallExpression) => {
+				if (!isEffectOrEffectPre(node) || node.arguments.length !== 1) {
+					return;
+				}
+
+				const argument = node.arguments[0];
+				if (!isValidFunctionArgument(argument)) {
+					return;
+				}
+
+				const statement = argument.body.body[0];
+				if (!isValidAssignment(statement)) {
+					return;
+				}
+
+				const { left, right } = statement.expression;
+				const scope = getScope(context, statement);
+				const reference = scope.references.find(
+					(ref) => ref.identifier.type === 'Identifier' && ref.identifier.name === left.name
+				);
+
+				const def = reference?.resolved?.defs?.[0];
+				if (!def || def.type !== 'Variable' || def.node.type !== 'VariableDeclarator') {
+					return;
+				}
+
+				const { init } = def.node;
+				if (!isStateVariable(init)) {
+					return;
+				}
+
+				context.report({
+					node: def.node,
+					messageId: 'unexpected',
+					suggest: [
+						{
+							messageId: 'suggestRewrite',
+							fix: (fixer) => {
+								const rightCode = context.sourceCode.getText(right);
+								return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)];
+							}
+						}
+					]
+				});
+			}
+		};
+	}
+});
diff --git a/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts
new file mode 100644
index 0000000..ed3c3ad
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/rules/require-event-prefix.ts
@@ -0,0 +1,123 @@
+import { createRule } from '../utils/index.js';
+import {
+	type TSTools,
+	getTypeScriptTools,
+	isMethodSymbol,
+	isPropertySignatureKind,
+	isFunctionTypeKind,
+	isMethodSignatureKind,
+	isTypeReferenceKind,
+	isIdentifierKind
+} from '../utils/ts-utils/index.js';
+import type { Symbol, Type } from 'typescript';
+import type { CallExpression } from 'estree';
+
+export default createRule('require-event-prefix', {
+	meta: {
+		docs: {
+			description: 'require component event names to start with "on"',
+			category: 'Stylistic Issues',
+			conflictWithPrettier: false,
+			recommended: false
+		},
+		schema: [
+			{
+				type: 'object',
+				properties: {
+					checkAsyncFunctions: {
+						type: 'boolean'
+					}
+				},
+				additionalProperties: false
+			}
+		],
+		messages: {
+			nonPrefixedFunction: 'Component event name must start with "on".'
+		},
+		type: 'suggestion',
+		conditions: [
+			{
+				svelteVersions: ['5'],
+				svelteFileTypes: ['.svelte']
+			}
+		]
+	},
+	create(context) {
+		const tsTools = getTypeScriptTools(context);
+		if (!tsTools) {
+			return {};
+		}
+
+		const checkAsyncFunctions = context.options[0]?.checkAsyncFunctions ?? false;
+
+		return {
+			CallExpression(node) {
+				const propsType = getPropsType(node, tsTools);
+				if (propsType === undefined) {
+					return;
+				}
+				for (const property of propsType.getProperties()) {
+					if (
+						isFunctionLike(property, tsTools) &&
+						!property.getName().startsWith('on') &&
+						(checkAsyncFunctions || !isFunctionAsync(property, tsTools))
+					) {
+						const declarationTsNode = property.getDeclarations()?.[0];
+						const declarationEstreeNode =
+							declarationTsNode !== undefined
+								? tsTools.service.tsNodeToESTreeNodeMap.get(declarationTsNode)
+								: undefined;
+						context.report({
+							node: declarationEstreeNode ?? node,
+							messageId: 'nonPrefixedFunction'
+						});
+					}
+				}
+			}
+		};
+	}
+});
+
+function getPropsType(node: CallExpression, tsTools: TSTools): Type | undefined {
+	if (
+		node.callee.type !== 'Identifier' ||
+		node.callee.name !== '$props' ||
+		node.parent.type !== 'VariableDeclarator'
+	) {
+		return undefined;
+	}
+
+	const tsNode = tsTools.service.esTreeNodeToTSNodeMap.get(node.parent.id);
+	if (tsNode === undefined) {
+		return undefined;
+	}
+
+	return tsTools.service.program.getTypeChecker().getTypeAtLocation(tsNode);
+}
+
+function isFunctionLike(functionSymbol: Symbol, tsTools: TSTools): boolean {
+	return (
+		isMethodSymbol(functionSymbol, tsTools.ts) ||
+		(functionSymbol.valueDeclaration !== undefined &&
+			isPropertySignatureKind(functionSymbol.valueDeclaration, tsTools.ts) &&
+			functionSymbol.valueDeclaration.type !== undefined &&
+			isFunctionTypeKind(functionSymbol.valueDeclaration.type, tsTools.ts))
+	);
+}
+
+function isFunctionAsync(functionSymbol: Symbol, tsTools: TSTools): boolean {
+	return (
+		functionSymbol.getDeclarations()?.some((declaration) => {
+			if (!isMethodSignatureKind(declaration, tsTools.ts)) {
+				return false;
+			}
+			if (declaration.type === undefined || !isTypeReferenceKind(declaration.type, tsTools.ts)) {
+				return false;
+			}
+			return (
+				isIdentifierKind(declaration.type.typeName, tsTools.ts) &&
+				declaration.type.typeName.escapedText === 'Promise'
+			);
+		}) ?? false
+	);
+}
diff --git a/packages/eslint-plugin-svelte/src/utils/expression-affixes.ts b/packages/eslint-plugin-svelte/src/utils/expression-affixes.ts
new file mode 100644
index 0000000..6c96074
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/utils/expression-affixes.ts
@@ -0,0 +1,209 @@
+import type { TSESTree } from '@typescript-eslint/types';
+import { findVariable } from './ast-utils.js';
+import type { RuleContext } from '../types.js';
+import type { SvelteLiteral } from 'svelte-eslint-parser/lib/ast';
+
+// Variable prefix extraction
+
+export function extractExpressionPrefixVariable(
+	context: RuleContext,
+	expression: TSESTree.Expression
+): TSESTree.Identifier | null {
+	switch (expression.type) {
+		case 'BinaryExpression':
+			return extractBinaryExpressionPrefixVariable(context, expression);
+		case 'Identifier':
+			return extractVariablePrefixVariable(context, expression);
+		case 'MemberExpression':
+			return extractMemberExpressionPrefixVariable(expression);
+		case 'TemplateLiteral':
+			return extractTemplateLiteralPrefixVariable(context, expression);
+		default:
+			return null;
+	}
+}
+
+function extractBinaryExpressionPrefixVariable(
+	context: RuleContext,
+	expression: TSESTree.BinaryExpression
+): TSESTree.Identifier | null {
+	return expression.left.type !== 'PrivateIdentifier'
+		? extractExpressionPrefixVariable(context, expression.left)
+		: null;
+}
+
+function extractVariablePrefixVariable(
+	context: RuleContext,
+	expression: TSESTree.Identifier
+): TSESTree.Identifier | null {
+	const variable = findVariable(context, expression);
+	if (
+		variable === null ||
+		variable.identifiers.length !== 1 ||
+		variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
+		variable.identifiers[0].parent.init === null
+	) {
+		return expression;
+	}
+	return (
+		extractExpressionPrefixVariable(context, variable.identifiers[0].parent.init) ?? expression
+	);
+}
+
+function extractMemberExpressionPrefixVariable(
+	expression: TSESTree.MemberExpression
+): TSESTree.Identifier | null {
+	return expression.property.type === 'Identifier' ? expression.property : null;
+}
+
+function extractTemplateLiteralPrefixVariable(
+	context: RuleContext,
+	expression: TSESTree.TemplateLiteral
+): TSESTree.Identifier | null {
+	const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) =>
+		a.range[0] < b.range[0] ? -1 : 1
+	);
+	for (const part of literalParts) {
+		if (part.type === 'TemplateElement' && part.value.raw === '') {
+			// Skip empty quasi in the begining
+			continue;
+		}
+		if (part.type !== 'TemplateElement') {
+			return extractExpressionPrefixVariable(context, part);
+		}
+		return null;
+	}
+	return null;
+}
+
+// Literal prefix extraction
+
+export function extractExpressionPrefixLiteral(
+	context: RuleContext,
+	expression: SvelteLiteral | TSESTree.Node
+): string | null {
+	switch (expression.type) {
+		case 'BinaryExpression':
+			return extractBinaryExpressionPrefixLiteral(context, expression);
+		case 'Identifier':
+			return extractVariablePrefixLiteral(context, expression);
+		case 'Literal':
+			return typeof expression.value === 'string' ? expression.value : null;
+		case 'SvelteLiteral':
+			return expression.value;
+		case 'TemplateLiteral':
+			return extractTemplateLiteralPrefixLiteral(context, expression);
+		default:
+			return null;
+	}
+}
+
+function extractBinaryExpressionPrefixLiteral(
+	context: RuleContext,
+	expression: TSESTree.BinaryExpression
+): string | null {
+	return expression.left.type !== 'PrivateIdentifier'
+		? extractExpressionPrefixLiteral(context, expression.left)
+		: null;
+}
+
+function extractVariablePrefixLiteral(
+	context: RuleContext,
+	expression: TSESTree.Identifier
+): string | null {
+	const variable = findVariable(context, expression);
+	if (
+		variable === null ||
+		variable.identifiers.length !== 1 ||
+		variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
+		variable.identifiers[0].parent.init === null
+	) {
+		return null;
+	}
+	return extractExpressionPrefixLiteral(context, variable.identifiers[0].parent.init);
+}
+
+function extractTemplateLiteralPrefixLiteral(
+	context: RuleContext,
+	expression: TSESTree.TemplateLiteral
+): string | null {
+	const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) =>
+		a.range[0] < b.range[0] ? -1 : 1
+	);
+	for (const part of literalParts) {
+		if (part.type === 'TemplateElement') {
+			if (part.value.raw === '') {
+				// Skip empty quasi
+				continue;
+			}
+			return part.value.raw;
+		}
+		return extractExpressionPrefixLiteral(context, part);
+	}
+	return null;
+}
+
+// Literal suffix extraction
+
+export function extractExpressionSuffixLiteral(
+	context: RuleContext,
+	expression: SvelteLiteral | TSESTree.Node
+): string | null {
+	switch (expression.type) {
+		case 'BinaryExpression':
+			return extractBinaryExpressionSuffixLiteral(context, expression);
+		case 'Identifier':
+			return extractVariableSuffixLiteral(context, expression);
+		case 'Literal':
+			return typeof expression.value === 'string' ? expression.value : null;
+		case 'SvelteLiteral':
+			return expression.value;
+		case 'TemplateLiteral':
+			return extractTemplateLiteralSuffixLiteral(context, expression);
+		default:
+			return null;
+	}
+}
+
+function extractBinaryExpressionSuffixLiteral(
+	context: RuleContext,
+	expression: TSESTree.BinaryExpression
+): string | null {
+	return extractExpressionSuffixLiteral(context, expression.right);
+}
+
+function extractVariableSuffixLiteral(
+	context: RuleContext,
+	expression: TSESTree.Identifier
+): string | null {
+	const variable = findVariable(context, expression);
+	if (
+		variable === null ||
+		variable.identifiers.length !== 1 ||
+		variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
+		variable.identifiers[0].parent.init === null
+	) {
+		return null;
+	}
+	return extractExpressionSuffixLiteral(context, variable.identifiers[0].parent.init);
+}
+
+function extractTemplateLiteralSuffixLiteral(
+	context: RuleContext,
+	expression: TSESTree.TemplateLiteral
+): string | null {
+	const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) =>
+		a.range[0] < b.range[0] ? -1 : 1
+	);
+	for (const part of literalParts.reverse()) {
+		if (part.type === 'TemplateElement') {
+			if (part.value.raw === '') {
+				// Skip empty quasi
+				continue;
+			}
+			return part.value.raw;
+		}
+		return extractExpressionSuffixLiteral(context, part);
+	}
+	return null;
+}
diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts
index 3151d17..e79cd96 100644
--- a/packages/eslint-plugin-svelte/src/utils/rules.ts
+++ b/packages/eslint-plugin-svelte/src/utils/rules.ts
@@ -60,8 +60,10 @@ import preferClassDirective from '../rules/prefer-class-directive.js';
 import preferConst from '../rules/prefer-const.js';
 import preferDestructuredStoreProps from '../rules/prefer-destructured-store-props.js';
 import preferStyleDirective from '../rules/prefer-style-directive.js';
+import preferWritableDerived from '../rules/prefer-writable-derived.js';
 import requireEachKey from '../rules/require-each-key.js';
 import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js';
+import requireEventPrefix from '../rules/require-event-prefix.js';
 import requireOptimizedStyleAttribute from '../rules/require-optimized-style-attribute.js';
 import requireStoreCallbacksUseSetParam from '../rules/require-store-callbacks-use-set-param.js';
 import requireStoreReactiveAccess from '../rules/require-store-reactive-access.js';
@@ -135,8 +137,10 @@ export const rules = [
 	preferConst,
 	preferDestructuredStoreProps,
 	preferStyleDirective,
+	preferWritableDerived,
 	requireEachKey,
 	requireEventDispatcherTypes,
+	requireEventPrefix,
 	requireOptimizedStyleAttribute,
 	requireStoreCallbacksUseSetParam,
 	requireStoreReactiveAccess,
diff --git a/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts b/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts
index f386698..00762be 100644
--- a/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts
+++ b/packages/eslint-plugin-svelte/src/utils/ts-utils/index.ts
@@ -306,3 +306,48 @@ export function getTypeOfPropertyOfType(
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- getTypeOfPropertyOfType is an internal API of TS.
 	return (checker as any).getTypeOfPropertyOfType(type, name);
 }
+
+/**
+ * Check whether the given symbol is a method type or not.
+ */
+export function isMethodSymbol(type: TS.Symbol, ts: TypeScript): boolean {
+	return (type.getFlags() & ts.SymbolFlags.Method) !== 0;
+}
+
+/**
+ * Check whether the given node is a property signature kind or not.
+ */
+export function isPropertySignatureKind(
+	node: TS.Node,
+	ts: TypeScript
+): node is TS.PropertySignature {
+	return node.kind === ts.SyntaxKind.PropertySignature;
+}
+
+/**
+ * Check whether the given node is a function type kind or not.
+ */
+export function isFunctionTypeKind(node: TS.Node, ts: TypeScript): node is TS.FunctionTypeNode {
+	return node.kind === ts.SyntaxKind.FunctionType;
+}
+
+/**
+ * Check whether the given node is a method signature kind or not.
+ */
+export function isMethodSignatureKind(node: TS.Node, ts: TypeScript): node is TS.MethodSignature {
+	return node.kind === ts.SyntaxKind.MethodSignature;
+}
+
+/**
+ * Check whether the given node is a type reference kind or not.
+ */
+export function isTypeReferenceKind(node: TS.Node, ts: TypeScript): node is TS.TypeReferenceNode {
+	return node.kind === ts.SyntaxKind.TypeReference;
+}
+
+/**
+ * Check whether the given node is an identifier kind or not.
+ */
+export function isIdentifierKind(node: TS.Node, ts: TypeScript): node is TS.Identifier {
+	return node.kind === ts.SyntaxKind.Identifier;
+}


Test patch:
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-prefix01-input.svelte
new file mode 100644
index 0000000..d3437cb
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-prefix01-input.svelte
@@ -0,0 +1,33 @@
+<script>
+  import { value } from "package";
+
+  const derived = "link-three-" + value;
+</script>
+
+<a>Click me!</a>
+
+<a class={"link-one-" + value}>Click me two!</a>
+
+<a class={"link-one-" + value}>Click me two!</a>
+
+<a class={`link-two-${value}`}>Click me three!</a>
+
+<a class={`link-two-${value}`}>Click me three!</a>
+
+<a class={derived}>Click me four!</a>
+
+<a class={derived}>Click me four!</a>
+
+<style>
+  .link-one-foo {
+    color: red;
+  }
+
+  .link-two-foo {
+    color: red;
+  }
+
+  .link-three-foo {
+    color: red;
+  }
+</style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte
new file mode 100644
index 0000000..5d6b127
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-suffix01-input.svelte
@@ -0,0 +1,34 @@
+<script>
+  import { value } from "package";
+
+  const derived = value + "-link-three";
+</script>
+
+<a>Click me!</a>
+
+<a class={value + "-link-one"}>Click me two!</a>
+
+<a class={value + "-link-one"}>Click me two!</a>
+
+<a class={`${value}-link-two`}>Click me three!</a>
+
+<a class={`${value}-link-two`}>Click me three!</a>
+
+
+<a class={derived}>Click me four!</a>
+
+<a class={derived}>Click me four!</a>
+
+<style>
+  .foo-link-one {
+    color: red;
+  }
+
+  .foo-link-two {
+    color: red;
+  }
+
+  .foo-link-three {
+    color: red;
+  }
+</style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-universal01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-universal01-input.svelte
new file mode 100644
index 0000000..72ea834
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class-dynamic-universal01-input.svelte
@@ -0,0 +1,23 @@
+<script>
+  import { value } from "package";
+</script>
+
+<a>Click me!</a>
+
+<a class={value}>Click me two!</a>
+
+<a class={value}>Click me two!</a>
+
+<style>
+  .link-one {
+    color: red;
+  }
+
+  .link-two {
+    color: red;
+  }
+
+  .link-three {
+    color: red;
+  }
+</style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-prefix01-input.svelte
new file mode 100644
index 0000000..3e6743a
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-prefix01-input.svelte
@@ -0,0 +1,27 @@
+<script>
+  import { value } from "package";
+
+  const derived = "link-three-" + value;
+</script>
+
+<a>Click me!</a>
+
+<a id={"link-one-" + value}>Click me two!</a>
+
+<a id={`link-two-${value}`}>Click me three!</a>
+
+<a id={derived}>Click me four!</a>
+
+<style>
+  #link-one-foo {
+    color: red;
+  }
+
+  #link-two-foo {
+    color: red;
+  }
+
+  #link-three-foo {
+    color: red;
+  }
+</style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-suffix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-suffix01-input.svelte
new file mode 100644
index 0000000..dc701ed
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-suffix01-input.svelte
@@ -0,0 +1,27 @@
+<script>
+  import { value } from "package";
+
+  const derived = value + "-link-three";
+</script>
+
+<a>Click me!</a>
+
+<a id={value + "-link-one"}>Click me two!</a>
+
+<a id={`${value}-link-two`}>Click me three!</a>
+
+<a id={derived}>Click me four!</a>
+
+<style>
+  #foo-link-one {
+    color: red;
+  }
+
+  #foo-link-two {
+    color: red;
+  }
+
+  #foo-link-three {
+    color: red;
+  }
+</style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-universal01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-universal01-input.svelte
new file mode 100644
index 0000000..4579365
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/id-dynamic-universal01-input.svelte
@@ -0,0 +1,23 @@
+<script>
+  import { value } from "package";
+</script>
+
+<a>Click me!</a>
+
+<a id={value}>Click me two!</a>
+
+<a id={value}>Click me two!</a>
+
+<style>
+  #link-one {
+    color: red;
+  }
+
+  #link-two {
+    color: red;
+  }
+
+  #link-three {
+    color: red;
+  }
+</style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-config.json
new file mode 100644
index 0000000..e68fc5b
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-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-bind1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-input.svelte
new file mode 100644
index 0000000..a59bfcf
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind1-input.svelte
@@ -0,0 +1,8 @@
+<script lang="ts">
+	import { SvelteSet } from 'svelte/reactivity';
+	import Bug3 from './Bug3.svelte';
+
+	let svelteSet = $state(new SvelteSet<number>([]));
+</script>
+
+<Bug3 bind:svelteSet />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-config.json
new file mode 100644
index 0000000..e68fc5b
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-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-bind2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-input.svelte
new file mode 100644
index 0000000..ec454a7
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unnecessary-state-wrap/valid/allow-reassign-bind2-input.svelte
@@ -0,0 +1,8 @@
+<script lang="ts">
+	import { SvelteSet } from 'svelte/reactivity';
+	import Bug3 from './Bug3.svelte';
+
+	let svelteSet = $state(new SvelteSet<number>([]));
+</script>
+
+<Bug3 bind:svelteSet={() => svelteSet, (v) => (svelteSet = v)} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-errors.yaml
new file mode 100644
index 0000000..89c1a84
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-errors.yaml
@@ -0,0 +1,4 @@
+- message: "'test' is an unused Props property."
+  line: 7
+  column: 8
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-input.svelte
new file mode 100644
index 0000000..59f2319
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/alias-input.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+	type Props = {
+		test: string;
+		'aria-label'?: string;
+	};
+
+	const { 'aria-label': foo }: Props = $props();
+</script>
+
+{foo}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/alias-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/alias-input.svelte
new file mode 100644
index 0000000..857e653
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/alias-input.svelte
@@ -0,0 +1,11 @@
+<script lang="ts">
+	type Props = {
+		test: string;
+		'aria-label'?: string;
+	};
+
+	const { test, 'aria-label': ariaLabel }: Props = $props();
+</script>
+
+<h1>{test}</h1>
+<div aria-label={ariaLabel}>svelte/no-unused-props does not always respect aliases</div>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-config.json
new file mode 100644
index 0000000..57afa3f
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-config.json
@@ -0,0 +1,7 @@
+{
+	"options": [
+		{
+			"allowUnusedNestedProperties": true
+		}
+	]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-input.svelte
new file mode 100644
index 0000000..8293761
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused-input.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+	interface Props {
+		user: {
+			name: string;
+			location: string;
+		};
+	}
+	let props: Props = $props();
+	console.log(props.user.name);
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-config.json
new file mode 100644
index 0000000..57afa3f
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-config.json
@@ -0,0 +1,7 @@
+{
+	"options": [
+		{
+			"allowUnusedNestedProperties": true
+		}
+	]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-input.svelte
new file mode 100644
index 0000000..0d38a31
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/nested-unused2-input.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+	interface Props {
+		user: {
+			name: string;
+			age: number; // Won't be reported as unused
+		};
+	}
+	let { user }: Props = $props();
+	console.log(user.name);
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json
new file mode 100644
index 0000000..0192b10
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0-0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml
new file mode 100644
index 0000000..600473a
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml
@@ -0,0 +1,15 @@
+- message: Prefer using writable $derived instead of $state and $effect
+  line: 4
+  column: 6
+  suggestions:
+    - desc: Rewrite $state and $effect to $derived
+      messageId: suggestRewrite
+      output: |
+        <script>
+        	const { albumName } = $props();
+
+        	let newAlbumName = $derived(albumName);
+        	;
+        </script>
+
+        <input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte
new file mode 100644
index 0000000..51523f5
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte
@@ -0,0 +1,10 @@
+<script>
+	const { albumName } = $props();
+
+	let newAlbumName = $state(albumName);
+	$effect(() => {
+		newAlbumName = albumName;
+	});
+</script>
+
+<input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml
new file mode 100644
index 0000000..b89390b
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml
@@ -0,0 +1,15 @@
+- message: Prefer using writable $derived instead of $state and $effect
+  line: 4
+  column: 6
+  suggestions:
+    - desc: Rewrite $state and $effect to $derived
+      messageId: suggestRewrite
+      output: |
+        <script>
+        	const { albumName } = $props();
+
+        	let newAlbumName = $derived(albumName + albumName);
+        	;
+        </script>
+
+        <input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte
new file mode 100644
index 0000000..e8b6769
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte
@@ -0,0 +1,10 @@
+<script>
+	const { albumName } = $props();
+
+	let newAlbumName = $state(albumName);
+	$effect(() => {
+		newAlbumName = albumName + albumName;
+	});
+</script>
+
+<input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml
new file mode 100644
index 0000000..600473a
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml
@@ -0,0 +1,15 @@
+- message: Prefer using writable $derived instead of $state and $effect
+  line: 4
+  column: 6
+  suggestions:
+    - desc: Rewrite $state and $effect to $derived
+      messageId: suggestRewrite
+      output: |
+        <script>
+        	const { albumName } = $props();
+
+        	let newAlbumName = $derived(albumName);
+        	;
+        </script>
+
+        <input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte
new file mode 100644
index 0000000..fdafad8
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte
@@ -0,0 +1,10 @@
+<script>
+	const { albumName } = $props();
+
+	let newAlbumName = $state(albumName);
+	$effect.pre(() => {
+		newAlbumName = albumName;
+	});
+</script>
+
+<input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml
new file mode 100644
index 0000000..b89390b
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml
@@ -0,0 +1,15 @@
+- message: Prefer using writable $derived instead of $state and $effect
+  line: 4
+  column: 6
+  suggestions:
+    - desc: Rewrite $state and $effect to $derived
+      messageId: suggestRewrite
+      output: |
+        <script>
+        	const { albumName } = $props();
+
+        	let newAlbumName = $derived(albumName + albumName);
+        	;
+        </script>
+
+        <input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte
new file mode 100644
index 0000000..ee0e91b
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte
@@ -0,0 +1,10 @@
+<script>
+	const { albumName } = $props();
+
+	let newAlbumName = $state(albumName);
+	$effect.pre(() => {
+		newAlbumName = albumName + albumName;
+	});
+</script>
+
+<input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml
new file mode 100644
index 0000000..f443ce8
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml
@@ -0,0 +1,19 @@
+- message: Prefer using writable $derived instead of $state and $effect
+  line: 4
+  column: 6
+  suggestions:
+    - desc: Rewrite $state and $effect to $derived
+      messageId: suggestRewrite
+      output: |
+        <script>
+        	const { albumName } = $props();
+
+        	let newAlbumName = $derived(albumName);
+        	;
+
+        	setInterval(() => {
+        		newAlbumName = albumName + albumName;
+        	}, 1000);
+        </script>
+
+        <input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte
new file mode 100644
index 0000000..52fce48
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte
@@ -0,0 +1,14 @@
+<script>
+	const { albumName } = $props();
+
+	let newAlbumName = $state(albumName);
+	$effect(() => {
+		newAlbumName = albumName;
+	});
+
+	setInterval(() => {
+		newAlbumName = albumName + albumName;
+	}, 1000);
+</script>
+
+<input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml
new file mode 100644
index 0000000..d771364
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml
@@ -0,0 +1,19 @@
+- message: Prefer using writable $derived instead of $state and $effect
+  line: 4
+  column: 6
+  suggestions:
+    - desc: Rewrite $state and $effect to $derived
+      messageId: suggestRewrite
+      output: |
+        <script>
+        	const { albumName } = $props();
+
+        	let newAlbumName = $derived(albumName);
+        	;
+        </script>
+
+        <FooComponent
+        	doSomething={(value) => {
+        		newAlbumName = value;
+        	}}
+        />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte
new file mode 100644
index 0000000..9f42e65
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte
@@ -0,0 +1,14 @@
+<script>
+	const { albumName } = $props();
+
+	let newAlbumName = $state(albumName);
+	$effect(() => {
+		newAlbumName = albumName;
+	});
+</script>
+
+<FooComponent
+	doSomething={(value) => {
+		newAlbumName = value;
+	}}
+/>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml
new file mode 100644
index 0000000..0118d5e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml
@@ -0,0 +1,38 @@
+- message: Prefer using writable $derived instead of $state and $effect
+  line: 4
+  column: 6
+  suggestions:
+    - desc: Rewrite $state and $effect to $derived
+      messageId: suggestRewrite
+      output: |
+        <script>
+        	const { albumName } = $props();
+
+        	let newAlbumName = $derived(albumName);
+        	;
+
+        	$effect(() => {
+        		newAlbumName = albumName;
+        	});
+        </script>
+
+        <input bind:value={newAlbumName} />
+- message: Prefer using writable $derived instead of $state and $effect
+  line: 4
+  column: 6
+  suggestions:
+    - desc: Rewrite $state and $effect to $derived
+      messageId: suggestRewrite
+      output: |
+        <script>
+        	const { albumName } = $props();
+
+        	let newAlbumName = $derived(albumName);
+        	$effect(() => {
+        		newAlbumName = albumName;
+        	});
+
+        	;
+        </script>
+
+        <input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte
new file mode 100644
index 0000000..bb6169e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte
@@ -0,0 +1,14 @@
+<script>
+	const { albumName } = $props();
+
+	let newAlbumName = $state(albumName);
+	$effect(() => {
+		newAlbumName = albumName;
+	});
+
+	$effect(() => {
+		newAlbumName = albumName;
+	});
+</script>
+
+<input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json
new file mode 100644
index 0000000..0192b10
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0-0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte
new file mode 100644
index 0000000..57bd658
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte
@@ -0,0 +1,13 @@
+<script>
+	const { albumName } = $props();
+
+	let newAlbumName = $state(albumName);
+	$effect(() => {
+		if (albumName === '') {
+			return;
+		}
+		newAlbumName = albumName;
+	});
+</script>
+
+<input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte
new file mode 100644
index 0000000..9083881
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte
@@ -0,0 +1,17 @@
+<script>
+	const { albumName } = $props();
+
+	let newAlbumName = $state(albumName);
+
+	// In practice, this can be converted to $derived, but it’s difficult to detect in all cases.
+	// So the rule doesn’t report it for now.
+	$effect(() => {
+		if (albumName === '') {
+			newAlbumName = albumName + albumName;
+		} else {
+			newAlbumName = albumName;
+		}
+	});
+</script>
+
+<input bind:value={newAlbumName} />
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/_requirements.json
new file mode 100644
index 0000000..4986613
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/_requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_config.json
new file mode 100644
index 0000000..a0f52ed
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_config.json
@@ -0,0 +1,3 @@
+{
+	"options": [{ "checkAsyncFunctions": true }]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_requirements.json
new file mode 100644
index 0000000..4986613
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/_requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-errors.yaml
new file mode 100644
index 0000000..4c26d4e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Component event name must start with "on".
+  line: 3
+  column: 5
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-input.svelte
new file mode 100644
index 0000000..0b05029
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async-arrow01-input.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+  interface Props {
+    custom: () => Promise<void>;
+  }
+
+  let { custom }: Props = $props();
+
+  void custom();
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-errors.yaml
new file mode 100644
index 0000000..4c26d4e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Component event name must start with "on".
+  line: 3
+  column: 5
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-input.svelte
new file mode 100644
index 0000000..4f6f3ee
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/checkAsyncFunctions/async01-input.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+  interface Props {
+    custom(): Promise<void>;
+  }
+
+  let { custom }: Props = $props();
+
+  void custom();
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-errors.yaml
new file mode 100644
index 0000000..4c26d4e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Component event name must start with "on".
+  line: 3
+  column: 5
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-input.svelte
new file mode 100644
index 0000000..7874e51
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-arrow01-input.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+  interface Props {
+    custom: () => void;
+  }
+
+  let { custom }: Props = $props();
+
+  custom();
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-errors.yaml
new file mode 100644
index 0000000..affa6e1
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Component event name must start with "on".
+  line: 2
+  column: 21
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-input.svelte
new file mode 100644
index 0000000..3fc3c61
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix-inline-type01-input.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+  let { custom }: { custom(): void } = $props();
+
+  custom();
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-errors.yaml
new file mode 100644
index 0000000..4c26d4e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Component event name must start with "on".
+  line: 3
+  column: 5
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-input.svelte
new file mode 100644
index 0000000..036a1d6
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/invalid/no-prefix01-input.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+  interface Props {
+    custom(): void;
+  }
+
+  let { custom }: Props = $props();
+
+  custom();
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/_requirements.json
new file mode 100644
index 0000000..4986613
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/_requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/any01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/any01-input.svelte
new file mode 100644
index 0000000..deeb825
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/any01-input.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+  interface Props {
+    custom: any;
+  }
+
+  let { custom }: Props = $props();
+
+  console.log(custom);
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/async01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/async01-input.svelte
new file mode 100644
index 0000000..4f6f3ee
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/async01-input.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+  interface Props {
+    custom(): Promise<void>;
+  }
+
+  let { custom }: Props = $props();
+
+  void custom();
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/non-function01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/non-function01-input.svelte
new file mode 100644
index 0000000..d23e387
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/non-function01-input.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+  interface Props {
+    custom: number;
+  }
+
+  let { custom }: Props = $props();
+
+  console.log(custom);
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/with-prefix01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/with-prefix01-input.svelte
new file mode 100644
index 0000000..e3396af
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-event-prefix/valid/with-prefix01-input.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+  interface Props {
+    oncustom(): void;
+  }
+
+  let { oncustom }: Props = $props();
+
+  oncustom();
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/src/configs/all.ts b/packages/eslint-plugin-svelte/tests/src/configs/all.ts
index 3ad9a2a..124b195 100644
--- a/packages/eslint-plugin-svelte/tests/src/configs/all.ts
+++ b/packages/eslint-plugin-svelte/tests/src/configs/all.ts
@@ -1,16 +1,20 @@
 import assert from 'assert';
-import semver from 'semver';
 import plugin from '../../../src/index.js';
-import { ESLint } from '../../utils/eslint-compat.js';
+import { loadESLint, type ESLint as ESLintClass } from 'eslint';
 
 describe('`all` config', () => {
+	let ESLint: typeof ESLintClass;
+
+	before(async () => {
+		ESLint = await loadESLint({ useFlatConfig: true });
+	});
+
 	it('`all` config should work. ', async () => {
-		if (semver.satisfies(ESLint.version, '<8.0.0')) return;
 		const code = `<script>const a = 1, b = 2;</script>{@html a+b}`;
 
 		const linter = new ESLint({
-			overrideConfigFile: true as never,
-			overrideConfig: plugin.configs['flat/all'] as never
+			overrideConfigFile: true,
+			overrideConfig: plugin.configs['flat/all']
 		});
 		const result = await linter.lintText(code, { filePath: 'test.svelte' });
 		const messages = result[0].messages;
diff --git a/packages/eslint-plugin-svelte/tests/src/configs/base.ts b/packages/eslint-plugin-svelte/tests/src/configs/base.ts
index ac070c0..25553e8 100644
--- a/packages/eslint-plugin-svelte/tests/src/configs/base.ts
+++ b/packages/eslint-plugin-svelte/tests/src/configs/base.ts
@@ -1,17 +1,21 @@
 import assert from 'assert';
-import semver from 'semver';
 import plugin from '../../../src/index.js';
-import { ESLint } from '../../utils/eslint-compat.js';
+import { loadESLint, type ESLint as ESLintClass } from 'eslint';
 
 describe('`base` config', () => {
+	let ESLint: typeof ESLintClass;
+
+	before(async () => {
+		ESLint = await loadESLint({ useFlatConfig: true });
+	});
+
 	it('`base` config should work. ', async () => {
-		if (semver.satisfies(ESLint.version, '<8.0.0')) return;
 		const code = `<script>const a = 1, b = 2;</script>
 <!-- eslint-disable-next-line svelte/no-at-html-tags -->
 {@html a+b}
 {@html a+b}`;
 		const linter = new ESLint({
-			overrideConfigFile: true as never,
+			overrideConfigFile: true,
 			overrideConfig: [
 				...plugin.configs['flat/base'],
 				{
@@ -19,7 +23,7 @@ describe('`base` config', () => {
 						'svelte/no-at-html-tags': 'error'
 					}
 				}
-			] as never
+			]
 		});
 		const result = await linter.lintText(code, { filePath: 'test.svelte' });
 		const messages = result[0].messages;
diff --git a/packages/eslint-plugin-svelte/tests/src/configs/recommended.ts b/packages/eslint-plugin-svelte/tests/src/configs/recommended.ts
index c284e27..0ab4e22 100644
--- a/packages/eslint-plugin-svelte/tests/src/configs/recommended.ts
+++ b/packages/eslint-plugin-svelte/tests/src/configs/recommended.ts
@@ -1,16 +1,20 @@
 import assert from 'assert';
-import semver from 'semver';
 import plugin from '../../../src/index.js';
-import { ESLint } from '../../utils/eslint-compat.js';
+import { loadESLint, type ESLint as ESLintClass } from 'eslint';
 
 describe('`recommended` config', () => {
+	let ESLint: typeof ESLintClass;
+
+	before(async () => {
+		ESLint = await loadESLint({ useFlatConfig: true });
+	});
+
 	it('`recommended` config should work. ', async () => {
-		if (semver.satisfies(ESLint.version, '<8.0.0')) return;
 		const code = `<script>const a = 1, b = 2;</script>{@html a+b}`;
 
 		const linter = new ESLint({
-			overrideConfigFile: true as never,
-			overrideConfig: plugin.configs['flat/recommended'] as never
+			overrideConfigFile: true,
+			overrideConfig: plugin.configs['flat/recommended']
 		});
 		const result = await linter.lintText(code, { filePath: 'test.svelte' });
 		const messages = result[0].messages;
diff --git a/packages/eslint-plugin-svelte/tests/src/rules/comment-directive.ts b/packages/eslint-plugin-svelte/tests/src/rules/comment-directive.ts
index 65efc02..5dec048 100644
--- a/packages/eslint-plugin-svelte/tests/src/rules/comment-directive.ts
+++ b/packages/eslint-plugin-svelte/tests/src/rules/comment-directive.ts
@@ -1,36 +1,40 @@
 import assert from 'assert';
 import plugin from '../../../src/index.js';
-import { ESLint } from '../../utils/eslint-compat.js';
+import { loadESLint, type ESLint as ESLintClass } from 'eslint';
 import * as svelteParser from 'svelte-eslint-parser';
 
 // -----------------------------------------------------------------------------
 // Tests
 // -----------------------------------------------------------------------------
 
-// Initialize linter.
-const linter = new ESLint({
-	// @ts-expect-error -- Type error?
-	baseConfig: {
-		languageOptions: {
-			parser: svelteParser,
-			ecmaVersion: 2020
-		},
-		plugins: {
-			svelte: plugin
-		},
-		rules: {
-			'no-undef': 'error',
-			'space-infix-ops': 'error',
-			'svelte/no-at-html-tags': 'error',
-			'svelte/comment-directive': 'error'
-		},
-		processor: 'svelte/svelte',
-		files: ['**']
-	},
-	overrideConfigFile: true
-});
-
 describe('comment-directive', () => {
+	let ESLint: typeof ESLintClass;
+	let linter: InstanceType<typeof ESLintClass>;
+
+	before(async () => {
+		ESLint = await loadESLint({ useFlatConfig: true });
+		linter = new ESLint({
+			baseConfig: {
+				languageOptions: {
+					parser: svelteParser,
+					ecmaVersion: 2020
+				},
+				plugins: {
+					svelte: plugin
+				},
+				rules: {
+					'no-undef': 'error',
+					'space-infix-ops': 'error',
+					'svelte/no-at-html-tags': 'error',
+					'svelte/comment-directive': 'error'
+				},
+				processor: 'svelte/svelte',
+				files: ['**']
+			},
+			overrideConfigFile: true
+		});
+	});
+
 	describe('eslint-disable/eslint-enable', () => {
 		it('disable all rules if <!-- eslint-disable -->', async () => {
 			const code = `
@@ -351,27 +355,29 @@ describe('comment-directive', () => {
 	});
 
 	describe('reportUnusedDisableDirectives', () => {
-		const linter = new ESLint({
-			// @ts-expect-error -- Type error?
-			baseConfig: {
-				languageOptions: {
-					parser: svelteParser,
-					ecmaVersion: 2020
-				},
-				plugins: {
-					svelte: plugin
-				},
-				rules: {
-					'no-unused-vars': 'error',
-					'svelte/comment-directive': ['error', { reportUnusedDisableDirectives: true }],
-					'svelte/no-at-html-tags': 'error',
-					'svelte/no-at-debug-tags': 'error'
+		before(() => {
+			linter = new ESLint({
+				baseConfig: {
+					languageOptions: {
+						parser: svelteParser,
+						ecmaVersion: 2020
+					},
+					plugins: {
+						svelte: plugin
+					},
+					rules: {
+						'no-unused-vars': 'error',
+						'svelte/comment-directive': ['error', { reportUnusedDisableDirectives: true }],
+						'svelte/no-at-html-tags': 'error',
+						'svelte/no-at-debug-tags': 'error'
+					},
+					files: ['**'],
+					processor: 'svelte/svelte'
 				},
-				files: ['**'],
-				processor: 'svelte/svelte'
-			},
-			overrideConfigFile: true
+				overrideConfigFile: true
+			});
 		});
+
 		it('report unused <!-- eslint-disable -->', async () => {
 			const code = `
         <!-- eslint-disable -->
diff --git a/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts
new file mode 100644
index 0000000..80e9065
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from '../../utils/eslint-compat.js';
+import rule from '../../../src/rules/prefer-writable-derived.js';
+import { loadTestCases } from '../../utils/utils.js';
+
+const tester = new RuleTester({
+	languageOptions: {
+		ecmaVersion: 2020,
+		sourceType: 'module'
+	}
+});
+
+tester.run('prefer-writable-derived', rule as any, loadTestCases('prefer-writable-derived'));
diff --git a/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts b/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts
new file mode 100644
index 0000000..7818735
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/src/rules/require-event-prefix.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from '../../utils/eslint-compat.js';
+import rule from '../../../src/rules/require-event-prefix.js';
+import { loadTestCases } from '../../utils/utils.js';
+
+const tester = new RuleTester({
+	languageOptions: {
+		ecmaVersion: 2020,
+		sourceType: 'module'
+	}
+});
+
+tester.run('require-event-prefix', rule as any, loadTestCases('require-event-prefix'));
diff --git a/packages/eslint-plugin-svelte/tests/src/settings/ignore-warnings.ts b/packages/eslint-plugin-svelte/tests/src/settings/ignore-warnings.ts
index e6223dc..685bff9 100644
--- a/packages/eslint-plugin-svelte/tests/src/settings/ignore-warnings.ts
+++ b/packages/eslint-plugin-svelte/tests/src/settings/ignore-warnings.ts
@@ -1,9 +1,15 @@
 import assert from 'assert';
 import plugin from '../../../src/index.js';
-import { ESLint } from '../../utils/eslint-compat.js';
+import { loadESLint, type ESLint as ESLintClass } from 'eslint';
 import * as svelteParser from 'svelte-eslint-parser';
 
 describe('ignore-warnings', () => {
+	let ESLint: typeof ESLintClass;
+
+	before(async () => {
+		ESLint = await loadESLint({ useFlatConfig: true });
+	});
+
 	it('disable rules if ignoreWarnings: [ruleName]', async () => {
 		const code = `
       {@html a+b}
diff --git a/packages/eslint-plugin-svelte/tests/utils/eslint-compat.ts b/packages/eslint-plugin-svelte/tests/utils/eslint-compat.ts
index 7cfa3dd..7c9e93f 100644
--- a/packages/eslint-plugin-svelte/tests/utils/eslint-compat.ts
+++ b/packages/eslint-plugin-svelte/tests/utils/eslint-compat.ts
@@ -1,9 +1,9 @@
-import { getLegacyESLint, getESLint } from 'eslint-compat-utils/eslint';
-import { getLinter } from 'eslint-compat-utils/linter';
-import { getRuleTester, getRuleIdPrefix } from 'eslint-compat-utils/rule-tester';
+import * as eslint from 'eslint';
+import * as experimental from 'eslint/use-at-your-own-risk';
 
-export const LegacyESLint = getLegacyESLint();
-export const ESLint = getESLint();
-export const RuleTester = getRuleTester();
-export const TEST_RULE_ID_PREFIX = getRuleIdPrefix();
-export const Linter = getLinter();
+type MaybeHasRuleTester = {
+	FlatRuleTester?: typeof eslint.RuleTester;
+};
+
+export const RuleTester =
+	(experimental as never as MaybeHasRuleTester).FlatRuleTester ?? eslint.RuleTester;
diff --git a/packages/eslint-plugin-svelte/tests/utils/utils.ts b/packages/eslint-plugin-svelte/tests/utils/utils.ts
index 2177447..845ea65 100644
--- a/packages/eslint-plugin-svelte/tests/utils/utils.ts
+++ b/packages/eslint-plugin-svelte/tests/utils/utils.ts
@@ -7,7 +7,7 @@ import { applyFixes } from './source-code-fixer.js';
 import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
 import semver from 'semver';
 import { writeAndFormat } from '../../tools/lib/write.js';
-import { Linter } from './eslint-compat.js';
+import { Linter } from 'eslint';
 import * as svelteParser from 'svelte-eslint-parser';
 import * as typescriptParser from '@typescript-eslint/parser';
 import Module from 'module';