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

Issue:
New rule: `require-slot-types`
### Motivation

It is possible to add types to component slots and their props:

```
<script lang="ts">
    interface $$Slots {
        default: { prop: boolean; };
    }
</script>

<slot prop={true} />
```

I would like to propose a rule to enforce these slot types.

### Description

The rule should report any component that includes a `<slot>` and does not declare the `$$Slots` interface.

### Examples

```svelte
<!-- ✓ GOOD -->
<script>
</script>

<b>No slots here!</b>



<script>
  interface $$Slots {
    default: Record<string, never>;
  }
</script>

<slot />




<script lang="ts">
    interface $$Slots {
        default: { prop: boolean; };
    }
</script>

<slot prop={true} />


<script lang="ts">
    interface $$Slots {
        named: Record<string, never>;
    }
</script>

<slot name = "named" />

<!-- ✗ BAD -->
<script>
</script>

<slot />
```


### Additional comments

The relevant [RFC](https://github.com/dummdidumm/rfcs/blob/ts-typedefs-within-svelte-components/text/ts-typing-props-slots-events.md#typing-slots).

Pull request description:
feat: Added the experimental-require-slot-types rule
Closes #347

Depends on #365 being merged first - it introduces the experimental rules section


Patch:
diff --git a/.changeset/strong-wombats-worry.md b/.changeset/strong-wombats-worry.md
new file mode 100644
index 0000000..aa6742b
--- /dev/null
+++ b/.changeset/strong-wombats-worry.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+feat: added the `svelte/experimental-require-slot-types` rule
diff --git a/README.md b/README.md
index 1919fcf..4ae3870 100644
--- a/README.md
+++ b/README.md
@@ -379,6 +379,7 @@ These rules extend the rules provided by ESLint itself, or other plugins to work
 
 | Rule ID | Description |    |
 |:--------|:------------|:---|
+| [svelte/experimental-require-slot-types](https://ota-meshi.github.io/eslint-plugin-svelte/rules/experimental-require-slot-types/) | require slot type declaration using the `$$Slots` interface |  |
 | [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
diff --git a/docs/rules.md b/docs/rules.md
index 4635b3a..73b79d5 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -98,6 +98,7 @@ These rules extend the rules provided by ESLint itself, or other plugins to work
 
 | Rule ID | Description |    |
 |:--------|:------------|:---|
+| [svelte/experimental-require-slot-types](./rules/experimental-require-slot-types.md) | require slot type declaration using the `$$Slots` interface |  |
 | [svelte/experimental-require-strict-events](./rules/experimental-require-strict-events.md) | require the strictEvents attribute on `<script>` tags |  |
 
 ## System
diff --git a/docs/rules/experimental-require-slot-types.md b/docs/rules/experimental-require-slot-types.md
new file mode 100644
index 0000000..5ab9477
--- /dev/null
+++ b/docs/rules/experimental-require-slot-types.md
@@ -0,0 +1,113 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/experimental-require-slot-types"
+description: "require slot type declaration using the `$$Slots` interface"
+---
+
+# svelte/experimental-require-slot-types
+
+> require slot type declaration using the `$$Slots` interface
+
+- :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 `$$Slots` interface if any slots are present in the component. This interface declares all of the used slots and their props and enables typechecking both in the component itself as well as all components that include it.
+The  `$$Slots` interface is experimental and is documented in [svelte RFC #38](https://github.com/dummdidumm/rfcs/blob/ts-typedefs-within-svelte-components/text/ts-typing-props-slots-events.md#typing-slots).
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<!-- ✓ GOOD -->
+<script>
+  /* eslint svelte/experimental-require-slot-types: "error" */
+</script>
+
+<b>No slots here!</b>
+```
+
+</ESLintCodeBlock>
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<!-- ✓ GOOD -->
+<script>
+  /* eslint svelte/experimental-require-slot-types: "error" */
+
+  interface $$Slots {
+    default: Record<string, never>;
+  }
+</script>
+
+<slot />
+```
+
+</ESLintCodeBlock>
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<!-- ✓ GOOD -->
+<script lang="ts">
+  /* eslint svelte/experimental-require-slot-types: "error" */
+
+  interface $$Slots {
+    default: { prop: boolean; };
+  }
+</script>
+
+<slot prop={true} />
+```
+
+</ESLintCodeBlock>
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<!-- ✓ GOOD -->
+<script lang="ts">
+  /* eslint svelte/experimental-require-slot-types: "error" */
+
+  interface $$Slots {
+    named: Record<string, never>;
+  }
+</script>
+
+<slot name = "named" />
+```
+
+</ESLintCodeBlock>
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<!-- ✗ BAD -->
+<script>
+  /* eslint svelte/experimental-require-slot-types: "error" */
+</script>
+
+<slot />
+```
+
+</ESLintCodeBlock>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/experimental-require-slot-types.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/experimental-require-slot-types.ts)
diff --git a/docs/rules/experimental-require-strict-events.md b/docs/rules/experimental-require-strict-events.md
index af1b766..8c6a8bf 100644
--- a/docs/rules/experimental-require-strict-events.md
+++ b/docs/rules/experimental-require-strict-events.md
@@ -20,10 +20,9 @@ This rule enforces the presence of the `strictEvents` attribute on the main `<sc
 <!--eslint-skip-->
 
 ```svelte
-<!-- eslint svelte/experimental-require-strict-events: "error" -->
-
 <!-- ✓ GOOD -->
 <script lang="ts" strictEvents>
+/* eslint svelte/experimental-require-strict-events: "error" */
 </script>
 ```
 
@@ -34,10 +33,9 @@ This rule enforces the presence of the `strictEvents` attribute on the main `<sc
 <!--eslint-skip-->
 
 ```svelte
-<!-- eslint svelte/experimental-require-strict-events: "error" -->
-
 <!-- ✓ GOOD -->
 <script lang="ts">
+  /* eslint svelte/experimental-require-strict-events: "error" */
   interface $$Events {}
 </script>
 ```
@@ -49,10 +47,9 @@ This rule enforces the presence of the `strictEvents` attribute on the main `<sc
 <!--eslint-skip-->
 
 ```svelte
-<!-- eslint svelte/experimental-require-strict-events: "error" -->
-
 <!-- ✗ BAD -->
 <script lang="ts">
+/* eslint svelte/experimental-require-strict-events: "error" */
 </script>
 ```
 
diff --git a/package.json b/package.json
index ba0faf0..127d737 100644
--- a/package.json
+++ b/package.json
@@ -157,8 +157,8 @@
     "sass": "^1.51.0",
     "semver": "^7.3.5",
     "simple-git-hooks": "^2.8.0",
-    "stylelint": "^14.0.0",
-    "stylelint-config-standard": "^29.0.0",
+    "stylelint": "^15.0.0",
+    "stylelint-config-standard": "^30.0.0",
     "stylus": "^0.59.0",
     "svelte": "^3.46.1",
     "svelte-adapter-ghpages": "0.1.0",
diff --git a/src/rules/experimental-require-slot-types.ts b/src/rules/experimental-require-slot-types.ts
new file mode 100644
index 0000000..32f31ae
--- /dev/null
+++ b/src/rules/experimental-require-slot-types.ts
@@ -0,0 +1,50 @@
+import { createRule } from "../utils"
+import { getLangValue } from "../utils/ast-utils"
+
+export default createRule("experimental-require-slot-types", {
+  meta: {
+    docs: {
+      description:
+        "require slot type declaration using the `$$Slots` interface",
+      category: "Experimental",
+      recommended: false,
+    },
+    schema: [],
+    messages: {
+      missingSlotsInterface: `The component must define the $$Slots interface.`,
+    },
+    type: "suggestion",
+  },
+  create(context) {
+    let isTs = false
+    let hasSlot = false
+    let hasInterface = false
+    return {
+      SvelteScriptElement(node) {
+        const lang = getLangValue(node)?.toLowerCase()
+        isTs = lang === "ts" || lang === "typescript"
+      },
+      SvelteElement(node) {
+        if (node.name.type === "SvelteName" && node.name.name === "slot") {
+          hasSlot = true
+        }
+      },
+      TSInterfaceDeclaration(node) {
+        if (node.id.name === "$$Slots") {
+          hasInterface = true
+        }
+      },
+      "Program:exit"() {
+        if (isTs && hasSlot && !hasInterface) {
+          context.report({
+            loc: {
+              line: 1,
+              column: 1,
+            },
+            messageId: "missingSlotsInterface",
+          })
+        }
+      },
+    }
+  },
+})
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index 5d051d4..bc8c481 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 experimentalRequireSlotTypes from "../rules/experimental-require-slot-types"
 import experimentalRequireStrictEvents from "../rules/experimental-require-strict-events"
 import firstAttributeLinebreak from "../rules/first-attribute-linebreak"
 import htmlClosingBracketSpacing from "../rules/html-closing-bracket-spacing"
@@ -56,6 +57,7 @@ export const rules = [
   buttonHasType,
   commentDirective,
   derivedHasSameInputsOutputs,
+  experimentalRequireSlotTypes,
   experimentalRequireStrictEvents,
   firstAttributeLinebreak,
   htmlClosingBracketSpacing,


Test patch:
diff --git a/tests/fixtures/rules/experimental-require-slot-types/invalid/no-slot-types01-errors.yaml b/tests/fixtures/rules/experimental-require-slot-types/invalid/no-slot-types01-errors.yaml
new file mode 100644
index 0000000..628f8ec
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-slot-types/invalid/no-slot-types01-errors.yaml
@@ -0,0 +1,4 @@
+- message: The component must define the $$Slots interface.
+  line: 1
+  column: 2
+  suggestions: null
diff --git a/tests/fixtures/rules/experimental-require-slot-types/invalid/no-slot-types01-input.svelte b/tests/fixtures/rules/experimental-require-slot-types/invalid/no-slot-types01-input.svelte
new file mode 100644
index 0000000..2c02c7e
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-slot-types/invalid/no-slot-types01-input.svelte
@@ -0,0 +1,4 @@
+<script lang="ts">
+</script>
+
+<slot />
diff --git a/tests/fixtures/rules/experimental-require-slot-types/valid/has-slot-types01-input.svelte b/tests/fixtures/rules/experimental-require-slot-types/valid/has-slot-types01-input.svelte
new file mode 100644
index 0000000..c902e4a
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-slot-types/valid/has-slot-types01-input.svelte
@@ -0,0 +1,7 @@
+<script lang="ts">
+  interface $$Slots {
+    defalt: Record<string, never>
+  }
+</script>
+
+<slot />
diff --git a/tests/fixtures/rules/experimental-require-slot-types/valid/named-slot01-input.svelte b/tests/fixtures/rules/experimental-require-slot-types/valid/named-slot01-input.svelte
new file mode 100644
index 0000000..ac2713e
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-slot-types/valid/named-slot01-input.svelte
@@ -0,0 +1,7 @@
+<script lang="ts">
+  interface $$Slots {
+    named: Record<string, never>
+  }
+</script>
+
+<slot name="named" />
diff --git a/tests/fixtures/rules/experimental-require-slot-types/valid/no-slots01-input.svelte b/tests/fixtures/rules/experimental-require-slot-types/valid/no-slots01-input.svelte
new file mode 100644
index 0000000..d0857d0
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-slot-types/valid/no-slots01-input.svelte
@@ -0,0 +1,4 @@
+<script lang="ts">
+</script>
+
+content
diff --git a/tests/fixtures/rules/experimental-require-slot-types/valid/no-typescript01-input.svelte b/tests/fixtures/rules/experimental-require-slot-types/valid/no-typescript01-input.svelte
new file mode 100644
index 0000000..9cc1fe5
--- /dev/null
+++ b/tests/fixtures/rules/experimental-require-slot-types/valid/no-typescript01-input.svelte
@@ -0,0 +1,4 @@
+<script>
+</script>
+
+<slot />
diff --git a/tests/src/rules/experimental-require-slot-types.ts b/tests/src/rules/experimental-require-slot-types.ts
new file mode 100644
index 0000000..af9978d
--- /dev/null
+++ b/tests/src/rules/experimental-require-slot-types.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/experimental-require-slot-types"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: "module",
+  },
+})
+
+tester.run(
+  "experimental-require-slot-types",
+  rule as any,
+  loadTestCases("experimental-require-slot-types"),
+)