Skip to content

Commit 995f0df

Browse files
committed
feat(prefer-let): add rule
1 parent f02b292 commit 995f0df

File tree

16 files changed

+235
-3
lines changed

16 files changed

+235
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ These rules relate to better ways of doing things to help you avoid problems:
370370
| [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
371371
| [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: |
372372
| [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: |
373+
| [svelte/prefer-let](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-let/) | Prefer `let` over `const` for Svelte 5 reactive variable declarations. | :wrench: |
373374
| [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | |
374375
| [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | |
375376
| [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | |

docs/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ These rules relate to better ways of doing things to help you avoid problems:
6767
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
6868
| [svelte/prefer-const](./rules/prefer-const.md) | Require `const` declarations for variables that are never reassigned after declared | :wrench: |
6969
| [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
70+
| [svelte/prefer-let](./rules/prefer-let.md) | Prefer `let` over `const` for Svelte 5 reactive variable declarations. | :wrench: |
7071
| [svelte/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | |
7172
| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | |
7273
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |

docs/rules/prefer-let.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
pageClass: 'rule-details'
3+
sidebarDepth: 0
4+
title: 'svelte/prefer-let'
5+
description: 'Prefer `let` over `const` for Svelte 5 reactive variable declarations.'
6+
---
7+
8+
# svelte/prefer-let
9+
10+
> Prefer `let` over `const` for Svelte 5 reactive variable declarations.
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 usages of `const` variable declarations on Svelte reactive
18+
function assignments. While values may not be reassigned in the code itself,
19+
they are reassigned by Svelte.
20+
21+
<!--eslint-skip-->
22+
23+
```svelte
24+
<script>
25+
/* eslint svelte/prefer-let: "error" */
26+
27+
// ✓ GOOD
28+
let { a, b } = $props();
29+
let c = $state('');
30+
let d = $derived(a * 2);
31+
let e = $derived.by(() => b * 2);
32+
33+
// ✗ BAD
34+
const g = $state(0);
35+
const h = $derived({ count: g });
36+
</script>
37+
```
38+
39+
## :wrench: Options
40+
41+
```json
42+
{
43+
"svelte/prefer-const": [
44+
"error",
45+
{
46+
"exclude": ["$props", "$derived", "$derived.by", "$state", "$state.raw"]
47+
}
48+
]
49+
}
50+
```
51+
52+
- `exclude`: The reactive assignments that you want to exclude from being
53+
reported..
54+
55+
## :mag: Implementation
56+
57+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/prefer-let.ts)
58+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/prefer-let.ts)

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ export interface RuleOptions {
285285
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/
286286
*/
287287
'svelte/prefer-destructured-store-props'?: Linter.RuleEntry<[]>
288+
/**
289+
* Prefer `let` over `const` for Svelte 5 reactive variable declarations.
290+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-let/
291+
*/
292+
'svelte/prefer-let'?: Linter.RuleEntry<SveltePreferLet>
288293
/**
289294
* require style directives instead of style attribute
290295
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/
@@ -513,6 +518,10 @@ type SveltePreferConst = []|[{
513518
destructuring?: ("any" | "all")
514519
ignoreReadBeforeAssign?: boolean
515520
}]
521+
// ----- svelte/prefer-let -----
522+
type SveltePreferLet = []|[{
523+
exclude?: ("$props" | "$derived" | "$derived.by" | "$state" | "$state.raw")[]
524+
}]
516525
// ----- svelte/shorthand-attribute -----
517526
type SvelteShorthandAttribute = []|[{
518527
prefer?: ("always" | "never")
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { TSESTree } from '@typescript-eslint/types';
2+
3+
import { createRule } from '../utils/index.js';
4+
5+
type ReactiveFunction = '$props' | '$derived' | '$derived.by' | '$state' | '$state.raw';
6+
const DEFAULT_FUNCTIONS: ReactiveFunction[] = [
7+
'$props',
8+
'$derived',
9+
'$derived.by',
10+
'$state',
11+
'$state.raw'
12+
];
13+
14+
function getReactiveFunction(callExpr: TSESTree.CallExpression, validNames: string[]) {
15+
if (callExpr.callee.type === 'Identifier') {
16+
if (validNames.includes(callExpr.callee.name)) {
17+
return callExpr.callee.name as ReactiveFunction;
18+
}
19+
} else if (
20+
callExpr.callee.type === 'MemberExpression' &&
21+
callExpr.callee.object.type === 'Identifier' &&
22+
callExpr.callee.property.type === 'Identifier'
23+
) {
24+
const fullName = `${callExpr.callee.object.name}.${callExpr.callee.property.name}`;
25+
26+
if (validNames.includes(fullName)) {
27+
return fullName as ReactiveFunction;
28+
}
29+
}
30+
31+
return null;
32+
}
33+
34+
export default createRule('prefer-let', {
35+
meta: {
36+
docs: {
37+
description: 'Prefer `let` over `const` for Svelte 5 reactive variable declarations.',
38+
category: 'Best Practices',
39+
recommended: false
40+
},
41+
schema: [
42+
{
43+
type: 'object',
44+
properties: {
45+
exclude: {
46+
type: 'array',
47+
items: {
48+
enum: ['$props', '$derived', '$derived.by', '$state', '$state.raw']
49+
},
50+
uniqueItems: true
51+
}
52+
},
53+
additionalProperties: false
54+
}
55+
],
56+
messages: {
57+
'use-let': "'const' is used for a reactive declaration from {{rune}}. Use 'let' instead."
58+
},
59+
type: 'suggestion',
60+
fixable: 'code'
61+
},
62+
create(context) {
63+
const exclude = context.options[0]?.exclude ?? [];
64+
const allowedNames = DEFAULT_FUNCTIONS.filter((name) => !exclude.includes(name));
65+
66+
return {
67+
VariableDeclaration(node: TSESTree.VariableDeclaration) {
68+
if (node.kind === 'const') {
69+
node.declarations.forEach((declarator) => {
70+
const init = declarator.init;
71+
72+
if (!init || init.type !== 'CallExpression') {
73+
return;
74+
}
75+
76+
const rune = getReactiveFunction(init, allowedNames);
77+
if (rune) {
78+
context.report({
79+
node,
80+
messageId: 'use-let',
81+
data: { rune },
82+
fix: (fixer) => fixer.replaceTextRange([node.range[0], node.range[0] + 5], 'let')
83+
});
84+
}
85+
});
86+
}
87+
}
88+
};
89+
}
90+
});

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import noUselessMustaches from '../rules/no-useless-mustaches.js';
5656
import preferClassDirective from '../rules/prefer-class-directive.js';
5757
import preferConst from '../rules/prefer-const.js';
5858
import preferDestructuredStoreProps from '../rules/prefer-destructured-store-props.js';
59+
import preferLet from '../rules/prefer-let.js';
5960
import preferStyleDirective from '../rules/prefer-style-directive.js';
6061
import requireEachKey from '../rules/require-each-key.js';
6162
import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js';
@@ -127,6 +128,7 @@ export const rules = [
127128
preferClassDirective,
128129
preferConst,
129130
preferDestructuredStoreProps,
131+
preferLet,
130132
preferStyleDirective,
131133
requireEachKey,
132134
requireEventDispatcherTypes,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "options": [{ "exclude": ["$derived", "$derived.by"] }] }
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'const' is used for a reactive declaration from $state. Use 'let' instead."
2+
line: 2
3+
column: 2
4+
suggestions: null
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
const a = $state();
3+
const b = $derived(a);
4+
const c = $derived.by(() => b);
5+
</script>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
let a = $state();
3+
const b = $derived(a);
4+
const c = $derived.by(() => b);
5+
</script>

0 commit comments

Comments
 (0)