Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/small-months-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-svelte': major
---

Adds `prefer-let` rule
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [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/prefer-let](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-let/) | Prefer `let` over `const` for Svelte 5 reactive variable declarations. | :wrench: |
| [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` | |
| [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | |
Expand Down
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [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/prefer-let](./rules/prefer-let.md) | Prefer `let` over `const` for Svelte 5 reactive variable declarations. | :wrench: |
| [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` | |
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |
Expand Down
58 changes: 58 additions & 0 deletions docs/rules/prefer-let.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
pageClass: 'rule-details'
sidebarDepth: 0
title: 'svelte/prefer-let'
description: 'Prefer `let` over `const` for Svelte 5 reactive variable declarations.'
---

# svelte/prefer-let

> Prefer `let` over `const` for Svelte 5 reactive variable declarations.

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
- :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 usages of `const` variable declarations on Svelte reactive
function assignments. While values may not be reassigned in the code itself,
they are reassigned by Svelte.

<!--eslint-skip-->

```svelte
<script>
/* eslint svelte/prefer-let: "error" */

// ✓ GOOD
let { a, b } = $props();
let c = $state('');
let d = $derived(a * 2);
let e = $derived.by(() => b * 2);

// ✗ BAD
const g = $state(0);
const h = $derived({ count: g });
</script>
```

## :wrench: Options

```json
{
"svelte/prefer-const": [
"error",
{
"exclude": ["$props", "$derived", "$derived.by", "$state", "$state.raw"]
}
]
}
```

- `exclude`: The reactive assignments that you want to exclude from being
reported..

## :mag: Implementation

- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/prefer-let.ts)
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/prefer-let.ts)
9 changes: 9 additions & 0 deletions packages/eslint-plugin-svelte/src/rule-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ export interface RuleOptions {
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/
*/
'svelte/prefer-destructured-store-props'?: Linter.RuleEntry<[]>
/**
* Prefer `let` over `const` for Svelte 5 reactive variable declarations.
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-let/
*/
'svelte/prefer-let'?: Linter.RuleEntry<SveltePreferLet>
/**
* require style directives instead of style attribute
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/
Expand Down Expand Up @@ -513,6 +518,10 @@ type SveltePreferConst = []|[{
destructuring?: ("any" | "all")
ignoreReadBeforeAssign?: boolean
}]
// ----- svelte/prefer-let -----
type SveltePreferLet = []|[{
exclude?: ("$props" | "$derived" | "$derived.by" | "$state" | "$state.raw")[]
}]
// ----- svelte/shorthand-attribute -----
type SvelteShorthandAttribute = []|[{
prefer?: ("always" | "never")
Expand Down
90 changes: 90 additions & 0 deletions packages/eslint-plugin-svelte/src/rules/prefer-let.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { TSESTree } from '@typescript-eslint/types';

import { createRule } from '../utils/index.js';

type ReactiveFunction = '$props' | '$derived' | '$derived.by' | '$state' | '$state.raw';
const DEFAULT_FUNCTIONS: ReactiveFunction[] = [
'$props',
'$derived',
'$derived.by',
'$state',
'$state.raw'
];

function getReactiveFunction(callExpr: TSESTree.CallExpression, validNames: string[]) {
if (callExpr.callee.type === 'Identifier') {
if (validNames.includes(callExpr.callee.name)) {
return callExpr.callee.name as ReactiveFunction;
}
} else if (
callExpr.callee.type === 'MemberExpression' &&
callExpr.callee.object.type === 'Identifier' &&
callExpr.callee.property.type === 'Identifier'
) {
const fullName = `${callExpr.callee.object.name}.${callExpr.callee.property.name}`;

if (validNames.includes(fullName)) {
return fullName as ReactiveFunction;
}
}

return null;
}

export default createRule('prefer-let', {
meta: {
docs: {
description: 'Prefer `let` over `const` for Svelte 5 reactive variable declarations.',
category: 'Best Practices',
recommended: false
},
schema: [
{
type: 'object',
properties: {
exclude: {
type: 'array',
items: {
enum: ['$props', '$derived', '$derived.by', '$state', '$state.raw']
},
uniqueItems: true
}
},
additionalProperties: false
}
],
messages: {
'use-let': "'const' is used for a reactive declaration from {{rune}}. Use 'let' instead."
},
type: 'suggestion',
fixable: 'code'
},
create(context) {
const exclude = context.options[0]?.exclude ?? [];
const allowedNames = DEFAULT_FUNCTIONS.filter((name) => !exclude.includes(name));

return {
VariableDeclaration(node: TSESTree.VariableDeclaration) {
if (node.kind === 'const') {
node.declarations.forEach((declarator) => {
const init = declarator.init;

if (!init || init.type !== 'CallExpression') {
return;
}

const rune = getReactiveFunction(init, allowedNames);
if (rune) {
context.report({
node,
messageId: 'use-let',
data: { rune },
fix: (fixer) => fixer.replaceTextRange([node.range[0], node.range[0] + 5], 'let')
});
}
});
}
}
};
}
});
2 changes: 2 additions & 0 deletions packages/eslint-plugin-svelte/src/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ 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 preferLet from '../rules/prefer-let.js';
import preferStyleDirective from '../rules/prefer-style-directive.js';
import requireEachKey from '../rules/require-each-key.js';
import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js';
Expand Down Expand Up @@ -127,6 +128,7 @@ export const rules = [
preferClassDirective,
preferConst,
preferDestructuredStoreProps,
preferLet,
preferStyleDirective,
requireEachKey,
requireEventDispatcherTypes,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "options": [{ "exclude": ["$derived", "$derived.by"] }] }
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- message: "'const' is used for a reactive declaration from $state. Use 'let' instead."
line: 2
column: 2
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
const a = $state();
const b = $derived(a);
const c = $derived.by(() => b);
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let a = $state();
const b = $derived(a);
const c = $derived.by(() => b);
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
- message: "'const' is used for a reactive declaration from $props. Use 'let' instead."
line: 2
column: 2
suggestions: null
- message: "'const' is used for a reactive declaration from $state. Use 'let' instead."
line: 3
column: 2
suggestions: null
- message: "'const' is used for a reactive declaration from $state.raw. Use 'let'
instead."
line: 4
column: 2
suggestions: null
- message: "'const' is used for a reactive declaration from $derived. Use 'let'
instead."
line: 5
column: 2
suggestions: null
- message: "'const' is used for a reactive declaration from $derived.by. Use 'let'
instead."
line: 6
column: 2
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
const { prop } = $props();
const state = $state();
const raw = $state.raw();
const derived = $derived(state + 1);
const derivedBy = $derived.by(() => prop + state);
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
let { prop } = $props();
let state = $state();
let raw = $state.raw();
let derived = $derived(state + 1);
let derivedBy = $derived.by(() => prop + state);
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
let { prop } = $props();
let state = $state();
let raw = $state.raw();
let derived = $derived(state + 1);
let derivedBy = $derived.by(() => prop + state);
</script>
6 changes: 3 additions & 3 deletions packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RuleTester } from '../../utils/eslint-compat';
import rule from '../../../src/rules/prefer-const';
import { loadTestCases } from '../../utils/utils';
import { RuleTester } from '../../utils/eslint-compat.js';
import rule from '../../../src/rules/prefer-const.js';
import { loadTestCases } from '../../utils/utils.js';

const tester = new RuleTester({
languageOptions: {
Expand Down
12 changes: 12 additions & 0 deletions packages/eslint-plugin-svelte/tests/src/rules/prefer-let.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { RuleTester } from '../../utils/eslint-compat.js';
import rule from '../../../src/rules/prefer-let.js';
import { loadTestCases } from '../../utils/utils.js';

const tester = new RuleTester({
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module'
},
});

tester.run('prefer-let', rule as any, loadTestCases('prefer-let'));
Loading