UNPKG

eslint-plugin-vue

Version:

Official ESLint plugin for Vue.js

337 lines (315 loc) 9.12 kB
const { getTypeScript, isAny, isUnknown, isNever, isNull, isObject, isFunction, isStringLike, isNumberLike, isBooleanLike, isBigIntLike, isArrayLikeObject, isReferenceObject } = require('./typescript') /** * @typedef {import('@typescript-eslint/types').TSESTree.Node} TSESTreeNode * @typedef {import('typescript').Type} Type * @typedef {import('typescript').TypeChecker} TypeChecker * @typedef {import('typescript').Node} TypeScriptNode */ /** * @typedef {import('../index').ComponentInferTypeProp} ComponentInferTypeProp * @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 } /** * @typedef {object} Services * @property {typeof import("typescript")} ts * @property {Map<ESNode | TSNode | TSESTreeNode, TypeScriptNode>} tsNodeMap * @property {import('typescript').TypeChecker} checker */ /** * Get TypeScript parser services. * @param {RuleContext} context The ESLint rule context object. * @returns {Services|null} */ function getTSParserServices(context) { const sourceCode = context.getSourceCode() const tsNodeMap = sourceCode.parserServices.esTreeNodeToTSNodeMap if (!tsNodeMap) return null const hasFullTypeInformation = sourceCode.parserServices.hasFullTypeInformation !== false const checker = (hasFullTypeInformation && sourceCode.parserServices.program && sourceCode.parserServices.program.getTypeChecker()) || null if (!checker) return null const ts = getTypeScript() if (!ts) return null return { ts, tsNodeMap, checker } } /** * Get all props by looking at all component's properties * @param {RuleContext} context The ESLint rule context object. * @param {TypeNode} propsNode Type with props definition * @return {(ComponentInferTypeProp|ComponentUnknownProp)[]} Array of component props */ function getComponentPropsFromTypeDefineTypes(context, propsNode) { const services = getTSParserServices(context) const tsNode = services && services.tsNodeMap.get(propsNode) const type = tsNode && services.checker.getTypeAtLocation(tsNode) if ( !type || isAny(type) || isUnknown(type) || isNever(type) || isNull(type) ) { return [ { type: 'unknown', propName: null, node: propsNode } ] } return [...extractRuntimeProps(type, tsNode, propsNode, services)] } /** * 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 {(ComponentInferTypeEmit|ComponentUnknownEmit)[]} Array of component emits */ function getComponentEmitsFromTypeDefineTypes(context, emitsNode) { const services = getTSParserServices(context) const tsNode = services && services.tsNodeMap.get(emitsNode) const type = tsNode && services.checker.getTypeAtLocation(tsNode) if ( !type || isAny(type) || isUnknown(type) || isNever(type) || isNull(type) ) { return [ { type: 'unknown', emitName: null, node: 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 * @returns {string[]} */ function inferRuntimeTypeFromTypeNode(context, node) { const services = getTSParserServices(context) const tsNode = services && services.tsNodeMap.get(node) const type = tsNode && services.checker.getTypeAtLocation(tsNode) if (!type) { return ['null'] } return inferRuntimeTypeInternal(type, services) } /** * @param {Type} type * @param {TypeScriptNode} tsNode * @param {TypeNode} propsNode Type with props definition * @param {Services} services * @returns {IterableIterator<ComponentInferTypeProp>} */ function* extractRuntimeProps(type, tsNode, propsNode, services) { const { ts, checker } = services for (const property of type.getProperties()) { const isOptional = (property.flags & ts.SymbolFlags.Optional) !== 0 const name = property.getName() const type = checker.getTypeOfSymbolAtLocation(property, tsNode) yield { type: 'infer-type', propName: name, required: !isOptional, node: propsNode, types: inferRuntimeTypeInternal(type, services) } } } /** * @param {Type} type * @param {Services} services * @returns {string[]} */ function inferRuntimeTypeInternal(type, services) { const { checker } = services /** @type {Set<string>} */ const types = new Set() // handle generic parameter types if (type.isTypeParameter()) { const constraint = type.getConstraint() if (constraint) { for (const t of inferRuntimeTypeInternal(constraint, services)) { types.add(t) } } return [...types] } for (const targetType of iterateTypes(checker.getNonNullableType(type))) { if ( isAny(targetType) || isUnknown(targetType) || isNever(targetType) || isNull(targetType) ) { types.add('null') } else if (isStringLike(targetType)) { types.add('String') } else if (isNumberLike(targetType) || isBigIntLike(targetType)) { types.add('Number') } else if (isBooleanLike(targetType)) { types.add('Boolean') } else if (isFunction(targetType)) { types.add('Function') } else if ( isArrayLikeObject(targetType) || (targetType.isClassOrInterface() && ['Array', 'ReadonlyArray'].includes( checker.getFullyQualifiedName(targetType.symbol) )) ) { types.add('Array') } else if (isObject(targetType)) { types.add('Object') } } if (types.size <= 0) types.add('null') return [...types] } /** * @param {Type} type * @param {TypeScriptNode} tsNode * @param {TypeNode} emitsNode Type with emits definition * @param {Services} services * @returns {IterableIterator<ComponentInferTypeEmit|ComponentUnknownEmit>} */ function* extractRuntimeEmits(type, tsNode, emitsNode, services) { const { checker } = services if (isFunction(type)) { for (const signature of type.getCallSignatures()) { const param = signature.getParameters()[0] if (!param) { yield { type: 'unknown', emitName: null, node: emitsNode } continue } const type = checker.getTypeOfSymbolAtLocation(param, tsNode) for (const targetType of iterateTypes(type)) { yield targetType.isStringLiteral() ? { type: 'infer-type', emitName: targetType.value, node: emitsNode } : { type: 'unknown', emitName: null, node: emitsNode } } } } else if (isObject(type)) { for (const property of type.getProperties()) { const name = property.getName() yield { type: 'infer-type', emitName: name, node: emitsNode } } } else { yield { type: 'unknown', emitName: null, node: emitsNode } } } /** * @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>} */ function* iterateTypes(type) { if (isReferenceObject(type) && type.target !== type) { yield* iterateTypes(type.target) } else if (type.isUnion() && !isBooleanLike(type)) { for (const t of type.types) { yield* iterateTypes(t) } } else { yield type } }