Skip to content

Commit 6b71add

Browse files
authored
feat: add svelte/require-each-key rule (#473)
1 parent 6290345 commit 6b71add

10 files changed

+145
-0
lines changed

.changeset/gentle-doors-impress.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-svelte": minor
3+
---
4+
5+
feat: add `svelte/require-each-key` rule

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ These rules relate to better ways of doing things to help you avoid problems:
346346
| [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
347347
| [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
348348
| [svelte/prefer-destructured-store-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
349+
| [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | |
349350
| [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | |
350351
| [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | |
351352
| [svelte/require-stores-init](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store | |

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ These rules relate to better ways of doing things to help you avoid problems:
5959
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
6060
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
6161
| [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
62+
| [svelte/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | |
6263
| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | |
6364
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |
6465
| [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store | |

docs/rules/require-each-key.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/require-each-key"
5+
description: "require keyed `{#each}` block"
6+
---
7+
8+
# svelte/require-each-key
9+
10+
> require keyed `{#each}` block
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
14+
## :book: Rule Details
15+
16+
This rule reports `{#each}` block without key
17+
18+
<ESLintCodeBlock>
19+
20+
<!--eslint-skip-->
21+
22+
```svelte
23+
<script>
24+
/* eslint svelte/require-each-key: "error" */
25+
</script>
26+
27+
<!-- ✓ GOOD -->
28+
{#each things as thing (thing.id)}
29+
<Thing name={thing.name} />
30+
{/each}
31+
32+
<!-- ✗ BAD -->
33+
{#each things as thing}
34+
<Thing name={thing.name} />
35+
{/each}
36+
```
37+
38+
</ESLintCodeBlock>
39+
40+
## :wrench: Options
41+
42+
Nothing.
43+
44+
## :books: Further Reading
45+
46+
- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/keyed-each-blocks)
47+
48+
## :mag: Implementation
49+
50+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/require-each-key.ts)
51+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/require-each-key.ts)

src/rules/require-each-key.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import { createRule } from "../utils"
3+
4+
export default createRule("require-each-key", {
5+
meta: {
6+
docs: {
7+
description: "require keyed `{#each}` block",
8+
category: "Best Practices",
9+
recommended: false,
10+
},
11+
schema: [],
12+
messages: { expectedKey: "Each block should have a key" },
13+
type: "suggestion",
14+
},
15+
create(context) {
16+
return {
17+
SvelteEachBlock(node: AST.SvelteEachBlock) {
18+
if (node.key == null) {
19+
context.report({
20+
node,
21+
messageId: "expectedKey",
22+
})
23+
}
24+
},
25+
}
26+
},
27+
})

src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import noUselessMustaches from "../rules/no-useless-mustaches"
4545
import preferClassDirective from "../rules/prefer-class-directive"
4646
import preferDestructuredStoreProps from "../rules/prefer-destructured-store-props"
4747
import preferStyleDirective from "../rules/prefer-style-directive"
48+
import requireEachKey from "../rules/require-each-key"
4849
import requireEventDispatcherTypes from "../rules/require-event-dispatcher-types"
4950
import requireOptimizedStyleAttribute from "../rules/require-optimized-style-attribute"
5051
import requireStoreCallbacksUseSetParam from "../rules/require-store-callbacks-use-set-param"
@@ -102,6 +103,7 @@ export const rules = [
102103
preferClassDirective,
103104
preferDestructuredStoreProps,
104105
preferStyleDirective,
106+
requireEachKey,
105107
requireEventDispatcherTypes,
106108
requireOptimizedStyleAttribute,
107109
requireStoreCallbacksUseSetParam,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Each block should have a key
2+
line: 19
3+
column: 1
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script>
2+
import Thing from "./Thing.svelte"
3+
4+
let things = [
5+
{ id: 1, name: "apple" },
6+
{ id: 2, name: "banana" },
7+
{ id: 3, name: "carrot" },
8+
{ id: 4, name: "doughnut" },
9+
{ id: 5, name: "egg" },
10+
]
11+
12+
function handleClick() {
13+
things = things.slice(1)
14+
}
15+
</script>
16+
17+
<button on:click={handleClick}> Remove first thing </button>
18+
19+
{#each things as thing}
20+
<Thing name={thing.name} />
21+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script>
2+
import Thing from "./Thing.svelte"
3+
4+
let things = [
5+
{ id: 1, name: "apple" },
6+
{ id: 2, name: "banana" },
7+
{ id: 3, name: "carrot" },
8+
{ id: 4, name: "doughnut" },
9+
{ id: 5, name: "egg" },
10+
]
11+
12+
function handleClick() {
13+
things = things.slice(1)
14+
}
15+
</script>
16+
17+
<button on:click={handleClick}> Remove first thing </button>
18+
19+
{#each things as thing (thing.id)}
20+
<Thing name={thing.name} />
21+
{/each}

tests/src/rules/require-each-key.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../src/rules/require-each-key"
3+
import { loadTestCases } from "../../utils/utils"
4+
5+
const tester = new RuleTester({
6+
parserOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: "module",
9+
},
10+
})
11+
12+
tester.run("require-each-key", rule as any, loadTestCases("require-each-key"))

0 commit comments

Comments
 (0)