Skip to content

Commit 634f38d

Browse files
mussinbenarbiaMussin Benarbiaota-meshi
authored
feat: implement require-explicit-slots (#2325)
Co-authored-by: Mussin Benarbia <[email protected]> Co-authored-by: Yosuke Ota <[email protected]>
1 parent e7b87ff commit 634f38d

File tree

4 files changed

+466
-0
lines changed

4 files changed

+466
-0
lines changed

Diff for: docs/rules/require-explicit-emits.md

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export default {
112112
## :couple: Related Rules
113113

114114
- [vue/no-unused-emit-declarations](./no-unused-emit-declarations.md)
115+
- [vue/require-explicit-slots](./require-explicit-slots.md)
115116

116117
## :books: Further Reading
117118

Diff for: docs/rules/require-explicit-slots.md

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/require-explicit-slots
5+
description: require slots to be explicitly defined with defineSlots
6+
---
7+
8+
# vue/require-explicit-slots
9+
10+
> require slots to be explicitly defined
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
13+
14+
## :book: Rule Details
15+
16+
This rule enforces all slots used in the template to be defined once either in the `script setup` block with the [`defineSlots`](https://vuejs.org/api/sfc-script-setup.html) macro, or with the [`slots property`](https://vuejs.org/api/options-rendering.html#slots) in the Options API.
17+
18+
<eslint-code-block :rules="{'vue/require-explicit-slots': ['error']}">
19+
20+
```vue
21+
<template>
22+
<div>
23+
<!-- ✓ GOOD -->
24+
<slot />
25+
<slot name="foo" />
26+
<!-- ✗ BAD -->
27+
<slot name="bar" />
28+
</div>
29+
</template>
30+
<script setup lang="ts">
31+
defineSlots<{
32+
default(props: { msg: string }): any
33+
foo(props: { msg: string }): any
34+
}>()
35+
</script>
36+
```
37+
38+
</eslint-code-block>
39+
40+
<eslint-code-block :rules="{'vue/require-explicit-slots': ['error']}">
41+
42+
```vue
43+
<template>
44+
<div>
45+
<!-- ✓ GOOD -->
46+
<slot />
47+
<slot name="foo" />
48+
<!-- ✗ BAD -->
49+
<slot name="bar" />
50+
</div>
51+
</template>
52+
<script lang="ts">
53+
import { SlotsType } from 'vue'
54+
55+
defineComponent({
56+
slots: Object as SlotsType<{
57+
default: { msg: string }
58+
foo: { msg: string }
59+
}>
60+
})
61+
</script>
62+
```
63+
64+
</eslint-code-block>
65+
66+
## :wrench: Options
67+
68+
Nothing.

Diff for: lib/rules/require-explicit-slots.js

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* @author Mussin Benarbia
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
/**
10+
* @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TypeNode
11+
*/
12+
13+
module.exports = {
14+
meta: {
15+
type: 'problem',
16+
docs: {
17+
description: 'require slots to be explicitly defined',
18+
categories: undefined,
19+
url: 'https://eslint.vuejs.org/rules/require-explicit-slots.html'
20+
},
21+
fixable: null,
22+
schema: [],
23+
messages: {
24+
requireExplicitSlots: 'Slots must be explicitly defined.',
25+
alreadyDefinedSlot: 'Slot {{slotName}} is already defined.'
26+
}
27+
},
28+
/** @param {RuleContext} context */
29+
create(context) {
30+
const sourceCode = context.getSourceCode()
31+
const documentFragment =
32+
sourceCode.parserServices.getDocumentFragment &&
33+
sourceCode.parserServices.getDocumentFragment()
34+
if (!documentFragment) {
35+
return {}
36+
}
37+
const scripts = documentFragment.children.filter(
38+
(element) => utils.isVElement(element) && element.name === 'script'
39+
)
40+
if (scripts.every((script) => !utils.hasAttribute(script, 'lang', 'ts'))) {
41+
return {}
42+
}
43+
const slotsDefined = new Set()
44+
45+
return utils.compositingVisitors(
46+
utils.defineScriptSetupVisitor(context, {
47+
onDefineSlotsEnter(node) {
48+
const typeArguments =
49+
'typeArguments' in node ? node.typeArguments : node.typeParameters
50+
const param = /** @type {TypeNode|undefined} */ (
51+
typeArguments?.params[0]
52+
)
53+
if (!param) return
54+
55+
if (param.type === 'TSTypeLiteral') {
56+
for (const memberNode of param.members) {
57+
const slotName = memberNode.key.name
58+
if (slotsDefined.has(slotName)) {
59+
context.report({
60+
node: memberNode,
61+
messageId: 'alreadyDefinedSlot',
62+
data: {
63+
slotName
64+
}
65+
})
66+
} else {
67+
slotsDefined.add(slotName)
68+
}
69+
}
70+
}
71+
}
72+
}),
73+
utils.executeOnVue(context, (obj) => {
74+
const slotsProperty = utils.findProperty(obj, 'slots')
75+
if (!slotsProperty) return
76+
77+
const slotsTypeHelper =
78+
slotsProperty.value.typeAnnotation?.typeName.name === 'SlotsType' &&
79+
slotsProperty.value.typeAnnotation
80+
if (!slotsTypeHelper) return
81+
82+
const typeArguments =
83+
'typeArguments' in slotsTypeHelper
84+
? slotsTypeHelper.typeArguments
85+
: slotsTypeHelper.typeParameters
86+
const param = /** @type {TypeNode|undefined} */ (
87+
typeArguments?.params[0]
88+
)
89+
if (!param) return
90+
91+
if (param.type === 'TSTypeLiteral') {
92+
for (const memberNode of param.members) {
93+
const slotName = memberNode.key.name
94+
if (slotsDefined.has(slotName)) {
95+
context.report({
96+
node: memberNode,
97+
messageId: 'alreadyDefinedSlot',
98+
data: {
99+
slotName
100+
}
101+
})
102+
} else {
103+
slotsDefined.add(slotName)
104+
}
105+
}
106+
}
107+
}),
108+
utils.defineTemplateBodyVisitor(context, {
109+
"VElement[name='slot']"(node) {
110+
let slotName = 'default'
111+
112+
const slotNameAttr = utils.getAttribute(node, 'name')
113+
114+
if (slotNameAttr) {
115+
slotName = slotNameAttr.value.value
116+
}
117+
118+
if (!slotsDefined.has(slotName)) {
119+
context.report({
120+
node,
121+
messageId: 'requireExplicitSlots'
122+
})
123+
}
124+
}
125+
})
126+
)
127+
}
128+
}

0 commit comments

Comments
 (0)