{
+ 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