Skip to content

Commit 71eca84

Browse files
feat(prefer-const): add rule (#933)
Co-authored-by: baseballyama <[email protected]>
1 parent 117e60d commit 71eca84

File tree

13 files changed

+228
-0
lines changed

13 files changed

+228
-0
lines changed

.changeset/green-squids-compete.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
Add `prefer-const` rule

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ These rules relate to better ways of doing things to help you avoid problems:
367367
| [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 | |
368368
| [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
369369
| [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
370+
| [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: |
370371
| [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: |
371372
| [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | |
372373
| [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | |

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ These rules relate to better ways of doing things to help you avoid problems:
6464
| [svelte/no-unused-class-name](./rules/no-unused-class-name.md) | disallow the use of a class in the template without a corresponding style | |
6565
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
6666
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
67+
| [svelte/prefer-const](./rules/prefer-const.md) | Require `const` declarations for variables that are never reassigned after declared | :wrench: |
6768
| [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
6869
| [svelte/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | |
6970
| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | |

docs/rules/prefer-const.md

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
pageClass: 'rule-details'
3+
sidebarDepth: 0
4+
title: 'svelte/prefer-const'
5+
description: 'Require `const` declarations for variables that are never reassigned after declared'
6+
---
7+
8+
# svelte/prefer-const
9+
10+
> Require `const` declarations for variables that are never reassigned after declared
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
- :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.
14+
15+
## :book: Rule Details
16+
17+
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.
18+
19+
<!--eslint-skip-->
20+
21+
```svelte
22+
<script>
23+
/* eslint svelte/prefer-const: "error" */
24+
25+
// ✓ GOOD
26+
const { a, b } = $props();
27+
let c = $state('');
28+
let d = $derived(a * 2);
29+
let e = $derived.by(() => b * 2);
30+
31+
// ✗ BAD
32+
let obj = { a, b };
33+
let g = $state(0);
34+
let h = $state({ count: 1 });
35+
</script>
36+
37+
<input bind:value={c} />
38+
<input bind:value={h.count} />
39+
```
40+
41+
## :wrench: Options
42+
43+
```json
44+
{
45+
"svelte/prefer-const": [
46+
"error",
47+
{
48+
"destructuring": "any",
49+
"ignoreReadonly": true
50+
}
51+
]
52+
}
53+
```
54+
55+
- `destructuring`: The kind of the way to address variables in destructuring. There are 2 values:
56+
- `any` (default): if any variables in destructuring should be const, this rule warns for those variables.
57+
- `all`: if all variables in destructuring should be const, this rule warns the variables. Otherwise, ignores them.
58+
- `ignoreReadonly`: If `true`, this rule will ignore variables that are read between the declaration and the _first_ assignment.
59+
60+
## :books: Further Reading
61+
62+
- See [ESLint `prefer-const` rule](https://eslint.org/docs/latest/rules/prefer-const) for more information about the base rule.
63+
64+
## :mag: Implementation
65+
66+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/prefer-const.ts)
67+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts)
68+
69+
<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/prefer-const)</sup>

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

+10
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ export interface RuleOptions {
264264
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-class-directive/
265265
*/
266266
'svelte/prefer-class-directive'?: Linter.RuleEntry<SveltePreferClassDirective>
267+
/**
268+
* Require `const` declarations for variables that are never reassigned after declared
269+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-const/
270+
*/
271+
'svelte/prefer-const'?: Linter.RuleEntry<SveltePreferConst>
267272
/**
268273
* destructure values from object stores for better change tracking & fewer redraws
269274
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/
@@ -485,6 +490,11 @@ type SvelteNoUselessMustaches = []|[{
485490
type SveltePreferClassDirective = []|[{
486491
prefer?: ("always" | "empty")
487492
}]
493+
// ----- svelte/prefer-const -----
494+
type SveltePreferConst = []|[{
495+
destructuring?: ("any" | "all")
496+
ignoreReadBeforeAssign?: boolean
497+
}]
488498
// ----- svelte/shorthand-attribute -----
489499
type SvelteShorthandAttribute = []|[{
490500
prefer?: ("always" | "never")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { TSESTree } from '@typescript-eslint/types';
2+
3+
import { createRule } from '../utils/index.js';
4+
import { defineWrapperListener, getCoreRule } from '../utils/eslint-core.js';
5+
6+
const coreRule = getCoreRule('prefer-const');
7+
8+
/**
9+
* Finds and returns the callee of a declaration node within variable declarations or object patterns.
10+
*/
11+
function findDeclarationCallee(node: TSESTree.Expression) {
12+
const { parent } = node;
13+
if (parent.type === 'VariableDeclarator' && parent.init?.type === 'CallExpression') {
14+
return parent.init.callee;
15+
}
16+
17+
return null;
18+
}
19+
20+
/**
21+
* Determines if a declaration should be skipped in the const preference analysis.
22+
* Specifically checks for Svelte's state management utilities ($props, $derived).
23+
*/
24+
function shouldSkipDeclaration(declaration: TSESTree.Expression | null) {
25+
if (!declaration) {
26+
return false;
27+
}
28+
29+
const callee = findDeclarationCallee(declaration);
30+
if (!callee) {
31+
return false;
32+
}
33+
34+
if (callee.type === 'Identifier' && ['$props', '$derived'].includes(callee.name)) {
35+
return true;
36+
}
37+
38+
if (callee.type !== 'MemberExpression' || callee.object.type !== 'Identifier') {
39+
return false;
40+
}
41+
42+
if (
43+
callee.object.name === '$derived' &&
44+
callee.property.type === 'Identifier' &&
45+
callee.property.name === 'by'
46+
) {
47+
return true;
48+
}
49+
50+
return false;
51+
}
52+
53+
export default createRule('prefer-const', {
54+
meta: {
55+
...coreRule.meta,
56+
docs: {
57+
description: coreRule.meta.docs.description,
58+
category: 'Best Practices',
59+
recommended: false,
60+
extensionRule: 'prefer-const'
61+
}
62+
},
63+
create(context) {
64+
return defineWrapperListener(coreRule, context, {
65+
createListenerProxy(coreListener) {
66+
return {
67+
...coreListener,
68+
VariableDeclaration(node) {
69+
for (const decl of node.declarations) {
70+
if (shouldSkipDeclaration(decl.init)) {
71+
return;
72+
}
73+
}
74+
75+
coreListener.VariableDeclaration?.(node);
76+
}
77+
};
78+
}
79+
});
80+
}
81+
});

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

+2
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ export interface SourceCode {
232232

233233
getLines(): string[];
234234

235+
getDeclaredVariables(node: TSESTree.Node): Variable[];
236+
235237
getAllComments(): AST.Comment[];
236238

237239
getComments(node: NodeOrToken): {

packages/eslint-plugin-svelte/src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import noUnusedClassName from '../rules/no-unused-class-name.js';
5252
import noUnusedSvelteIgnore from '../rules/no-unused-svelte-ignore.js';
5353
import noUselessMustaches from '../rules/no-useless-mustaches.js';
5454
import preferClassDirective from '../rules/prefer-class-directive.js';
55+
import preferConst from '../rules/prefer-const.js';
5556
import preferDestructuredStoreProps from '../rules/prefer-destructured-store-props.js';
5657
import preferStyleDirective from '../rules/prefer-style-directive.js';
5758
import requireEachKey from '../rules/require-each-key.js';
@@ -120,6 +121,7 @@ export const rules = [
120121
noUnusedSvelteIgnore,
121122
noUselessMustaches,
122123
preferClassDirective,
124+
preferConst,
123125
preferDestructuredStoreProps,
124126
preferStyleDirective,
125127
requireEachKey,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
- message: "'zero' is never reassigned. Use 'const' instead."
2+
line: 3
3+
column: 6
4+
suggestions: null
5+
- message: "'state' is never reassigned. Use 'const' instead."
6+
line: 4
7+
column: 6
8+
suggestions: null
9+
- message: "'raw' is never reassigned. Use 'const' instead."
10+
line: 5
11+
column: 6
12+
suggestions: null
13+
- message: "'doubled' is never reassigned. Use 'const' instead."
14+
line: 6
15+
column: 6
16+
suggestions: null
17+
- message: "'calculated' is never reassigned. Use 'const' instead."
18+
line: 8
19+
column: 6
20+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
let { prop1, prop2 } = $props();
3+
let zero = 0;
4+
let state = $state(0);
5+
let raw = $state.raw(0);
6+
let doubled = state * 2;
7+
let derived = $derived(state * 2);
8+
let calculated = calc();
9+
let derivedBy = $derived.by(calc());
10+
let noInit;
11+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
let { prop1, prop2 } = $props();
3+
const zero = 0;
4+
const state = $state(0);
5+
const raw = $state.raw(0);
6+
const doubled = state * 2;
7+
let derived = $derived(state * 2);
8+
const calculated = calc();
9+
let derivedBy = $derived.by(calc());
10+
let noInit;
11+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script>
2+
const a = {};
3+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { RuleTester } from '../../utils/eslint-compat';
2+
import rule from '../../../src/rules/prefer-const';
3+
import { loadTestCases } from '../../utils/utils';
4+
5+
const tester = new RuleTester({
6+
languageOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: 'module'
9+
},
10+
});
11+
12+
tester.run('prefer-const', rule as any, loadTestCases('prefer-const'));

0 commit comments

Comments
 (0)