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

Issue:
New rule: `require-strict-events`
### Motivation

Hi,
when using TypeScript with svelte, component events aren't automatically typed - e. g. you can have an `on:nonexistent={() => {}}` handle for a non-existent event. However, svelte does check this, if the opening script tag of the *child* component has the `strictEvents` attribute. (`<script lang="ts" strictEvents>`).

I propose to add a rule that would enforce this.

### Description

The rule should enforce that components have the `strictEvents` attribute present in their opening svelte tag.

I propose several an option to choose for which components the attribute should be enforced:
1. Require the attribute on all components. This makes sense since for a component that does not dispatch any event, the attribute still makes svelte check that no event listeners are present. I propose that this should be the default.
2. Only check components that actually dispatch an event - this would probably be more complicated to implement

Additional note: For some components that have event mixins, the `strictEvents` attribute does not work and you have to declare the `$$Events` interface. In that case, I think the rule shouldn't fire - so effectively, it would require *either* the `strictEvents` attribute or a manually declared interface `$$Events` for the component.

### Examples

```svelte
<!-- ✓ GOOD -->
<script lang="ts" strictEvents>
</script>

<script lang="ts">
  interface $$Events {}
</script>

<!-- ✗ BAD -->
<script lang="ts">
</script>
```


### Additional comments

The relevant [RFC](https://github.com/dummdidumm/rfcs/blob/ts-typedefs-within-svelte-components/text/ts-typing-props-slots-events.md#typing-events).

Pull request description:
feat: Added the `experimental-require-strict-events` rule
Closes #345 

Patch:
diff --git a/.changeset/tasty-houses-behave.md b/.changeset/tasty-houses-behave.md
new file mode 100644
index 0000000..e3a2d86
--- /dev/null
+++ b/.changeset/tasty-houses-behave.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+Added the experimental-require-strict-events rule
diff --git a/README.md b/README.md
index 534bc64..fa50c9e 100644
--- a/README.md
+++ b/README.md
@@ -373,6 +373,14 @@ These rules extend the rules provided by ESLint itself, or other plugins to work
 | [svelte/no-inner-declarations](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-inner-declarations/) | disallow variable or `function` declarations in nested blocks | :star: |
 | [svelte/no-trailing-spaces](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-trailing-spaces/) | disallow trailing whitespace at the end of lines | :wrench: |
 
+## Experimental
+
+:warning: These rules are considered experimental and may change or be removed in the future:
+
+| Rule ID | Description |    |
+|:--------|:------------|:---|
+| [svelte/experimental-require-strict-events](https://ota-meshi.github.io/eslint-plugin-svelte/rules/experimental-require-strict-events/) | require the strictEvents attribute on <script> tags |  |
+
 ## System
 
 These rules relate to this plugin works:
diff --git a/docs/rules.md b/docs/rules.md
index 1b53df6..fe20f80 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -92,6 +92,14 @@ These rules extend the rules provided by ESLint itself, or other plugins to work
 | [svelte/no-inner-declarations](./rules/no-inner-declarations.md) | disallow variable or `function` declarations in nested blocks | :star: |
 | [svelte/no-trailing-spaces](./rules/no-trailing-spaces.md) | disallow trailing whitespace at the end of lines | :wrench: |
 
+## Experimental
+
+:warning: These rules are considered experimental and may change or be removed in the future:
+
+| Rule ID | Description |    |
+|:--------|:------------|:---|
+| [svelte/experimental-require-strict-events](./rules/experimental-require-strict-events.md) | require the strictEvents attribute on <script> tags |  |
+
 ## System
 
 These rules relate to this plugin works:
diff --git a/docs/rules/experimental-require-strict-events.md b/docs/rules/experimental-require-strict-events.md
new file mode 100644
index 0000000..4fd4bfd
--- /dev/null
+++ b/docs/rules/experimental-require-strict-events.md
@@ -0,0 +1,47 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/experimental-require-strict-events"
+description: "require the strictEvents attribute on <script> tags"
+---
+
+# svelte/experimental-require-strict-events
+
+> require the strictEvents attribute on <script> tags
+
+- :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
+
+This rule enforces the presence of the `strictEvents` attribute on the main `<script>` tag of all components. This attributes enforces typechecking of events dispatched by the component, e.g. making it a typescript error to listen to any non-existent events. Alternatively, the event types may be defined manually by declaring the `$$Events` interface. The `strictEvents` attribute and the `$$Events` interface are experimental and are documented in [svelte RFC #38](https://github.com/dummdidumm/rfcs/blob/ts-typedefs-within-svelte-components/text/ts-typing-props-slots-events.md#typing-events).
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<!-- eslint svelte/experimental-require-strict-events: "error" -->
+
+<!-- ✓ GOOD -->
+<script lang="ts" strictEvents>
+</script>
+
+<script lang="ts">
+  interface $$Events {}
+</script>
+
+<!-- ✗ BAD -->
+<script lang="ts">
+</script>
+```
+
+</ESLintCodeBlock>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/experimental-require-strict-events.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/experimental-require-strict-events.ts)
diff --git a/src/rules/experimental-require-strict-events.ts b/src/rules/experimental-require-strict-events.ts
new file mode 100644
index 0000000..0451f8c
--- /dev/null
+++ b/src/rules/experimental-require-strict-events.ts
@@ -0,0 +1,46 @@
+import type { AST } from "svelte-eslint-parser"
+
+import { createRule } from "../utils"
+import { findAttribute, getLangValue } from "../utils/ast-utils"
+
+export default createRule("experimental-require-strict-events", {
+  meta: {
+    docs: {
+      description: "require the strictEvents attribute on <script> tags",
+      category: "Experimental",
+      recommended: false,
+    },
+    schema: [],
+    messages: {
+      missingStrictEvents: `The component must have the strictEvents attribute on its <script> tag or it must define the $$Events interface.`,
+    },
+    type: "suggestion",
+  },
+  create(context) {
+    let isTs = false
+    let hasAttribute = false
+    let hasInterface = false
+    let scriptNode: AST.SvelteScriptElement
+    return {
+      SvelteScriptElement(node) {
+        const lang = getLangValue(node)?.toLowerCase()
+        isTs = lang === "ts" || lang === "typescript"
+        hasAttribute = findAttribute(node, "strictEvents") !== null
+        scriptNode = node
+      },
+      TSInterfaceDeclaration(node) {
+        if (node.id.name === "$$Events") {
+          hasInterface = true
+        }
+      },
+      "Program:exit"() {
+        if (isTs && !hasAttribute && !hasInterface) {
+          context.report({
+            node: scriptNode,
+            messageId: "missingStrictEvents",
+          })
+        }
+      },
+    }
+  },
+})
diff --git a/src/types.ts b/src/types.ts
index accfd15..038047a 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -53,6 +53,7 @@ export type RuleCategory =
   | "Best Practices"
   | "Stylistic Issues"
   | "Extension Rules"
+  | "Experimental"
   | "System"
 
 export interface RuleMetaData {
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index e9d10b5..5d051d4 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -3,6 +3,7 @@ import typescriptEslintNoUnnecessaryCondition from "../rules/@typescript-eslint/
 import buttonHasType from "../rules/button-has-type"
 import commentDirective from "../rules/comment-directive"
 import derivedHasSameInputsOutputs from "../rules/derived-has-same-inputs-outputs"
+import experimentalRequireStrictEvents from "../rules/experimental-require-strict-events"
 import firstAttributeLinebreak from "../rules/first-attribute-linebreak"
 import htmlClosingBracketSpacing from "../rules/html-closing-bracket-spacing"
 import htmlQuotes from "../rules/html-quotes"
@@ -55,6 +56,7 @@ export const rules = [
   buttonHasType,
   commentDirective,
   derivedHasSameInputsOutputs,
+  experimentalRequireStrictEvents,
   firstAttributeLinebreak,
   htmlClosingBracketSpacing,
   htmlQuotes,
diff --git a/tools/render-rules.ts b/tools/render-rules.ts
index 9393516..454c240 100644
--- a/tools/render-rules.ts
+++ b/tools/render-rules.ts
@@ -7,6 +7,7 @@ const categories = [
   "Best Practices",
   "Stylistic Issues",
   "Extension Rules",
+  "Experimental",
   "System",
 ] as const
 
@@ -21,6 +22,8 @@ const descriptions: Record<(typeof categories)[number], string> = {
     "These rules relate to style guidelines, and are therefore quite subjective:",
   "Extension Rules":
     "These rules extend the rules provided by ESLint itself, or other plugins to work well in Svelte:",
+  Experimental:
+    ":warning: These rules are considered experimental and may change or be removed in the future:",
   System: "These rules relate to this plugin works:",
 }
 


Test patch:
diff --git a/tests/fixtures/rules/experimental-require-strict-events/invalid/no-strict-events01-errors.yaml b/tests/fixtures/rules/experimental-require-strict-events/invalid/no-strict-events01-errors.yaml
new file mode 100644
index 0000000..058f144
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-strict-events/invalid/no-strict-events01-errors.yaml
@@ -0,0 +1,6 @@
+- message:
+    The component must have the strictEvents attribute on its <script> tag
+    or it must define the $$Events interface.
+  line: 1
+  column: 1
+  suggestions: null
diff --git a/tests/fixtures/rules/experimental-require-strict-events/invalid/no-strict-events01-input.svelte b/tests/fixtures/rules/experimental-require-strict-events/invalid/no-strict-events01-input.svelte
new file mode 100644
index 0000000..0fbba99
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-strict-events/invalid/no-strict-events01-input.svelte
@@ -0,0 +1,2 @@
+<script lang="ts">
+</script>
diff --git a/tests/fixtures/rules/experimental-require-strict-events/valid/has-events-interface01-input.svelte b/tests/fixtures/rules/experimental-require-strict-events/valid/has-events-interface01-input.svelte
new file mode 100644
index 0000000..73e589e
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-strict-events/valid/has-events-interface01-input.svelte
@@ -0,0 +1,3 @@
+<script lang="ts">
+  interface $$Events {}
+</script>
diff --git a/tests/fixtures/rules/experimental-require-strict-events/valid/has-strict-events01-input.svelte b/tests/fixtures/rules/experimental-require-strict-events/valid/has-strict-events01-input.svelte
new file mode 100644
index 0000000..e96af6e
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-strict-events/valid/has-strict-events01-input.svelte
@@ -0,0 +1,2 @@
+<script lang="ts" strictEvents>
+</script>
diff --git a/tests/fixtures/rules/experimental-require-strict-events/valid/no-typescript01-input.svelte b/tests/fixtures/rules/experimental-require-strict-events/valid/no-typescript01-input.svelte
new file mode 100644
index 0000000..49f5fcd
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-strict-events/valid/no-typescript01-input.svelte
@@ -0,0 +1,2 @@
+<script>
+</script>
diff --git a/tests/fixtures/rules/experimental-require-strict-events/valid/script-module-context01-input.svelte b/tests/fixtures/rules/experimental-require-strict-events/valid/script-module-context01-input.svelte
new file mode 100644
index 0000000..5e40c34
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-strict-events/valid/script-module-context01-input.svelte
@@ -0,0 +1,5 @@
+<script lang="ts" context="module">
+</script>
+
+<script lang="ts" strictEvents>
+</script>
diff --git a/tests/src/rules/experimental-require-strict-events.ts b/tests/src/rules/experimental-require-strict-events.ts
new file mode 100644
index 0000000..8abea25
--- /dev/null
+++ b/tests/src/rules/experimental-require-strict-events.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/experimental-require-strict-events"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: "module",
+  },
+})
+
+tester.run(
+  "experimental-require-strict-events",
+  rule as any,
+  loadTestCases("experimental-require-strict-events"),
+)