Skip to content

Commit

Permalink
fix(require-explicit-slots): add support for type references (#2617)
Browse files Browse the repository at this point in the history
  • Loading branch information
chadwickellis authored Nov 27, 2024
1 parent a270df8 commit 618f49c
Show file tree
Hide file tree
Showing 8 changed files with 775 additions and 31 deletions.
40 changes: 16 additions & 24 deletions lib/rules/require-explicit-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,30 +98,22 @@ module.exports = {

return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
onDefineSlotsEnter(node) {
const typeArguments =
'typeArguments' in node ? node.typeArguments : node.typeParameters
const param = /** @type {TypeNode|undefined} */ (
typeArguments?.params[0]
)
if (!param) return

if (param.type === 'TSTypeLiteral') {
for (const memberNode of param.members) {
const slotName = getSlotsName(memberNode)
if (!slotName) continue

if (slotsDefined.has(slotName)) {
context.report({
node: memberNode,
messageId: 'alreadyDefinedSlot',
data: {
slotName
}
})
} else {
slotsDefined.add(slotName)
}
onDefineSlotsEnter(_node, slots) {
for (const slot of slots) {
if (!slot.slotName) {
continue
}

if (slotsDefined.has(slot.slotName)) {
context.report({
node: slot.node,
messageId: 'alreadyDefinedSlot',
data: {
slotName: slot.slotName
}
})
} else {
slotsDefined.add(slot.slotName)
}
}
}
Expand Down
30 changes: 29 additions & 1 deletion lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const { getScope } = require('./scope')
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeEmit} ComponentInferTypeEmit
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownEmit} ComponentUnknownEmit
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentEmit} ComponentEmit
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeSlot} ComponentTypeSlot
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeSlot} ComponentInferTypeSlot
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownSlot} ComponentUnknownSlot
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentSlot} ComponentSlot
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentModelName} ComponentModelName
* @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentModel} ComponentModel
*/
Expand Down Expand Up @@ -70,6 +74,7 @@ const {
const {
getComponentPropsFromTypeDefine,
getComponentEmitsFromTypeDefine,
getComponentSlotsFromTypeDefine,
isTypeNode
} = require('./ts-utils')

Expand Down Expand Up @@ -1435,7 +1440,7 @@ module.exports = {
'onDefineSlotsEnter',
'onDefineSlotsExit',
(candidateMacro, node) => candidateMacro === node,
() => undefined
getComponentSlotsFromDefineSlots
),
new MacroListener(
'defineExpose',
Expand Down Expand Up @@ -3372,6 +3377,28 @@ function getComponentEmitsFromDefineEmits(context, node) {
}
]
}

/**
* Get all slots from `defineSlots` call expression.
* @param {RuleContext} context The rule context object.
* @param {CallExpression} node `defineSlots` call expression
* @return {ComponentSlot[]} Array of component slots
*/
function getComponentSlotsFromDefineSlots(context, node) {
const typeArguments =
'typeArguments' in node ? node.typeArguments : node.typeParameters
if (typeArguments && typeArguments.params.length > 0) {
return getComponentSlotsFromTypeDefine(context, typeArguments.params[0])
}
return [
{
type: 'unknown',
slotName: null,
node: null
}
]
}

/**
* Get model info from `defineModel` call expression.
* @param {RuleContext} _context The rule context object.
Expand Down Expand Up @@ -3414,6 +3441,7 @@ function getComponentModelFromDefineModel(_context, node) {
typeNode: null
}
}

/**
* Get all props by looking at all component's properties
* @param {ObjectExpression|ArrayExpression} propsNode Object with props definition
Expand Down
39 changes: 36 additions & 3 deletions lib/utils/ts-utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ const {
isTSTypeLiteralOrTSFunctionType,
extractRuntimeEmits,
flattenTypeNodes,
isTSInterfaceBody
isTSInterfaceBody,
extractRuntimeSlots
} = require('./ts-ast')
const {
getComponentPropsFromTypeDefineTypes,
getComponentEmitsFromTypeDefineTypes
getComponentEmitsFromTypeDefineTypes,
getComponentSlotsFromTypeDefineTypes
} = require('./ts-types')

/**
Expand All @@ -22,12 +24,16 @@ const {
* @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit
* @typedef {import('../index').ComponentInferTypeEmit} ComponentInferTypeEmit
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
* @typedef {import('../index').ComponentTypeSlot} ComponentTypeSlot
* @typedef {import('../index').ComponentInferTypeSlot} ComponentInferTypeSlot
* @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
*/

module.exports = {
isTypeNode,
getComponentPropsFromTypeDefine,
getComponentEmitsFromTypeDefine
getComponentEmitsFromTypeDefine,
getComponentSlotsFromTypeDefine
}

/**
Expand Down Expand Up @@ -86,3 +92,30 @@ function getComponentEmitsFromTypeDefine(context, emitsNode) {
}
return result
}

/**
* Get all slots by looking at all component's properties
* @param {RuleContext} context The ESLint rule context object.
* @param {TypeNode} slotsNode Type with slots definition
* @return {(ComponentTypeSlot|ComponentInferTypeSlot|ComponentUnknownSlot)[]} Array of component slots
*/
function getComponentSlotsFromTypeDefine(context, slotsNode) {
/** @type {(ComponentTypeSlot|ComponentInferTypeSlot|ComponentUnknownSlot)[]} */
const result = []
for (const defNode of flattenTypeNodes(
context,
/** @type {TSESTreeTypeNode} */ (slotsNode)
)) {
if (isTSInterfaceBody(defNode) || isTSTypeLiteral(defNode)) {
result.push(...extractRuntimeSlots(defNode))
} else {
result.push(
...getComponentSlotsFromTypeDefineTypes(
context,
/** @type {TypeNode} */ (defNode)
)
)
}
}
return result
}
37 changes: 36 additions & 1 deletion lib/utils/ts-utils/ts-ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const { inferRuntimeTypeFromTypeNode } = require('./ts-types')
* @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp
* @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
* @typedef {import('../index').ComponentTypeSlot} ComponentTypeSlot
* @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
*/

const noop = Function.prototype
Expand All @@ -26,7 +28,8 @@ module.exports = {
isTSTypeLiteral,
isTSTypeLiteralOrTSFunctionType,
extractRuntimeProps,
extractRuntimeEmits
extractRuntimeEmits,
extractRuntimeSlots
}

/**
Expand Down Expand Up @@ -209,6 +212,38 @@ function* extractRuntimeEmits(node) {
}
}

/**
* @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody} node
* @returns {IterableIterator<ComponentTypeSlot | ComponentUnknownSlot>}
*/
function* extractRuntimeSlots(node) {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
for (const member of members) {
if (
member.type === 'TSPropertySignature' ||
member.type === 'TSMethodSignature'
) {
if (member.key.type !== 'Identifier' && member.key.type !== 'Literal') {
yield {
type: 'unknown',
slotName: null,
node: /** @type {Expression} */ (member.key)
}
continue
}
yield {
type: 'type',
key: /** @type {Identifier | Literal} */ (member.key),
slotName:
member.key.type === 'Identifier'
? member.key.name
: `${member.key.value}`,
node: /** @type {TSPropertySignature | TSMethodSignature} */ (member)
}
}
}
}

/**
* @param {TSESTreeParameter} eventName
* @param {TSCallSignatureDeclaration | TSFunctionType} member
Expand Down
48 changes: 48 additions & 0 deletions lib/utils/ts-utils/ts-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ const {
* @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp
* @typedef {import('../index').ComponentInferTypeEmit} ComponentInferTypeEmit
* @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
* @typedef {import('../index').ComponentInferTypeSlot} ComponentInferTypeSlot
* @typedef {import('../index').ComponentUnknownSlot} ComponentUnknownSlot
*/

module.exports = {
getComponentPropsFromTypeDefineTypes,
getComponentEmitsFromTypeDefineTypes,
getComponentSlotsFromTypeDefineTypes,
inferRuntimeTypeFromTypeNode
}

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

/**
* Get all slots by looking at all component's properties
* @param {RuleContext} context The ESLint rule context object.
* @param {TypeNode} slotsNode Type with slots definition
* @return {(ComponentInferTypeSlot|ComponentUnknownSlot)[]} Array of component slots
*/
function getComponentSlotsFromTypeDefineTypes(context, slotsNode) {
const services = getTSParserServices(context)
const tsNode = services && services.tsNodeMap.get(slotsNode)
const type = tsNode && services.checker.getTypeAtLocation(tsNode)
if (
!type ||
isAny(type) ||
isUnknown(type) ||
isNever(type) ||
isNull(type)
) {
return [
{
type: 'unknown',
slotName: null,
node: slotsNode
}
]
}
return [...extractRuntimeSlots(type, slotsNode)]
}

/**
* @param {RuleContext} context The ESLint rule context object.
* @param {TypeNode|Expression} node
Expand Down Expand Up @@ -259,6 +290,23 @@ function* extractRuntimeEmits(type, tsNode, emitsNode, services) {
}
}

/**
* @param {Type} type
* @param {TypeNode} slotsNode Type with slots definition
* @returns {IterableIterator<ComponentInferTypeSlot>}
*/
function* extractRuntimeSlots(type, slotsNode) {
for (const property of type.getProperties()) {
const name = property.getName()

yield {
type: 'infer-type',
slotName: name,
node: slotsNode
}
}
}

/**
* @param {Type} type
* @returns {Iterable<Type>}
Expand Down
Loading

0 comments on commit 618f49c

Please sign in to comment.