From 7a6eba7728030888ab8fae7ffa9145fed00234f8 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Mon, 31 Mar 2025 17:18:53 +0900 Subject: [PATCH 01/13] implement base --- docs/rules/prefer-writable-derived.md | 33 +++++ .../src/rules/prefer-writable-derived.ts | 117 ++++++++++++++++++ .../eslint-plugin-svelte/src/utils/rules.ts | 2 + .../invalid/basic1-errors.yaml | 4 + .../invalid/basic1-input.svelte | 10 ++ .../invalid/basic1-output.svelte | 8 ++ .../invalid/basic2-errors.yaml | 4 + .../invalid/basic2-input.svelte | 10 ++ .../invalid/basic2-output.svelte | 8 ++ .../invalid/effect-pre1-errors.yaml | 4 + .../invalid/effect-pre1-input.svelte | 10 ++ .../invalid/effect-pre1-output.svelte | 8 ++ .../invalid/effect-pre2-errors.yaml | 4 + .../invalid/effect-pre2-input.svelte | 10 ++ .../invalid/effect-pre2-output.svelte | 8 ++ .../valid/has-condition-input.svelte | 13 ++ .../src/rules/prefer-writable-derived.ts | 12 ++ 17 files changed, 265 insertions(+) create mode 100644 docs/rules/prefer-writable-derived.md create mode 100644 packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-output.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-output.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-output.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-output.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/has-condition-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts diff --git a/docs/rules/prefer-writable-derived.md b/docs/rules/prefer-writable-derived.md new file mode 100644 index 000000000..9dad218f9 --- /dev/null +++ b/docs/rules/prefer-writable-derived.md @@ -0,0 +1,33 @@ +# (svelte/prefer-writable-derived) + +> description + +## :book: Rule Details + +This rule reports ???. + + + +```svelte + + + + + +``` + +## :wrench: Options + +```json +{ + "svelte/prefer-writable-derived": ["error", {}] +} +``` + +- + +## :books: Further Reading + +- diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts new file mode 100644 index 000000000..5698da978 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts @@ -0,0 +1,117 @@ +import type { TSESTree } from '@typescript-eslint/types'; +import { createRule } from '../utils/index.js'; +import { getScope } from 'src/utils/ast-utils.js'; +import { getSourceCode } from 'src/utils/compat.js'; + +function isEffectOrEffectPre(node: TSESTree.CallExpression) { + if (node.callee.type === 'Identifier') { + return node.callee.name === '$effect'; + } + if (node.callee.type === 'MemberExpression') { + return ( + node.callee.object.type === 'Identifier' && + node.callee.object.name === '$effect' && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'pre' + ); + } + + return false; +} + +export default createRule('prefer-writable-derived', { + meta: { + docs: { + description: 'Prefer using writable $derived instead of $state and $effect', + category: 'Best Practices', + recommended: true + }, + schema: [], + messages: { + unexpected: 'Prefer using writable $derived instead of $state and $effect' + }, + type: 'suggestion', + conditions: [], + fixable: 'code' + }, + create(context) { + return { + CallExpression: (node: TSESTree.CallExpression) => { + if (!isEffectOrEffectPre(node)) { + return; + } + + if (node.arguments.length !== 1) { + return; + } + + const argument = node.arguments[0]; + if (argument.type !== 'FunctionExpression' && argument.type !== 'ArrowFunctionExpression') { + return; + } + + if (argument.params.length !== 0) { + return; + } + + if (argument.body.type !== 'BlockStatement') { + return; + } + + const body = argument.body.body; + if (body.length !== 1) { + return; + } + + const statement = body[0]; + if (statement.type !== 'ExpressionStatement') { + return; + } + + const expression = statement.expression; + if (expression.type !== 'AssignmentExpression') { + return; + } + + const { left, right, operator } = expression; + if (operator !== '=' || left.type !== 'Identifier') { + return; + } + + const scope = getScope(context, statement); + const reference = scope.references.find((reference) => { + return ( + reference.identifier.type === 'Identifier' && reference.identifier.name === left.name + ); + }); + const defs = reference?.resolved?.defs; + if (defs == null || defs.length !== 1) { + return; + } + + const def = defs[0]; + if (def.type !== 'Variable' || def.node.type !== 'VariableDeclarator') { + return; + } + + const init = def.node.init; + if (init == null || init.type !== 'CallExpression') { + return; + } + + if (init.callee.type !== 'Identifier' || init.callee.name !== '$state') { + return; + } + + context.report({ + node: def.node, + messageId: 'unexpected', + fix: (fixer) => { + const rightCode = getSourceCode(context).getText(right); + return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)]; + } + }); + } + }; + } +}); diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index 3151d17f1..71a014a32 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -60,6 +60,7 @@ 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 preferStyleDirective from '../rules/prefer-style-directive.js'; +import preferWritableDerived from 'src/rules/prefer-writable-derived.js'; import requireEachKey from '../rules/require-each-key.js'; import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js'; import requireOptimizedStyleAttribute from '../rules/require-optimized-style-attribute.js'; @@ -135,6 +136,7 @@ export const rules = [ preferConst, preferDestructuredStoreProps, preferStyleDirective, + preferWritableDerived, requireEachKey, requireEventDispatcherTypes, requireOptimizedStyleAttribute, diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml new file mode 100644 index 000000000..07e89510f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml @@ -0,0 +1,4 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte new file mode 100644 index 000000000..51523f5c3 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-output.svelte new file mode 100644 index 000000000..02d372d54 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-output.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml new file mode 100644 index 000000000..07e89510f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml @@ -0,0 +1,4 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte new file mode 100644 index 000000000..e8b6769c6 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-output.svelte new file mode 100644 index 000000000..f9ce9bfeb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-output.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml new file mode 100644 index 000000000..07e89510f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml @@ -0,0 +1,4 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte new file mode 100644 index 000000000..fdafad808 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-output.svelte new file mode 100644 index 000000000..02d372d54 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-output.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml new file mode 100644 index 000000000..07e89510f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml @@ -0,0 +1,4 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte new file mode 100644 index 000000000..ee0e91b2b --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-output.svelte new file mode 100644 index 000000000..f9ce9bfeb --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-output.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/has-condition-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/has-condition-input.svelte new file mode 100644 index 000000000..57bd65848 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/has-condition-input.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts new file mode 100644 index 000000000..80e9065a7 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat.js'; +import rule from '../../../src/rules/prefer-writable-derived.js'; +import { loadTestCases } from '../../utils/utils.js'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('prefer-writable-derived', rule as any, loadTestCases('prefer-writable-derived')); From 160294db4e2614f29b6c8190c40a575d43f60543 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Mon, 31 Mar 2025 18:50:39 +0900 Subject: [PATCH 02/13] implement --- README.md | 1 + docs/rules.md | 1 + docs/rules/prefer-writable-derived.md | 47 +++++++++++++++---- .../src/configs/flat/recommended.ts | 1 + .../eslint-plugin-svelte/src/rule-types.ts | 5 ++ .../eslint-plugin-svelte/src/utils/rules.ts | 2 +- .../invalid/multiple-reassign1-errors.yaml | 4 ++ .../invalid/multiple-reassign1-input.svelte | 14 ++++++ .../invalid/multiple-reassign1-output.svelte | 12 +++++ .../invalid/multiple-reassign2-errors.yaml | 4 ++ .../invalid/multiple-reassign2-input.svelte | 14 ++++++ .../invalid/multiple-reassign2-output.svelte | 12 +++++ ...n-input.svelte => condition1-input.svelte} | 0 .../valid/condition2-input.svelte | 14 ++++++ 14 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-output.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-output.svelte rename packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/{has-condition-input.svelte => condition1-input.svelte} (100%) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte diff --git a/README.md b/README.md index c379a3d82..572e0cf12 100644 --- a/README.md +++ b/README.md @@ -310,6 +310,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 | :star::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-writable-derived](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/) | Prefer using writable $derived instead of $state and $effect | :star::wrench: | | [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | :star: | | [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | :star: | | [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | | diff --git a/docs/rules.md b/docs/rules.md index df2ca5ec4..d77dfbf8c 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/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :star::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-writable-derived](./rules/prefer-writable-derived.md) | Prefer using writable $derived instead of $state and $effect | :star::wrench: | | [svelte/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | :star: | | [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | :star: | | [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | | diff --git a/docs/rules/prefer-writable-derived.md b/docs/rules/prefer-writable-derived.md index 9dad218f9..b8a97c430 100644 --- a/docs/rules/prefer-writable-derived.md +++ b/docs/rules/prefer-writable-derived.md @@ -1,23 +1,48 @@ -# (svelte/prefer-writable-derived) +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/prefer-writable-derived' +description: 'Prefer using writable $derived instead of $state and $effect' +--- -> description +# svelte/prefer-writable-derived + +> Prefer using writable $derived instead of $state and $effect + +- :exclamation: **_This rule has not been released yet._** +- :gear: This rule is included in `"plugin:svelte/recommended"`. +- :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 ???. +This rule reports when you use a combination of `$state` and `$effect` to create a derived value that can be written to. It encourages using the more concise and clearer `$derived` syntax instead. ```svelte + const { initialValue } = $props(); - + // ✓ GOOD + let value1 = $derived(initialValue); - + // ✗ BAD + let value2 = $state(initialValue); + $effect(() => { + value2 = initialValue; + }); + ``` +The rule specifically looks for patterns where: + +1. You initialize a variable with `$state()` +2. You then use `$effect()` or `$effect.pre()` to assign a new value to that same variable +3. The effect function contains only a single assignment statement + +When this pattern is detected, the rule suggests refactoring to use `$derived()` instead, which provides the same functionality in a more concise way. + ## :wrench: Options ```json @@ -26,8 +51,14 @@ This rule reports ???. } ``` -- +- This rule has no options. ## :books: Further Reading -- +- [Svelte Documentation on Reactivity Primitives](https://svelte.dev/docs/svelte-components#script-2-assignments-are-reactive) +- [Svelte RFC for Reactivity Primitives](https://github.com/sveltejs/rfcs/blob/rfc-better-primitives/text/0000-better-primitives.md) + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/prefer-writable-derived.ts) diff --git a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts index b1b2d51ad..7a08befc8 100644 --- a/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts +++ b/packages/eslint-plugin-svelte/src/configs/flat/recommended.ts @@ -37,6 +37,7 @@ const config: Linter.Config[] = [ 'svelte/no-unused-svelte-ignore': 'error', 'svelte/no-useless-children-snippet': 'error', 'svelte/no-useless-mustaches': 'error', + 'svelte/prefer-writable-derived': 'error', 'svelte/require-each-key': 'error', 'svelte/require-event-dispatcher-types': 'error', 'svelte/require-store-reactive-access': 'error', diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 19e30a345..5da4f15a4 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -306,6 +306,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/ */ 'svelte/prefer-style-directive'?: Linter.RuleEntry<[]> + /** + * Prefer using writable $derived instead of $state and $effect + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/ + */ + 'svelte/prefer-writable-derived'?: Linter.RuleEntry<[]> /** * require keyed `{#each}` block * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/ diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index 71a014a32..baf9f45b4 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -60,7 +60,7 @@ 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 preferStyleDirective from '../rules/prefer-style-directive.js'; -import preferWritableDerived from 'src/rules/prefer-writable-derived.js'; +import preferWritableDerived from '../rules/prefer-writable-derived.js'; import requireEachKey from '../rules/require-each-key.js'; import requireEventDispatcherTypes from '../rules/require-event-dispatcher-types.js'; import requireOptimizedStyleAttribute from '../rules/require-optimized-style-attribute.js'; diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml new file mode 100644 index 000000000..07e89510f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml @@ -0,0 +1,4 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte new file mode 100644 index 000000000..52fce48ad --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-input.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-output.svelte new file mode 100644 index 000000000..aa9fb8f32 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-output.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml new file mode 100644 index 000000000..07e89510f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml @@ -0,0 +1,4 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte new file mode 100644 index 000000000..9f42e657a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-input.svelte @@ -0,0 +1,14 @@ + + + { + newAlbumName = value; + }} +/> diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-output.svelte new file mode 100644 index 000000000..c922fb1ad --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-output.svelte @@ -0,0 +1,12 @@ + + + { + newAlbumName = value; + }} +/> diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/has-condition-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte similarity index 100% rename from packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/has-condition-input.svelte rename to packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition1-input.svelte diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte new file mode 100644 index 000000000..ee79308d3 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte @@ -0,0 +1,14 @@ + + + From 45d66f0cef5040f3420f00a8658c264c6aed6103 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Mon, 31 Mar 2025 18:54:34 +0900 Subject: [PATCH 03/13] more --- .../src/rules/prefer-writable-derived.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts index 5698da978..4ea6b648d 100644 --- a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts +++ b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts @@ -2,6 +2,11 @@ import type { TSESTree } from '@typescript-eslint/types'; import { createRule } from '../utils/index.js'; import { getScope } from 'src/utils/ast-utils.js'; import { getSourceCode } from 'src/utils/compat.js'; +import { VERSION as SVELTE_VERSION } from 'svelte/compiler'; +import semver from 'semver'; + +// Writable derived were introduced in Svelte 5.25.0 +const shouldRun = semver.satisfies(SVELTE_VERSION, '>=5.25.0'); function isEffectOrEffectPre(node: TSESTree.CallExpression) { if (node.callee.type === 'Identifier') { @@ -35,6 +40,9 @@ export default createRule('prefer-writable-derived', { fixable: 'code' }, create(context) { + if (!shouldRun) { + return {}; + } return { CallExpression: (node: TSESTree.CallExpression) => { if (!isEffectOrEffectPre(node)) { From 0df0b7beeb7440f71dbda322042c09e3aa459c39 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Mon, 31 Mar 2025 18:55:11 +0900 Subject: [PATCH 04/13] add changeset --- .changeset/ready-views-burn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ready-views-burn.md diff --git a/.changeset/ready-views-burn.md b/.changeset/ready-views-burn.md new file mode 100644 index 000000000..a5f11d84b --- /dev/null +++ b/.changeset/ready-views-burn.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +feat: add `prefer-writable-derived` rule From 805e533c6b4d5fbd3c4f4a45ad4dd32dba28ac6f Mon Sep 17 00:00:00 2001 From: baseballyama Date: Mon, 31 Mar 2025 18:57:40 +0900 Subject: [PATCH 05/13] add condition --- .../src/rules/prefer-writable-derived.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts index 4ea6b648d..3cc0e2b81 100644 --- a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts +++ b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts @@ -36,7 +36,12 @@ export default createRule('prefer-writable-derived', { unexpected: 'Prefer using writable $derived instead of $state and $effect' }, type: 'suggestion', - conditions: [], + conditions: [ + { + svelteVersions: ['5'], + runes: [true, 'undetermined'] + } + ], fixable: 'code' }, create(context) { From fba4149812a4d97c4c05f799ce6a409bcf4cf84c Mon Sep 17 00:00:00 2001 From: baseballyama Date: Mon, 31 Mar 2025 18:59:08 +0900 Subject: [PATCH 06/13] oops --- .../rules/prefer-writable-derived/invalid/_requirements.json | 3 +++ .../rules/prefer-writable-derived/valid/_requirements.json | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json new file mode 100644 index 000000000..0192b1098 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/_requirements.json @@ -0,0 +1,3 @@ +{ + "svelte": ">=5.0.0-0" +} From 7ee424500d31872fcf9dd77d0d9eb287f4f37b19 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Mon, 31 Mar 2025 19:12:59 +0900 Subject: [PATCH 07/13] fix --- .../eslint-plugin-svelte/src/rules/prefer-writable-derived.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts index 3cc0e2b81..2955a63f9 100644 --- a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts +++ b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts @@ -1,7 +1,7 @@ import type { TSESTree } from '@typescript-eslint/types'; import { createRule } from '../utils/index.js'; -import { getScope } from 'src/utils/ast-utils.js'; -import { getSourceCode } from 'src/utils/compat.js'; +import { getScope } from '../utils/ast-utils.js'; +import { getSourceCode } from '../utils/compat.js'; import { VERSION as SVELTE_VERSION } from 'svelte/compiler'; import semver from 'semver'; From d815818e89fe79700e0b2bf6d614ba36b7fb294a Mon Sep 17 00:00:00 2001 From: baseballyama Date: Mon, 31 Mar 2025 23:52:58 +0900 Subject: [PATCH 08/13] fix --- .../eslint-plugin-svelte/src/rules/prefer-writable-derived.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts index 2955a63f9..5f853ba29 100644 --- a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts +++ b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts @@ -1,7 +1,6 @@ import type { TSESTree } from '@typescript-eslint/types'; import { createRule } from '../utils/index.js'; import { getScope } from '../utils/ast-utils.js'; -import { getSourceCode } from '../utils/compat.js'; import { VERSION as SVELTE_VERSION } from 'svelte/compiler'; import semver from 'semver'; @@ -120,7 +119,7 @@ export default createRule('prefer-writable-derived', { node: def.node, messageId: 'unexpected', fix: (fixer) => { - const rightCode = getSourceCode(context).getText(right); + const rightCode = context.sourceCode.getText(right); return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)]; } }); From f833e0cac70f836cfa61c3cbce44100a8ebdfe19 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Mon, 31 Mar 2025 23:54:01 +0900 Subject: [PATCH 09/13] update --- docs/rules/prefer-writable-derived.md | 6 +----- packages/eslint-plugin-svelte/src/meta.ts | 12 +++++++----- packages/eslint-plugin-svelte/src/rule-types.ts | 4 +--- .../eslint-plugin-svelte/src/type-defs/estree.d.ts | 8 +++++--- packages/eslint-plugin-svelte/src/types-for-node.ts | 8 +++++--- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/rules/prefer-writable-derived.md b/docs/rules/prefer-writable-derived.md index b8a97c430..ca6a844b2 100644 --- a/docs/rules/prefer-writable-derived.md +++ b/docs/rules/prefer-writable-derived.md @@ -45,11 +45,7 @@ When this pattern is detected, the rule suggests refactoring to use `$derived()` ## :wrench: Options -```json -{ - "svelte/prefer-writable-derived": ["error", {}] -} -``` +Nothing. - This rule has no options. diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index 3c8075c6f..a8e3bcc19 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -1,5 +1,7 @@ -// IMPORTANT! -// This file has been automatically generated, -// in order to update its content execute "pnpm run update" -export const name = 'eslint-plugin-svelte'; -export const version = '3.4.0'; +/* + * IMPORTANT! + * This file has been automatically generated, + * in order to update its content execute "pnpm run update" + */ +export const name = 'eslint-plugin-svelte' as const; +export const version = '3.4.0' as const; diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 5da4f15a4..04146b1a1 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -488,9 +488,7 @@ type SvelteNoInlineStyles = []|[{ allowTransitions?: boolean }] // ----- svelte/no-inner-declarations ----- -type SvelteNoInnerDeclarations = []|[("functions" | "both")]|[("functions" | "both"), { - blockScopedFunctions?: ("allow" | "disallow") -}] +type SvelteNoInnerDeclarations = []|[("functions" | "both")] // ----- svelte/no-navigation-without-base ----- type SvelteNoNavigationWithoutBase = []|[{ ignoreGoto?: boolean diff --git a/packages/eslint-plugin-svelte/src/type-defs/estree.d.ts b/packages/eslint-plugin-svelte/src/type-defs/estree.d.ts index fb6a22a2c..a4f6bf062 100644 --- a/packages/eslint-plugin-svelte/src/type-defs/estree.d.ts +++ b/packages/eslint-plugin-svelte/src/type-defs/estree.d.ts @@ -1,6 +1,8 @@ -// IMPORTANT! -// This file has been automatically generated, -// in order to update its content execute "pnpm run update" +/* + * IMPORTANT! + * This file has been automatically generated, + * in order to update its content execute "pnpm run update" + */ // // Replace type information to use "@typescript-eslint/types" instead of "estree". // diff --git a/packages/eslint-plugin-svelte/src/types-for-node.ts b/packages/eslint-plugin-svelte/src/types-for-node.ts index e67a7c5b3..a62c01954 100644 --- a/packages/eslint-plugin-svelte/src/types-for-node.ts +++ b/packages/eslint-plugin-svelte/src/types-for-node.ts @@ -1,6 +1,8 @@ -// IMPORTANT! -// This file has been automatically generated, -// in order to update its content execute "pnpm run update" +/* + * IMPORTANT! + * This file has been automatically generated, + * in order to update its content execute "pnpm run update" + */ // // The information here can be calculated by calculating the type, // but is pre-defined to avoid the computational cost. From 54999bdd6ebaf655205ece39b638e276e9a5c5e6 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Mon, 31 Mar 2025 23:57:12 +0900 Subject: [PATCH 10/13] update --- packages/eslint-plugin-svelte/src/meta.ts | 12 +++++------- packages/eslint-plugin-svelte/src/rule-types.ts | 4 +++- .../eslint-plugin-svelte/src/type-defs/estree.d.ts | 8 +++----- packages/eslint-plugin-svelte/src/types-for-node.ts | 8 +++----- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index a8e3bcc19..3c8075c6f 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -1,7 +1,5 @@ -/* - * IMPORTANT! - * This file has been automatically generated, - * in order to update its content execute "pnpm run update" - */ -export const name = 'eslint-plugin-svelte' as const; -export const version = '3.4.0' as const; +// IMPORTANT! +// This file has been automatically generated, +// in order to update its content execute "pnpm run update" +export const name = 'eslint-plugin-svelte'; +export const version = '3.4.0'; diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 04146b1a1..5da4f15a4 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -488,7 +488,9 @@ type SvelteNoInlineStyles = []|[{ allowTransitions?: boolean }] // ----- svelte/no-inner-declarations ----- -type SvelteNoInnerDeclarations = []|[("functions" | "both")] +type SvelteNoInnerDeclarations = []|[("functions" | "both")]|[("functions" | "both"), { + blockScopedFunctions?: ("allow" | "disallow") +}] // ----- svelte/no-navigation-without-base ----- type SvelteNoNavigationWithoutBase = []|[{ ignoreGoto?: boolean diff --git a/packages/eslint-plugin-svelte/src/type-defs/estree.d.ts b/packages/eslint-plugin-svelte/src/type-defs/estree.d.ts index a4f6bf062..fb6a22a2c 100644 --- a/packages/eslint-plugin-svelte/src/type-defs/estree.d.ts +++ b/packages/eslint-plugin-svelte/src/type-defs/estree.d.ts @@ -1,8 +1,6 @@ -/* - * IMPORTANT! - * This file has been automatically generated, - * in order to update its content execute "pnpm run update" - */ +// IMPORTANT! +// This file has been automatically generated, +// in order to update its content execute "pnpm run update" // // Replace type information to use "@typescript-eslint/types" instead of "estree". // diff --git a/packages/eslint-plugin-svelte/src/types-for-node.ts b/packages/eslint-plugin-svelte/src/types-for-node.ts index a62c01954..e67a7c5b3 100644 --- a/packages/eslint-plugin-svelte/src/types-for-node.ts +++ b/packages/eslint-plugin-svelte/src/types-for-node.ts @@ -1,8 +1,6 @@ -/* - * IMPORTANT! - * This file has been automatically generated, - * in order to update its content execute "pnpm run update" - */ +// IMPORTANT! +// This file has been automatically generated, +// in order to update its content execute "pnpm run update" // // The information here can be calculated by calculating the type, // but is pre-defined to avoid the computational cost. From 15f088168ce5a31ab809f5f480c953faf0e09c3b Mon Sep 17 00:00:00 2001 From: baseballyama Date: Tue, 1 Apr 2025 00:13:00 +0900 Subject: [PATCH 11/13] suggestion --- .../src/rules/prefer-writable-derived.ts | 18 ++++++--- .../invalid/basic1-errors.yaml | 13 ++++++- .../invalid/basic1-output.svelte | 8 ---- .../invalid/basic2-errors.yaml | 13 ++++++- .../invalid/basic2-output.svelte | 8 ---- .../invalid/effect-pre1-errors.yaml | 13 ++++++- .../invalid/effect-pre1-output.svelte | 8 ---- .../invalid/effect-pre2-errors.yaml | 13 ++++++- .../invalid/effect-pre2-output.svelte | 8 ---- .../invalid/multiple-reassign1-errors.yaml | 17 ++++++++- .../invalid/multiple-reassign1-output.svelte | 12 ------ .../invalid/multiple-reassign2-errors.yaml | 17 ++++++++- .../invalid/multiple-reassign2-output.svelte | 12 ------ .../invalid/multiple-reassign3-errors.yaml | 38 +++++++++++++++++++ .../invalid/multiple-reassign3-input.svelte | 14 +++++++ .../valid/condition2-input.svelte | 3 ++ 16 files changed, 147 insertions(+), 68 deletions(-) delete mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-output.svelte delete mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-output.svelte delete mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-output.svelte delete mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-output.svelte delete mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-output.svelte delete mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-output.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts index 5f853ba29..3a02b5afb 100644 --- a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts +++ b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts @@ -32,7 +32,8 @@ export default createRule('prefer-writable-derived', { }, schema: [], messages: { - unexpected: 'Prefer using writable $derived instead of $state and $effect' + unexpected: 'Prefer using writable $derived instead of $state and $effect', + suggestRewrite: 'Rewrite $state and $effect to $derived' }, type: 'suggestion', conditions: [ @@ -41,7 +42,7 @@ export default createRule('prefer-writable-derived', { runes: [true, 'undetermined'] } ], - fixable: 'code' + hasSuggestions: true }, create(context) { if (!shouldRun) { @@ -118,10 +119,15 @@ export default createRule('prefer-writable-derived', { context.report({ node: def.node, messageId: 'unexpected', - fix: (fixer) => { - const rightCode = context.sourceCode.getText(right); - return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)]; - } + suggest: [ + { + messageId: 'suggestRewrite', + fix: (fixer) => { + const rightCode = context.sourceCode.getText(right); + return [fixer.replaceText(init, `$derived(${rightCode})`), fixer.remove(node)]; + } + } + ] }); } }; diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml index 07e89510f..600473adf 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-errors.yaml @@ -1,4 +1,15 @@ - message: Prefer using writable $derived instead of $state and $effect line: 4 column: 6 - suggestions: null + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-output.svelte deleted file mode 100644 index 02d372d54..000000000 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic1-output.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml index 07e89510f..b89390b86 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-errors.yaml @@ -1,4 +1,15 @@ - message: Prefer using writable $derived instead of $state and $effect line: 4 column: 6 - suggestions: null + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-output.svelte deleted file mode 100644 index f9ce9bfeb..000000000 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/basic2-output.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml index 07e89510f..600473adf 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-errors.yaml @@ -1,4 +1,15 @@ - message: Prefer using writable $derived instead of $state and $effect line: 4 column: 6 - suggestions: null + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-output.svelte deleted file mode 100644 index 02d372d54..000000000 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre1-output.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml index 07e89510f..b89390b86 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-errors.yaml @@ -1,4 +1,15 @@ - message: Prefer using writable $derived instead of $state and $effect line: 4 column: 6 - suggestions: null + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-output.svelte deleted file mode 100644 index f9ce9bfeb..000000000 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/effect-pre2-output.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml index 07e89510f..f443ce89a 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-errors.yaml @@ -1,4 +1,19 @@ - message: Prefer using writable $derived instead of $state and $effect line: 4 column: 6 - suggestions: null + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-output.svelte deleted file mode 100644 index aa9fb8f32..000000000 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign1-output.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml index 07e89510f..d77136489 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-errors.yaml @@ -1,4 +1,19 @@ - message: Prefer using writable $derived instead of $state and $effect line: 4 column: 6 - suggestions: null + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + { + newAlbumName = value; + }} + /> diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-output.svelte deleted file mode 100644 index c922fb1ad..000000000 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign2-output.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - { - newAlbumName = value; - }} -/> diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml new file mode 100644 index 000000000..0118d5e29 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-errors.yaml @@ -0,0 +1,38 @@ +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + +- message: Prefer using writable $derived instead of $state and $effect + line: 4 + column: 6 + suggestions: + - desc: Rewrite $state and $effect to $derived + messageId: suggestRewrite + output: | + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte new file mode 100644 index 000000000..bb6169e60 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/invalid/multiple-reassign3-input.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte index ee79308d3..9083881f1 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-writable-derived/valid/condition2-input.svelte @@ -2,6 +2,9 @@ const { albumName } = $props(); let newAlbumName = $state(albumName); + + // In practice, this can be converted to $derived, but it’s difficult to detect in all cases. + // So the rule doesn’t report it for now. $effect(() => { if (albumName === '') { newAlbumName = albumName + albumName; From 2c1d28bd631094e0dfc6110bd9f322d74488352f Mon Sep 17 00:00:00 2001 From: baseballyama Date: Tue, 1 Apr 2025 00:15:09 +0900 Subject: [PATCH 12/13] update --- README.md | 2 +- docs/rules.md | 2 +- docs/rules/prefer-writable-derived.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 572e0cf12..f9c7bfee2 100644 --- a/README.md +++ b/README.md @@ -310,7 +310,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 | :star::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-writable-derived](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/) | Prefer using writable $derived instead of $state and $effect | :star::wrench: | +| [svelte/prefer-writable-derived](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/) | Prefer using writable $derived instead of $state and $effect | :star::bulb: | | [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | :star: | | [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | :star: | | [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | | diff --git a/docs/rules.md b/docs/rules.md index d77dfbf8c..7433dee57 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -67,7 +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 | :star::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-writable-derived](./rules/prefer-writable-derived.md) | Prefer using writable $derived instead of $state and $effect | :star::wrench: | +| [svelte/prefer-writable-derived](./rules/prefer-writable-derived.md) | Prefer using writable $derived instead of $state and $effect | :star::bulb: | | [svelte/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | :star: | | [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | :star: | | [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | | diff --git a/docs/rules/prefer-writable-derived.md b/docs/rules/prefer-writable-derived.md index ca6a844b2..830bec2aa 100644 --- a/docs/rules/prefer-writable-derived.md +++ b/docs/rules/prefer-writable-derived.md @@ -11,7 +11,7 @@ description: 'Prefer using writable $derived instead of $state and $effect' - :exclamation: **_This rule has not been released yet._** - :gear: This rule is included in `"plugin:svelte/recommended"`. -- :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. +- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). ## :book: Rule Details From 238183ef17f0db1514d4752dd6e50b255ab8d04d Mon Sep 17 00:00:00 2001 From: baseballyama Date: Tue, 1 Apr 2025 00:25:36 +0900 Subject: [PATCH 13/13] refactor --- .../src/rules/prefer-writable-derived.ts | 108 ++++++++++-------- 1 file changed, 60 insertions(+), 48 deletions(-) diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts index 3a02b5afb..c7f859dbd 100644 --- a/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts +++ b/packages/eslint-plugin-svelte/src/rules/prefer-writable-derived.ts @@ -7,6 +7,20 @@ import semver from 'semver'; // Writable derived were introduced in Svelte 5.25.0 const shouldRun = semver.satisfies(SVELTE_VERSION, '>=5.25.0'); +type ValidFunctionType = TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression; +type ValidFunction = ValidFunctionType & { + body: TSESTree.BlockStatement; +}; + +type ValidAssignmentExpression = TSESTree.AssignmentExpression & { + operator: '='; + left: TSESTree.Identifier; +}; + +type ValidExpressionStatement = TSESTree.ExpressionStatement & { + expression: ValidAssignmentExpression; +}; + function isEffectOrEffectPre(node: TSESTree.CallExpression) { if (node.callee.type === 'Identifier') { return node.callee.name === '$effect'; @@ -23,6 +37,40 @@ function isEffectOrEffectPre(node: TSESTree.CallExpression) { return false; } +function isValidFunctionArgument(argument: TSESTree.Node): argument is ValidFunction { + if ( + (argument.type !== 'FunctionExpression' && argument.type !== 'ArrowFunctionExpression') || + argument.params.length !== 0 + ) { + return false; + } + + if (argument.body.type !== 'BlockStatement') { + return false; + } + + return argument.body.body.length === 1; +} + +function isValidAssignment(statement: TSESTree.Statement): statement is ValidExpressionStatement { + if (statement.type !== 'ExpressionStatement') return false; + + const { expression } = statement; + return ( + expression.type === 'AssignmentExpression' && + expression.operator === '=' && + expression.left.type === 'Identifier' + ); +} + +function isStateVariable(init: TSESTree.Expression | null): init is TSESTree.CallExpression { + return ( + init?.type === 'CallExpression' && + init.callee.type === 'Identifier' && + init.callee.name === '$state' + ); +} + export default createRule('prefer-writable-derived', { meta: { docs: { @@ -50,69 +98,33 @@ export default createRule('prefer-writable-derived', { } return { CallExpression: (node: TSESTree.CallExpression) => { - if (!isEffectOrEffectPre(node)) { - return; - } - - if (node.arguments.length !== 1) { + if (!isEffectOrEffectPre(node) || node.arguments.length !== 1) { return; } const argument = node.arguments[0]; - if (argument.type !== 'FunctionExpression' && argument.type !== 'ArrowFunctionExpression') { - return; - } - - if (argument.params.length !== 0) { - return; - } - - if (argument.body.type !== 'BlockStatement') { - return; - } - - const body = argument.body.body; - if (body.length !== 1) { - return; - } - - const statement = body[0]; - if (statement.type !== 'ExpressionStatement') { + if (!isValidFunctionArgument(argument)) { return; } - const expression = statement.expression; - if (expression.type !== 'AssignmentExpression') { - return; - } - - const { left, right, operator } = expression; - if (operator !== '=' || left.type !== 'Identifier') { + const statement = argument.body.body[0]; + if (!isValidAssignment(statement)) { return; } + const { left, right } = statement.expression; const scope = getScope(context, statement); - const reference = scope.references.find((reference) => { - return ( - reference.identifier.type === 'Identifier' && reference.identifier.name === left.name - ); - }); - const defs = reference?.resolved?.defs; - if (defs == null || defs.length !== 1) { - return; - } - - const def = defs[0]; - if (def.type !== 'Variable' || def.node.type !== 'VariableDeclarator') { - return; - } + const reference = scope.references.find( + (ref) => ref.identifier.type === 'Identifier' && ref.identifier.name === left.name + ); - const init = def.node.init; - if (init == null || init.type !== 'CallExpression') { + const def = reference?.resolved?.defs?.[0]; + if (!def || def.type !== 'Variable' || def.node.type !== 'VariableDeclarator') { return; } - if (init.callee.type !== 'Identifier' || init.callee.name !== '$state') { + const { init } = def.node; + if (!isStateVariable(init)) { return; }