Repository: ota-meshi/eslint-plugin-svelte (ts)

Issue:
New Rule: `svelte/no-dupe-on-directives` and `svelte/no-dupe-use-directives`
### Motivation

We can define any number of `on:` and `use:` directives with the same name, but duplicate directives with the exact same name and expression are probably a mistake.
I would like to find it in the new rule.

See also https://github.com/sveltejs/svelte/pull/8048.

### Description

The new rule reports `on:` directives with exactly the same name and expression, and `use:` directives with exactly the same name and expression.

### Examples

```svelte
<!-- ✓ GOOD -->
<button on:click on:click={myHandler}  />
<button on:click={foo} on:click={bar}  />
<div use:clickOutside use:clickOutside={param}  />
<div use:clickOutside={foo} use:clickOutside={bar} />

<!-- ✗ BAD -->
<button on:click on:click />
<button on:click={myHandler} on:click={myHandler} />
<div use:clickOutside use:clickOutside  />
<div use:clickOutside={param} use:clickOutside={param}  />
```


### Additional comments

As for `on:`, maybe some users want to report even if the expression is different. I think we can add an option to the rule in that case.

```svelte
<!-- maybe BAD? -->
<button on:click={foo} on:click={bar}  />

<!-- GOOD? -->
<script>
function handler(evt) {
  foo(evt);
  bar(evt);
}
</script>
<button on:click={handler} />
```



Pull request description:
feat: add `svelte/no-dupe-on-directives` and `svelte/no-dupe-use-directives` rules
close #304 

Patch:
diff --git a/.changeset/little-points-poke.md b/.changeset/little-points-poke.md
new file mode 100644
index 0000000..803d2f6
--- /dev/null
+++ b/.changeset/little-points-poke.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+feat: add `svelte/no-dupe-use-directives` rule
diff --git a/.changeset/pink-zoos-wonder.md b/.changeset/pink-zoos-wonder.md
new file mode 100644
index 0000000..696d6eb
--- /dev/null
+++ b/.changeset/pink-zoos-wonder.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+feat: add `svelte/no-dupe-on-directives` rule
diff --git a/README.md b/README.md
index bc66951..7a557f6 100644
--- a/README.md
+++ b/README.md
@@ -300,7 +300,9 @@ These rules relate to possible syntax or logic errors in Svelte code:
 |:--------|:------------|:---|
 | [svelte/no-dom-manipulating](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dom-manipulating/) | disallow DOM manipulating |  |
 | [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: |
+| [svelte/no-dupe-on-directives](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-on-directives/) | disallow duplicate `on:` directives |  |
 | [svelte/no-dupe-style-properties](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-style-properties/) | disallow duplicate style properties | :star: |
+| [svelte/no-dupe-use-directives](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-use-directives/) | disallow duplicate `use:` directives |  |
 | [svelte/no-dynamic-slot-name](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dynamic-slot-name/) | disallow dynamic slot name | :star::wrench: |
 | [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. |  |
 | [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: |
diff --git a/docs-svelte-kit/src/app.css b/docs-svelte-kit/src/app.css
index 3dbe420..7ab76d3 100644
--- a/docs-svelte-kit/src/app.css
+++ b/docs-svelte-kit/src/app.css
@@ -99,3 +99,7 @@ button:focus:not(:focus-visible) {
     font-size: 2.4rem;
   }
 }
+
+:target {
+  scroll-margin-top: 80px;
+}
diff --git a/docs-svelte-kit/src/lib/eslint/scripts/monaco-loader.js b/docs-svelte-kit/src/lib/eslint/scripts/monaco-loader.js
index 877bc84..838100d 100644
--- a/docs-svelte-kit/src/lib/eslint/scripts/monaco-loader.js
+++ b/docs-svelte-kit/src/lib/eslint/scripts/monaco-loader.js
@@ -13,11 +13,6 @@ async function setupMonaco() {
       paths: {
         vs: monacoScript.src.replace(/\/vs\/.*$/u, "/vs"),
       },
-      "vs/nls": {
-        availableLanguages: {
-          "*": "ja",
-        },
-      },
     })
   }
 }
diff --git a/docs/rules.md b/docs/rules.md
index 953e7c2..59e794a 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -18,7 +18,9 @@ These rules relate to possible syntax or logic errors in Svelte code:
 |:--------|:------------|:---|
 | [svelte/no-dom-manipulating](./rules/no-dom-manipulating.md) | disallow DOM manipulating |  |
 | [svelte/no-dupe-else-if-blocks](./rules/no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
+| [svelte/no-dupe-on-directives](./rules/no-dupe-on-directives.md) | disallow duplicate `on:` directives |  |
 | [svelte/no-dupe-style-properties](./rules/no-dupe-style-properties.md) | disallow duplicate style properties | :star: |
+| [svelte/no-dupe-use-directives](./rules/no-dupe-use-directives.md) | disallow duplicate `use:` directives |  |
 | [svelte/no-dynamic-slot-name](./rules/no-dynamic-slot-name.md) | disallow dynamic slot name | :star::wrench: |
 | [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. |  |
 | [svelte/no-not-function-handler](./rules/no-not-function-handler.md) | disallow use of not function in event handler | :star: |
diff --git a/docs/rules/no-dom-manipulating.md b/docs/rules/no-dom-manipulating.md
index cc99729..709cdae 100644
--- a/docs/rules/no-dom-manipulating.md
+++ b/docs/rules/no-dom-manipulating.md
@@ -96,7 +96,7 @@ This rule only tracks and checks variables given with `bind:this={}`. In other w
 
 </ESLintCodeBlock>
 
-See also <https://svelte.jp/examples/custom-js-transitions>.
+See also <https://svelte.dev/examples/custom-js-transitions>.
 
 ## :wrench: Options
 
diff --git a/docs/rules/no-dupe-on-directives.md b/docs/rules/no-dupe-on-directives.md
new file mode 100644
index 0000000..a8022ac
--- /dev/null
+++ b/docs/rules/no-dupe-on-directives.md
@@ -0,0 +1,53 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/no-dupe-on-directives"
+description: "disallow duplicate `on:` directives"
+---
+
+# svelte/no-dupe-on-directives
+
+> disallow duplicate `on:` directives
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
+
+## :book: Rule Details
+
+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.
+This rule reports reports `on:` directives with exactly the same event name and expression.
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<script>
+  /* eslint svelte/no-dupe-on-directives: "error" */
+</script>
+
+<!-- ✓ GOOD -->
+<button on:click on:click={myHandler} />
+<button on:click={foo} on:click={bar} />
+
+<!-- ✗ BAD -->
+<button on:click on:click />
+<button on:click={myHandler} on:click={myHandler} />
+
+<input
+  on:focus|once
+  on:focus
+  on:keydown={() => console.log("foo")}
+  on:keydown={() => console.log("foo")}
+/>
+```
+
+</ESLintCodeBlock>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-dupe-on-directives.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-dupe-on-directives.ts)
diff --git a/docs/rules/no-dupe-use-directives.md b/docs/rules/no-dupe-use-directives.md
new file mode 100644
index 0000000..b4b4761
--- /dev/null
+++ b/docs/rules/no-dupe-use-directives.md
@@ -0,0 +1,46 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/no-dupe-use-directives"
+description: "disallow duplicate `use:` directives"
+---
+
+# svelte/no-dupe-use-directives
+
+> disallow duplicate `use:` directives
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
+
+## :book: Rule Details
+
+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.
+This rule reports reports `use:` directives with exactly the same action and expression.
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<script>
+  /* eslint svelte/no-dupe-use-directives: "error" */
+</script>
+
+<!-- ✓ GOOD -->
+<div use:clickOutside use:clickOutside={param} />
+<div use:clickOutside={foo} use:clickOutside={bar} />
+
+<!-- ✗ BAD -->
+<div use:clickOutside use:clickOutside />
+<div use:clickOutside={param} use:clickOutside={param} />
+```
+
+</ESLintCodeBlock>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-dupe-use-directives.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-dupe-use-directives.ts)
diff --git a/src/rules/no-dupe-on-directives.ts b/src/rules/no-dupe-on-directives.ts
new file mode 100644
index 0000000..65bd06b
--- /dev/null
+++ b/src/rules/no-dupe-on-directives.ts
@@ -0,0 +1,84 @@
+import type { AST } from "svelte-eslint-parser"
+import type { TSESTree } from "@typescript-eslint/types"
+import { createRule } from "../utils"
+import { equalTokens } from "../utils/ast-utils"
+
+export default createRule("no-dupe-on-directives", {
+  meta: {
+    docs: {
+      description: "disallow duplicate `on:` directives",
+      category: "Possible Errors",
+      recommended: false,
+    },
+    schema: [],
+    messages: {
+      duplication:
+        "This `on:{{type}}` directive is the same and duplicate directives in L{{lineNo}}.",
+    },
+    type: "problem",
+  },
+  create(context) {
+    const sourceCode = context.getSourceCode()
+
+    const directiveDataMap = new Map<
+      string, // event type
+      {
+        expression: null | TSESTree.Expression
+        nodes: AST.SvelteEventHandlerDirective[]
+      }[]
+    >()
+    return {
+      SvelteDirective(node) {
+        if (node.kind !== "EventHandler") return
+
+        const directiveDataList = directiveDataMap.get(node.key.name.name)
+        if (!directiveDataList) {
+          directiveDataMap.set(node.key.name.name, [
+            {
+              expression: node.expression,
+              nodes: [node],
+            },
+          ])
+          return
+        }
+        const directiveData = directiveDataList.find((data) => {
+          if (!data.expression || !node.expression) {
+            return data.expression === node.expression
+          }
+          return equalTokens(data.expression, node.expression, sourceCode)
+        })
+        if (!directiveData) {
+          directiveDataList.push({
+            expression: node.expression,
+            nodes: [node],
+          })
+          return
+        }
+
+        directiveData.nodes.push(node)
+      },
+      "SvelteStartTag:exit"() {
+        for (const [type, directiveDataList] of directiveDataMap) {
+          for (const { nodes } of directiveDataList) {
+            if (nodes.length < 2) {
+              continue
+            }
+            for (const node of nodes) {
+              context.report({
+                node,
+                messageId: "duplication",
+                data: {
+                  type,
+                  lineNo: String(
+                    (nodes[0] !== node ? nodes[0] : nodes[1]).loc.start.line,
+                  ),
+                },
+              })
+            }
+          }
+        }
+        directiveDataMap.clear()
+      },
+    }
+  },
+})
diff --git a/src/rules/no-dupe-use-directives.ts b/src/rules/no-dupe-use-directives.ts
new file mode 100644
index 0000000..9433d99
--- /dev/null
+++ b/src/rules/no-dupe-use-directives.ts
@@ -0,0 +1,86 @@
+import type { AST } from "svelte-eslint-parser"
+import type { TSESTree } from "@typescript-eslint/types"
+import { createRule } from "../utils"
+import { equalTokens, getAttributeKeyText } from "../utils/ast-utils"
+
+export default createRule("no-dupe-use-directives", {
+  meta: {
+    docs: {
+      description: "disallow duplicate `use:` directives",
+      category: "Possible Errors",
+      recommended: false,
+    },
+    schema: [],
+    messages: {
+      duplication:
+        "This `{{keyText}}` directive is the same and duplicate directives in L{{lineNo}}.",
+    },
+    type: "problem",
+  },
+  create(context) {
+    const sourceCode = context.getSourceCode()
+
+    const directiveDataMap = new Map<
+      string, // key text
+      {
+        expression: null | TSESTree.Expression
+        nodes: AST.SvelteActionDirective[]
+      }[]
+    >()
+    return {
+      SvelteDirective(node) {
+        if (node.kind !== "Action") return
+
+        const keyText = getAttributeKeyText(node, context)
+
+        const directiveDataList = directiveDataMap.get(keyText)
+        if (!directiveDataList) {
+          directiveDataMap.set(keyText, [
+            {
+              expression: node.expression,
+              nodes: [node],
+            },
+          ])
+          return
+        }
+        const directiveData = directiveDataList.find((data) => {
+          if (!data.expression || !node.expression) {
+            return data.expression === node.expression
+          }
+          return equalTokens(data.expression, node.expression, sourceCode)
+        })
+        if (!directiveData) {
+          directiveDataList.push({
+            expression: node.expression,
+            nodes: [node],
+          })
+          return
+        }
+
+        directiveData.nodes.push(node)
+      },
+      "SvelteStartTag:exit"() {
+        for (const [keyText, directiveDataList] of directiveDataMap) {
+          for (const { nodes } of directiveDataList) {
+            if (nodes.length < 2) {
+              continue
+            }
+            for (const node of nodes) {
+              context.report({
+                node,
+                messageId: "duplication",
+                data: {
+                  keyText,
+                  lineNo: String(
+                    (nodes[0] !== node ? nodes[0] : nodes[1]).loc.start.line,
+                  ),
+                },
+              })
+            }
+          }
+        }
+        directiveDataMap.clear()
+      },
+    }
+  },
+})
diff --git a/src/types-for-node.ts b/src/types-for-node.ts
index e73a5be..3f7fa33 100644
--- a/src/types-for-node.ts
+++ b/src/types-for-node.ts
@@ -304,9 +304,6 @@ export type ASTNodeListener = {
     node: TSESTree.TSReadonlyKeyword & ASTNodeWithParent,
   ) => void
   TSRestType?: (node: TSESTree.TSRestType & ASTNodeWithParent) => void
-  TSSatisfiesExpression?: (
-    node: TSESTree.TSSatisfiesExpression & ASTNodeWithParent,
-  ) => void
   TSStaticKeyword?: (node: TSESTree.TSStaticKeyword & ASTNodeWithParent) => void
   TSStringKeyword?: (node: TSESTree.TSStringKeyword & ASTNodeWithParent) => void
   TSSymbolKeyword?: (node: TSESTree.TSSymbolKeyword & ASTNodeWithParent) => void
@@ -666,9 +663,6 @@ export type TSNodeListener = {
     node: TSESTree.TSReadonlyKeyword & ASTNodeWithParent,
   ) => void
   TSRestType?: (node: TSESTree.TSRestType & ASTNodeWithParent) => void
-  TSSatisfiesExpression?: (
-    node: TSESTree.TSSatisfiesExpression & ASTNodeWithParent,
-  ) => void
   TSStaticKeyword?: (node: TSESTree.TSStaticKeyword & ASTNodeWithParent) => void
   TSStringKeyword?: (node: TSESTree.TSStringKeyword & ASTNodeWithParent) => void
   TSSymbolKeyword?: (node: TSESTree.TSSymbolKeyword & ASTNodeWithParent) => void
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index ac31ae0..5a9a822 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -14,7 +14,9 @@ import noAtDebugTags from "../rules/no-at-debug-tags"
 import noAtHtmlTags from "../rules/no-at-html-tags"
 import noDomManipulating from "../rules/no-dom-manipulating"
 import noDupeElseIfBlocks from "../rules/no-dupe-else-if-blocks"
+import noDupeOnDirectives from "../rules/no-dupe-on-directives"
 import noDupeStyleProperties from "../rules/no-dupe-style-properties"
+import noDupeUseDirectives from "../rules/no-dupe-use-directives"
 import noDynamicSlotName from "../rules/no-dynamic-slot-name"
 import noExportLoadInSvelteModuleInKitPages from "../rules/no-export-load-in-svelte-module-in-kit-pages"
 import noExtraReactiveCurlies from "../rules/no-extra-reactive-curlies"
@@ -62,7 +64,9 @@ export const rules = [
   noAtHtmlTags,
   noDomManipulating,
   noDupeElseIfBlocks,
+  noDupeOnDirectives,
   noDupeStyleProperties,
+  noDupeUseDirectives,
   noDynamicSlotName,
   noExportLoadInSvelteModuleInKitPages,
   noExtraReactiveCurlies,


Test patch:
diff --git a/tests/fixtures/rules/no-dupe-on-directives/invalid/inline-expression-errors.yaml b/tests/fixtures/rules/no-dupe-on-directives/invalid/inline-expression-errors.yaml
new file mode 100644
index 0000000..1b9e470
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/invalid/inline-expression-errors.yaml
@@ -0,0 +1,16 @@
+- message: This `on:keydown` directive is the same and duplicate directives in L3.
+  line: 2
+  column: 3
+  suggestions: null
+- message: This `on:keydown` directive is the same and duplicate directives in L2.
+  line: 3
+  column: 3
+  suggestions: null
+- message: This `on:keydown` directive is the same and duplicate directives in L10.
+  line: 7
+  column: 3
+  suggestions: null
+- message: This `on:keydown` directive is the same and duplicate directives in L7.
+  line: 10
+  column: 3
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dupe-on-directives/invalid/inline-expression-input.svelte b/tests/fixtures/rules/no-dupe-on-directives/invalid/inline-expression-input.svelte
new file mode 100644
index 0000000..ca1ebcb
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/invalid/inline-expression-input.svelte
@@ -0,0 +1,14 @@
+<button
+  on:keydown={() => console.log("foo")}
+  on:keydown={() => console.log("foo")}
+/>
+
+<button
+  on:keydown={() =>
+    // foo
+    console.log("foo")}
+  on:keydown={() =>
+    console
+      // bar
+      .log("foo")}
+/>
diff --git a/tests/fixtures/rules/no-dupe-on-directives/invalid/modifier-errors.yaml b/tests/fixtures/rules/no-dupe-on-directives/invalid/modifier-errors.yaml
new file mode 100644
index 0000000..6482f91
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/invalid/modifier-errors.yaml
@@ -0,0 +1,20 @@
+- message: This `on:click` directive is the same and duplicate directives in L3.
+  line: 2
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 3
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 4
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 5
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 6
+  column: 3
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dupe-on-directives/invalid/modifier-input.svelte b/tests/fixtures/rules/no-dupe-on-directives/invalid/modifier-input.svelte
new file mode 100644
index 0000000..2ac189a
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/invalid/modifier-input.svelte
@@ -0,0 +1,7 @@
+<button
+  on:click|once
+  on:click
+  on:click|self
+  on:click|capture
+  on:click|stopPropagation|self
+/>
diff --git a/tests/fixtures/rules/no-dupe-on-directives/invalid/multiple-element-errors.yaml b/tests/fixtures/rules/no-dupe-on-directives/invalid/multiple-element-errors.yaml
new file mode 100644
index 0000000..5fd970f
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/invalid/multiple-element-errors.yaml
@@ -0,0 +1,56 @@
+- message: This `on:mousemove` directive is the same and duplicate directives in L1.
+  line: 1
+  column: 6
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L1.
+  line: 1
+  column: 19
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L2.
+  line: 2
+  column: 8
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L2.
+  line: 2
+  column: 21
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L4.
+  line: 4
+  column: 10
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L4.
+  line: 4
+  column: 23
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L6.
+  line: 6
+  column: 10
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L6.
+  line: 6
+  column: 23
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L10.
+  line: 10
+  column: 10
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L10.
+  line: 10
+  column: 23
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L12.
+  line: 12
+  column: 10
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L12.
+  line: 12
+  column: 23
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L14.
+  line: 14
+  column: 10
+  suggestions: null
+- message: This `on:mousemove` directive is the same and duplicate directives in L14.
+  line: 14
+  column: 23
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dupe-on-directives/invalid/multiple-element-input.svelte b/tests/fixtures/rules/no-dupe-on-directives/invalid/multiple-element-input.svelte
new file mode 100644
index 0000000..728b999
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/invalid/multiple-element-input.svelte
@@ -0,0 +1,16 @@
+<div on:mousemove on:mousemove>
+  <div on:mousemove on:mousemove>
+    <div on:mousemove />
+    <div on:mousemove on:mousemove />
+    <div on:mousemove />
+    <div on:mousemove on:mousemove />
+    <div on:mousemove />
+  </div>
+  <div on:mousemove>
+    <div on:mousemove on:mousemove />
+    <div on:mousemove />
+    <div on:mousemove on:mousemove />
+    <div on:mousemove />
+    <div on:mousemove on:mousemove />
+  </div>
+</div>
diff --git a/tests/fixtures/rules/no-dupe-on-directives/invalid/test01-errors.yaml b/tests/fixtures/rules/no-dupe-on-directives/invalid/test01-errors.yaml
new file mode 100644
index 0000000..3b1efc1
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/invalid/test01-errors.yaml
@@ -0,0 +1,16 @@
+- message: This `on:click` directive is the same and duplicate directives in L12.
+  line: 12
+  column: 9
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L12.
+  line: 12
+  column: 18
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L13.
+  line: 13
+  column: 9
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L13.
+  line: 13
+  column: 30
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dupe-on-directives/invalid/test01-input.svelte b/tests/fixtures/rules/no-dupe-on-directives/invalid/test01-input.svelte
new file mode 100644
index 0000000..7b65187
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/invalid/test01-input.svelte
@@ -0,0 +1,13 @@
+<script>
+  function myHandler() {}
+  function foo() {}
+  function bar() {}
+</script>
+
+<!-- ✓ GOOD -->
+<button on:click on:click={myHandler} />
+<button on:click={foo} on:click={bar} />
+
+<!-- ✗ BAD -->
+<button on:click on:click />
+<button on:click={myHandler} on:click={myHandler} />
diff --git a/tests/fixtures/rules/no-dupe-on-directives/invalid/test02-errors.yaml b/tests/fixtures/rules/no-dupe-on-directives/invalid/test02-errors.yaml
new file mode 100644
index 0000000..b40b25e
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/invalid/test02-errors.yaml
@@ -0,0 +1,32 @@
+- message: This `on:click` directive is the same and duplicate directives in L3.
+  line: 2
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 3
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 4
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 5
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 6
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 7
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 8
+  column: 3
+  suggestions: null
+- message: This `on:click` directive is the same and duplicate directives in L2.
+  line: 9
+  column: 3
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dupe-on-directives/invalid/test02-input.svelte b/tests/fixtures/rules/no-dupe-on-directives/invalid/test02-input.svelte
new file mode 100644
index 0000000..c7b7605
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/invalid/test02-input.svelte
@@ -0,0 +1,10 @@
+<button
+  on:click
+  on:click
+  on:click
+  on:click
+  on:click
+  on:click
+  on:click
+  on:click
+/>
diff --git a/tests/fixtures/rules/no-dupe-on-directives/valid/test01-input.svelte b/tests/fixtures/rules/no-dupe-on-directives/valid/test01-input.svelte
new file mode 100644
index 0000000..7ef75d1
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/valid/test01-input.svelte
@@ -0,0 +1,9 @@
+<script>
+  function myHandler() {}
+  function foo() {}
+  function bar() {}
+</script>
+
+<!-- ✓ GOOD -->
+<button on:click on:click={myHandler} />
+<button on:click={foo} on:click={bar} />
diff --git a/tests/fixtures/rules/no-dupe-on-directives/valid/test02-input.svelte b/tests/fixtures/rules/no-dupe-on-directives/valid/test02-input.svelte
new file mode 100644
index 0000000..c911efb
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-on-directives/valid/test02-input.svelte
@@ -0,0 +1,6 @@
+<button
+  on:focus|once
+  on:focus={(evt) => console.log(evt)}
+  on:keydown={() => console.log("foo")}
+  on:keydown={(evt) => console.log(evt)}
+/>
diff --git a/tests/fixtures/rules/no-dupe-use-directives/invalid/inline-expression-errors.yaml b/tests/fixtures/rules/no-dupe-use-directives/invalid/inline-expression-errors.yaml
new file mode 100644
index 0000000..8cad248
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-use-directives/invalid/inline-expression-errors.yaml
@@ -0,0 +1,16 @@
+- message: This `use:foo` directive is the same and duplicate directives in L6.
+  line: 6
+  column: 9
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L6.
+  line: 6
+  column: 44
+  suggestions: null
+- message: This `use:bar` directive is the same and duplicate directives in L12.
+  line: 9
+  column: 3
+  suggestions: null
+- message: This `use:bar` directive is the same and duplicate directives in L9.
+  line: 12
+  column: 3
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dupe-use-directives/invalid/inline-expression-input.svelte b/tests/fixtures/rules/no-dupe-use-directives/invalid/inline-expression-input.svelte
new file mode 100644
index 0000000..7b04a66
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-use-directives/invalid/inline-expression-input.svelte
@@ -0,0 +1,16 @@
+<script>
+  function foo() {}
+  function bar() {}
+</script>
+
+<button use:foo={() => console.log("foo")} use:foo={() => console.log("foo")} />
+
+<button
+  use:bar={() =>
+    // foo
+    console.log("foo")}
+  use:bar={() =>
+    console
+      // bar
+      .log("foo")}
+/>
diff --git a/tests/fixtures/rules/no-dupe-use-directives/invalid/multiple-element-errors.yaml b/tests/fixtures/rules/no-dupe-use-directives/invalid/multiple-element-errors.yaml
new file mode 100644
index 0000000..30a86dd
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-use-directives/invalid/multiple-element-errors.yaml
@@ -0,0 +1,56 @@
+- message: This `use:foo` directive is the same and duplicate directives in L5.
+  line: 5
+  column: 6
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L5.
+  line: 5
+  column: 14
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L6.
+  line: 6
+  column: 8
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L6.
+  line: 6
+  column: 16
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L8.
+  line: 8
+  column: 10
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L8.
+  line: 8
+  column: 18
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L10.
+  line: 10
+  column: 10
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L10.
+  line: 10
+  column: 18
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L14.
+  line: 14
+  column: 10
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L14.
+  line: 14
+  column: 18
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L16.
+  line: 16
+  column: 10
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L16.
+  line: 16
+  column: 18
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L18.
+  line: 18
+  column: 10
+  suggestions: null
+- message: This `use:foo` directive is the same and duplicate directives in L18.
+  line: 18
+  column: 18
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dupe-use-directives/invalid/multiple-element-input.svelte b/tests/fixtures/rules/no-dupe-use-directives/invalid/multiple-element-input.svelte
new file mode 100644
index 0000000..a424e6f
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-use-directives/invalid/multiple-element-input.svelte
@@ -0,0 +1,20 @@
+<script>
+  function foo() {}
+</script>
+
+<div use:foo use:foo>
+  <div use:foo use:foo>
+    <div use:foo />
+    <div use:foo use:foo />
+    <div use:foo />
+    <div use:foo use:foo />
+    <div use:foo />
+  </div>
+  <div use:foo>
+    <div use:foo use:foo />
+    <div use:foo />
+    <div use:foo use:foo />
+    <div use:foo />
+    <div use:foo use:foo />
+  </div>
+</div>
diff --git a/tests/fixtures/rules/no-dupe-use-directives/invalid/test01-errors.yaml b/tests/fixtures/rules/no-dupe-use-directives/invalid/test01-errors.yaml
new file mode 100644
index 0000000..db8b0b5
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-use-directives/invalid/test01-errors.yaml
@@ -0,0 +1,24 @@
+- message:
+    This `use:clickOutside` directive is the same and duplicate directives
+    in L13.
+  line: 13
+  column: 6
+  suggestions: null
+- message:
+    This `use:clickOutside` directive is the same and duplicate directives
+    in L13.
+  line: 13
+  column: 23
+  suggestions: null
+- message:
+    This `use:clickOutside` directive is the same and duplicate directives
+    in L14.
+  line: 14
+  column: 6
+  suggestions: null
+- message:
+    This `use:clickOutside` directive is the same and duplicate directives
+    in L14.
+  line: 14
+  column: 31
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dupe-use-directives/invalid/test01-input.svelte b/tests/fixtures/rules/no-dupe-use-directives/invalid/test01-input.svelte
new file mode 100644
index 0000000..7f2d9c9
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-use-directives/invalid/test01-input.svelte
@@ -0,0 +1,14 @@
+<script>
+  function clickOutside() {}
+  const param = {}
+  const foo = {}
+  const bar = {}
+</script>
+
+<!-- ✓ GOOD -->
+<div use:clickOutside use:clickOutside={param} />
+<div use:clickOutside={foo} use:clickOutside={bar} />
+
+<!-- ✗ BAD -->
+<div use:clickOutside use:clickOutside />
+<div use:clickOutside={param} use:clickOutside={param} />
diff --git a/tests/fixtures/rules/no-dupe-use-directives/invalid/test02-errors.yaml b/tests/fixtures/rules/no-dupe-use-directives/invalid/test02-errors.yaml
new file mode 100644
index 0000000..bc1e725
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-use-directives/invalid/test02-errors.yaml
@@ -0,0 +1,60 @@
+- message: This `use:x` directive is the same and duplicate directives in L7.
+  line: 6
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 7
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 8
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 9
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 10
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 11
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 12
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 13
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 14
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 15
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 16
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 17
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 18
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 19
+  column: 3
+  suggestions: null
+- message: This `use:x` directive is the same and duplicate directives in L6.
+  line: 20
+  column: 3
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dupe-use-directives/invalid/test02-input.svelte b/tests/fixtures/rules/no-dupe-use-directives/invalid/test02-input.svelte
new file mode 100644
index 0000000..1d7eba4
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-use-directives/invalid/test02-input.svelte
@@ -0,0 +1,21 @@
+<script>
+  function x() {}
+</script>
+
+<div
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+  use:x
+/>
diff --git a/tests/fixtures/rules/no-dupe-use-directives/valid/inline-expression-input.svelte b/tests/fixtures/rules/no-dupe-use-directives/valid/inline-expression-input.svelte
new file mode 100644
index 0000000..6d516c3
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-use-directives/valid/inline-expression-input.svelte
@@ -0,0 +1,10 @@
+<script>
+  function foo() {}
+  function bar() {}
+  const param = {}
+</script>
+
+<div use:foo use:foo={param} />
+<div use:foo={{ a: 42 }} use:bar={{ a: 42 }} />
+<div use:foo={{ a: 42 }} use:foo={{ b: 42 }} />
+<div use:bar={{ a: 42 }} use:bar={{ a: 42, b: 42 }} />
diff --git a/tests/fixtures/rules/no-dupe-use-directives/valid/test01-input.svelte b/tests/fixtures/rules/no-dupe-use-directives/valid/test01-input.svelte
new file mode 100644
index 0000000..07cbfa8
--- /dev/null
+++ b/tests/fixtures/rules/no-dupe-use-directives/valid/test01-input.svelte
@@ -0,0 +1,10 @@
+<script>
+  function clickOutside() {}
+  const param = {}
+  const foo = {}
+  const bar = {}
+</script>
+
+<!-- ✓ GOOD -->
+<div use:clickOutside use:clickOutside={param} />
+<div use:clickOutside={foo} use:clickOutside={bar} />
diff --git a/tests/src/rules/no-dupe-on-directives.ts b/tests/src/rules/no-dupe-on-directives.ts
new file mode 100644
index 0000000..3e000c7
--- /dev/null
+++ b/tests/src/rules/no-dupe-on-directives.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/no-dupe-on-directives"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: "module",
+  },
+})
+
+tester.run(
+  "no-dupe-on-directives",
+  rule as any,
+  loadTestCases("no-dupe-on-directives"),
+)
diff --git a/tests/src/rules/no-dupe-use-directives.ts b/tests/src/rules/no-dupe-use-directives.ts
new file mode 100644
index 0000000..574b0e5
--- /dev/null
+++ b/tests/src/rules/no-dupe-use-directives.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/no-dupe-use-directives"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: "module",
+  },
+})
+
+tester.run(
+  "no-dupe-use-directives",
+  rule as any,
+  loadTestCases("no-dupe-use-directives"),
+)