Repository: sveltejs/eslint-plugin-svelte (ts)

Issue:
`consistent-selector-style`: Class names used in `class:` directives can't be rewritten
### Before You File a Bug Report Please Confirm You Have Done The Following...

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have updated to the latest version of the packages.

### What version of ESLint are you using?

9

### What version of `eslint-plugin-svelte` are you using?

3.0.2

### What did you do?

```svelte
<div class:myClass={true} />

<style>
  .myClass {}
</style>
```


### What did you expect to happen?

No issue

### What actually happened?

Issue suggesting a rewrite to a type selector

### Link to **GitHub Repo** with Minimal Reproducible Example

N/A, will send PR instead

### Additional comments

_No response_

Pull request description:
fix(consistent-selector-style): not reporting class selectors used in class directives
Closes #1105


Patch:
diff --git a/.changeset/purple-months-report.md b/.changeset/purple-months-report.md
new file mode 100644
index 0000000..1cea846
--- /dev/null
+++ b/.changeset/purple-months-report.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': patch
+---
+
+fix(consistent-selector-style): not reporting class selectors used in class directives
diff --git a/.changeset/sixty-cars-fail.md b/.changeset/sixty-cars-fail.md
new file mode 100644
index 0000000..b98c40f
--- /dev/null
+++ b/.changeset/sixty-cars-fail.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+feat: add `excludedRunes` option to the `prefer-const` rule
diff --git a/.changeset/warm-plums-smile.md b/.changeset/warm-plums-smile.md
new file mode 100644
index 0000000..5475ec7
--- /dev/null
+++ b/.changeset/warm-plums-smile.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': patch
+---
+
+fix(no-navigation-without-base): ignoring fragment links
diff --git a/docs/rules/prefer-const.md b/docs/rules/prefer-const.md
index a6ad021..8029ea8 100644
--- a/docs/rules/prefer-const.md
+++ b/docs/rules/prefer-const.md
@@ -14,7 +14,7 @@ since: 'v3.0.0-next.6'
 
 ## :book: Rule Details
 
-This rule reports the same as the base ESLint `prefer-const` rule, except that ignores Svelte reactive values such as `$derived` and `$props`. If this rule is active, make sure to disable the base `prefer-const` rule, as it will conflict with this rule.
+This rule reports the same as the base ESLint `prefer-const` rule, except that ignores Svelte reactive values such as `$derived` and `$props` as default. If this rule is active, make sure to disable the base `prefer-const` rule, as it will conflict with this rule.
 
 <!--eslint-skip-->
 
@@ -46,7 +46,8 @@ This rule reports the same as the base ESLint `prefer-const` rule, except that i
     "error",
     {
       "destructuring": "any",
-      "ignoreReadonly": true
+      "ignoreReadonly": true,
+      "excludedRunes": ["$props", "$derived"]
     }
   ]
 }
@@ -56,6 +57,7 @@ This rule reports the same as the base ESLint `prefer-const` rule, except that i
   - `any` (default): if any variables in destructuring should be const, this rule warns for those variables.
   - `all`: if all variables in destructuring should be const, this rule warns the variables. Otherwise, ignores them.
 - `ignoreReadonly`: If `true`, this rule will ignore variables that are read between the declaration and the _first_ assignment.
+- `excludedRunes`: An array of rune names that should be ignored. Even if a rune is declared with `let`, it will still be ignored.
 
 ## :books: Further Reading
 
diff --git a/docs/rules/require-each-key.md b/docs/rules/require-each-key.md
index af8186b..cd0069c 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/keyed-each-blocks)
+- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/svelte/keyed-each-blocks)
 
 ## :rocket: Version
 
diff --git a/package.json b/package.json
index 1551d4a..038a031 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
   "private": true,
   "license": "MIT",
   "funding": "https://github.com/sponsors/ota-meshi",
-  "packageManager": "pnpm@9.15.6",
+  "packageManager": "pnpm@9.15.7",
   "scripts": {
     "prerelease": "cd packages/eslint-plugin-svelte && pnpm clean && pnpm run build && cd ../.. && cp README.md packages/eslint-plugin-svelte",
     "release": "changeset publish",
@@ -24,7 +24,7 @@
     "@typescript-eslint/parser": "^8.16.0",
     "c8": "^10.1.2",
     "env-cmd": "^10.1.0",
-    "eslint": "~9.21.0",
+    "eslint": "~9.22.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 c508428..d50af00 100644
--- a/packages/eslint-plugin-svelte/CHANGELOG.md
+++ b/packages/eslint-plugin-svelte/CHANGELOG.md
@@ -1,5 +1,11 @@
 # eslint-plugin-svelte
 
+## 3.0.3
+
+### Patch Changes
+
+- [#1115](https://github.com/sveltejs/eslint-plugin-svelte/pull/1115) [`7f756b8`](https://github.com/sveltejs/eslint-plugin-svelte/commit/7f756b8535818008742eead569ebf03065db776b) Thanks [@baseballyama](https://github.com/baseballyama)! - fix: align required Node version with ESLint
+
 ## 3.0.2
 
 ### Patch Changes
diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json
index e8cf5ba..b56698a 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.0.2",
+  "version": "3.0.3",
   "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",
@@ -8,7 +8,7 @@
   "funding": "https://github.com/sponsors/ota-meshi",
   "license": "MIT",
   "engines": {
-    "node": "^18.20.4 || ^20.18.0 || >=22.10.0"
+    "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
   },
   "type": "module",
   "main": "lib/index.js",
@@ -65,7 +65,7 @@
     "postcss-load-config": "^3.1.4",
     "postcss-safe-parser": "^7.0.0",
     "semver": "^7.6.3",
-    "svelte-eslint-parser": "^1.0.0"
+    "svelte-eslint-parser": "^1.0.1"
   },
   "devDependencies": {
     "@babel/core": "^7.26.0",
diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts
index 3de6bf4..c00ff5b 100644
--- a/packages/eslint-plugin-svelte/src/meta.ts
+++ b/packages/eslint-plugin-svelte/src/meta.ts
@@ -2,4 +2,4 @@
 // 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.0.2';
+export const version = '3.0.3';
diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts
index 5e38cc0..88c07ac 100644
--- a/packages/eslint-plugin-svelte/src/rule-types.ts
+++ b/packages/eslint-plugin-svelte/src/rule-types.ts
@@ -530,6 +530,7 @@ type SveltePreferClassDirective = []|[{
 type SveltePreferConst = []|[{
   destructuring?: ("any" | "all")
   ignoreReadBeforeAssign?: boolean
+  excludedRunes?: string[]
 }]
 // ----- svelte/shorthand-attribute -----
 type SvelteShorthandAttribute = []|[{
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 00134c5..5af44ce 100644
--- a/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts
+++ b/packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts
@@ -62,6 +62,7 @@ export default createRule('consistent-selector-style', {
 		const checkGlobal = context.options[0]?.checkGlobal ?? false;
 		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();
@@ -109,6 +110,9 @@ export default createRule('consistent-selector-style', {
 		 * Checks a class selector
 		 */
 		function checkClassSelector(node: SelectorClass): void {
+			if (whitelistedClasses.includes(node.value)) {
+				return;
+			}
 			const selection = classSelections.get(node.value) ?? [];
 			for (const styleValue of style) {
 				if (styleValue === 'class') {
@@ -194,6 +198,9 @@ export default createRule('consistent-selector-style', {
 					addToArrayMap(classSelections, className, 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') {
 						continue;
 					}
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 6f2a209..aaeb4c6 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
@@ -96,14 +96,15 @@ export default createRule('no-navigation-without-base', {
 				}
 				const hrefValue = node.value[0];
 				if (hrefValue.type === 'SvelteLiteral') {
-					if (!expressionIsAbsolute(hrefValue)) {
+					if (!expressionIsAbsolute(hrefValue) && !expressionIsFragment(hrefValue)) {
 						context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' });
 					}
 					return;
 				}
 				if (
 					!expressionStartsWithBase(context, hrefValue.expression, basePathNames) &&
-					!expressionIsAbsolute(hrefValue.expression)
+					!expressionIsAbsolute(hrefValue.expression) &&
+					!expressionIsFragment(hrefValue.expression)
 				) {
 					context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' });
 				}
@@ -348,3 +349,33 @@ function templateLiteralIsAbsolute(url: TSESTree.TemplateLiteral): boolean {
 function urlValueIsAbsolute(url: string): boolean {
 	return url.includes('://');
 }
+
+function expressionIsFragment(url: SvelteLiteral | TSESTree.Expression): boolean {
+	switch (url.type) {
+		case 'BinaryExpression':
+			return binaryExpressionIsFragment(url);
+		case 'Literal':
+			return typeof url.value === 'string' && urlValueIsFragment(url.value);
+		case 'SvelteLiteral':
+			return urlValueIsFragment(url.value);
+		case 'TemplateLiteral':
+			return templateLiteralIsFragment(url);
+		default:
+			return false;
+	}
+}
+
+function binaryExpressionIsFragment(url: TSESTree.BinaryExpression): boolean {
+	return url.left.type !== 'PrivateIdentifier' && expressionIsFragment(url.left);
+}
+
+function templateLiteralIsFragment(url: TSESTree.TemplateLiteral): boolean {
+	return (
+		(url.expressions.length >= 1 && expressionIsFragment(url.expressions[0])) ||
+		(url.quasis.length >= 1 && urlValueIsFragment(url.quasis[0].value.raw))
+	);
+}
+
+function urlValueIsFragment(url: string): boolean {
+	return url.startsWith('#');
+}
diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-const.ts b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts
index 0d079b1..842bbc9 100644
--- a/packages/eslint-plugin-svelte/src/rules/prefer-const.ts
+++ b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts
@@ -21,7 +21,7 @@ function findDeclarationCallee(node: TSESTree.Expression) {
  * Determines if a declaration should be skipped in the const preference analysis.
  * Specifically checks for Svelte's state management utilities ($props, $derived).
  */
-function shouldSkipDeclaration(declaration: TSESTree.Expression | null) {
+function shouldSkipDeclaration(declaration: TSESTree.Expression | null, excludedRunes: string[]) {
 	if (!declaration) {
 		return false;
 	}
@@ -31,7 +31,7 @@ function shouldSkipDeclaration(declaration: TSESTree.Expression | null) {
 		return false;
 	}
 
-	if (callee.type === 'Identifier' && ['$props', '$derived'].includes(callee.name)) {
+	if (callee.type === 'Identifier' && excludedRunes.includes(callee.name)) {
 		return true;
 	}
 
@@ -39,11 +39,7 @@ function shouldSkipDeclaration(declaration: TSESTree.Expression | null) {
 		return false;
 	}
 
-	if (
-		callee.object.name === '$derived' &&
-		callee.property.type === 'Identifier' &&
-		callee.property.name === 'by'
-	) {
+	if (excludedRunes.includes(callee.object.name)) {
 		return true;
 	}
 
@@ -58,16 +54,35 @@ export default createRule('prefer-const', {
 			category: 'Best Practices',
 			recommended: false,
 			extensionRule: 'prefer-const'
-		}
+		},
+		schema: [
+			{
+				type: 'object',
+				properties: {
+					destructuring: { enum: ['any', 'all'] },
+					ignoreReadBeforeAssign: { type: 'boolean' },
+					excludedRunes: {
+						type: 'array',
+						items: {
+							type: 'string'
+						}
+					}
+				},
+				additionalProperties: false
+			}
+		]
 	},
 	create(context) {
+		const config = context.options[0] ?? {};
+		const excludedRunes = config.excludedRunes ?? ['$props', '$derived'];
+
 		return defineWrapperListener(coreRule, context, {
 			createListenerProxy(coreListener) {
 				return {
 					...coreListener,
 					VariableDeclaration(node) {
 						for (const decl of node.declarations) {
-							if (shouldSkipDeclaration(decl.init)) {
+							if (shouldSkipDeclaration(decl.init, excludedRunes)) {
 								return;
 							}
 						}
diff --git a/packages/eslint-plugin-svelte/src/utils/svelte-context.ts b/packages/eslint-plugin-svelte/src/utils/svelte-context.ts
index e4cb124..dfcc9ae 100644
--- a/packages/eslint-plugin-svelte/src/utils/svelte-context.ts
+++ b/packages/eslint-plugin-svelte/src/utils/svelte-context.ts
@@ -62,7 +62,7 @@ function getSvelteFileType(filePath: string): SvelteContext['svelteFileType'] {
 }
 
 function getSvelteKitFileTypeFromFilePath(filePath: string): SvelteContext['svelteKitFileType'] {
-	const fileName = filePath.split('/').pop();
+	const fileName = filePath.split(/[/\\]/).pop();
 	switch (fileName) {
 		case '+page.svelte': {
 			return '+page.svelte';


Test patch:
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte
index f3564d8..b849abc 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-class-type/class01-input.svelte
@@ -8,6 +8,8 @@
 
 <b data-key="val">Text 2</b>
 
+<b class:conditional={true}>Text 3</b>
+
 <style>
   .link {
     color: red;
@@ -32,4 +34,8 @@
   .bold::before {
     color: red;
   }
+
+  .conditional {
+    color: red;
+  }
 </style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-type-class/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-type-class/class01-input.svelte
index 878799b..ea39102 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-type-class/class01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/id-type-class/class01-input.svelte
@@ -8,6 +8,8 @@
 
 <b data-key="val">Text 2</b>
 
+<b class:conditional={true}>Text 3</b>
+
 <style>
   .link {
     color: red;
@@ -32,4 +34,8 @@
   .bold::before {
     color: red;
   }
+
+  .conditional {
+    color: red;
+  }
 </style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-class-id/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-class-id/class01-input.svelte
index b770c05..01c9315 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-class-id/class01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-class-id/class01-input.svelte
@@ -8,6 +8,8 @@
 
 <b data-key="val">Text 3</b>
 
+<b class:conditional={true}>Text 4</b>
+
 <style>
   .link {
     color: red;
@@ -32,4 +34,8 @@
   .bold::before {
     color: red;
   }
+
+  .conditional {
+    color: red;
+  }
 </style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class01-input.svelte
index 92e8c4e..18eb6ab 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id-class/class01-input.svelte
@@ -10,6 +10,12 @@
 
 <b data-key="val">Text 3</b>
 
+<b class:conditional={true}>Text 4</b>
+
+<b class="conditional-two">Text 5</b>
+
+<b class:conditional-two={true}>Text 6</b>
+
 <style>
   .link {
     color: red;
@@ -34,4 +40,12 @@
   .bold::before {
     color: red;
   }
+
+  .conditional {
+    color: red;
+  }
+
+  .conditional-two {
+    color: red;
+  }
 </style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id/class01-input.svelte
index 92e8c4e..d9a2ea3 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id/class01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type-id/class01-input.svelte
@@ -10,6 +10,8 @@
 
 <b data-key="val">Text 3</b>
 
+<b class:conditinal={true}>Text 4</b>
+
 <style>
   .link {
     color: red;
@@ -34,4 +36,8 @@
   .bold::before {
     color: red;
   }
+
+  .conditinal {
+    color: red;
+  }
 </style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type/class01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type/class01-input.svelte
index 2f1bc20..73e331a 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type/class01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/consistent-selector-style/valid/type/class01-input.svelte
@@ -12,6 +12,8 @@
 
 <b data-key="val">Text 3</b>
 
+<b class:conditional={true}>Text 4</b>
+
 <style>
   .link {
     color: red;
@@ -41,4 +43,7 @@
     color: red;
   }
 
+  .conditional {
+    color: red;
+  }
 </style>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-with-fragment01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-with-fragment01-errors.yaml
new file mode 100644
index 0000000..e006d4d
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-with-fragment01-errors.yaml
@@ -0,0 +1,16 @@
+- message: Found a link with a url that isn't prefixed with the base path.
+  line: 4
+  column: 10
+  suggestions: null
+- message: Found a link with a url that isn't prefixed with the base path.
+  line: 5
+  column: 9
+  suggestions: null
+- message: Found a link with a url that isn't prefixed with the base path.
+  line: 6
+  column: 9
+  suggestions: null
+- message: Found a link with a url that isn't prefixed with the base path.
+  line: 7
+  column: 9
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-with-fragment01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-with-fragment01-input.svelte
new file mode 100644
index 0000000..2e6a11d
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/invalid/link-with-fragment01-input.svelte
@@ -0,0 +1,7 @@
+<script>
+	const value = "/foo#section";
+</script>
+<a href="/foo#section">Click me!</a>
+<a href={'/foo#section'}>Click me!</a>
+<a href={'/' + 'foo#section'}>Click me!</a>
+<a href={value}>Click me!</a>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-fragment-url01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-fragment-url01-input.svelte
new file mode 100644
index 0000000..eeaa162
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/link-fragment-url01-input.svelte
@@ -0,0 +1,10 @@
+<script>
+	const section = 'sectionName';
+</script>
+
+<a href="#">Click me!</a>
+<a href="#section">Click me!</a>
+<a href={'#section'}>Click me!</a>
+<a href={'#' + 'section'}>Click me!</a>
+<a href={'#' + section}>Click me!</a>
+<a href={`#${section}`}>Click me!</a>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/_config.json
new file mode 100644
index 0000000..0751a96
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/_config.json
@@ -0,0 +1,3 @@
+{
+	"options": [{ "excludedRunes": [] }]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/test01-errors.yaml
new file mode 100644
index 0000000..03d2d8e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/test01-errors.yaml
@@ -0,0 +1,20 @@
+- message: "'prop1' is never reassigned. Use 'const' instead."
+  line: 2
+  column: 8
+  suggestions: null
+- message: "'prop2' is never reassigned. Use 'const' instead."
+  line: 2
+  column: 15
+  suggestions: null
+- message: "'zero' is never reassigned. Use 'const' instead."
+  line: 3
+  column: 6
+  suggestions: null
+- message: "'derived' is never reassigned. Use 'const' instead."
+  line: 4
+  column: 6
+  suggestions: null
+- message: "'derivedBy' is never reassigned. Use 'const' instead."
+  line: 5
+  column: 6
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/test01-input.svelte
new file mode 100644
index 0000000..97f28d4
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/test01-input.svelte
@@ -0,0 +1,6 @@
+<script>
+	let { prop1, prop2 } = $props();
+	let zero = $state(0);
+	let derived = $derived(zero * 2);
+	let derivedBy = $derived.by(calc());
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/test01-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/test01-output.svelte
new file mode 100644
index 0000000..dd1b99c
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option1/test01-output.svelte
@@ -0,0 +1,6 @@
+<script>
+	const { prop1, prop2 } = $props();
+	const zero = $state(0);
+	const derived = $derived(zero * 2);
+	const derivedBy = $derived.by(calc());
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/_config.json
new file mode 100644
index 0000000..9021ae6
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/_config.json
@@ -0,0 +1,3 @@
+{
+	"options": [{ "excludedRunes": ["$state"] }]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/test01-errors.yaml
new file mode 100644
index 0000000..b784981
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/test01-errors.yaml
@@ -0,0 +1,16 @@
+- message: "'prop1' is never reassigned. Use 'const' instead."
+  line: 2
+  column: 8
+  suggestions: null
+- message: "'prop2' is never reassigned. Use 'const' instead."
+  line: 2
+  column: 15
+  suggestions: null
+- message: "'derived' is never reassigned. Use 'const' instead."
+  line: 4
+  column: 6
+  suggestions: null
+- message: "'derivedBy' is never reassigned. Use 'const' instead."
+  line: 5
+  column: 6
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/test01-input.svelte
new file mode 100644
index 0000000..97f28d4
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/test01-input.svelte
@@ -0,0 +1,6 @@
+<script>
+	let { prop1, prop2 } = $props();
+	let zero = $state(0);
+	let derived = $derived(zero * 2);
+	let derivedBy = $derived.by(calc());
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/test01-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/test01-output.svelte
new file mode 100644
index 0000000..2e25c28
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/option2/test01-output.svelte
@@ -0,0 +1,6 @@
+<script>
+	const { prop1, prop2 } = $props();
+	let zero = $state(0);
+	const derived = $derived(zero * 2);
+	const derivedBy = $derived.by(calc());
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option1/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option1/_config.json
new file mode 100644
index 0000000..0751a96
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option1/_config.json
@@ -0,0 +1,3 @@
+{
+	"options": [{ "excludedRunes": [] }]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option1/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option1/test01-input.svelte
new file mode 100644
index 0000000..dd1b99c
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option1/test01-input.svelte
@@ -0,0 +1,6 @@
+<script>
+	const { prop1, prop2 } = $props();
+	const zero = $state(0);
+	const derived = $derived(zero * 2);
+	const derivedBy = $derived.by(calc());
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option2/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option2/_config.json
new file mode 100644
index 0000000..03a86e4
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option2/_config.json
@@ -0,0 +1,3 @@
+{
+	"options": [{ "excludedRunes": ["$props", "$derived", "$state"] }]
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option2/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option2/test01-input.svelte
new file mode 100644
index 0000000..97f28d4
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/option2/test01-input.svelte
@@ -0,0 +1,6 @@
+<script>
+	let { prop1, prop2 } = $props();
+	let zero = $state(0);
+	let derived = $derived(zero * 2);
+	let derivedBy = $derived.by(calc());
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte
index 5c768eb..33ac253 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte
@@ -1,3 +1,6 @@
 <script>
-	const a = {};
+	let { prop1, prop2 } = $props();
+	const zero = $state(0);
+	let derived = $derived(zero * 2);
+	let derivedBy = $derived.by(calc());
 </script>