Skip to content

Commit 9cfb0d8

Browse files
committed
feat: signal-prefer-let rule
1 parent f8f377f commit 9cfb0d8

File tree

11 files changed

+163
-0
lines changed

11 files changed

+163
-0
lines changed

.changeset/proud-donuts-tickle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
New svelte/signal-prefer-let rule

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ These rules relate to better ways of doing things to help you avoid problems:
430430
| [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | |
431431
| [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | |
432432
| [svelte/require-stores-init](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store | |
433+
| [svelte/signal-prefer-let](https://sveltejs.github.io/eslint-plugin-svelte/rules/signal-prefer-let/) | use let instead of const for signals values | :bulb: |
433434
| [svelte/valid-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-each-key/) | enforce keys to use variables defined in the `{#each}` block | |
434435

435436
## Stylistic Issues

docs/rules.md

+1
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/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | |
6868
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |
6969
| [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store | |
70+
| [svelte/signal-prefer-let](./rules/signal-prefer-let.md) | use let instead of const for signals values | :bulb: |
7071
| [svelte/valid-each-key](./rules/valid-each-key.md) | enforce keys to use variables defined in the `{#each}` block | |
7172

7273
## Stylistic Issues

docs/rules/signal-prefer-let.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
pageClass: 'rule-details'
3+
sidebarDepth: 0
4+
title: 'svelte/signal-prefer-let'
5+
description: 'use let instead of const for signals values'
6+
---
7+
8+
# svelte/signal-prefer-let
9+
10+
> use let instead of const for signals values
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+
- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
14+
15+
## :book: Rule Details
16+
17+
This rule reports whenever a signal is assigned to a const.
18+
In JavaScript `const` are defined as immutable references which cannot be reassigned.
19+
Signals are by definition changing and are reassigned by Svelte's reactivity system.
20+
21+
<ESLintCodeBlock>
22+
23+
<!--eslint-skip-->
24+
25+
```svelte
26+
<script>
27+
/* eslint svelte/signal-prefer-let: "error" */
28+
29+
/* ✓ GOOD */
30+
let { value } = $props();
31+
32+
let doubled = $derived(value * 2);
33+
34+
/* ✗ BAD */
35+
const { value } = $props();
36+
37+
const doubled = $derived(value * 2);
38+
</script>
39+
```
40+
41+
</ESLintCodeBlock>
42+
43+
## :wrench: Options
44+
45+
Nothing
46+
47+
## :mag: Implementation
48+
49+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/signal-prefer-let.ts)
50+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/signal-prefer-let.ts)

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

+5
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,11 @@ export interface RuleOptions {
299299
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-directive/
300300
*/
301301
'svelte/shorthand-directive'?: Linter.RuleEntry<SvelteShorthandDirective>
302+
/**
303+
* use let instead of const for signals values
304+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/signal-prefer-let/
305+
*/
306+
'svelte/signal-prefer-let'?: Linter.RuleEntry<[]>
302307
/**
303308
* enforce order of attributes
304309
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-attributes/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { TSESTree } from '@typescript-eslint/types';
2+
import { createRule } from '../utils';
3+
4+
export default createRule('signal-prefer-let', {
5+
meta: {
6+
docs: {
7+
description: 'use let instead of const for signals values',
8+
category: 'Best Practices',
9+
recommended: false
10+
},
11+
schema: [],
12+
messages: {
13+
useLet: "const is used for a signal value. Use 'let' instead.",
14+
replaceConst: "Replace 'const' with 'let'"
15+
},
16+
type: 'suggestion',
17+
hasSuggestions: true
18+
},
19+
create(context) {
20+
function preferLet(node: TSESTree.VariableDeclaration) {
21+
if (node.kind !== 'const') {
22+
return;
23+
}
24+
context.report({
25+
node,
26+
messageId: 'useLet',
27+
suggest: [
28+
{
29+
messageId: 'replaceConst',
30+
fix: (fixer) => fixer.replaceTextRange([node.range[0], node.range[0] + 5], 'let')
31+
}
32+
]
33+
});
34+
}
35+
36+
return {
37+
'VariableDeclaration > VariableDeclarator > CallExpression > Identifier'(
38+
node: TSESTree.Identifier
39+
) {
40+
if (['$props', '$derived', '$state'].includes(node.name)) {
41+
preferLet(node.parent.parent?.parent as TSESTree.VariableDeclaration);
42+
}
43+
},
44+
'VariableDeclaration > VariableDeclarator > CallExpression > MemberExpression > Identifier'(
45+
node: TSESTree.Identifier
46+
) {
47+
if (
48+
node.name === 'by' &&
49+
((node.parent as TSESTree.MemberExpression).object as TSESTree.Identifier).name ===
50+
'$derived'
51+
) {
52+
preferLet(node.parent.parent?.parent?.parent as TSESTree.VariableDeclaration);
53+
}
54+
}
55+
};
56+
}
57+
});

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

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import requireStoreReactiveAccess from '../rules/require-store-reactive-access';
5959
import requireStoresInit from '../rules/require-stores-init';
6060
import shorthandAttribute from '../rules/shorthand-attribute';
6161
import shorthandDirective from '../rules/shorthand-directive';
62+
import signalPreferLet from '../rules/signal-prefer-let';
6263
import sortAttributes from '../rules/sort-attributes';
6364
import spacedHtmlComment from '../rules/spaced-html-comment';
6465
import system from '../rules/system';
@@ -124,6 +125,7 @@ export const rules = [
124125
requireStoresInit,
125126
shorthandAttribute,
126127
shorthandDirective,
128+
signalPreferLet,
127129
sortAttributes,
128130
spacedHtmlComment,
129131
system,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
- message: "const is used for a signal value. Use 'let' instead."
2+
line: 2
3+
column: 2
4+
suggestions:
5+
- desc: "Replace 'const' with 'let'"
6+
output: |
7+
<script>
8+
let { value, fn } = $props();
9+
10+
const x = $derived.by(fn);
11+
</script>
12+
- message: "const is used for a signal value. Use 'let' instead."
13+
line: 4
14+
column: 2
15+
suggestions:
16+
- desc: "Replace 'const' with 'let'"
17+
output: |
18+
<script>
19+
const { value, fn } = $props();
20+
21+
let x = $derived.by(fn);
22+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
const { value, fn } = $props();
3+
4+
const x = $derived.by(fn);
5+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script>
2+
let { value } = $props();
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/signal-prefer-let';
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('signal-prefer-let', rule as any, loadTestCases('signal-prefer-let'));

0 commit comments

Comments
 (0)