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

Issue:
New rule: `require-event-dispatcher-types`
### Motivation

Hi,
when using svelte with TypeScript, it is possible to add types to events by typing the `createEventDispatcher` function so:

```svelte
import { createEventDispatcher } from "svelte";

const dispatch = createEventDispatcher<{one: never, two: number}>();

dispatch("one"); // OK
dispatch("two", 3); // OK
dispatch("one", "Hello world"); // Fails
dispatch("two"); // Fails
dispatch("two", "Hello world"); // Fails
```

I propose a rule that would require all calls to `createEventDispatcher` to include the templated type.

### Description

The rule would trigger on any call of `createEventDispatcher` witchou the templated type

### Examples

```svelte
import { createEventDispatcher } from "svelte";

<!-- ✓ GOOD -->
const dispatch = createEventDispatcher<{one: never, two: number}>();

const dispatch = createEventDispatcher<any>();

const dispatch = createEventDispatcher<{}>();

<!-- ✗ BAD -->
const dispatch = createEventDispatcher();
```


### Additional comments

_No response_

Pull request:
Added the `require-event-dispatcher-types` rule
Closes #346 

Patch:
diff --git a/.changeset/orange-impalas-speak.md b/.changeset/orange-impalas-speak.md
new file mode 100644
index 0000000..f42aadb
--- /dev/null
+++ b/.changeset/orange-impalas-speak.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+fix: update svelte-eslint-parser to v0.23
diff --git a/.changeset/witty-donuts-jog.md b/.changeset/witty-donuts-jog.md
new file mode 100644
index 0000000..680a059
--- /dev/null
+++ b/.changeset/witty-donuts-jog.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+Added the require-event-dispatcher-types rule
diff --git a/README.md b/README.md
index 5fb3265..48cfa6e 100644
--- a/README.md
+++ b/README.md
@@ -336,6 +336,7 @@ These rules relate to better ways of doing things to help you avoid problems:
 | [svelte/no-unused-svelte-ignore](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
 | [svelte/no-useless-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
 | [svelte/prefer-destructured-store-props](https://ota-meshi.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
+| [svelte/require-event-dispatcher-types](https://ota-meshi.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for createEventDispatcher |  |
 | [svelte/require-optimized-style-attribute](https://ota-meshi.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized |  |
 | [svelte/require-stores-init](https://ota-meshi.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store |  |
 
diff --git a/docs/rules.md b/docs/rules.md
index 59e794a..b3d5e24 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -55,6 +55,7 @@ These rules relate to better ways of doing things to help you avoid problems:
 | [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
 | [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
 | [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
+| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for createEventDispatcher |  |
 | [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized |  |
 | [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store |  |
 
diff --git a/docs/rules/require-event-dispatcher-types.md b/docs/rules/require-event-dispatcher-types.md
new file mode 100644
index 0000000..344e1df
--- /dev/null
+++ b/docs/rules/require-event-dispatcher-types.md
@@ -0,0 +1,50 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/require-event-dispatcher-types"
+description: "require type parameters for createEventDispatcher"
+---
+
+# svelte/require-event-dispatcher-types
+
+> require type parameters for createEventDispatcher
+
+## :book: Rule Details
+
+This rule is aimed to enforce type parameters when calling `createEventDispatcher`. Adding types makes all `dispatch` calls as well as all event listeners typechecked. For more information, see the [svelte docs](https://github.com/sveltejs/language-tools/blob/master/docs/preprocessors/typescript.md#typing-component-events). 
+
+<ESLintCodeBlock language="javascript">
+
+<!--eslint-skip-->
+
+```svelte
+<script lang="ts">
+  /* eslint svelte/require-event-dispatcher-types: "error" */
+
+  import { createEventDispatcher } from "svelte"
+
+  /* ✓ GOOD */
+  const dispatch1 = createEventDispatcher<{ one: never; two: number }>()
+  const dispatch2 = createEventDispatcher<Record<string, never>>()
+  const dispatch3 = createEventDispatcher<any>()
+  const dispatch4 = createEventDispatcher<unknown>()
+
+  /* ✗ BAD */
+  const dispatch5 = createEventDispatcher()
+</script>
+```
+
+</ESLintCodeBlock>
+
+## :wrench: Options
+
+Nothing.
+
+## :rocket: Version
+
+This rule was introduced in eslint-plugin-svelte v2.16.0
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/require-event-dispatcher-types.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/require-event-dispatcher-types.ts)
diff --git a/package.json b/package.json
index f708c7a..d78e852 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
     "postcss": "^8.4.5",
     "postcss-load-config": "^3.1.4",
     "postcss-safe-parser": "^6.0.0",
-    "svelte-eslint-parser": "^0.22.0"
+    "svelte-eslint-parser": "^0.23.0"
   },
   "devDependencies": {
     "@1stg/browserslist-config": "^1.2.3",
diff --git a/src/rules/reference-helpers/svelte-createEventDispatcher.ts b/src/rules/reference-helpers/svelte-createEventDispatcher.ts
new file mode 100644
index 0000000..523f64e
--- /dev/null
+++ b/src/rules/reference-helpers/svelte-createEventDispatcher.ts
@@ -0,0 +1,20 @@
+import type { TSESTree } from "@typescript-eslint/types"
+import { ReferenceTracker } from "eslint-utils"
+import type { RuleContext } from "../../types"
+
+/** Extract 'svelte createEventDispatcher' references */
+export function* extractCreateEventDispatcherReferences(
+  context: RuleContext,
+): Generator<TSESTree.CallExpression, void> {
+  const referenceTracker = new ReferenceTracker(context.getScope())
+  for (const { node } of referenceTracker.iterateEsmReferences({
+    svelte: {
+      [ReferenceTracker.ESM]: true,
+      createEventDispatcher: {
+        [ReferenceTracker.CALL]: true,
+      },
+    },
+  })) {
+    yield node as TSESTree.CallExpression
+  }
+}
diff --git a/src/rules/require-event-dispatcher-types.ts b/src/rules/require-event-dispatcher-types.ts
new file mode 100644
index 0000000..6c27f71
--- /dev/null
+++ b/src/rules/require-event-dispatcher-types.ts
@@ -0,0 +1,39 @@
+import { createRule } from "../utils"
+import { getLangValue } from "../utils/ast-utils"
+import { extractCreateEventDispatcherReferences } from "./reference-helpers/svelte-createEventDispatcher"
+
+export default createRule("require-event-dispatcher-types", {
+  meta: {
+    docs: {
+      description: "require type parameters for createEventDispatcher",
+      category: "Best Practices",
+      recommended: false,
+    },
+    schema: [],
+    messages: {
+      missingTypeParameter: `Type parameters missing for the createEventDispatcher function call.`,
+    },
+    type: "suggestion",
+  },
+  create(context) {
+    let isTs = false
+    return {
+      SvelteScriptElement(node) {
+        const lang = getLangValue(node)?.toLowerCase()
+        if (lang === "ts" || lang === "typescript") {
+          isTs = true
+        }
+      },
+      "Program:exit"() {
+        if (!isTs) {
+          return
+        }
+        for (const node of extractCreateEventDispatcherReferences(context)) {
+          if (node.typeParameters === undefined) {
+            context.report({ node, messageId: "missingTypeParameter" })
+          }
+        }
+      },
+    }
+  },
+})
diff --git a/src/utils/get-package-json.ts b/src/utils/get-package-json.ts
index 88e1c50..5076eee 100644
--- a/src/utils/get-package-json.ts
+++ b/src/utils/get-package-json.ts
@@ -6,7 +6,12 @@ import fs from "fs"
 import path from "path"
 import { createCache } from "./cache"
 
-type PackageJson = Record<string, any> & { filePath: string }
+type PackageJson = {
+  name?: unknown
+  dependencies?: { [key in string]?: unknown }
+  devDependencies?: { [key in string]?: unknown }
+  filePath: string
+}
 
 const isRunOnBrowser = !fs.readFileSync
 const cache = createCache<PackageJson | null>()
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index 5a9a822..ceba1f7 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -36,6 +36,7 @@ import noUselessMustaches from "../rules/no-useless-mustaches"
 import preferClassDirective from "../rules/prefer-class-directive"
 import preferDestructuredStoreProps from "../rules/prefer-destructured-store-props"
 import preferStyleDirective from "../rules/prefer-style-directive"
+import requireEventDispatcherTypes from "../rules/require-event-dispatcher-types"
 import requireOptimizedStyleAttribute from "../rules/require-optimized-style-attribute"
 import requireStoreCallbacksUseSetParam from "../rules/require-store-callbacks-use-set-param"
 import requireStoreReactiveAccess from "../rules/require-store-reactive-access"
@@ -86,6 +87,7 @@ export const rules = [
   preferClassDirective,
   preferDestructuredStoreProps,
   preferStyleDirective,
+  requireEventDispatcherTypes,
   requireOptimizedStyleAttribute,
   requireStoreCallbacksUseSetParam,
   requireStoreReactiveAccess,
diff --git a/src/utils/svelte-kit.ts b/src/utils/svelte-kit.ts
index 4e4352a..2bb5205 100644
--- a/src/utils/svelte-kit.ts
+++ b/src/utils/svelte-kit.ts
@@ -50,8 +50,8 @@ function hasSvelteKit(filePath: string): boolean {
       // So always it returns true if it runs on the package.
       return true
     return Boolean(
-      packageJson.dependencies["@sveltejs/kit"] ??
-        packageJson.devDependencies["@sveltejs/kit"],
+      packageJson.dependencies?.["@sveltejs/kit"] ??
+        packageJson.devDependencies?.["@sveltejs/kit"],
     )
   } catch (_e) {
     return false


Test patch:
diff --git a/tests/fixtures/rules/require-event-dispatcher-types/invalid/import-alias01-errors.yaml b/tests/fixtures/rules/require-event-dispatcher-types/invalid/import-alias01-errors.yaml
new file mode 100644
index 0000000..50e8447
--- /dev/null
+++ b/tests/fixtures/rules/require-event-dispatcher-types/invalid/import-alias01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Type parameters missing for the createEventDispatcher function call.
+  line: 4
+  column: 20
+  suggestions: null
diff --git a/tests/fixtures/rules/require-event-dispatcher-types/invalid/import-alias01-input.svelte b/tests/fixtures/rules/require-event-dispatcher-types/invalid/import-alias01-input.svelte
new file mode 100644
index 0000000..6c57eeb
--- /dev/null
+++ b/tests/fixtures/rules/require-event-dispatcher-types/invalid/import-alias01-input.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+  import { createEventDispatcher as ced } from "svelte"
+
+  const dispatch = ced()
+</script>
diff --git a/tests/fixtures/rules/require-event-dispatcher-types/invalid/no-types01-errors.yaml b/tests/fixtures/rules/require-event-dispatcher-types/invalid/no-types01-errors.yaml
new file mode 100644
index 0000000..50e8447
--- /dev/null
+++ b/tests/fixtures/rules/require-event-dispatcher-types/invalid/no-types01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Type parameters missing for the createEventDispatcher function call.
+  line: 4
+  column: 20
+  suggestions: null
diff --git a/tests/fixtures/rules/require-event-dispatcher-types/invalid/no-types01-input.svelte b/tests/fixtures/rules/require-event-dispatcher-types/invalid/no-types01-input.svelte
new file mode 100644
index 0000000..64c45eb
--- /dev/null
+++ b/tests/fixtures/rules/require-event-dispatcher-types/invalid/no-types01-input.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+  import { createEventDispatcher } from "svelte"
+
+  const dispatch = createEventDispatcher()
+</script>
diff --git a/tests/fixtures/rules/require-event-dispatcher-types/valid/has-types01-input.svelte b/tests/fixtures/rules/require-event-dispatcher-types/valid/has-types01-input.svelte
new file mode 100644
index 0000000..167bfde
--- /dev/null
+++ b/tests/fixtures/rules/require-event-dispatcher-types/valid/has-types01-input.svelte
@@ -0,0 +1,8 @@
+<script lang="ts">
+  import { createEventDispatcher } from "svelte"
+
+  const dispatch1 = createEventDispatcher<{ one: never; two: number }>()
+  const dispatch2 = createEventDispatcher<Record<string, never>>()
+  const dispatch3 = createEventDispatcher<any>()
+  const dispatch4 = createEventDispatcher<unknown>()
+</script>
diff --git a/tests/fixtures/rules/require-event-dispatcher-types/valid/no-typescript01-input.svelte b/tests/fixtures/rules/require-event-dispatcher-types/valid/no-typescript01-input.svelte
new file mode 100644
index 0000000..957318e
--- /dev/null
+++ b/tests/fixtures/rules/require-event-dispatcher-types/valid/no-typescript01-input.svelte
@@ -0,0 +1,5 @@
+<script>
+  import { createEventDispatcher } from "svelte"
+
+  const dispatch = createEventDispatcher()
+</script>
diff --git a/tests/fixtures/rules/require-event-dispatcher-types/valid/non-svelte-dispatcher01-input.svelte b/tests/fixtures/rules/require-event-dispatcher-types/valid/non-svelte-dispatcher01-input.svelte
new file mode 100644
index 0000000..e025c70
--- /dev/null
+++ b/tests/fixtures/rules/require-event-dispatcher-types/valid/non-svelte-dispatcher01-input.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+  import { createEventDispatcher } from "./unknown"
+
+  const dispatch = createEventDispatcher()
+</script>
diff --git a/tests/fixtures/rules/sort-attributes/.eslintrc.js b/tests/fixtures/rules/sort-attributes/.eslintrc.js
new file mode 100644
index 0000000..ba7aa7d
--- /dev/null
+++ b/tests/fixtures/rules/sort-attributes/.eslintrc.js
@@ -0,0 +1,5 @@
+module.exports = {
+  rules: {
+    "no-use-before-define": "off",
+  },
+}
diff --git a/tests/src/rules/require-event-dispatcher-types.ts b/tests/src/rules/require-event-dispatcher-types.ts
new file mode 100644
index 0000000..a9ea97e
--- /dev/null
+++ b/tests/src/rules/require-event-dispatcher-types.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/require-event-dispatcher-types"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: "module",
+  },
+})
+
+tester.run(
+  "require-event-dispatcher-types",
+  rule as any,
+  loadTestCases("require-event-dispatcher-types"),
+)