diff --git a/.changeset/green-squids-compete.md b/.changeset/green-squids-compete.md
new file mode 100644
index 000000000..7666a2ad1
--- /dev/null
+++ b/.changeset/green-squids-compete.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+Add `prefer-const` rule
diff --git a/README.md b/README.md
index 2601875d0..e492f8a76 100644
--- a/README.md
+++ b/README.md
@@ -367,6 +367,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [svelte/no-unused-class-name](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/) | disallow the use of a class in the template without a corresponding style | |
| [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
| [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :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/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | |
| [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | |
diff --git a/docs/rules.md b/docs/rules.md
index 29701ff7e..d2dd3d7a7 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -64,6 +64,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [svelte/no-unused-class-name](./rules/no-unused-class-name.md) | disallow the use of a class in the template without a corresponding style | |
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :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/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | |
| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | |
diff --git a/docs/rules/prefer-const.md b/docs/rules/prefer-const.md
new file mode 100644
index 000000000..f24b70fe2
--- /dev/null
+++ b/docs/rules/prefer-const.md
@@ -0,0 +1,69 @@
+---
+pageClass: 'rule-details'
+sidebarDepth: 0
+title: 'svelte/prefer-const'
+description: 'Require `const` declarations for variables that are never reassigned after declared'
+---
+
+# svelte/prefer-const
+
+> Require `const` declarations for variables that are never reassigned after declared
+
+- :exclamation: **_This rule has not been released yet._**
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :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.
+
+
+
+```svelte
+
+
+
+
+```
+
+## :wrench: Options
+
+```json
+{
+ "svelte/prefer-const": [
+ "error",
+ {
+ "destructuring": "any",
+ "ignoreReadonly": true
+ }
+ ]
+}
+```
+
+- `destructuring`: The kind of the way to address variables in destructuring. There are 2 values:
+ - `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.
+
+## :books: Further Reading
+
+- See [ESLint `prefer-const` rule](https://eslint.org/docs/latest/rules/prefer-const) for more information about the base rule.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/prefer-const.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts)
+
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/prefer-const)
diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts
index 8b7cbe80c..71eae844b 100644
--- a/packages/eslint-plugin-svelte/src/rule-types.ts
+++ b/packages/eslint-plugin-svelte/src/rule-types.ts
@@ -264,6 +264,11 @@ export interface RuleOptions {
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-class-directive/
*/
'svelte/prefer-class-directive'?: Linter.RuleEntry
+ /**
+ * Require `const` declarations for variables that are never reassigned after declared
+ * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-const/
+ */
+ 'svelte/prefer-const'?: Linter.RuleEntry
/**
* destructure values from object stores for better change tracking & fewer redraws
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/
@@ -485,6 +490,11 @@ type SvelteNoUselessMustaches = []|[{
type SveltePreferClassDirective = []|[{
prefer?: ("always" | "empty")
}]
+// ----- svelte/prefer-const -----
+type SveltePreferConst = []|[{
+ destructuring?: ("any" | "all")
+ ignoreReadBeforeAssign?: boolean
+}]
// ----- svelte/shorthand-attribute -----
type SvelteShorthandAttribute = []|[{
prefer?: ("always" | "never")
diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-const.ts b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts
new file mode 100644
index 000000000..0d079b1e1
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts
@@ -0,0 +1,81 @@
+import type { TSESTree } from '@typescript-eslint/types';
+
+import { createRule } from '../utils/index.js';
+import { defineWrapperListener, getCoreRule } from '../utils/eslint-core.js';
+
+const coreRule = getCoreRule('prefer-const');
+
+/**
+ * Finds and returns the callee of a declaration node within variable declarations or object patterns.
+ */
+function findDeclarationCallee(node: TSESTree.Expression) {
+ const { parent } = node;
+ if (parent.type === 'VariableDeclarator' && parent.init?.type === 'CallExpression') {
+ return parent.init.callee;
+ }
+
+ return null;
+}
+
+/**
+ * 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) {
+ if (!declaration) {
+ return false;
+ }
+
+ const callee = findDeclarationCallee(declaration);
+ if (!callee) {
+ return false;
+ }
+
+ if (callee.type === 'Identifier' && ['$props', '$derived'].includes(callee.name)) {
+ return true;
+ }
+
+ if (callee.type !== 'MemberExpression' || callee.object.type !== 'Identifier') {
+ return false;
+ }
+
+ if (
+ callee.object.name === '$derived' &&
+ callee.property.type === 'Identifier' &&
+ callee.property.name === 'by'
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+export default createRule('prefer-const', {
+ meta: {
+ ...coreRule.meta,
+ docs: {
+ description: coreRule.meta.docs.description,
+ category: 'Best Practices',
+ recommended: false,
+ extensionRule: 'prefer-const'
+ }
+ },
+ create(context) {
+ return defineWrapperListener(coreRule, context, {
+ createListenerProxy(coreListener) {
+ return {
+ ...coreListener,
+ VariableDeclaration(node) {
+ for (const decl of node.declarations) {
+ if (shouldSkipDeclaration(decl.init)) {
+ return;
+ }
+ }
+
+ coreListener.VariableDeclaration?.(node);
+ }
+ };
+ }
+ });
+ }
+});
diff --git a/packages/eslint-plugin-svelte/src/types.ts b/packages/eslint-plugin-svelte/src/types.ts
index 7b6452b52..96a6abd9b 100644
--- a/packages/eslint-plugin-svelte/src/types.ts
+++ b/packages/eslint-plugin-svelte/src/types.ts
@@ -232,6 +232,8 @@ export interface SourceCode {
getLines(): string[];
+ getDeclaredVariables(node: TSESTree.Node): Variable[];
+
getAllComments(): AST.Comment[];
getComments(node: NodeOrToken): {
diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts
index 296a7700d..bba9a9975 100644
--- a/packages/eslint-plugin-svelte/src/utils/rules.ts
+++ b/packages/eslint-plugin-svelte/src/utils/rules.ts
@@ -52,6 +52,7 @@ import noUnusedClassName from '../rules/no-unused-class-name.js';
import noUnusedSvelteIgnore from '../rules/no-unused-svelte-ignore.js';
import noUselessMustaches from '../rules/no-useless-mustaches.js';
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 requireEachKey from '../rules/require-each-key.js';
@@ -120,6 +121,7 @@ export const rules = [
noUnusedSvelteIgnore,
noUselessMustaches,
preferClassDirective,
+ preferConst,
preferDestructuredStoreProps,
preferStyleDirective,
requireEachKey,
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-errors.yaml
new file mode 100644
index 000000000..fed423c4a
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-errors.yaml
@@ -0,0 +1,20 @@
+- message: "'zero' is never reassigned. Use 'const' instead."
+ line: 3
+ column: 6
+ suggestions: null
+- message: "'state' is never reassigned. Use 'const' instead."
+ line: 4
+ column: 6
+ suggestions: null
+- message: "'raw' is never reassigned. Use 'const' instead."
+ line: 5
+ column: 6
+ suggestions: null
+- message: "'doubled' is never reassigned. Use 'const' instead."
+ line: 6
+ column: 6
+ suggestions: null
+- message: "'calculated' is never reassigned. Use 'const' instead."
+ line: 8
+ column: 6
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-input.svelte
new file mode 100644
index 000000000..2a9ff2bba
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-input.svelte
@@ -0,0 +1,11 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-output.svelte
new file mode 100644
index 000000000..fb6da26ca
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-output.svelte
@@ -0,0 +1,11 @@
+
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
new file mode 100644
index 000000000..5c768eb34
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte
@@ -0,0 +1,3 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts b/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts
new file mode 100644
index 000000000..ffe176d0a
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from '../../utils/eslint-compat';
+import rule from '../../../src/rules/prefer-const';
+import { loadTestCases } from '../../utils/utils';
+
+const tester = new RuleTester({
+ languageOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ },
+});
+
+tester.run('prefer-const', rule as any, loadTestCases('prefer-const'));