Skip to content

Commit a9c4912

Browse files
authored
feat: add svelte/no-dupe-on-directives and svelte/no-dupe-use-directives rules (#308)
1 parent cbcf494 commit a9c4912

34 files changed

+781
-6
lines changed

.changeset/little-points-poke.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/no-dupe-use-directives` rule

.changeset/pink-zoos-wonder.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/no-dupe-on-directives` rule

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,9 @@ These rules relate to possible syntax or logic errors in Svelte code:
300300
|:--------|:------------|:---|
301301
| [svelte/no-dom-manipulating](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dom-manipulating/) | disallow DOM manipulating | |
302302
| [svelte/no-dupe-else-if-blocks](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-else-if-blocks/) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
303+
| [svelte/no-dupe-on-directives](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-on-directives/) | disallow duplicate `on:` directives | |
303304
| [svelte/no-dupe-style-properties](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-style-properties/) | disallow duplicate style properties | :star: |
305+
| [svelte/no-dupe-use-directives](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-use-directives/) | disallow duplicate `use:` directives | |
304306
| [svelte/no-dynamic-slot-name](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dynamic-slot-name/) | disallow dynamic slot name | :star::wrench: |
305307
| [svelte/no-export-load-in-svelte-module-in-kit-pages](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-export-load-in-svelte-module-in-kit-pages/) | disallow exporting load functions in `*.svelte` module in Svelte Kit page components. | |
306308
| [svelte/no-not-function-handler](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-not-function-handler/) | disallow use of not function in event handler | :star: |

docs/rules.md

+2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ These rules relate to possible syntax or logic errors in Svelte code:
1818
|:--------|:------------|:---|
1919
| [svelte/no-dom-manipulating](./rules/no-dom-manipulating.md) | disallow DOM manipulating | |
2020
| [svelte/no-dupe-else-if-blocks](./rules/no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
21+
| [svelte/no-dupe-on-directives](./rules/no-dupe-on-directives.md) | disallow duplicate `on:` directives | |
2122
| [svelte/no-dupe-style-properties](./rules/no-dupe-style-properties.md) | disallow duplicate style properties | :star: |
23+
| [svelte/no-dupe-use-directives](./rules/no-dupe-use-directives.md) | disallow duplicate `use:` directives | |
2224
| [svelte/no-dynamic-slot-name](./rules/no-dynamic-slot-name.md) | disallow dynamic slot name | :star::wrench: |
2325
| [svelte/no-export-load-in-svelte-module-in-kit-pages](./rules/no-export-load-in-svelte-module-in-kit-pages.md) | disallow exporting load functions in `*.svelte` module in Svelte Kit page components. | |
2426
| [svelte/no-not-function-handler](./rules/no-not-function-handler.md) | disallow use of not function in event handler | :star: |

docs/rules/no-dupe-on-directives.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/no-dupe-on-directives"
5+
description: "disallow duplicate `on:` directives"
6+
---
7+
8+
# svelte/no-dupe-on-directives
9+
10+
> disallow duplicate `on:` directives
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+
We can define any number of `on:` directive with the same event name, but duplicate directives with the exact same event name and expression are probably a mistake.
17+
This rule reports reports `on:` directives with exactly the same event name and expression.
18+
19+
<ESLintCodeBlock>
20+
21+
<!--eslint-skip-->
22+
23+
```svelte
24+
<script>
25+
/* eslint svelte/no-dupe-on-directives: "error" */
26+
</script>
27+
28+
<!-- ✓ GOOD -->
29+
<button on:click on:click={myHandler} />
30+
<button on:click={foo} on:click={bar} />
31+
32+
<!-- ✗ BAD -->
33+
<button on:click on:click />
34+
<button on:click={myHandler} on:click={myHandler} />
35+
36+
<input
37+
on:focus|once
38+
on:focus
39+
on:keydown={() => console.log("foo")}
40+
on:keydown={() => console.log("foo")}
41+
/>
42+
```
43+
44+
</ESLintCodeBlock>
45+
46+
## :wrench: Options
47+
48+
Nothing.
49+
50+
## :mag: Implementation
51+
52+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-dupe-on-directives.ts)
53+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-dupe-on-directives.ts)

docs/rules/no-dupe-use-directives.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/no-dupe-use-directives"
5+
description: "disallow duplicate `use:` directives"
6+
---
7+
8+
# svelte/no-dupe-use-directives
9+
10+
> disallow duplicate `use:` directives
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+
We can define any number of `use:` directive with the same action, but duplicate directives with the exact same action and expression are probably a mistake.
17+
This rule reports reports `use:` directives with exactly the same action and expression.
18+
19+
<ESLintCodeBlock>
20+
21+
<!--eslint-skip-->
22+
23+
```svelte
24+
<script>
25+
/* eslint svelte/no-dupe-use-directives: "error" */
26+
</script>
27+
28+
<!-- ✓ GOOD -->
29+
<div use:clickOutside use:clickOutside={param} />
30+
<div use:clickOutside={foo} use:clickOutside={bar} />
31+
32+
<!-- ✗ BAD -->
33+
<div use:clickOutside use:clickOutside />
34+
<div use:clickOutside={param} use:clickOutside={param} />
35+
```
36+
37+
</ESLintCodeBlock>
38+
39+
## :wrench: Options
40+
41+
Nothing.
42+
43+
## :mag: Implementation
44+
45+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-dupe-use-directives.ts)
46+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-dupe-use-directives.ts)

src/rules/no-dupe-on-directives.ts

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import type { TSESTree } from "@typescript-eslint/types"
3+
import { createRule } from "../utils"
4+
import { equalTokens } from "../utils/ast-utils"
5+
6+
export default createRule("no-dupe-on-directives", {
7+
meta: {
8+
docs: {
9+
description: "disallow duplicate `on:` directives",
10+
category: "Possible Errors",
11+
recommended: false,
12+
},
13+
schema: [],
14+
messages: {
15+
duplication:
16+
"This `on:{{type}}` directive is the same and duplicate directives in L{{lineNo}}.",
17+
},
18+
type: "problem",
19+
},
20+
create(context) {
21+
const sourceCode = context.getSourceCode()
22+
23+
const directiveDataMap = new Map<
24+
string, // event type
25+
{
26+
expression: null | TSESTree.Expression
27+
nodes: AST.SvelteEventHandlerDirective[]
28+
}[]
29+
>()
30+
return {
31+
SvelteDirective(node) {
32+
if (node.kind !== "EventHandler") return
33+
34+
const directiveDataList = directiveDataMap.get(node.key.name.name)
35+
if (!directiveDataList) {
36+
directiveDataMap.set(node.key.name.name, [
37+
{
38+
expression: node.expression,
39+
nodes: [node],
40+
},
41+
])
42+
return
43+
}
44+
const directiveData = directiveDataList.find((data) => {
45+
if (!data.expression || !node.expression) {
46+
return data.expression === node.expression
47+
}
48+
return equalTokens(data.expression, node.expression, sourceCode)
49+
})
50+
if (!directiveData) {
51+
directiveDataList.push({
52+
expression: node.expression,
53+
nodes: [node],
54+
})
55+
return
56+
}
57+
58+
directiveData.nodes.push(node)
59+
},
60+
"SvelteStartTag:exit"() {
61+
for (const [type, directiveDataList] of directiveDataMap) {
62+
for (const { nodes } of directiveDataList) {
63+
if (nodes.length < 2) {
64+
continue
65+
}
66+
for (const node of nodes) {
67+
context.report({
68+
node,
69+
messageId: "duplication",
70+
data: {
71+
type,
72+
lineNo: String(
73+
(nodes[0] !== node ? nodes[0] : nodes[1]).loc.start.line,
74+
),
75+
},
76+
})
77+
}
78+
}
79+
}
80+
directiveDataMap.clear()
81+
},
82+
}
83+
},
84+
})

src/rules/no-dupe-use-directives.ts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import type { TSESTree } from "@typescript-eslint/types"
3+
import { createRule } from "../utils"
4+
import { equalTokens, getAttributeKeyText } from "../utils/ast-utils"
5+
6+
export default createRule("no-dupe-use-directives", {
7+
meta: {
8+
docs: {
9+
description: "disallow duplicate `use:` directives",
10+
category: "Possible Errors",
11+
recommended: false,
12+
},
13+
schema: [],
14+
messages: {
15+
duplication:
16+
"This `{{keyText}}` directive is the same and duplicate directives in L{{lineNo}}.",
17+
},
18+
type: "problem",
19+
},
20+
create(context) {
21+
const sourceCode = context.getSourceCode()
22+
23+
const directiveDataMap = new Map<
24+
string, // key text
25+
{
26+
expression: null | TSESTree.Expression
27+
nodes: AST.SvelteActionDirective[]
28+
}[]
29+
>()
30+
return {
31+
SvelteDirective(node) {
32+
if (node.kind !== "Action") return
33+
34+
const keyText = getAttributeKeyText(node, context)
35+
36+
const directiveDataList = directiveDataMap.get(keyText)
37+
if (!directiveDataList) {
38+
directiveDataMap.set(keyText, [
39+
{
40+
expression: node.expression,
41+
nodes: [node],
42+
},
43+
])
44+
return
45+
}
46+
const directiveData = directiveDataList.find((data) => {
47+
if (!data.expression || !node.expression) {
48+
return data.expression === node.expression
49+
}
50+
return equalTokens(data.expression, node.expression, sourceCode)
51+
})
52+
if (!directiveData) {
53+
directiveDataList.push({
54+
expression: node.expression,
55+
nodes: [node],
56+
})
57+
return
58+
}
59+
60+
directiveData.nodes.push(node)
61+
},
62+
"SvelteStartTag:exit"() {
63+
for (const [keyText, directiveDataList] of directiveDataMap) {
64+
for (const { nodes } of directiveDataList) {
65+
if (nodes.length < 2) {
66+
continue
67+
}
68+
for (const node of nodes) {
69+
context.report({
70+
node,
71+
messageId: "duplication",
72+
data: {
73+
keyText,
74+
lineNo: String(
75+
(nodes[0] !== node ? nodes[0] : nodes[1]).loc.start.line,
76+
),
77+
},
78+
})
79+
}
80+
}
81+
}
82+
directiveDataMap.clear()
83+
},
84+
}
85+
},
86+
})

src/types-for-node.ts

-6
Original file line numberDiff line numberDiff line change
@@ -304,9 +304,6 @@ export type ASTNodeListener = {
304304
node: TSESTree.TSReadonlyKeyword & ASTNodeWithParent,
305305
) => void
306306
TSRestType?: (node: TSESTree.TSRestType & ASTNodeWithParent) => void
307-
TSSatisfiesExpression?: (
308-
node: TSESTree.TSSatisfiesExpression & ASTNodeWithParent,
309-
) => void
310307
TSStaticKeyword?: (node: TSESTree.TSStaticKeyword & ASTNodeWithParent) => void
311308
TSStringKeyword?: (node: TSESTree.TSStringKeyword & ASTNodeWithParent) => void
312309
TSSymbolKeyword?: (node: TSESTree.TSSymbolKeyword & ASTNodeWithParent) => void
@@ -666,9 +663,6 @@ export type TSNodeListener = {
666663
node: TSESTree.TSReadonlyKeyword & ASTNodeWithParent,
667664
) => void
668665
TSRestType?: (node: TSESTree.TSRestType & ASTNodeWithParent) => void
669-
TSSatisfiesExpression?: (
670-
node: TSESTree.TSSatisfiesExpression & ASTNodeWithParent,
671-
) => void
672666
TSStaticKeyword?: (node: TSESTree.TSStaticKeyword & ASTNodeWithParent) => void
673667
TSStringKeyword?: (node: TSESTree.TSStringKeyword & ASTNodeWithParent) => void
674668
TSSymbolKeyword?: (node: TSESTree.TSSymbolKeyword & ASTNodeWithParent) => void

src/utils/rules.ts

+4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import noAtDebugTags from "../rules/no-at-debug-tags"
1414
import noAtHtmlTags from "../rules/no-at-html-tags"
1515
import noDomManipulating from "../rules/no-dom-manipulating"
1616
import noDupeElseIfBlocks from "../rules/no-dupe-else-if-blocks"
17+
import noDupeOnDirectives from "../rules/no-dupe-on-directives"
1718
import noDupeStyleProperties from "../rules/no-dupe-style-properties"
19+
import noDupeUseDirectives from "../rules/no-dupe-use-directives"
1820
import noDynamicSlotName from "../rules/no-dynamic-slot-name"
1921
import noExportLoadInSvelteModuleInKitPages from "../rules/no-export-load-in-svelte-module-in-kit-pages"
2022
import noExtraReactiveCurlies from "../rules/no-extra-reactive-curlies"
@@ -62,7 +64,9 @@ export const rules = [
6264
noAtHtmlTags,
6365
noDomManipulating,
6466
noDupeElseIfBlocks,
67+
noDupeOnDirectives,
6568
noDupeStyleProperties,
69+
noDupeUseDirectives,
6670
noDynamicSlotName,
6771
noExportLoadInSvelteModuleInKitPages,
6872
noExtraReactiveCurlies,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
- message: This `on:keydown` directive is the same and duplicate directives in L3.
2+
line: 2
3+
column: 3
4+
suggestions: null
5+
- message: This `on:keydown` directive is the same and duplicate directives in L2.
6+
line: 3
7+
column: 3
8+
suggestions: null
9+
- message: This `on:keydown` directive is the same and duplicate directives in L10.
10+
line: 7
11+
column: 3
12+
suggestions: null
13+
- message: This `on:keydown` directive is the same and duplicate directives in L7.
14+
line: 10
15+
column: 3
16+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<button
2+
on:keydown={() => console.log("foo")}
3+
on:keydown={() => console.log("foo")}
4+
/>
5+
6+
<button
7+
on:keydown={() =>
8+
// foo
9+
console.log("foo")}
10+
on:keydown={() =>
11+
console
12+
// bar
13+
.log("foo")}
14+
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
- message: This `on:click` directive is the same and duplicate directives in L3.
2+
line: 2
3+
column: 3
4+
suggestions: null
5+
- message: This `on:click` directive is the same and duplicate directives in L2.
6+
line: 3
7+
column: 3
8+
suggestions: null
9+
- message: This `on:click` directive is the same and duplicate directives in L2.
10+
line: 4
11+
column: 3
12+
suggestions: null
13+
- message: This `on:click` directive is the same and duplicate directives in L2.
14+
line: 5
15+
column: 3
16+
suggestions: null
17+
- message: This `on:click` directive is the same and duplicate directives in L2.
18+
line: 6
19+
column: 3
20+
suggestions: null

0 commit comments

Comments
 (0)