From 9fd05c89eccb5fbe4d5e984600d25bf357a0b7f7 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Sat, 30 Jul 2022 21:28:44 -0700 Subject: [PATCH 1/4] wip: add prefer-reactive-destructuring rule --- README.md | 1 + docs/rules.md | 1 + docs/rules/prefer-reactive-destructuring.md | 55 ++++++++++++++++ src/rules/prefer-reactive-destructuring.ts | 64 +++++++++++++++++++ src/utils/rules.ts | 2 + .../invalid/test01-errors.json | 26 ++++++++ .../invalid/test01-input.svelte | 5 ++ .../valid/test01-input.svelte | 6 ++ .../rules/prefer-reactive-destructuring.ts | 16 +++++ 9 files changed, 176 insertions(+) create mode 100644 docs/rules/prefer-reactive-destructuring.md create mode 100644 src/rules/prefer-reactive-destructuring.ts create mode 100644 tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json create mode 100644 tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte create mode 100644 tests/fixtures/rules/prefer-reactive-destructuring/valid/test01-input.svelte create mode 100644 tests/src/rules/prefer-reactive-destructuring.ts diff --git a/README.md b/README.md index f0a95ec47..6c7e8add9 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/no-reactive-literals](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-reactive-literals/) | Don't assign literal values in reactive statements | :bulb: | | [svelte/no-unused-svelte-ignore](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: | | [svelte/no-useless-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: | +| [svelte/prefer-reactive-destructuring](https://ota-meshi.github.io/eslint-plugin-svelte/rules/prefer-reactive-destructuring/) | Prefer destructuring from objects in reactive statements | | | [svelte/require-optimized-style-attribute](https://ota-meshi.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | | ## Stylistic Issues diff --git a/docs/rules.md b/docs/rules.md index 407aa65d5..61534782f 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -45,6 +45,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/no-reactive-literals](./rules/no-reactive-literals.md) | Don't assign literal values in reactive statements | :bulb: | | [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: | | [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: | +| [svelte/prefer-reactive-destructuring](./rules/prefer-reactive-destructuring.md) | Prefer destructuring from objects in reactive statements | | | [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | | ## Stylistic Issues diff --git a/docs/rules/prefer-reactive-destructuring.md b/docs/rules/prefer-reactive-destructuring.md new file mode 100644 index 000000000..6481dd366 --- /dev/null +++ b/docs/rules/prefer-reactive-destructuring.md @@ -0,0 +1,55 @@ +--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "svelte/prefer-reactive-destructuring" +description: "Prefer destructuring from objects in reactive statements" +--- + +# svelte/prefer-reactive-destructuring + +> Prefer destructuring from objects in reactive statements + +- :exclamation: **_This rule has not been released yet._** + +## :book: Rule Details + +This rule triggers whenever a reactive statement contains an assignment that could be rewritten using destructuring. This is beneficial because it allows svelte's change-tracking to prevent wasteful redraws whenever the object is changed. + +This [svelte REPL](https://svelte.dev/repl/96759f4772314d0a840e11370ef76711) example shows just how effective this can be at preventing bogus redraws. + + + + + +```svelte + + + +``` + + + +## :wrench: Options + +Nothing + +## :heart: Compatibility + +This rule was taken from [@tivac/eslint-plugin-svelte]. +This rule is compatible with `@tivac/svelte/reactive-destructuring` rule. + +[@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/ + +## :mag: Implementation + +- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/prefer-reactive-destructuring.ts) +- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/prefer-reactive-destructuring.ts) diff --git a/src/rules/prefer-reactive-destructuring.ts b/src/rules/prefer-reactive-destructuring.ts new file mode 100644 index 000000000..66e6e8a8d --- /dev/null +++ b/src/rules/prefer-reactive-destructuring.ts @@ -0,0 +1,64 @@ +import type { TSESTree } from "@typescript-eslint/types" +import { createRule } from "../utils" + +export default createRule("prefer-reactive-destructuring", { + meta: { + docs: { + description: "Prefer destructuring from objects in reactive statements", + category: "Best Practices", + recommended: false, + }, + hasSuggestions: true, + schema: [], + messages: { + useDestructuring: `Prefer destructuring in reactive statements`, + suggestDestructuring: `Use destructuring to get finer-grained redraws`, + }, + type: "suggestion", + }, + create(context) { + return { + // Finds: $: info = foo.info + // Suggests: $: ({ info } = foo); + [`SvelteReactiveStatement > ExpressionStatement > AssignmentExpression[left.type="Identifier"][right.type="MemberExpression"]`]( + node: TSESTree.AssignmentExpression, + ) { + const left = node.left as TSESTree.Identifier + const right = node.right as TSESTree.MemberExpression + + const prop = (right.property as TSESTree.Identifier).name + + const source = context.getSourceCode() + const lTokens = source.getTokens(left) + const rTokens = source.getTokens(right) + const matched = prop === left.name + + return context.report({ + node, + loc: node.loc, + messageId: "useDestructuring", + suggest: [ + { + messageId: "suggestDestructuring", + fix: (fixer) => [ + fixer.insertTextBefore( + lTokens[0], + matched ? `({ ` : `({ ${prop}: `, + ), + fixer.insertTextAfter(lTokens[0], ` }`), + fixer.replaceTextRange( + [ + rTokens[rTokens.length - 2].range[0], + rTokens[rTokens.length - 1].range[1], + ], + "", + ), + fixer.insertTextAfter(rTokens[rTokens.length - 1], ")"), + ], + }, + ], + }) + }, + } + }, +}) diff --git a/src/utils/rules.ts b/src/utils/rules.ts index 4874d321f..bc3b599e5 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -24,6 +24,7 @@ import noUnknownStyleDirectiveProperty from "../rules/no-unknown-style-directive import noUnusedSvelteIgnore from "../rules/no-unused-svelte-ignore" import noUselessMustaches from "../rules/no-useless-mustaches" import preferClassDirective from "../rules/prefer-class-directive" +import preferReactiveDestructuring from "../rules/prefer-reactive-destructuring" import preferStyleDirective from "../rules/prefer-style-directive" import requireOptimizedStyleAttribute from "../rules/require-optimized-style-attribute" import shorthandAttribute from "../rules/shorthand-attribute" @@ -59,6 +60,7 @@ export const rules = [ noUnusedSvelteIgnore, noUselessMustaches, preferClassDirective, + preferReactiveDestructuring, preferStyleDirective, requireOptimizedStyleAttribute, shorthandAttribute, diff --git a/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json new file mode 100644 index 000000000..e4207bbd0 --- /dev/null +++ b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json @@ -0,0 +1,26 @@ +[ + { + "message": "Prefer destructuring in reactive statements", + "line": 3, + "column": 8, + "suggestions": [ + { + "desc": "Use destructuring to get finer-grained redraws", + "messageId": "suggestDestructuring", + "output": "\n\n" + } + ] + }, + { + "message": "Prefer destructuring in reactive statements", + "line": 4, + "column": 8, + "suggestions": [ + { + "desc": "Use destructuring to get finer-grained redraws", + "messageId": "suggestDestructuring", + "output": "\n\n" + } + ] + } +] diff --git a/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte new file mode 100644 index 000000000..cad60a0c2 --- /dev/null +++ b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte @@ -0,0 +1,5 @@ + + diff --git a/tests/fixtures/rules/prefer-reactive-destructuring/valid/test01-input.svelte b/tests/fixtures/rules/prefer-reactive-destructuring/valid/test01-input.svelte new file mode 100644 index 000000000..7e544d5a4 --- /dev/null +++ b/tests/fixtures/rules/prefer-reactive-destructuring/valid/test01-input.svelte @@ -0,0 +1,6 @@ + + diff --git a/tests/src/rules/prefer-reactive-destructuring.ts b/tests/src/rules/prefer-reactive-destructuring.ts new file mode 100644 index 000000000..ce3ee66a0 --- /dev/null +++ b/tests/src/rules/prefer-reactive-destructuring.ts @@ -0,0 +1,16 @@ +import { RuleTester } from "eslint" +import rule from "../../../src/rules/prefer-reactive-destructuring" +import { loadTestCases } from "../../utils/utils" + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, +}) + +tester.run( + "prefer-reactive-destructuring", + rule as any, + loadTestCases("prefer-reactive-destructuring"), +) From 00ab7dd8b05ac59de51b047b1412fe23454cc028 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Mon, 1 Aug 2022 13:50:17 -0700 Subject: [PATCH 2/4] wip: smarter token usage --- src/rules/prefer-reactive-destructuring.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/rules/prefer-reactive-destructuring.ts b/src/rules/prefer-reactive-destructuring.ts index 66e6e8a8d..2b7c8c579 100644 --- a/src/rules/prefer-reactive-destructuring.ts +++ b/src/rules/prefer-reactive-destructuring.ts @@ -29,8 +29,11 @@ export default createRule("prefer-reactive-destructuring", { const prop = (right.property as TSESTree.Identifier).name const source = context.getSourceCode() - const lTokens = source.getTokens(left) - const rTokens = source.getTokens(right) + const lToken = source.getFirstToken(left) + const rTokens = source.getLastTokens(right, { + includeComments: true, + count: 2, + }) const matched = prop === left.name return context.report({ @@ -42,18 +45,15 @@ export default createRule("prefer-reactive-destructuring", { messageId: "suggestDestructuring", fix: (fixer) => [ fixer.insertTextBefore( - lTokens[0], + lToken, matched ? `({ ` : `({ ${prop}: `, ), - fixer.insertTextAfter(lTokens[0], ` }`), + fixer.insertTextAfter(lToken, ` }`), fixer.replaceTextRange( - [ - rTokens[rTokens.length - 2].range[0], - rTokens[rTokens.length - 1].range[1], - ], + [rTokens[0].range[0], rTokens[1].range[1]], "", ), - fixer.insertTextAfter(rTokens[rTokens.length - 1], ")"), + fixer.insertTextAfter(rTokens[1], ")"), ], }, ], From e245485482735333988a0f187df7d2dcb62b7800 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Tue, 2 Aug 2022 23:37:42 -0700 Subject: [PATCH 3/4] wip: address feedback --- src/rules/prefer-reactive-destructuring.ts | 38 ++++++++++--------- .../invalid/test01-errors.json | 10 ++++- .../invalid/test01-input.svelte | 1 + 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/rules/prefer-reactive-destructuring.ts b/src/rules/prefer-reactive-destructuring.ts index 2b7c8c579..bc0dd6c7f 100644 --- a/src/rules/prefer-reactive-destructuring.ts +++ b/src/rules/prefer-reactive-destructuring.ts @@ -40,23 +40,27 @@ export default createRule("prefer-reactive-destructuring", { node, loc: node.loc, messageId: "useDestructuring", - suggest: [ - { - messageId: "suggestDestructuring", - fix: (fixer) => [ - fixer.insertTextBefore( - lToken, - matched ? `({ ` : `({ ${prop}: `, - ), - fixer.insertTextAfter(lToken, ` }`), - fixer.replaceTextRange( - [rTokens[0].range[0], rTokens[1].range[1]], - "", - ), - fixer.insertTextAfter(rTokens[1], ")"), - ], - }, - ], + suggest: + // Don't show suggestions for entries like $: info = foo.bar.info, the destructuring + // just looks too gross and complicates the rule too much + right.object.type === "MemberExpression" + ? [] + : [ + { + messageId: "suggestDestructuring", + fix: (fixer) => [ + fixer.insertTextBefore( + lToken, + matched ? `({ ` : `({ ${prop}: `, + ), + fixer.insertTextAfter(lToken, ` }`), + fixer.replaceTextRange( + [rTokens[0].range[0], rTokens[1].range[1]], + ")", + ), + ], + }, + ], }) }, } diff --git a/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json index e4207bbd0..5611e7326 100644 --- a/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json +++ b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json @@ -7,7 +7,7 @@ { "desc": "Use destructuring to get finer-grained redraws", "messageId": "suggestDestructuring", - "output": "\n\n" + "output": "\n\n" } ] }, @@ -19,8 +19,14 @@ { "desc": "Use destructuring to get finer-grained redraws", "messageId": "suggestDestructuring", - "output": "\n\n" + "output": "\n\n" } ] + }, + { + "message": "Prefer destructuring in reactive statements", + "line": 5, + "column": 8, + "suggestions": null } ] diff --git a/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte index cad60a0c2..770829484 100644 --- a/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte +++ b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte @@ -2,4 +2,5 @@ From 6cf01f5d2ae58414a019d8da777fcb22426e12d5 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Tue, 9 Aug 2022 23:14:30 -0700 Subject: [PATCH 4/4] wip: feedback --- src/rules/prefer-reactive-destructuring.ts | 5 ++--- .../invalid/test01-errors.json | 16 ++++++++++++++-- .../invalid/test01-input.svelte | 2 ++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/rules/prefer-reactive-destructuring.ts b/src/rules/prefer-reactive-destructuring.ts index bc0dd6c7f..1bf64316e 100644 --- a/src/rules/prefer-reactive-destructuring.ts +++ b/src/rules/prefer-reactive-destructuring.ts @@ -41,9 +41,8 @@ export default createRule("prefer-reactive-destructuring", { loc: node.loc, messageId: "useDestructuring", suggest: - // Don't show suggestions for entries like $: info = foo.bar.info, the destructuring - // just looks too gross and complicates the rule too much - right.object.type === "MemberExpression" + // Don't show suggestions for complex right-hand values, too tricky to get it right + right.object.type !== "Identifier" || right.computed ? [] : [ { diff --git a/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json index 5611e7326..4236393d4 100644 --- a/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json +++ b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-errors.json @@ -7,7 +7,7 @@ { "desc": "Use destructuring to get finer-grained redraws", "messageId": "suggestDestructuring", - "output": "\n\n" + "output": "\n\n" } ] }, @@ -19,7 +19,7 @@ { "desc": "Use destructuring to get finer-grained redraws", "messageId": "suggestDestructuring", - "output": "\n\n" + "output": "\n\n" } ] }, @@ -28,5 +28,17 @@ "line": 5, "column": 8, "suggestions": null + }, + { + "message": "Prefer destructuring in reactive statements", + "line": 6, + "column": 8, + "suggestions": null + }, + { + "message": "Prefer destructuring in reactive statements", + "line": 7, + "column": 8, + "suggestions": null } ] diff --git a/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte index 770829484..207dbfe4b 100644 --- a/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte +++ b/tests/fixtures/rules/prefer-reactive-destructuring/invalid/test01-input.svelte @@ -3,4 +3,6 @@ $: info = foo.info $: bar = foo.something $: baz = foo.bar.baz + $: qux = foo[bar] + $: dux = foo.baz().bar