Skip to content

Commit abac19f

Browse files
authored
feat: add svelte/valid-each-key rule (#475)
1 parent 6b71add commit abac19f

19 files changed

+267
-0
lines changed

.changeset/shy-moles-deliver.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/valid-each-key` rule

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ These rules relate to better ways of doing things to help you avoid problems:
350350
| [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | |
351351
| [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | |
352352
| [svelte/require-stores-init](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store | |
353+
| [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 | |
353354

354355
## Stylistic Issues
355356

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ These rules relate to better ways of doing things to help you avoid problems:
6363
| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | |
6464
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |
6565
| [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store | |
66+
| [svelte/valid-each-key](./rules/valid-each-key.md) | enforce keys to use variables defined in the `{#each}` block | |
6667

6768
## Stylistic Issues
6869

docs/rules/require-each-key.md

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ This rule reports `{#each}` block without key
4141

4242
Nothing.
4343

44+
## :couple: Related Rules
45+
46+
- [svelte/valid-each-key](./valid-each-key.md)
47+
4448
## :books: Further Reading
4549

4650
- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/keyed-each-blocks)

docs/rules/valid-each-key.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/valid-each-key"
5+
description: "enforce keys to use variables defined in the `{#each}` block"
6+
---
7+
8+
# svelte/valid-each-key
9+
10+
> enforce keys to use variables defined in the `{#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 that `{#each}` block keys does not use the variables which are defined by the `{#each}` block.
17+
18+
<ESLintCodeBlock>
19+
20+
<!--eslint-skip-->
21+
22+
```svelte
23+
<script>
24+
/* eslint svelte/valid-each-key: "error" */
25+
26+
let things = [
27+
{ id: 1, name: "apple" },
28+
{ id: 2, name: "banana" },
29+
{ id: 3, name: "carrot" },
30+
{ id: 4, name: "doughnut" },
31+
{ id: 5, name: "egg" },
32+
]
33+
let foo = 42
34+
</script>
35+
36+
<!-- ✓ GOOD -->
37+
{#each things as thing (thing.id)}
38+
<Thing name={thing.name} />
39+
{/each}
40+
41+
<!-- ✗ BAD -->
42+
{#each things as thing (foo)}
43+
<Thing name={thing.name} />
44+
{/each}
45+
```
46+
47+
</ESLintCodeBlock>
48+
49+
## :wrench: Options
50+
51+
Nothing.
52+
53+
## :couple: Related Rules
54+
55+
- [svelte/require-each-key](./require-each-key.md)
56+
57+
## :books: Further Reading
58+
59+
- [Svelte - Tutorial > 4. Logic / Keyed each blocks](https://svelte.dev/tutorial/keyed-each-blocks)
60+
61+
## :mag: Implementation
62+
63+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/valid-each-key.ts)
64+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/valid-each-key.ts)

src/rules/valid-each-key.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import { createRule } from "../utils"
3+
import { getScope } from "../utils/ast-utils"
4+
5+
export default createRule("valid-each-key", {
6+
meta: {
7+
docs: {
8+
description:
9+
"enforce keys to use variables defined in the `{#each}` block",
10+
category: "Best Practices",
11+
// TODO Switch to recommended in the major version.
12+
recommended: false,
13+
},
14+
schema: [],
15+
messages: {
16+
keyUseEachVars:
17+
"Expected key to use the variables which are defined by the `{#each}` block.",
18+
},
19+
type: "suggestion",
20+
},
21+
create(context) {
22+
return {
23+
SvelteEachBlock(node: AST.SvelteEachBlock) {
24+
if (node.key == null) {
25+
return
26+
}
27+
const scope = getScope(context, node.key)
28+
for (const variable of scope.variables) {
29+
if (
30+
!variable.defs.some(
31+
(def) =>
32+
(node.context.range[0] <= def.name.range[0] &&
33+
def.name.range[1] <= node.context.range[1]) ||
34+
(node.index &&
35+
node.index.range[0] <= def.name.range[0] &&
36+
def.name.range[1] <= node.index.range[1]),
37+
)
38+
) {
39+
// It's not an iteration variable.
40+
continue
41+
}
42+
for (const reference of variable.references) {
43+
if (
44+
node.key.range[0] <= reference.identifier.range[0] &&
45+
reference.identifier.range[1] <= node.key.range[1]
46+
) {
47+
// A variable is used in the key.
48+
return
49+
}
50+
}
51+
}
52+
context.report({
53+
node: node.key,
54+
messageId: "keyUseEachVars",
55+
})
56+
},
57+
}
58+
},
59+
})

src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import sortAttributes from "../rules/sort-attributes"
5757
import spacedHtmlComment from "../rules/spaced-html-comment"
5858
import system from "../rules/system"
5959
import validCompile from "../rules/valid-compile"
60+
import validEachKey from "../rules/valid-each-key"
6061
import validPropNamesInKitPages from "../rules/valid-prop-names-in-kit-pages"
6162

6263
export const rules = [
@@ -115,5 +116,6 @@ export const rules = [
115116
spacedHtmlComment,
116117
system,
117118
validCompile,
119+
validEachKey,
118120
validPropNamesInKitPages,
119121
] as RuleModule[]

tests/fixtures/rules/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module.exports = {
2020
"one-var": "off",
2121
"func-style": "off",
2222
"no-console": "off",
23+
"no-use-before-define": "off",
2324
"node/no-unsupported-features/es-syntax": "off",
2425

2526
"@typescript-eslint/await-thenable": "off",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Expected key to use the variables which are defined by the `{#each}` block.
2+
line: 11
3+
column: 25
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
let things = [
3+
{ id: 1, name: "apple" },
4+
{ id: 2, name: "banana" },
5+
{ id: 3, name: "carrot" },
6+
{ id: 4, name: "doughnut" },
7+
{ id: 5, name: "egg" },
8+
]
9+
</script>
10+
11+
{#each things as thing (key)}
12+
{@const key = thing.id}
13+
{thing.name}
14+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Expected key to use the variables which are defined by the `{#each}` block.
2+
line: 12
3+
column: 25
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
let things = [
3+
{ id: 1, name: "apple" },
4+
{ id: 2, name: "banana" },
5+
{ id: 3, name: "carrot" },
6+
{ id: 4, name: "doughnut" },
7+
{ id: 5, name: "egg" },
8+
]
9+
const foo = "key"
10+
</script>
11+
12+
{#each things as thing (foo)}
13+
{thing.name}
14+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script>
2+
let things = [
3+
{ id: 1, name: "apple" },
4+
{ id: 2, name: "banana" },
5+
{ id: 3, name: "carrot" },
6+
{ id: 4, name: "doughnut" },
7+
{ id: 5, name: "egg" },
8+
]
9+
function fn(thing) {
10+
return thing.id
11+
}
12+
</script>
13+
14+
{#each things as thing (fn(thing))}
15+
{thing.name}
16+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let things = [
3+
{ id: 1, name: "apple" },
4+
{ id: 2, name: "banana" },
5+
{ id: 3, name: "carrot" },
6+
{ id: 4, name: "doughnut" },
7+
{ id: 5, name: "egg" },
8+
]
9+
</script>
10+
11+
{#each things as { id, name } (id)}
12+
{name}
13+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let things = [
3+
{ id: 1, name: "apple" },
4+
{ id: 2, name: "banana" },
5+
{ id: 3, name: "carrot" },
6+
{ id: 4, name: "doughnut" },
7+
{ id: 5, name: "egg" },
8+
]
9+
</script>
10+
11+
{#each things as thing (`thing_id=${thing.id}`)}
12+
{thing.name}
13+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
let things = [
3+
{ id: 1, name: "apple" },
4+
{ id: 2, name: "banana" },
5+
{ id: 3, name: "carrot" },
6+
{ id: 4, name: "doughnut" },
7+
{ id: 5, name: "egg" },
8+
]
9+
const foo = "thing_id="
10+
</script>
11+
12+
{#each things as thing (foo + thing.id)}
13+
{thing.name}
14+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let things = [
3+
{ id: 1, name: "apple" },
4+
{ id: 2, name: "banana" },
5+
{ id: 3, name: "carrot" },
6+
{ id: 4, name: "doughnut" },
7+
{ id: 5, name: "egg" },
8+
]
9+
</script>
10+
11+
{#each things as thing, index (index)}
12+
{thing.name}
13+
{/each}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let things = [
3+
{ id: 1, name: "apple" },
4+
{ id: 2, name: "banana" },
5+
{ id: 3, name: "carrot" },
6+
{ id: 4, name: "doughnut" },
7+
{ id: 5, name: "egg" },
8+
]
9+
</script>
10+
11+
{#each things as thing (thing.id)}
12+
{thing.name}
13+
{/each}

tests/src/rules/valid-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/valid-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("valid-each-key", rule as any, loadTestCases("valid-each-key"))

0 commit comments

Comments
 (0)