diff --git a/.eslintrc.js b/.eslintrc.js index 57826c457..04e5d321d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -43,6 +43,7 @@ module.exports = { }, { files: ["*.svelte"], + extends: ["plugin:svelte/base"], parser: "svelte-eslint-parser", parserOptions: { parser: { diff --git a/README.md b/README.md index f0a95ec47..31bd2ba07 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-destructured-store-props](https://ota-meshi.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/) | (no description) | | | [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..006ad9039 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-destructured-store-props](./rules/prefer-destructured-store-props.md) | (no description) | | | [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-destructured-store-props.md b/docs/rules/prefer-destructured-store-props.md new file mode 100644 index 000000000..7bde2d11d --- /dev/null +++ b/docs/rules/prefer-destructured-store-props.md @@ -0,0 +1,53 @@ +--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "svelte/prefer-destructured-store-props" +description: "Destructure store props for more efficient redraws" +--- + +# svelte/prefer-destructured-store-props + +> Destructure store props for more efficient redraws + +- :exclamation: **_This rule has not been released yet._** + +## :book: Rule Details + +This rule reports on directly accessing properties of a store containing an object. These usages can instead be written as a reactive statement using destructuring to allow for more granular change-tracking and reduced redraws in the component. + +An example of the improvements can be see in this [REPL](https://svelte.dev/repl/7de86fea94ff40c48abb82da534dfb89) + + + + + +```svelte + + + +{foo} + + +{$store.foo} +``` + + + +## :wrench: Options + +Nothing + +## :heart: Compatibility + +This rule was taken from [@tivac/eslint-plugin-svelte]. +This rule is compatible with `@tivac/svelte/store-prop-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-destructured-store-props.ts) +- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/prefer-destructured-store-props.ts) diff --git a/src/rules/prefer-destructured-store-props.ts b/src/rules/prefer-destructured-store-props.ts new file mode 100644 index 000000000..8db050067 --- /dev/null +++ b/src/rules/prefer-destructured-store-props.ts @@ -0,0 +1,130 @@ +import type * as ESTree from "estree" +import type { TSESTree } from "@typescript-eslint/types" +import type { AST } from "svelte-eslint-parser" +import { createRule } from "../utils" +import { getStringIfConstant } from "../utils/ast-utils" + +type NodeRecord = { property: string; node: TSESTree.MemberExpression } + +export default createRule("prefer-destructured-store-props", { + meta: { + docs: { + description: + "Destructure values from object stores for better change tracking & fewer redraws", + category: "Best Practices", + recommended: false, + }, + hasSuggestions: true, + schema: [], + messages: { + useDestructuring: `Destructure {{property}} from {{store}} for better change tracking & fewer redraws`, + fixUseDestructuring: `Using destructuring like $: ({ {{property}} } = {{store}}); will run faster`, + }, + type: "suggestion", + }, + create(context) { + let script: AST.SvelteScriptElement + + // Store off instances of probably-destructurable statements + const reports: NodeRecord[] = [] + + // Store off defined variable names so we can avoid offering a suggestion in those cases + const defined: Set = new Set() + + return { + [`SvelteScriptElement`](node: AST.SvelteScriptElement) { + script = node + }, + + // Capture import names + [`ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier`]( + node: TSESTree.ImportSpecifier, + ) { + const { name } = node.local + + defined.add(name) + }, + + // Capture variable names + [`VariableDeclarator[id.type="Identifier"]`]( + node: TSESTree.VariableDeclarator, + ) { + const { name } = node.id as TSESTree.Identifier + + defined.add(name) + }, + + // {$foo.bar} + // should be + // $: ({ bar } = $foo); + // {bar} + // Same with {$foo["bar"]} + [`MemberExpression[object.name=/^\\$/]`]( + node: TSESTree.MemberExpression, + ) { + const property = + node.property.type === "Identifier" + ? node.property.name + : getStringIfConstant(node.property as ESTree.Expression) + + if (!property) { + return + } + + reports.push({ property, node }) + }, + + [`Program:exit`]() { + reports.forEach(({ property, node }) => { + const store = (node.object as TSESTree.Identifier).name + + context.report({ + node, + messageId: "useDestructuring", + data: { + store, + property, + }, + + // Avoid suggestions for: + // dynamic accesses like {$foo[bar]} + // no \n\n{bar}\n\n{$foo[baz]}\n\n\n{$foo[\"qux\"]}\n" + } + ] + }, + { + "message": "Destructure baz from $foo for better change tracking & fewer redraws", + "line": 8, + "column": 2, + "suggestions": null + }, + { + "message": "Destructure qux from $foo for better change tracking & fewer redraws", + "line": 11, + "column": 2, + "suggestions": null + } +] diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/test01-input.svelte b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test01-input.svelte new file mode 100644 index 000000000..aba95036a --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test01-input.svelte @@ -0,0 +1,11 @@ + + + +{$foo.bar} + +{$foo[baz]} + + +{$foo["qux"]} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/test02-errors.json b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test02-errors.json new file mode 100644 index 000000000..332ca84b4 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test02-errors.json @@ -0,0 +1,20 @@ +[ + { + "message": "Destructure bar from $foo for better change tracking & fewer redraws", + "line": 2, + "column": 2, + "suggestions": null + }, + { + "message": "Destructure bar from $foo for better change tracking & fewer redraws", + "line": 4, + "column": 2, + "suggestions": null + }, + { + "message": "Destructure bar from $foo for better change tracking & fewer redraws", + "line": 7, + "column": 2, + "suggestions": null + } +] diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/test02-input.svelte b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test02-input.svelte new file mode 100644 index 000000000..73b5fe2e6 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test02-input.svelte @@ -0,0 +1,7 @@ + +{$foo.bar} + +{$foo[bar]} + + +{$foo["bar"]} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/test03-errors.json b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test03-errors.json new file mode 100644 index 000000000..96010a682 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test03-errors.json @@ -0,0 +1,20 @@ +[ + { + "message": "Destructure foo from $store for better change tracking & fewer redraws", + "line": 8, + "column": 11, + "suggestions": null + }, + { + "message": "Destructure bar from $store for better change tracking & fewer redraws", + "line": 9, + "column": 11, + "suggestions": null + }, + { + "message": "Destructure baz from $store for better change tracking & fewer redraws", + "line": 22, + "column": 9, + "suggestions": null + } +] diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/test03-input.svelte b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test03-input.svelte new file mode 100644 index 000000000..9df5ed9e9 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test03-input.svelte @@ -0,0 +1,23 @@ + + + +
+ + foo: {foo + " " + Date.now()} +
+
+ + bar: {bar + " " + Date.now()} +
+
+ baz: {$store.baz} +
diff --git a/tests/fixtures/rules/prefer-destructured-store-props/valid/test01-input.svelte b/tests/fixtures/rules/prefer-destructured-store-props/valid/test01-input.svelte new file mode 100644 index 000000000..ccbf18077 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/valid/test01-input.svelte @@ -0,0 +1,4 @@ + +{$foo[`bar${baz}`]} +{foo$.bar} +{f$oo.bar} diff --git a/tests/src/rules/prefer-destructured-store-props.ts b/tests/src/rules/prefer-destructured-store-props.ts new file mode 100644 index 000000000..7cdbf8e3c --- /dev/null +++ b/tests/src/rules/prefer-destructured-store-props.ts @@ -0,0 +1,16 @@ +import { RuleTester } from "eslint" +import rule from "../../../src/rules/prefer-destructured-store-props" +import { loadTestCases } from "../../utils/utils" + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, +}) + +tester.run( + "prefer-destructured-store-props", + rule as any, + loadTestCases("prefer-destructured-store-props"), +)