Skip to content

Commit 618f49c

Browse files
fix(require-explicit-slots): add support for type references (#2617)
1 parent a270df8 commit 618f49c

File tree

8 files changed

+775
-31
lines changed

8 files changed

+775
-31
lines changed

lib/rules/require-explicit-slots.js

+16-24
Original file line numberDiff line numberDiff line change
@@ -98,30 +98,22 @@ module.exports = {
9898

9999
return utils.compositingVisitors(
100100
utils.defineScriptSetupVisitor(context, {
101-
onDefineSlotsEnter(node) {
102-
const typeArguments =
103-
'typeArguments' in node ? node.typeArguments : node.typeParameters
104-
const param = /** @type {TypeNode|undefined} */ (
105-
typeArguments?.params[0]
106-
)
107-
if (!param) return
108-
109-
if (param.type === 'TSTypeLiteral') {
110-
for (const memberNode of param.members) {
111-
const slotName = getSlotsName(memberNode)
112-
if (!slotName) continue
113-
114-
if (slotsDefined.has(slotName)) {
115-
context.report({
116-
node: memberNode,
117-
messageId: 'alreadyDefinedSlot',
118-
data: {
119-
slotName
120-
}
121-
})
122-
} else {
123-
slotsDefined.add(slotName)
124-
}
101+
onDefineSlotsEnter(_node, slots) {
102+
for (const slot of slots) {
103+
if (!slot.slotName) {
104+
continue
105+
}
106+
107+
if (slotsDefined.has(slot.slotName)) {
108+
context.report({
109+
node: slot.node,
110+
messageId: 'alreadyDefinedSlot',
111+
data: {
112+
slotName: slot.slotName
113+
}
114+
})
115+
} else {
116+
slotsDefined.add(slot.slotName)
125117
}
126118
}
127119
}

lib/utils/index.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const { getScope } = require('./scope')
2626
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeEmit} ComponentInferTypeEmit
2727
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownEmit} ComponentUnknownEmit
2828
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentEmit} ComponentEmit
29+
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeSlot} ComponentTypeSlot
30+
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeSlot} ComponentInferTypeSlot
31+
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownSlot} ComponentUnknownSlot
32+
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentSlot} ComponentSlot
2933
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentModelName} ComponentModelName
3034
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentModel} ComponentModel
3135
*/
@@ -70,6 +74,7 @@ const {
7074
const {
7175
getComponentPropsFromTypeDefine,
7276
getComponentEmitsFromTypeDefine,
77+
getComponentSlotsFromTypeDefine,
7378
isTypeNode
7479
} = require('./ts-utils')
7580

@@ -1435,7 +1440,7 @@ module.exports = {
14351440
'onDefineSlotsEnter',
14361441
'onDefineSlotsExit',
14371442
(candidateMacro, node) => candidateMacro === node,
1438-
() => undefined
1443+
getComponentSlotsFromDefineSlots
14391444
),
14401445
new MacroListener(
14411446
'defineExpose',
@@ -3372,6 +3377,28 @@ function getComponentEmitsFromDefineEmits(context, node) {
33723377
}
33733378
]
33743379
}
3380+
3381+
/**
3382+
* Get all slots from `defineSlots` call expression.
3383+
* @param {RuleContext} context The rule context object.
3384+
* @param {CallExpression} node `defineSlots` call expression
3385+
* @return {ComponentSlot[]} Array of component slots
3386+
*/
3387+
function getComponentSlotsFromDefineSlots(context, node) {
3388+
const typeArguments =
3389+
'typeArguments' in node ? node.typeArguments : node.typeParameters
3390+
if (typeArguments && typeArguments.params.length > 0) {
3391+
return getComponentSlotsFromTypeDefine(context, typeArguments.params[0])
3392+
}
3393+
return [
3394+
{
3395+
type: 'unknown',
3396+
slotName: null,
3397+
node: null
3398+
}
3399+
]
3400+
}
3401+
33753402
/**
33763403
* Get model info from `defineModel` call expression.
33773404
* @param {RuleContext} _context The rule context object.
@@ -3414,6 +3441,7 @@ function getComponentModelFromDefineModel(_context, node) {
34143441
typeNode: null
34153442
}
34163443
}
3444+
34173445
/**
34183446
* Get all props by looking at all component's properties
34193447
* @param {ObjectExpression|ArrayExpression} propsNode Object with props definition

lib/utils/ts-utils/index.js

+36-3
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ const {
55
isTSTypeLiteralOrTSFunctionType,
66
extractRuntimeEmits,
77
flattenTypeNodes,
8-
isTSInterfaceBody
8+
isTSInterfaceBody,
9+
extractRuntimeSlots
910
} = require('./ts-ast')
1011
const {
1112
getComponentPropsFromTypeDefineTypes,
12-
getComponentEmitsFromTypeDefineTypes
13+
getComponentEmitsFromTypeDefineTypes,
14+
getComponentSlotsFromTypeDefineTypes
1315
} = require('./ts-types')
1416

1517
/**
@@ -22,12 +24,16 @@ const {
2224
* @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit
2325
* @typedef {import('../index').ComponentInferTypeEmit} ComponentInferTypeEmit
2426
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
27+
* @typedef {import('../index').ComponentTypeSlot} ComponentTypeSlot
28+
* @typedef {import('../index').ComponentInferTypeSlot} ComponentInferTypeSlot
29+
* @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
2530
*/
2631

2732
module.exports = {
2833
isTypeNode,
2934
getComponentPropsFromTypeDefine,
30-
getComponentEmitsFromTypeDefine
35+
getComponentEmitsFromTypeDefine,
36+
getComponentSlotsFromTypeDefine
3137
}
3238

3339
/**
@@ -86,3 +92,30 @@ function getComponentEmitsFromTypeDefine(context, emitsNode) {
8692
}
8793
return result
8894
}
95+
96+
/**
97+
* Get all slots by looking at all component's properties
98+
* @param {RuleContext} context The ESLint rule context object.
99+
* @param {TypeNode} slotsNode Type with slots definition
100+
* @return {(ComponentTypeSlot|ComponentInferTypeSlot|ComponentUnknownSlot)[]} Array of component slots
101+
*/
102+
function getComponentSlotsFromTypeDefine(context, slotsNode) {
103+
/** @type {(ComponentTypeSlot|ComponentInferTypeSlot|ComponentUnknownSlot)[]} */
104+
const result = []
105+
for (const defNode of flattenTypeNodes(
106+
context,
107+
/** @type {TSESTreeTypeNode} */ (slotsNode)
108+
)) {
109+
if (isTSInterfaceBody(defNode) || isTSTypeLiteral(defNode)) {
110+
result.push(...extractRuntimeSlots(defNode))
111+
} else {
112+
result.push(
113+
...getComponentSlotsFromTypeDefineTypes(
114+
context,
115+
/** @type {TypeNode} */ (defNode)
116+
)
117+
)
118+
}
119+
}
120+
return result
121+
}

lib/utils/ts-utils/ts-ast.js

+36-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const { inferRuntimeTypeFromTypeNode } = require('./ts-types')
1515
* @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp
1616
* @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit
1717
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
18+
* @typedef {import('../index').ComponentTypeSlot} ComponentTypeSlot
19+
* @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
1820
*/
1921

2022
const noop = Function.prototype
@@ -26,7 +28,8 @@ module.exports = {
2628
isTSTypeLiteral,
2729
isTSTypeLiteralOrTSFunctionType,
2830
extractRuntimeProps,
29-
extractRuntimeEmits
31+
extractRuntimeEmits,
32+
extractRuntimeSlots
3033
}
3134

3235
/**
@@ -209,6 +212,38 @@ function* extractRuntimeEmits(node) {
209212
}
210213
}
211214

215+
/**
216+
* @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody} node
217+
* @returns {IterableIterator<ComponentTypeSlot | ComponentUnknownSlot>}
218+
*/
219+
function* extractRuntimeSlots(node) {
220+
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
221+
for (const member of members) {
222+
if (
223+
member.type === 'TSPropertySignature' ||
224+
member.type === 'TSMethodSignature'
225+
) {
226+
if (member.key.type !== 'Identifier' && member.key.type !== 'Literal') {
227+
yield {
228+
type: 'unknown',
229+
slotName: null,
230+
node: /** @type {Expression} */ (member.key)
231+
}
232+
continue
233+
}
234+
yield {
235+
type: 'type',
236+
key: /** @type {Identifier | Literal} */ (member.key),
237+
slotName:
238+
member.key.type === 'Identifier'
239+
? member.key.name
240+
: `${member.key.value}`,
241+
node: /** @type {TSPropertySignature | TSMethodSignature} */ (member)
242+
}
243+
}
244+
}
245+
}
246+
212247
/**
213248
* @param {TSESTreeParameter} eventName
214249
* @param {TSCallSignatureDeclaration | TSFunctionType} member

lib/utils/ts-utils/ts-types.js

+48
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@ const {
2424
* @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp
2525
* @typedef {import('../index').ComponentInferTypeEmit} ComponentInferTypeEmit
2626
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
27+
* @typedef {import('../index').ComponentInferTypeSlot} ComponentInferTypeSlot
28+
* @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
2729
*/
2830

2931
module.exports = {
3032
getComponentPropsFromTypeDefineTypes,
3133
getComponentEmitsFromTypeDefineTypes,
34+
getComponentSlotsFromTypeDefineTypes,
3235
inferRuntimeTypeFromTypeNode
3336
}
3437

@@ -122,6 +125,34 @@ function getComponentEmitsFromTypeDefineTypes(context, emitsNode) {
122125
return [...extractRuntimeEmits(type, tsNode, emitsNode, services)]
123126
}
124127

128+
/**
129+
* Get all slots by looking at all component's properties
130+
* @param {RuleContext} context The ESLint rule context object.
131+
* @param {TypeNode} slotsNode Type with slots definition
132+
* @return {(ComponentInferTypeSlot|ComponentUnknownSlot)[]} Array of component slots
133+
*/
134+
function getComponentSlotsFromTypeDefineTypes(context, slotsNode) {
135+
const services = getTSParserServices(context)
136+
const tsNode = services && services.tsNodeMap.get(slotsNode)
137+
const type = tsNode && services.checker.getTypeAtLocation(tsNode)
138+
if (
139+
!type ||
140+
isAny(type) ||
141+
isUnknown(type) ||
142+
isNever(type) ||
143+
isNull(type)
144+
) {
145+
return [
146+
{
147+
type: 'unknown',
148+
slotName: null,
149+
node: slotsNode
150+
}
151+
]
152+
}
153+
return [...extractRuntimeSlots(type, slotsNode)]
154+
}
155+
125156
/**
126157
* @param {RuleContext} context The ESLint rule context object.
127158
* @param {TypeNode|Expression} node
@@ -259,6 +290,23 @@ function* extractRuntimeEmits(type, tsNode, emitsNode, services) {
259290
}
260291
}
261292

293+
/**
294+
* @param {Type} type
295+
* @param {TypeNode} slotsNode Type with slots definition
296+
* @returns {IterableIterator<ComponentInferTypeSlot>}
297+
*/
298+
function* extractRuntimeSlots(type, slotsNode) {
299+
for (const property of type.getProperties()) {
300+
const name = property.getName()
301+
302+
yield {
303+
type: 'infer-type',
304+
slotName: name,
305+
node: slotsNode
306+
}
307+
}
308+
}
309+
262310
/**
263311
* @param {Type} type
264312
* @returns {Iterable<Type>}

0 commit comments

Comments
 (0)