diff --git a/lib/rules/return-in-emits-validator.js b/lib/rules/return-in-emits-validator.js index c4ced9074..03d197b18 100644 --- a/lib/rules/return-in-emits-validator.js +++ b/lib/rules/return-in-emits-validator.js @@ -9,6 +9,7 @@ const utils = require('../utils') /** * @typedef {import('../utils').ComponentArrayEmit} ComponentArrayEmit * @typedef {import('../utils').ComponentObjectEmit} ComponentObjectEmit + * @typedef {import('../utils').ComponentTypeEmit} ComponentTypeEmit */ /** @@ -67,25 +68,36 @@ module.exports = { */ let scopeStack = null - return Object.assign( - {}, + /** + * @param {(ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[]} emits + */ + function processEmits(emits) { + for (const emit of emits) { + if (!emit.value) { + continue + } + if ( + emit.value.type !== 'FunctionExpression' && + emit.value.type !== 'ArrowFunctionExpression' + ) { + continue + } + emitsValidators.push(emit) + } + } + return utils.compositingVisitors( + utils.defineScriptSetupVisitor(context, { + onDefineEmitsEnter(_node, emits) { + processEmits(emits) + } + }), utils.defineVueVisitor(context, { /** @param {ObjectExpression} obj */ onVueObjectEnter(obj) { - for (const emits of utils.getComponentEmits(obj)) { - if (!emits.value) { - continue - } - const emitsValue = emits.value - if ( - emitsValue.type !== 'FunctionExpression' && - emitsValue.type !== 'ArrowFunctionExpression' - ) { - continue - } - emitsValidators.push(emits) - } - }, + processEmits(utils.getComponentEmits(obj)) + } + }), + { /** @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node */ ':function'(node) { scopeStack = { @@ -135,7 +147,7 @@ module.exports = { scopeStack = scopeStack && scopeStack.upper } - }) + } ) } } diff --git a/lib/utils/index.js b/lib/utils/index.js index 0b93ef9c4..0a2dd4e22 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -15,39 +15,9 @@ * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentArrayProp} ComponentArrayProp * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentObjectProp} ComponentObjectProp * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp - */ -/** - * @typedef {object} ComponentArrayEmitDetectName - * @property {'array'} type - * @property {Literal | TemplateLiteral} key - * @property {string} emitName - * @property {null} value - * @property {Expression | SpreadElement} node - * - * @typedef {object} ComponentArrayEmitUnknownName - * @property {'array'} type - * @property {null} key - * @property {null} emitName - * @property {null} value - * @property {Expression | SpreadElement} node - * - * @typedef {ComponentArrayEmitDetectName | ComponentArrayEmitUnknownName} ComponentArrayEmit - * - * @typedef {object} ComponentObjectEmitDetectName - * @property {'object'} type - * @property {Expression} key - * @property {string} emitName - * @property {Expression} value - * @property {Property} node - * - * @typedef {object} ComponentObjectEmitUnknownName - * @property {'object'} type - * @property {null} key - * @property {null} emitName - * @property {Expression} value - * @property {Property} node - * - * @typedef {ComponentObjectEmitDetectName | ComponentObjectEmitUnknownName} ComponentObjectEmit + * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentArrayEmit} ComponentArrayEmit + * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentObjectEmit} ComponentObjectEmit + * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeEmit} ComponentTypeEmit */ /** * @typedef { {key: string | null, value: BlockStatement | null} } ComponentComputedProperty @@ -82,7 +52,10 @@ const path = require('path') const vueEslintParser = require('vue-eslint-parser') const traverseNodes = vueEslintParser.AST.traverseNodes const { findVariable } = require('eslint-utils') -const { getComponentPropsFromTypeDefine } = require('./ts-ast-utils') +const { + getComponentPropsFromTypeDefine, + getComponentEmitsFromTypeDefine +} = require('./ts-ast-utils') /** * @type { WeakMap } @@ -769,49 +742,7 @@ module.exports = { return [] } - if (emitsNode.value.type === 'ObjectExpression') { - return emitsNode.value.properties.filter(isProperty).map((prop) => { - const emitName = getStaticPropertyName(prop) - if (emitName != null) { - return { - type: 'object', - key: prop.key, - emitName, - value: skipTSAsExpression(prop.value), - node: prop - } - } - return { - type: 'object', - key: null, - emitName: null, - value: skipTSAsExpression(prop.value), - node: prop - } - }) - } else { - return emitsNode.value.elements.filter(isDef).map((prop) => { - if (prop.type === 'Literal' || prop.type === 'TemplateLiteral') { - const emitName = getStringLiteralValue(prop) - if (emitName != null) { - return { - type: 'array', - key: prop, - emitName, - value: null, - node: prop - } - } - } - return { - type: 'array', - key: null, - emitName: null, - value: null, - node: prop - } - }) - } + return getComponentEmitsFromDefine(emitsNode.value) }, /** @@ -1051,6 +982,8 @@ module.exports = { * * - `onDefinePropsEnter` ... Event when defineProps is found. * - `onDefinePropsExit` ... Event when defineProps visit ends. + * - `onDefineEmitsEnter` ... Event when defineEmits is found. + * - `onDefineEmitsExit` ... Event when defineEmits visit ends. * * @param {RuleContext} context The ESLint rule context object. * @param {ScriptSetupVisitor} visitor The visitor to traverse the AST nodes. @@ -1120,9 +1053,11 @@ module.exports = { return null } - if (visitor.onDefinePropsEnter || visitor.onDefinePropsExit) { - const definePropsMap = new Map() - + const hasPropsEvent = + visitor.onDefinePropsEnter || visitor.onDefinePropsExit + const hasEmitsEvent = + visitor.onDefineEmitsEnter || visitor.onDefineEmitsExit + if (hasPropsEvent || hasEmitsEvent) { /** @type {ESNode | null} */ let nested = null scriptSetupVisitor[':function, BlockStatement'] = (node) => { @@ -1135,6 +1070,8 @@ module.exports = { nested = null } } + const definePropsMap = new Map() + const defineEmitsMap = new Map() /** * @param {CallExpression} node */ @@ -1142,26 +1079,47 @@ module.exports = { if ( !nested && inScriptSetup(node) && - node.callee.type === 'Identifier' && - node.callee.name === 'defineProps' + node.callee.type === 'Identifier' ) { - /** @type {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} */ - let props = [] - if (node.arguments.length >= 1) { - const defNode = getObjectOrArray(node.arguments[0]) - if (defNode) { - props = getComponentPropsFromDefine(defNode) + if (hasPropsEvent && node.callee.name === 'defineProps') { + /** @type {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} */ + let props = [] + if (node.arguments.length >= 1) { + const defNode = getObjectOrArray(node.arguments[0]) + if (defNode) { + props = getComponentPropsFromDefine(defNode) + } + } else if ( + node.typeParameters && + node.typeParameters.params.length >= 1 + ) { + props = getComponentPropsFromTypeDefine( + context, + node.typeParameters.params[0] + ) } - } else if ( - node.typeParameters && - node.typeParameters.params.length >= 1 - ) { - props = getComponentPropsFromTypeDefine( - context, - node.typeParameters.params[0] - ) + callVisitor('onDefinePropsEnter', node, props) + definePropsMap.set(node, props) + } else if (hasEmitsEvent && node.callee.name === 'defineEmits') { + /** @type {(ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[]} */ + let emits = [] + if (node.arguments.length >= 1) { + const defNode = getObjectOrArray(node.arguments[0]) + if (defNode) { + emits = getComponentEmitsFromDefine(defNode) + } + } else if ( + node.typeParameters && + node.typeParameters.params.length >= 1 + ) { + emits = getComponentEmitsFromTypeDefine( + context, + node.typeParameters.params[0] + ) + } + callVisitor('onDefineEmitsEnter', node, emits) + defineEmitsMap.set(node, emits) } - callVisitor('onDefinePropsEnter', node, props) } callVisitor('CallExpression', node) } @@ -1171,6 +1129,10 @@ module.exports = { callVisitor('onDefinePropsExit', node, definePropsMap.get(node)) definePropsMap.delete(node) } + if (defineEmitsMap.has(node)) { + callVisitor('onDefineEmitsExit', node, defineEmitsMap.get(node)) + defineEmitsMap.delete(node) + } } } @@ -2472,3 +2434,54 @@ function getComponentPropsFromDefine(propsNode) { }) } } + +/** + * Get all emits by looking at all component's properties + * @param {ObjectExpression|ArrayExpression} emitsNode Object with emits definition + * @return {(ComponentArrayEmit | ComponentObjectEmit)[]} Array of component emits + */ +function getComponentEmitsFromDefine(emitsNode) { + if (emitsNode.type === 'ObjectExpression') { + return emitsNode.properties.filter(isProperty).map((prop) => { + const emitName = getStaticPropertyName(prop) + if (emitName != null) { + return { + type: 'object', + key: prop.key, + emitName, + value: skipTSAsExpression(prop.value), + node: prop + } + } + return { + type: 'object', + key: null, + emitName: null, + value: skipTSAsExpression(prop.value), + node: prop + } + }) + } else { + return emitsNode.elements.filter(isDef).map((emit) => { + if (emit.type === 'Literal' || emit.type === 'TemplateLiteral') { + const emitName = getStringLiteralValue(emit) + if (emitName != null) { + return { + type: 'array', + key: emit, + emitName, + value: null, + node: emit + } + } + } + return { + type: 'array', + key: null, + emitName: null, + value: null, + node: emit + } + }) + } +} diff --git a/lib/utils/ts-ast-utils.js b/lib/utils/ts-ast-utils.js index b88c06f87..f23a752e3 100644 --- a/lib/utils/ts-ast-utils.js +++ b/lib/utils/ts-ast-utils.js @@ -3,13 +3,17 @@ const { findVariable } = require('eslint-utils') * @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TypeNode * @typedef {import('@typescript-eslint/types').TSESTree.TSInterfaceBody} TSInterfaceBody * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeLiteral} TSTypeLiteral + * @typedef {import('@typescript-eslint/types').TSESTree.Parameter} TSESTreeParameter + * */ /** * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp + * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeEmit} ComponentTypeEmit */ module.exports = { - getComponentPropsFromTypeDefine + getComponentPropsFromTypeDefine, + getComponentEmitsFromTypeDefine } /** @@ -19,6 +23,13 @@ module.exports = { function isTSTypeLiteral(node) { return node.type === 'TSTypeLiteral' } +/** + * @param {TypeNode} node + * @returns {node is TSFunctionType} + */ +function isTSFunctionType(node) { + return node.type === 'TSFunctionType' +} /** * Get all props by looking at all component's properties @@ -35,6 +46,25 @@ function getComponentPropsFromTypeDefine(context, propsNode) { return [...extractRuntimeProps(context, defNode)] } +/** + * Get all emits by looking at all component's properties + * @param {RuleContext} context The ESLint rule context object. + * @param {TypeNode} emitsNode Type with emits definition + * @return {ComponentTypeEmit[]} Array of component emits + */ +function getComponentEmitsFromTypeDefine(context, emitsNode) { + /** @type {TSInterfaceBody | TSTypeLiteral | TSFunctionType | null} */ + const defNode = resolveQualifiedType( + context, + emitsNode, + (n) => isTSTypeLiteral(n) || isTSFunctionType(n) + ) + if (!defNode) { + return [] + } + return [...extractRuntimeEmits(defNode)] +} + /** * @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L1512 * @param {RuleContext} context The ESLint rule context object. @@ -68,6 +98,70 @@ function* extractRuntimeProps(context, node) { } } +/** + * @see https://github.com/vuejs/vue-next/blob/348c3b01e56383ffa70b180d1376fdf4ac12e274/packages/compiler-sfc/src/compileScript.ts#L1632 + * @param {TSTypeLiteral | TSInterfaceBody | TSFunctionType} node + * @returns {IterableIterator} + */ +function* extractRuntimeEmits(node) { + if (node.type === 'TSTypeLiteral' || node.type === 'TSInterfaceBody') { + const members = node.type === 'TSTypeLiteral' ? node.members : node.body + for (const t of members) { + if (t.type === 'TSCallSignatureDeclaration') { + yield* extractEventNames( + t.params[0], + /** @type {TSCallSignatureDeclaration} */ (t) + ) + } + } + return + } else { + yield* extractEventNames(node.params[0], node) + } + + /** + * @param {TSESTreeParameter} eventName + * @param {TSCallSignatureDeclaration | TSFunctionType} member + * @returns {IterableIterator} + */ + function* extractEventNames(eventName, member) { + if ( + eventName && + eventName.type === 'Identifier' && + eventName.typeAnnotation && + eventName.typeAnnotation.type === 'TSTypeAnnotation' + ) { + const typeNode = eventName.typeAnnotation.typeAnnotation + if ( + typeNode.type === 'TSLiteralType' && + typeNode.literal.type === 'Literal' + ) { + const emitName = String(typeNode.literal.value) + yield { + type: 'type', + key: /** @type {TSLiteralType} */ (typeNode), + emitName, + value: null, + node: member + } + } else if (typeNode.type === 'TSUnionType') { + for (const t of typeNode.types) { + if (t.type === 'TSLiteralType' && t.literal.type === 'Literal') { + const emitName = String(t.literal.value) + yield { + type: 'type', + key: /** @type {TSLiteralType} */ (t), + emitName, + value: null, + node: member + } + } + } + } + } + } +} + /** * @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L425 * diff --git a/tests/lib/rules/return-in-emits-validator.js b/tests/lib/rules/return-in-emits-validator.js index a1d3557ed..f47e8ca77 100644 --- a/tests/lib/rules/return-in-emits-validator.js +++ b/tests/lib/rules/return-in-emits-validator.js @@ -119,6 +119,18 @@ ruleTester.run('return-in-emits-validator', rule, { } ` + }, + { + filename: 'test.vue', + code: ` + + ` } ], @@ -307,6 +319,24 @@ ruleTester.run('return-in-emits-validator', rule, { line: 5 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: + 'Expected to return a boolean value in "foo" emits validator.', + line: 4 + } + ] } ] }) diff --git a/typings/eslint-plugin-vue/global.d.ts b/typings/eslint-plugin-vue/global.d.ts index 543eed56c..96ceee311 100644 --- a/typings/eslint-plugin-vue/global.d.ts +++ b/typings/eslint-plugin-vue/global.d.ts @@ -155,6 +155,9 @@ declare global { type TSTypeParameterInstantiation = VAST.TSTypeParameterInstantiation type TSPropertySignature = VAST.TSPropertySignature type TSMethodSignature = VAST.TSMethodSignature + type TSLiteralType = VAST.TSLiteralType + type TSCallSignatureDeclaration = VAST.TSCallSignatureDeclaration + type TSFunctionType = VAST.TSFunctionType // ---- JSX Nodes ---- diff --git a/typings/eslint-plugin-vue/util-types/ast/ts-ast.ts b/typings/eslint-plugin-vue/util-types/ast/ts-ast.ts index ef68b4514..a79d457f4 100644 --- a/typings/eslint-plugin-vue/util-types/ast/ts-ast.ts +++ b/typings/eslint-plugin-vue/util-types/ast/ts-ast.ts @@ -9,6 +9,9 @@ export type TSNode = | TSTypeParameterInstantiation | TSPropertySignature | TSMethodSignatureBase + | TSLiteralType + | TSCallSignatureDeclaration + | TSFunctionType export interface TSAsExpression extends HasParentNode { type: 'TSAsExpression' @@ -70,3 +73,20 @@ interface TSMethodSignatureNonComputedName extends TSMethodSignatureBase { key: TSESTree.PropertyNameNonComputed computed: false } + +export interface TSLiteralType extends HasParentNode { + type: 'TSLiteralType' + literal: ES.Literal | ES.UnaryExpression | ES.UpdateExpression +} + +interface TSFunctionSignatureBase extends HasParentNode { + params: TSESTree.Parameter[] + returnType?: TSESTree.TSTypeAnnotation + typeParameters?: TSESTree.TSTypeParameterDeclaration +} +export interface TSCallSignatureDeclaration extends TSFunctionSignatureBase { + type: 'TSCallSignatureDeclaration' +} +export interface TSFunctionType extends TSFunctionSignatureBase { + type: 'TSFunctionType' +} diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts index adece39a9..7258cb673 100644 --- a/typings/eslint-plugin-vue/util-types/utils.ts +++ b/typings/eslint-plugin-vue/util-types/utils.ts @@ -44,11 +44,23 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase { node: CallExpression, props: (ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[] ): void + onDefineEmitsEnter?( + node: CallExpression, + props: (ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[] + ): void + onDefineEmitsExit?( + node: CallExpression, + props: (ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[] + ): void [query: string]: | ((node: VAST.ParamNode) => void) | (( node: CallExpression, - props: (ComponentArrayProp | ComponentObjectProp)[] + props: (ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[] + ) => void) + | (( + node: CallExpression, + props: (ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[] ) => void) | undefined } @@ -99,3 +111,46 @@ export type ComponentTypeProp = { required: boolean types: string[] } + +type ComponentArrayEmitDetectName = { + type: 'array' + key: Literal | TemplateLiteral + emitName: string + value: null + node: Expression | SpreadElement +} +type ComponentArrayEmitUnknownName = { + type: 'array' + key: null + emitName: null + value: null + node: Expression | SpreadElement +} +export type ComponentArrayEmit = + | ComponentArrayEmitDetectName + | ComponentArrayEmitUnknownName +type ComponentObjectEmitDetectName = { + type: 'object' + key: Expression + emitName: string + value: Expression + node: Property +} +type ComponentObjectEmitUnknownName = { + type: 'object' + key: null + emitName: null + value: Expression + node: Property +} +export type ComponentObjectEmit = + | ComponentObjectEmitDetectName + | ComponentObjectEmitUnknownName + +export type ComponentTypeEmit = { + type: 'type' + key: TSLiteralType + emitName: string + value: null + node: TSCallSignatureDeclaration | TSFunctionType +}