diff --git a/.changeset/shy-moles-deliver.md b/.changeset/shy-moles-deliver.md
new file mode 100644
index 000000000..5f2213599
--- /dev/null
+++ b/.changeset/shy-moles-deliver.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+feat: add `svelte/valid-each-key` rule
diff --git a/README.md b/README.md
index 3b78b7407..d7ba245c5 100644
--- a/README.md
+++ b/README.md
@@ -350,6 +350,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | |
| [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | |
| [svelte/require-stores-init](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store | |
+| [svelte/valid-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-each-key/) | enforce keys to use variables defined in the `{#each}` block | |
## Stylistic Issues
diff --git a/docs/rules.md b/docs/rules.md
index 8dd077cc1..45391260c 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -63,6 +63,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | |
| [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 | |
+| [svelte/valid-each-key](./rules/valid-each-key.md) | enforce keys to use variables defined in the `{#each}` block | |
## Stylistic Issues
diff --git a/docs/rules/require-each-key.md b/docs/rules/require-each-key.md
index 0e15f8e40..830703c26 100644
--- a/docs/rules/require-each-key.md
+++ b/docs/rules/require-each-key.md
@@ -41,6 +41,10 @@ This rule reports `{#each}` block without key
Nothing.
+## :couple: Related Rules
+
+- [svelte/valid-each-key](./valid-each-key.md)
+
## :books: Further Reading
- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/keyed-each-blocks)
diff --git a/docs/rules/valid-each-key.md b/docs/rules/valid-each-key.md
new file mode 100644
index 000000000..ec003bb1c
--- /dev/null
+++ b/docs/rules/valid-each-key.md
@@ -0,0 +1,64 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/valid-each-key"
+description: "enforce keys to use variables defined in the `{#each}` block"
+---
+
+# svelte/valid-each-key
+
+> enforce keys to use variables defined in the `{#each}` block
+
+- :exclamation: **_This rule has not been released yet._**
+
+## :book: Rule Details
+
+This rule reports that `{#each}` block keys does not use the variables which are defined by the `{#each}` block.
+
+
+
+
+
+```svelte
+
+
+
+{#each things as thing (thing.id)}
+
+{/each}
+
+
+{#each things as thing (foo)}
+
+{/each}
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [svelte/require-each-key](./require-each-key.md)
+
+## :books: Further Reading
+
+- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/keyed-each-blocks)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/valid-each-key.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/valid-each-key.ts)
diff --git a/src/rules/valid-each-key.ts b/src/rules/valid-each-key.ts
new file mode 100644
index 000000000..09bb89acc
--- /dev/null
+++ b/src/rules/valid-each-key.ts
@@ -0,0 +1,59 @@
+import type { AST } from "svelte-eslint-parser"
+import { createRule } from "../utils"
+import { getScope } from "../utils/ast-utils"
+
+export default createRule("valid-each-key", {
+ meta: {
+ docs: {
+ description:
+ "enforce keys to use variables defined in the `{#each}` block",
+ category: "Best Practices",
+ // TODO Switch to recommended in the major version.
+ recommended: false,
+ },
+ schema: [],
+ messages: {
+ keyUseEachVars:
+ "Expected key to use the variables which are defined by the `{#each}` block.",
+ },
+ type: "suggestion",
+ },
+ create(context) {
+ return {
+ SvelteEachBlock(node: AST.SvelteEachBlock) {
+ if (node.key == null) {
+ return
+ }
+ const scope = getScope(context, node.key)
+ for (const variable of scope.variables) {
+ if (
+ !variable.defs.some(
+ (def) =>
+ (node.context.range[0] <= def.name.range[0] &&
+ def.name.range[1] <= node.context.range[1]) ||
+ (node.index &&
+ node.index.range[0] <= def.name.range[0] &&
+ def.name.range[1] <= node.index.range[1]),
+ )
+ ) {
+ // It's not an iteration variable.
+ continue
+ }
+ for (const reference of variable.references) {
+ if (
+ node.key.range[0] <= reference.identifier.range[0] &&
+ reference.identifier.range[1] <= node.key.range[1]
+ ) {
+ // A variable is used in the key.
+ return
+ }
+ }
+ }
+ context.report({
+ node: node.key,
+ messageId: "keyUseEachVars",
+ })
+ },
+ }
+ },
+})
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index 88965c3ca..c5bd38fa2 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -57,6 +57,7 @@ import sortAttributes from "../rules/sort-attributes"
import spacedHtmlComment from "../rules/spaced-html-comment"
import system from "../rules/system"
import validCompile from "../rules/valid-compile"
+import validEachKey from "../rules/valid-each-key"
import validPropNamesInKitPages from "../rules/valid-prop-names-in-kit-pages"
export const rules = [
@@ -115,5 +116,6 @@ export const rules = [
spacedHtmlComment,
system,
validCompile,
+ validEachKey,
validPropNamesInKitPages,
] as RuleModule[]
diff --git a/tests/fixtures/rules/.eslintrc.js b/tests/fixtures/rules/.eslintrc.js
index c5afba262..27774f3a5 100644
--- a/tests/fixtures/rules/.eslintrc.js
+++ b/tests/fixtures/rules/.eslintrc.js
@@ -20,6 +20,7 @@ module.exports = {
"one-var": "off",
"func-style": "off",
"no-console": "off",
+ "no-use-before-define": "off",
"node/no-unsupported-features/es-syntax": "off",
"@typescript-eslint/await-thenable": "off",
diff --git a/tests/fixtures/rules/valid-each-key/invalid/const-key01-errors.yaml b/tests/fixtures/rules/valid-each-key/invalid/const-key01-errors.yaml
new file mode 100644
index 000000000..090db68fc
--- /dev/null
+++ b/tests/fixtures/rules/valid-each-key/invalid/const-key01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Expected key to use the variables which are defined by the `{#each}` block.
+ line: 11
+ column: 25
+ suggestions: null
diff --git a/tests/fixtures/rules/valid-each-key/invalid/const-key01-input.svelte b/tests/fixtures/rules/valid-each-key/invalid/const-key01-input.svelte
new file mode 100644
index 000000000..6d338621b
--- /dev/null
+++ b/tests/fixtures/rules/valid-each-key/invalid/const-key01-input.svelte
@@ -0,0 +1,14 @@
+
+
+{#each things as thing (key)}
+ {@const key = thing.id}
+ {thing.name}
+{/each}
diff --git a/tests/fixtures/rules/valid-each-key/invalid/out-vars-key01-errors.yaml b/tests/fixtures/rules/valid-each-key/invalid/out-vars-key01-errors.yaml
new file mode 100644
index 000000000..09c08d5ea
--- /dev/null
+++ b/tests/fixtures/rules/valid-each-key/invalid/out-vars-key01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Expected key to use the variables which are defined by the `{#each}` block.
+ line: 12
+ column: 25
+ suggestions: null
diff --git a/tests/fixtures/rules/valid-each-key/invalid/out-vars-key01-input.svelte b/tests/fixtures/rules/valid-each-key/invalid/out-vars-key01-input.svelte
new file mode 100644
index 000000000..e62dc745f
--- /dev/null
+++ b/tests/fixtures/rules/valid-each-key/invalid/out-vars-key01-input.svelte
@@ -0,0 +1,14 @@
+
+
+{#each things as thing (foo)}
+ {thing.name}
+{/each}
diff --git a/tests/fixtures/rules/valid-each-key/valid/call-key01-input.svelte b/tests/fixtures/rules/valid-each-key/valid/call-key01-input.svelte
new file mode 100644
index 000000000..61873e1dc
--- /dev/null
+++ b/tests/fixtures/rules/valid-each-key/valid/call-key01-input.svelte
@@ -0,0 +1,16 @@
+
+
+{#each things as thing (fn(thing))}
+ {thing.name}
+{/each}
diff --git a/tests/fixtures/rules/valid-each-key/valid/destructure-key01-input.svelte b/tests/fixtures/rules/valid-each-key/valid/destructure-key01-input.svelte
new file mode 100644
index 000000000..a060ac008
--- /dev/null
+++ b/tests/fixtures/rules/valid-each-key/valid/destructure-key01-input.svelte
@@ -0,0 +1,13 @@
+
+
+{#each things as { id, name } (id)}
+ {name}
+{/each}
diff --git a/tests/fixtures/rules/valid-each-key/valid/expression-key01-input.svelte b/tests/fixtures/rules/valid-each-key/valid/expression-key01-input.svelte
new file mode 100644
index 000000000..be4855c52
--- /dev/null
+++ b/tests/fixtures/rules/valid-each-key/valid/expression-key01-input.svelte
@@ -0,0 +1,13 @@
+
+
+{#each things as thing (`thing_id=${thing.id}`)}
+ {thing.name}
+{/each}
diff --git a/tests/fixtures/rules/valid-each-key/valid/expression-key02-input.svelte b/tests/fixtures/rules/valid-each-key/valid/expression-key02-input.svelte
new file mode 100644
index 000000000..b15c95da0
--- /dev/null
+++ b/tests/fixtures/rules/valid-each-key/valid/expression-key02-input.svelte
@@ -0,0 +1,14 @@
+
+
+{#each things as thing (foo + thing.id)}
+ {thing.name}
+{/each}
diff --git a/tests/fixtures/rules/valid-each-key/valid/index-key01-input.svelte b/tests/fixtures/rules/valid-each-key/valid/index-key01-input.svelte
new file mode 100644
index 000000000..8da30f439
--- /dev/null
+++ b/tests/fixtures/rules/valid-each-key/valid/index-key01-input.svelte
@@ -0,0 +1,13 @@
+
+
+{#each things as thing, index (index)}
+ {thing.name}
+{/each}
diff --git a/tests/fixtures/rules/valid-each-key/valid/member-key01-input.svelte b/tests/fixtures/rules/valid-each-key/valid/member-key01-input.svelte
new file mode 100644
index 000000000..a812f5075
--- /dev/null
+++ b/tests/fixtures/rules/valid-each-key/valid/member-key01-input.svelte
@@ -0,0 +1,13 @@
+
+
+{#each things as thing (thing.id)}
+ {thing.name}
+{/each}
diff --git a/tests/src/rules/valid-each-key.ts b/tests/src/rules/valid-each-key.ts
new file mode 100644
index 000000000..94488c312
--- /dev/null
+++ b/tests/src/rules/valid-each-key.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/valid-each-key"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run("valid-each-key", rule as any, loadTestCases("valid-each-key"))