diff --git a/.changeset/proud-donuts-tickle.md b/.changeset/proud-donuts-tickle.md new file mode 100644 index 000000000..68a2fbaf0 --- /dev/null +++ b/.changeset/proud-donuts-tickle.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +New svelte/rune-prefer-let rule diff --git a/README.md b/README.md index 9a6c25600..b5bf563a5 100644 --- a/README.md +++ b/README.md @@ -430,6 +430,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [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 | | | [svelte/require-stores-init](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store | | +| [svelte/rune-prefer-let](https://sveltejs.github.io/eslint-plugin-svelte/rules/rune-prefer-let/) | use let instead of const for reactive variables created by runes | :wrench: | | [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 | | ## Stylistic Issues diff --git a/docs/rules.md b/docs/rules.md index 7f115da5c..2412386c8 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -67,6 +67,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [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 | | | [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store | | +| [svelte/rune-prefer-let](./rules/rune-prefer-let.md) | use let instead of const for reactive variables created by runes | :wrench: | | [svelte/valid-each-key](./rules/valid-each-key.md) | enforce keys to use variables defined in the `{#each}` block | | ## Stylistic Issues diff --git a/docs/rules/rune-prefer-let.md b/docs/rules/rune-prefer-let.md new file mode 100644 index 000000000..502d74fc1 --- /dev/null +++ b/docs/rules/rune-prefer-let.md @@ -0,0 +1,50 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/rune-prefer-let' +description: 'use let instead of const for reactive variables created by runes' +--- + +# svelte/rune-prefer-let + +> use let instead of const for reactive variables created by runes + +- :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 whenever a rune that creates a reactive value is assigned to a const. +In JavaScript `const` are defined as immutable references which cannot be reassigned. +Reactive variables can be reassigned by Svelte's reactivity system. + + + + + +```svelte + +``` + + + +## :wrench: Options + +Nothing + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/rune-prefer-let.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/rune-prefer-let.ts) diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 77d3c4ca4..1e65a9e90 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -289,6 +289,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-stores-init/ */ 'svelte/require-stores-init'?: Linter.RuleEntry<[]> + /** + * use let instead of const for reactive variables created by runes + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/rune-prefer-let/ + */ + 'svelte/rune-prefer-let'?: Linter.RuleEntry<[]> /** * enforce use of shorthand syntax in attribute * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-attribute/ diff --git a/packages/eslint-plugin-svelte/src/rules/rune-prefer-let.ts b/packages/eslint-plugin-svelte/src/rules/rune-prefer-let.ts new file mode 100644 index 000000000..4e552ebc2 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/rune-prefer-let.ts @@ -0,0 +1,65 @@ +import type { TSESTree } from '@typescript-eslint/types'; +import { createRule } from '../utils'; + +export default createRule('rune-prefer-let', { + meta: { + docs: { + description: 'use let instead of const for reactive variables created by runes', + category: 'Best Practices', + recommended: false + }, + schema: [], + messages: { + useLet: "const is used for a reactive variable from {{rune}}. Use 'let' instead." + }, + type: 'suggestion', + fixable: 'code' + }, + create(context) { + function preferLet(node: TSESTree.VariableDeclaration, rune: string) { + if (node.kind !== 'const') { + return; + } + context.report({ + node, + messageId: 'useLet', + data: { rune }, + fix: (fixer) => fixer.replaceTextRange([node.range[0], node.range[0] + 5], 'let') + }); + } + + return { + 'VariableDeclaration > VariableDeclarator > CallExpression > Identifier'( + node: TSESTree.Identifier + ) { + if (['$props', '$derived', '$state'].includes(node.name)) { + preferLet(node.parent.parent?.parent as TSESTree.VariableDeclaration, `${node.name}()`); + } + }, + 'VariableDeclaration > VariableDeclarator > CallExpression > MemberExpression > Identifier'( + node: TSESTree.Identifier + ) { + if ( + node.name === 'by' && + ((node.parent as TSESTree.MemberExpression).object as TSESTree.Identifier).name === + '$derived' + ) { + preferLet( + node.parent.parent?.parent?.parent as TSESTree.VariableDeclaration, + '$derived.by()' + ); + } + if ( + node.name === 'frozen' && + ((node.parent as TSESTree.MemberExpression).object as TSESTree.Identifier).name === + '$state' + ) { + preferLet( + node.parent.parent?.parent?.parent as TSESTree.VariableDeclaration, + '$state.frozen()' + ); + } + } + }; + } +}); diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index af0dd20e6..17f168121 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -57,6 +57,7 @@ import requireOptimizedStyleAttribute from '../rules/require-optimized-style-att import requireStoreCallbacksUseSetParam from '../rules/require-store-callbacks-use-set-param'; import requireStoreReactiveAccess from '../rules/require-store-reactive-access'; import requireStoresInit from '../rules/require-stores-init'; +import runePreferLet from '../rules/rune-prefer-let'; import shorthandAttribute from '../rules/shorthand-attribute'; import shorthandDirective from '../rules/shorthand-directive'; import sortAttributes from '../rules/sort-attributes'; @@ -122,6 +123,7 @@ export const rules = [ requireStoreCallbacksUseSetParam, requireStoreReactiveAccess, requireStoresInit, + runePreferLet, shorthandAttribute, shorthandDirective, sortAttributes, diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-errors.yaml new file mode 100644 index 000000000..43caffafb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-errors.yaml @@ -0,0 +1,20 @@ +- message: const is used for a reactive variable from $props(). Use 'let' instead. + line: 2 + column: 2 + suggestions: null +- message: const is used for a reactive variable from $state(). Use 'let' instead. + line: 4 + column: 2 + suggestions: null +- message: const is used for a reactive variable from $state.frozen(). Use 'let' instead. + line: 5 + column: 2 + suggestions: null +- message: const is used for a reactive variable from $derived(). Use 'let' instead. + line: 7 + column: 2 + suggestions: null +- message: const is used for a reactive variable from $derived.by(). Use 'let' instead. + line: 8 + column: 2 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-input.svelte new file mode 100644 index 000000000..942055dce --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-output.svelte new file mode 100644 index 000000000..ad50f1d6e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-output.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/valid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/valid/test01-input.svelte new file mode 100644 index 000000000..ad50f1d6e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/valid/test01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/eslint-plugin-svelte/tests/src/rules/rune-prefer-let.ts b/packages/eslint-plugin-svelte/tests/src/rules/rune-prefer-let.ts new file mode 100644 index 000000000..a7e923a3d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/rune-prefer-let.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat'; +import rule from '../../../src/rules/rune-prefer-let'; +import { loadTestCases } from '../../utils/utils'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('rune-prefer-let', rule as any, loadTestCases('rune-prefer-let'));