Repository: ota-meshi/eslint-plugin-svelte
Pull request #310 (2022-11-27 14:41:58)

feat: improve `svelte/indent` rule to support more ts syntax

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: ec5ec05e9ee4af7d71ff84850178881b583f7e38

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/ota-meshi/eslint-plugin-svelte/new/indent?filename=.changeset/kind-boats-clean.md&value=---%0A%22eslint-plugin-svelte%22%3A%20patch%0A---%0A%0Afeat%3A%20improve%20%60svelte%2Findent%60%20rule%20to%20support%20more%20ts%20syntax%0A)


codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/ota-meshi/eslint-plugin-svelte/pr/310/builds/320107) or the icon next to each commit SHA.

Patch:
diff --git a/.changeset/kind-boats-clean.md b/.changeset/kind-boats-clean.md
new file mode 100644
index 0000000..0699df1
--- /dev/null
+++ b/.changeset/kind-boats-clean.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+feat: improve `svelte/indent` rule to support more ts syntax
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/package.json b/package.json
index 67ac96a..4111290 100644
--- a/package.json
+++ b/package.json
@@ -110,8 +110,8 @@
     "@types/postcss-safe-parser": "^5.0.1",
     "@types/prismjs": "^1.26.0",
     "@types/stylus": "^0.48.38",
-    "@typescript-eslint/eslint-plugin": "^5.4.0",
-    "@typescript-eslint/parser": "^5.4.1-0",
+    "@typescript-eslint/eslint-plugin": "^5.45.0",
+    "@typescript-eslint/parser": "^5.45.0",
     "@typescript-eslint/parser-v4": "npm:@typescript-eslint/parser@4",
     "@typescript/vfs": "^1.4.0",
     "assert": "^2.0.0",
@@ -164,7 +164,7 @@
     "svelte-adapter-ghpages": "0.0.2",
     "svelte-i18n": "^3.4.0",
     "type-coverage": "^2.22.0",
-    "typescript": "^4.5.2",
+    "typescript": "^4.9.3",
     "vite": "^3.1.0-0",
     "vite-plugin-svelte-md": "^0.1.5",
     "yaml": "^2.1.1",
diff --git a/src/rules/indent-helpers/es.ts b/src/rules/indent-helpers/es.ts
index b193a58..cd1fdc6 100644
--- a/src/rules/indent-helpers/es.ts
+++ b/src/rules/indent-helpers/es.ts
@@ -187,14 +187,25 @@ export function defineVisitor(context: IndentContext): NodeListener {
     },
     CallExpression(node: TSESTree.CallExpression) {
       const firstToken = sourceCode.getFirstToken(node)
-      const leftParenToken = sourceCode.getTokenAfter(node.callee, {
-        filter: isOpeningParenToken,
-        includeComments: false,
-      })!
+      const leftParenToken = sourceCode.getTokenAfter(
+        node.typeParameters || node.callee,
+        {
+          filter: isOpeningParenToken,
+          includeComments: false,
+        },
+      )!
       const rightParenToken = sourceCode.getLastToken(node)
 
+      if (node.typeParameters) {
+        offsets.setOffsetToken(
+          sourceCode.getFirstToken(node.typeParameters),
+          1,
+          firstToken,
+        )
+      }
+
       for (const optionalToken of sourceCode.getTokensBetween(
-        sourceCode.getLastToken(node.callee),
+        sourceCode.getLastToken(node.typeParameters || node.callee),
         leftParenToken,
         { filter: isOptionalToken, includeComments: false },
       )) {
@@ -817,12 +828,17 @@ export function defineVisitor(context: IndentContext): NodeListener {
       const calleeTokens = getFirstAndLastTokens(sourceCode, node.callee)
       offsets.setOffsetToken(calleeTokens.firstToken, 1, newToken)
 
-      if (
-        node.arguments.length ||
-        calleeTokens.lastToken.range[1] < node.range[1]
-      ) {
+      if (node.typeParameters) {
+        offsets.setOffsetToken(
+          sourceCode.getFirstToken(node.typeParameters),
+          1,
+          calleeTokens.firstToken,
+        )
+      }
+      const leftParenBefore = node.typeParameters || calleeTokens.lastToken
+      if (node.arguments.length || leftParenBefore.range[1] < node.range[1]) {
         const rightParenToken = sourceCode.getLastToken(node)
-        const leftParenToken = sourceCode.getTokenAfter(calleeTokens.lastToken)!
+        const leftParenToken = sourceCode.getTokenAfter(leftParenBefore)!
 
         offsets.setOffsetToken(leftParenToken, 1, calleeTokens.firstToken)
         offsets.setOffsetElementList(
diff --git a/src/rules/indent-helpers/ts.ts b/src/rules/indent-helpers/ts.ts
index 33a1ebd..06d8416 100644
--- a/src/rules/indent-helpers/ts.ts
+++ b/src/rules/indent-helpers/ts.ts
@@ -41,33 +41,53 @@ export function defineVisitor(context: IndentContext): NodeListener {
         offsets.setOffsetToken(before, 1, baseToken)
       }
     },
-    TSAsExpression(node: TSESTree.TSAsExpression) {
+    TSAsExpression(
+      node: TSESTree.TSAsExpression | TSESTree.TSSatisfiesExpression,
+    ) {
       // foo as T
+      // or
+      // foo satisfies T
       const expressionTokens = getFirstAndLastTokens(
         sourceCode,
         node.expression,
       )
-      const asToken = sourceCode.getTokenAfter(expressionTokens.lastToken)!
+      const asOrSatisfiesToken = sourceCode.getTokenAfter(
+        expressionTokens.lastToken,
+      )!
       offsets.setOffsetToken(
         [
-          asToken,
+          asOrSatisfiesToken,
           getFirstAndLastTokens(sourceCode, node.typeAnnotation).firstToken,
         ],
         1,
         expressionTokens.firstToken,
       )
     },
-    TSTypeReference(node: TSESTree.TSTypeReference) {
+    TSSatisfiesExpression(node: TSESTree.TSSatisfiesExpression) {
+      // foo satisfies T
+      visitor.TSAsExpression(node)
+    },
+    TSTypeReference(
+      node: TSESTree.TSTypeReference | TSESTree.TSInstantiationExpression,
+    ) {
       // T<U>
+      // or
+      // const ErrorMap = Map<string, Error>
+      //                  ^^^^^^^^^^^^^^^^^^
       if (node.typeParameters) {
-        const typeNameTokens = getFirstAndLastTokens(sourceCode, node.typeName)
+        const firstToken = sourceCode.getFirstToken(node)
         offsets.setOffsetToken(
           sourceCode.getFirstToken(node.typeParameters),
           1,
-          typeNameTokens.firstToken,
+          firstToken,
         )
       }
     },
+    TSInstantiationExpression(node: TSESTree.TSInstantiationExpression) {
+      // const ErrorMap = Map<string, Error>
+      //                  ^^^^^^^^^^^^^^^^^^
+      visitor.TSTypeReference(node)
+    },
     TSTypeParameterInstantiation(
       node:
         | TSESTree.TSTypeParameterInstantiation
@@ -701,6 +721,7 @@ export function defineVisitor(context: IndentContext): NodeListener {
       node:
         | TSESTree.TSAbstractMethodDefinition
         | TSESTree.TSAbstractPropertyDefinition
+        | TSESTree.TSAbstractAccessorProperty
         | TSESTree.TSEnumMember,
     ) {
       const { keyNode, valueNode } =
@@ -754,6 +775,9 @@ export function defineVisitor(context: IndentContext): NodeListener {
     TSEnumMember(node: TSESTree.TSEnumMember) {
       visitor.TSAbstractMethodDefinition(node)
     },
+    TSAbstractAccessorProperty(node: TSESTree.TSAbstractAccessorProperty) {
+      visitor.TSAbstractMethodDefinition(node)
+    },
     TSOptionalType(
       node: TSESTree.TSOptionalType | TSESTree.TSNonNullExpression,
     ) {
@@ -960,6 +984,48 @@ export function defineVisitor(context: IndentContext): NodeListener {
         )
       }
     },
+    AccessorProperty(node: TSESTree.AccessorProperty) {
+      const keyNode = node.key
+      const valueNode = node.value
+      const firstToken = sourceCode.getFirstToken(node)
+      const keyTokens = getFirstAndLastTokens(sourceCode, keyNode)
+      const prefixTokens = sourceCode.getTokensBetween(
+        firstToken,
+        keyTokens.firstToken,
+      )
+      if (node.computed) {
+        prefixTokens.pop() // pop [
+      }
+      offsets.setOffsetToken(prefixTokens, 0, firstToken)
+      let lastKeyToken
+      if (node.computed) {
+        const leftBracketToken = sourceCode.getTokenBefore(
+          keyTokens.firstToken,
+        )!
+        const rightBracketToken = (lastKeyToken = sourceCode.getTokenAfter(
+          keyTokens.lastToken,
+        )!)
+        offsets.setOffsetToken(leftBracketToken, 0, firstToken)
+        offsets.setOffsetElementList(
+          [keyNode],
+          leftBracketToken,
+          rightBracketToken,
+          1,
+        )
+      } else {
+        offsets.setOffsetToken(keyTokens.firstToken, 0, firstToken)
+        lastKeyToken = keyTokens.lastToken
+      }
+
+      if (valueNode != null) {
+        const initToken = sourceCode.getFirstToken(valueNode)
+        offsets.setOffsetToken(
+          [...sourceCode.getTokensBetween(lastKeyToken, initToken), initToken],
+          1,
+          lastKeyToken,
+        )
+      }
+    },
     StaticBlock(node: TSESTree.StaticBlock) {
       const firstToken = sourceCode.getFirstToken(node)
       let next = sourceCode.getTokenAfter(firstToken)
@@ -1084,7 +1150,7 @@ export function defineVisitor(context: IndentContext): NodeListener {
   const commonsVisitor: any = {
     // Process semicolons.
     ["TSTypeAliasDeclaration, TSCallSignatureDeclaration, TSConstructSignatureDeclaration, TSImportEqualsDeclaration," +
-      "TSAbstractMethodDefinition, TSAbstractPropertyDefinition, TSEnumMember," +
+      "TSAbstractMethodDefinition, TSAbstractPropertyDefinition, AccessorProperty,  TSAbstractAccessorProperty, TSEnumMember," +
       "TSPropertySignature, TSIndexSignature, TSMethodSignature," +
       "TSAbstractClassProperty, ClassProperty"](node: TSESTree.Node) {
       const firstToken = sourceCode.getFirstToken(node)
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/indent/invalid/ts-v5/ts-abstract-accessor-property-errors.yaml b/tests/fixtures/rules/indent/invalid/ts-v5/ts-abstract-accessor-property-errors.yaml
new file mode 100644
index 0000000..83be458
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-abstract-accessor-property-errors.yaml
@@ -0,0 +1,28 @@
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 3
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 4
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 5
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 6
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 7
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 10
+  column: 1
+  suggestions: null
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 11
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-abstract-accessor-property-input.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-abstract-accessor-property-input.svelte
new file mode 100644
index 0000000..136ca13
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-abstract-accessor-property-input.svelte
@@ -0,0 +1,12 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+abstract class Foo {
+abstract accessor
+foo:
+number
+// parser v5 does not parse value.
+//    =
+//    1
+;
+}
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-abstract-accessor-property-output.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-abstract-accessor-property-output.svelte
new file mode 100644
index 0000000..26b0047
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-abstract-accessor-property-output.svelte
@@ -0,0 +1,12 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+  abstract class Foo {
+    abstract accessor
+    foo:
+      number
+    // parser v5 does not parse value.
+//    =
+//    1
+    ;
+  }
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property01-errors.yaml b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property01-errors.yaml
new file mode 100644
index 0000000..d3cced0
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property01-errors.yaml
@@ -0,0 +1,20 @@
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 3
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 4
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 5
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 6
+  column: 1
+  suggestions: null
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 7
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property01-input.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property01-input.svelte
new file mode 100644
index 0000000..43560c1
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property01-input.svelte
@@ -0,0 +1,8 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+class Foo {
+accessor
+foo
+;
+}
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property01-output.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property01-output.svelte
new file mode 100644
index 0000000..5471270
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property01-output.svelte
@@ -0,0 +1,8 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+  class Foo {
+    accessor
+    foo
+    ;
+  }
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property02-errors.yaml b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property02-errors.yaml
new file mode 100644
index 0000000..9f61a9f
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property02-errors.yaml
@@ -0,0 +1,28 @@
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 3
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 4
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 5
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 6
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 7
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 8
+  column: 1
+  suggestions: null
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 9
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property02-input.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property02-input.svelte
new file mode 100644
index 0000000..bbf9104
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property02-input.svelte
@@ -0,0 +1,10 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+class Foo {
+accessor
+foo
+=
+2
+;
+}
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property02-output.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property02-output.svelte
new file mode 100644
index 0000000..c1289b5
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property02-output.svelte
@@ -0,0 +1,10 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+  class Foo {
+    accessor
+    foo
+      =
+      2
+    ;
+  }
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property03-errors.yaml b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property03-errors.yaml
new file mode 100644
index 0000000..4c60dff
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property03-errors.yaml
@@ -0,0 +1,24 @@
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 3
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 4
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 5
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 6
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 7
+  column: 1
+  suggestions: null
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 8
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property03-input.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property03-input.svelte
new file mode 100644
index 0000000..c6920e2
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property03-input.svelte
@@ -0,0 +1,9 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+class Foo {
+declare accessor
+foo:
+number
+;
+}
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property03-output.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property03-output.svelte
new file mode 100644
index 0000000..aab3cb1
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property03-output.svelte
@@ -0,0 +1,9 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+  class Foo {
+    declare accessor
+    foo:
+      number
+    ;
+  }
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property04-errors.yaml b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property04-errors.yaml
new file mode 100644
index 0000000..9f61a9f
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property04-errors.yaml
@@ -0,0 +1,28 @@
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 3
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 4
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 5
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 6
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 7
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 8
+  column: 1
+  suggestions: null
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 9
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property04-input.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property04-input.svelte
new file mode 100644
index 0000000..f5f4328
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property04-input.svelte
@@ -0,0 +1,10 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+class Foo {
+override accessor
+foo
+=
+2
+;
+}
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property04-output.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property04-output.svelte
new file mode 100644
index 0000000..ada731b
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property04-output.svelte
@@ -0,0 +1,10 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+  class Foo {
+    override accessor
+    foo
+      =
+      2
+    ;
+  }
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property05-errors.yaml b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property05-errors.yaml
new file mode 100644
index 0000000..04fe222
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property05-errors.yaml
@@ -0,0 +1,36 @@
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 3
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 4
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 5
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 6
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 7
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 8
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 9
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 10
+  column: 1
+  suggestions: null
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 11
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property05-input.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property05-input.svelte
new file mode 100644
index 0000000..2962ff0
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property05-input.svelte
@@ -0,0 +1,12 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+class Foo {
+accessor
+[
+1
+]
+=
+2
+;
+}
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property05-output.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property05-output.svelte
new file mode 100644
index 0000000..a0c02dc
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-accessor-property05-output.svelte
@@ -0,0 +1,12 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+  class Foo {
+    accessor
+    [
+      1
+    ]
+      =
+      2
+    ;
+  }
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-instantiation-expression01-errors.yaml b/tests/fixtures/rules/indent/invalid/ts-v5/ts-instantiation-expression01-errors.yaml
new file mode 100644
index 0000000..ae342ac
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-instantiation-expression01-errors.yaml
@@ -0,0 +1,20 @@
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 3
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 4
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 5
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 6
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 7
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-instantiation-expression01-input.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-instantiation-expression01-input.svelte
new file mode 100644
index 0000000..850e86e
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-instantiation-expression01-input.svelte
@@ -0,0 +1,8 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+const ErrorMap = Map
+<
+string,
+Error
+>;
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-instantiation-expression01-output.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-instantiation-expression01-output.svelte
new file mode 100644
index 0000000..f03733b
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-instantiation-expression01-output.svelte
@@ -0,0 +1,8 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+  const ErrorMap = Map
+    <
+      string,
+      Error
+    >;
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-satisfies-operators01-errors.yaml b/tests/fixtures/rules/indent/invalid/ts-v5/ts-satisfies-operators01-errors.yaml
new file mode 100644
index 0000000..e1fb488
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-satisfies-operators01-errors.yaml
@@ -0,0 +1,20 @@
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 3
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 4
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 5
+  column: 1
+  suggestions: null
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 6
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 7
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-satisfies-operators01-input.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-satisfies-operators01-input.svelte
new file mode 100644
index 0000000..8210fd3
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-satisfies-operators01-input.svelte
@@ -0,0 +1,8 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+var a =
+{} satisfies
+Foo
+var b =
+{} satisfies Bar
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts-v5/ts-satisfies-operators01-output.svelte b/tests/fixtures/rules/indent/invalid/ts-v5/ts-satisfies-operators01-output.svelte
new file mode 100644
index 0000000..52bd3d4
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts-v5/ts-satisfies-operators01-output.svelte
@@ -0,0 +1,8 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+  var a =
+    {} satisfies
+      Foo
+  var b =
+    {} satisfies Bar
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters01-errors.yaml b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters01-errors.yaml
new file mode 100644
index 0000000..cbdad17
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters01-errors.yaml
@@ -0,0 +1,36 @@
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 3
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 4
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 5
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 6
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 7
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 8
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 9
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 10
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 11
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters01-input.svelte b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters01-input.svelte
new file mode 100644
index 0000000..1269332
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters01-input.svelte
@@ -0,0 +1,12 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+foo
+<
+T
+,
+U
+>
+(
+arg
+)
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters01-output.svelte b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters01-output.svelte
new file mode 100644
index 0000000..8a88ef8
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters01-output.svelte
@@ -0,0 +1,12 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+  foo
+    <
+      T
+      ,
+      U
+    >
+    (
+      arg
+    )
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters02-errors.yaml b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters02-errors.yaml
new file mode 100644
index 0000000..aa468a9
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters02-errors.yaml
@@ -0,0 +1,40 @@
+- message: Expected indentation of 2 spaces but found 0 spaces.
+  line: 3
+  column: 1
+  suggestions: null
+- message: Expected indentation of 4 spaces but found 0 spaces.
+  line: 4
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 5
+  column: 1
+  suggestions: null
+- message: Expected indentation of 8 spaces but found 0 spaces.
+  line: 6
+  column: 1
+  suggestions: null
+- message: Expected indentation of 8 spaces but found 0 spaces.
+  line: 7
+  column: 1
+  suggestions: null
+- message: Expected indentation of 8 spaces but found 0 spaces.
+  line: 8
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 9
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 10
+  column: 1
+  suggestions: null
+- message: Expected indentation of 8 spaces but found 0 spaces.
+  line: 11
+  column: 1
+  suggestions: null
+- message: Expected indentation of 6 spaces but found 0 spaces.
+  line: 12
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters02-input.svelte b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters02-input.svelte
new file mode 100644
index 0000000..84afba6
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters02-input.svelte
@@ -0,0 +1,13 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+const foo = new
+Foo
+<
+T
+,
+U
+>
+(
+arg
+)
+</script>
diff --git a/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters02-output.svelte b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters02-output.svelte
new file mode 100644
index 0000000..3b48b36
--- /dev/null
+++ b/tests/fixtures/rules/indent/invalid/ts/ts-type-parameters02-output.svelte
@@ -0,0 +1,13 @@
+<!-- prettier-ignore -->
+<script lang="ts">
+  const foo = new
+    Foo
+      <
+        T
+        ,
+        U
+      >
+      (
+        arg
+      )
+</script>
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"),
+)