diff --git a/.changeset/real-buses-complain.md b/.changeset/real-buses-complain.md new file mode 100644 index 000000000..d69feb254 --- /dev/null +++ b/.changeset/real-buses-complain.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-svelte": minor +--- + +feat: add `svelte/prefer-destructured-store-props` rule diff --git a/.eslintrc.js b/.eslintrc.js index 69840b3ff..2d1ef6538 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -50,6 +50,7 @@ module.exports = { }, { files: ["*.svelte"], + extends: ["plugin:svelte/base"], parser: "svelte-eslint-parser", parserOptions: { parser: { diff --git a/README.md b/README.md index 20aff841a..8485e59f8 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,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/) | destructure values from object stores for better change tracking & fewer redraws | :bulb: | | [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 | | | [svelte/require-stores-init](https://ota-meshi.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store | | diff --git a/docs-svelte-kit/src/lib/eslint/ESLintEditor.svelte b/docs-svelte-kit/src/lib/eslint/ESLintEditor.svelte index f269ce5a2..233471755 100644 --- a/docs-svelte-kit/src/lib/eslint/ESLintEditor.svelte +++ b/docs-svelte-kit/src/lib/eslint/ESLintEditor.svelte @@ -219,10 +219,11 @@ edits: [ { resource: model.uri, - edit: { + textEdit: { range: editRange, text: fix.text, }, + versionId: model.getVersionId(), }, ], }, diff --git a/docs-svelte-kit/src/lib/eslint/MonacoEditor.svelte b/docs-svelte-kit/src/lib/eslint/MonacoEditor.svelte index 5e1901bc8..5aa2f482b 100644 --- a/docs-svelte-kit/src/lib/eslint/MonacoEditor.svelte +++ b/docs-svelte-kit/src/lib/eslint/MonacoEditor.svelte @@ -58,7 +58,7 @@ monaco.languages.registerCodeActionProvider(language, { provideCodeActions(model, range, context) { const editor = getLeftEditor?.() - if (editor?.getModel().url !== model.url) { + if (editor?.getModel().uri !== model.uri) { return { actions: [], dispose() { diff --git a/docs/rules.md b/docs/rules.md index 2a5fb4ebc..df88e4fb9 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -47,6 +47,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) | destructure values from object stores for better change tracking & fewer redraws | :bulb: | | [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 | | diff --git a/docs/rules/prefer-destructured-store-props.md b/docs/rules/prefer-destructured-store-props.md new file mode 100644 index 000000000..bf40c5596 --- /dev/null +++ b/docs/rules/prefer-destructured-store-props.md @@ -0,0 +1,55 @@ +--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "svelte/prefer-destructured-store-props" +description: "destructure values from object stores for better change tracking & fewer redraws" +--- + +# svelte/prefer-destructured-store-props + +> destructure values from object stores for better change tracking & fewer redraws + +- :exclamation: **_This rule has not been released yet._** +- :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 + +This rule reports on directly accessing properties of a store containing an object in templates. 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/package.json b/package.json index a50fcd7c1..93b6770c3 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "dependencies": { "debug": "^4.3.1", "eslint-utils": "^3.0.0", + "esutils": "^2.0.3", "known-css-properties": "^0.25.0", "postcss": "^8.4.5", "postcss-load-config": "^3.1.4", @@ -100,6 +101,7 @@ "@types/eslint-utils": "^3.0.1", "@types/eslint-visitor-keys": "^1.0.0", "@types/estree": "^1.0.0", + "@types/esutils": "^2.0.0", "@types/less": "^3.0.3", "@types/markdown-it": "^12.2.3", "@types/markdown-it-container": "^2.0.5", @@ -172,7 +174,7 @@ "access": "public" }, "typeCoverage": { - "atLeast": 98.64, + "atLeast": 98.71, "cache": true, "detail": true, "ignoreAsAssertion": true, diff --git a/src/rules/prefer-destructured-store-props.ts b/src/rules/prefer-destructured-store-props.ts new file mode 100644 index 000000000..6bade7e7c --- /dev/null +++ b/src/rules/prefer-destructured-store-props.ts @@ -0,0 +1,280 @@ +import type { TSESTree } from "@typescript-eslint/types" +import { getPropertyName } from "eslint-utils" +import type { AST } from "svelte-eslint-parser" +import { keyword } from "esutils" +import type { SuggestionReportDescriptor } from "../types" +import { createRule } from "../utils" +import { + findAttribute, + isExpressionIdentifier, + findVariable, +} from "../utils/ast-utils" + +type StoreMemberExpression = TSESTree.MemberExpression & { + object: TSESTree.Identifier & { name: string } +} + +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`, + fixUseVariable: `Using the predefined reactive variable {{variable}}`, + }, + type: "suggestion", + }, + create(context) { + let mainScript: AST.SvelteScriptElement | null = null + + // Store off instances of probably-destructurable statements + const reports: StoreMemberExpression[] = [] + let inScriptElement = false + + const storeMemberAccessStack: { + node: StoreMemberExpression + // A list of Identifiers that make up the member expression. + identifiers: TSESTree.Identifier[] + }[] = [] + + /** Find for defined reactive variables. */ + function* findReactiveVariable( + object: TSESTree.Identifier, + propName: string, + ): Iterable { + const storeVar = findVariable(context, object) + if (!storeVar) { + return + } + + for (const reference of storeVar.references) { + const id = reference.identifier as TSESTree.Identifier + if (id.name !== object.name) continue + if (isReactiveVariableDefinitionWithMemberExpression(id)) { + // $: target = $store.prop + yield id.parent.parent.left + } else if (isReactiveVariableDefinitionWithDestructuring(id)) { + const prop = id.parent.left.properties.find( + ( + prop, + ): prop is TSESTree.Property & { value: TSESTree.Identifier } => + prop.type === "Property" && + prop.value.type === "Identifier" && + getPropertyName(prop) === propName, + ) + if (prop) { + // $: ({prop: target} = $store) + yield prop.value + } + } + } + + /** Checks whether the given node is reactive variable definition with member expression. */ + function isReactiveVariableDefinitionWithMemberExpression( + node: TSESTree.Identifier, + ): node is TSESTree.Identifier & { + parent: TSESTree.MemberExpression & { + parent: TSESTree.AssignmentExpression & { left: TSESTree.Identifier } + } + } { + return ( + node.parent?.type === "MemberExpression" && + node.parent.object === node && + getPropertyName(node.parent) === propName && + node.parent.parent?.type === "AssignmentExpression" && + node.parent.parent.right === node.parent && + node.parent.parent.left.type === "Identifier" && + node.parent.parent.parent?.type === "ExpressionStatement" && + ( + node.parent.parent.parent + .parent as never as AST.SvelteReactiveStatement + )?.type === "SvelteReactiveStatement" + ) + } + + /** Checks whether the given node is reactive variable definition with destructuring. */ + function isReactiveVariableDefinitionWithDestructuring( + node: TSESTree.Identifier, + ): node is TSESTree.Identifier & { + parent: TSESTree.AssignmentExpression & { + left: TSESTree.ObjectPattern + } + } { + return ( + node.parent?.type === "AssignmentExpression" && + node.parent.right === node && + node.parent.left.type === "ObjectPattern" && + node.parent.parent?.type === "ExpressionStatement" && + (node.parent.parent.parent as never as AST.SvelteReactiveStatement) + ?.type === "SvelteReactiveStatement" + ) + } + } + + /** Checks whether the given name is already defined as a variable. */ + function hasTopLevelVariable(name: string) { + const scopeManager = context.getSourceCode().scopeManager + if (scopeManager.globalScope?.set.has(name)) { + return true + } + const moduleScope = scopeManager.globalScope?.childScopes.find( + (s) => s.type === "module", + ) + return moduleScope?.set.has(name) || false + } + + return { + SvelteScriptElement(node) { + inScriptElement = true + const scriptContext = findAttribute(node, "context") + const contextValue = + scriptContext?.value.length === 1 && scriptContext.value[0] + if ( + contextValue && + contextValue.type === "SvelteLiteral" && + contextValue.value === "module" + ) { + // It is + + $foo: {foo} + bar: {$store.bar} + baz: {$store.baz} + var: {$store.var} + null: {$store.null} + undefined: {$store.undefined} +- message: Destructure bar from $store for better change tracking & fewer redraws + line: 6 + column: 7 + suggestions: + - desc: "Using destructuring like $: ({ bar } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + $foo: {$store.$foo} + bar: {bar} + baz: {$store.baz} + var: {$store.var} + null: {$store.null} + undefined: {$store.undefined} +- message: Destructure baz from $store for better change tracking & fewer redraws + line: 7 + column: 7 + suggestions: + - desc: "Using destructuring like $: ({ baz } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + $foo: {$store.$foo} + bar: {$store.bar} + baz: {baz} + var: {$store.var} + null: {$store.null} + undefined: {$store.undefined} +- message: Destructure var from $store for better change tracking & fewer redraws + line: 8 + column: 7 + suggestions: + - desc: "Using destructuring like $: ({ var } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + $foo: {$store.$foo} + bar: {$store.bar} + baz: {$store.baz} + var: {var1} + null: {$store.null} + undefined: {$store.undefined} +- message: Destructure null from $store for better change tracking & fewer redraws + line: 9 + column: 8 + suggestions: + - desc: "Using destructuring like $: ({ null } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + $foo: {$store.$foo} + bar: {$store.bar} + baz: {$store.baz} + var: {$store.var} + null: {null1} + undefined: {$store.undefined} +- message: Destructure undefined from $store for better change tracking & fewer redraws + line: 10 + column: 13 + suggestions: + - desc: "Using destructuring like $: ({ undefined } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + $foo: {$store.$foo} + bar: {$store.bar} + baz: {$store.baz} + var: {$store.var} + null: {$store.null} + undefined: {undefined1} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/fixer-test01-input.svelte b/tests/fixtures/rules/prefer-destructured-store-props/invalid/fixer-test01-input.svelte new file mode 100644 index 000000000..54e88f799 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/fixer-test01-input.svelte @@ -0,0 +1,10 @@ + + +$foo: {$store.$foo} +bar: {$store.bar} +baz: {$store.baz} +var: {$store.var} +null: {$store.null} +undefined: {$store.undefined} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/member01-errors.yaml b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member01-errors.yaml new file mode 100644 index 000000000..d422c50cb --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member01-errors.yaml @@ -0,0 +1,28 @@ +- message: Destructure foo from $store for better change tracking & fewer redraws + line: 5 + column: 11 + suggestions: + - desc: "Using destructuring like $: ({ foo } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + foo.bar: {foo.bar} + foo.baz: {$store.foo.baz} +- message: Destructure foo from $store for better change tracking & fewer redraws + line: 6 + column: 11 + suggestions: + - desc: "Using destructuring like $: ({ foo } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + foo.bar: {$store.foo.bar} + foo.baz: {foo.baz} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/member01-input.svelte b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member01-input.svelte new file mode 100644 index 000000000..f225e1f15 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member01-input.svelte @@ -0,0 +1,6 @@ + + +foo.bar: {$store.foo.bar} +foo.baz: {$store.foo.baz} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/member02-errors.yaml b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member02-errors.yaml new file mode 100644 index 000000000..d5aa9dbd7 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member02-errors.yaml @@ -0,0 +1,134 @@ +- message: Destructure foo from $store for better change tracking & fewer redraws + line: 8 + column: 11 + suggestions: + - desc: Using the predefined reactive variable foo + messageId: fixUseVariable + output: | + + + foo.bar: {foo.bar} + foo.baz: {foo.baz} + + bar.foo: {b.foo} + bar.baz: {$store.bar.baz} + + baz.foo: {bbb.foo} + baz.bar: {$store.baz.bar} + - desc: "Using destructuring like $: ({ foo } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + foo.bar: {foo.bar} + foo.baz: {foo1.baz} + + bar.foo: {b.foo} + bar.baz: {$store.bar.baz} + + baz.foo: {bbb.foo} + baz.bar: {$store.baz.bar} +- message: Destructure bar from $store for better change tracking & fewer redraws + line: 11 + column: 11 + suggestions: + - desc: Using the predefined reactive variable b + messageId: fixUseVariable + output: | + + + foo.bar: {foo.bar} + foo.baz: {$store.foo.baz} + + bar.foo: {b.foo} + bar.baz: {b.baz} + + baz.foo: {bbb.foo} + baz.bar: {$store.baz.bar} + - desc: "Using destructuring like $: ({ bar } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + foo.bar: {foo.bar} + foo.baz: {$store.foo.baz} + + bar.foo: {b.foo} + bar.baz: {bar.baz} + + baz.foo: {bbb.foo} + baz.bar: {$store.baz.bar} +- message: Destructure baz from $store for better change tracking & fewer redraws + line: 14 + column: 11 + suggestions: + - desc: Using the predefined reactive variable bbb + messageId: fixUseVariable + output: | + + + foo.bar: {foo.bar} + foo.baz: {$store.foo.baz} + + bar.foo: {b.foo} + bar.baz: {$store.bar.baz} + + baz.foo: {bbb.foo} + baz.bar: {bbb.bar} + - desc: Using the predefined reactive variable bbb + messageId: fixUseVariable + output: | + + + foo.bar: {foo.bar} + foo.baz: {$store.foo.baz} + + bar.foo: {b.foo} + bar.baz: {$store.bar.baz} + + baz.foo: {bbb.foo} + baz.bar: {bbb.bar} + - desc: "Using destructuring like $: ({ baz } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + foo.bar: {foo.bar} + foo.baz: {$store.foo.baz} + + bar.foo: {b.foo} + bar.baz: {$store.bar.baz} + + baz.foo: {bbb.foo} + baz.bar: {baz.bar} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/member02-input.svelte b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member02-input.svelte new file mode 100644 index 000000000..7eea0dad4 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member02-input.svelte @@ -0,0 +1,14 @@ + + +foo.bar: {foo.bar} +foo.baz: {$store.foo.baz} + +bar.foo: {b.foo} +bar.baz: {$store.bar.baz} + +baz.foo: {bbb.foo} +baz.bar: {$store.baz.bar} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/member03-errors.yaml b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member03-errors.yaml new file mode 100644 index 000000000..032898019 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member03-errors.yaml @@ -0,0 +1,69 @@ +- message: Destructure foo from $store for better change tracking & fewer redraws + line: 9 + column: 11 + suggestions: + - desc: "Using destructuring like $: ({ foo } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + foo.bar: {foo.bar} + foo.baz: {foo1.baz} + + bar.foo: {b.foo} + bar.baz: {$store.bar.baz} + + baz.foo: {bbb.foo} + baz.bar: {$store.baz.bar} +- message: Destructure bar from $store for better change tracking & fewer redraws + line: 12 + column: 11 + suggestions: + - desc: "Using destructuring like $: ({ bar } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + foo.bar: {foo.bar} + foo.baz: {$store.foo.baz} + + bar.foo: {b.foo} + bar.baz: {bar.baz} + + baz.foo: {bbb.foo} + baz.bar: {$store.baz.bar} +- message: Destructure baz from $store for better change tracking & fewer redraws + line: 15 + column: 11 + suggestions: + - desc: "Using destructuring like $: ({ baz } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + + foo.bar: {foo.bar} + foo.baz: {$store.foo.baz} + + bar.foo: {b.foo} + bar.baz: {$store.bar.baz} + + baz.foo: {bbb.foo} + baz.bar: {baz.bar} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/member03-input.svelte b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member03-input.svelte new file mode 100644 index 000000000..f821d794e --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/member03-input.svelte @@ -0,0 +1,15 @@ + + +foo.bar: {foo.bar} +foo.baz: {$store.foo.baz} + +bar.foo: {b.foo} +bar.baz: {$store.bar.baz} + +baz.foo: {bbb.foo} +baz.bar: {$store.baz.bar} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/module-errors.yaml b/tests/fixtures/rules/prefer-destructured-store-props/invalid/module-errors.yaml new file mode 100644 index 000000000..857b90f73 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/module-errors.yaml @@ -0,0 +1,4 @@ +- message: Destructure bar from $foo for better change tracking & fewer redraws + line: 4 + column: 2 + suggestions: null diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/module-input.svelte b/tests/fixtures/rules/prefer-destructured-store-props/invalid/module-input.svelte new file mode 100644 index 000000000..0f6c8ca64 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/module-input.svelte @@ -0,0 +1,4 @@ + + +{$foo.bar} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/test01-errors.yaml b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test01-errors.yaml new file mode 100644 index 000000000..fb703705e --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test01-errors.yaml @@ -0,0 +1,25 @@ +- message: Destructure bar from $foo for better change tracking & fewer redraws + line: 4 + column: 2 + suggestions: + - desc: "Using destructuring like $: ({ bar } = $foo); will run faster" + messageId: fixUseDestructuring + output: | + + + {bar} + + {$foo[baz]} + + + {$foo["qux"]} +- message: Destructure baz from $foo for better change tracking & fewer redraws + line: 6 + column: 2 + suggestions: null +- message: Destructure "qux" from $foo for better change tracking & fewer redraws + line: 9 + 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..daaab05a3 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test01-input.svelte @@ -0,0 +1,9 @@ + + +{$foo.bar} + +{$foo[baz]} + + +{$foo["qux"]} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/test02-errors.yaml b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test02-errors.yaml new file mode 100644 index 000000000..40d1bdc7a --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test02-errors.yaml @@ -0,0 +1,12 @@ +- message: Destructure bar from $foo for better change tracking & fewer redraws + line: 1 + column: 2 + suggestions: null +- message: Destructure bar from $foo for better change tracking & fewer redraws + line: 3 + column: 2 + suggestions: null +- message: Destructure "bar" from $foo for better change tracking & fewer redraws + line: 6 + 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..f824087aa --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test02-input.svelte @@ -0,0 +1,6 @@ +{$foo.bar} + +{$foo[bar]} + + +{$foo["bar"]} diff --git a/tests/fixtures/rules/prefer-destructured-store-props/invalid/test03-errors.yaml b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test03-errors.yaml new file mode 100644 index 000000000..5e4073642 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test03-errors.yaml @@ -0,0 +1,27 @@ +- message: Destructure baz from $store for better change tracking & fewer redraws + line: 18 + column: 9 + suggestions: + - desc: "Using destructuring like $: ({ baz } = $store); will run faster" + messageId: fixUseDestructuring + output: | + + +
+ foo: {`${foo} ${Date.now()}`} +
+
+ bar: {`${bar} ${Date.now()}`} +
+
+ baz: {baz} +
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..f5896ce13 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/invalid/test03-input.svelte @@ -0,0 +1,19 @@ + + +
+ foo: {`${foo} ${Date.now()}`} +
+
+ bar: {`${bar} ${Date.now()}`} +
+
+ baz: {$store.baz} +
diff --git a/tests/fixtures/rules/prefer-destructured-store-props/valid/script-test01-input.svelte b/tests/fixtures/rules/prefer-destructured-store-props/valid/script-test01-input.svelte new file mode 100644 index 000000000..b345e6885 --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/valid/script-test01-input.svelte @@ -0,0 +1,6 @@ + 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..84010d91d --- /dev/null +++ b/tests/fixtures/rules/prefer-destructured-store-props/valid/test01-input.svelte @@ -0,0 +1,6 @@ +{foo$.bar} +{f$oo.bar} + +{#each list as baz} + {$foo[`bar${baz}`]} +{/each} 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"), +) diff --git a/typings/eslint-utils/index.d.ts b/typings/eslint-utils/index.d.ts index fc14c75cc..944e2725a 100644 --- a/typings/eslint-utils/index.d.ts +++ b/typings/eslint-utils/index.d.ts @@ -1,9 +1,12 @@ import type { AST } from "svelte-eslint-parser" import type { Scope } from "eslint" import type * as ESTree from "estree" +import type { TSESTree } from "@typescript-eslint/types" export { ReferenceTracker, TrackedReferences, + getPropertyName, + getInnermostScope, } from "../../node_modules/@types/eslint-utils" type Token = { type: string; value: string } export function isArrowToken(token: Token): boolean @@ -31,5 +34,21 @@ export function isNotCommentToken(token: Token): boolean export function findVariable( initialScope: Scope.Scope, - nameOrNode: ESTree.Identifier | string, + nameOrNode: ESTree.Identifier | TSESTree.Identifier | string, ): Scope.Variable + +/** + * Get the property name from a MemberExpression node or a Property node. + */ +export function getPropertyName( + node: + | ESTree.MemberExpression + | ESTree.MethodDefinition + | ESTree.Property + | ESTree.PropertyDefinition + | TSESTree.MemberExpression + | TSESTree.MethodDefinition + | TSESTree.Property + | TSESTree.PropertyDefinition, + initialScope?: Scope.Scope, +): string | null