From 0a232bccb010e35b0adc1ae940be891ca30a0b3a Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Tue, 18 Apr 2023 22:16:11 +0900 Subject: [PATCH 1/9] Add support imported types in SFC macros --- docs/.vitepress/config.ts | 18 +- lib/rules/no-restricted-props.js | 15 +- lib/rules/no-unused-properties.js | 4 +- .../padding-lines-in-component-definition.js | 32 ++- lib/rules/prefer-prop-type-boolean-first.js | 3 + lib/rules/require-emit-validator.js | 23 +- lib/rules/require-explicit-emits.js | 5 +- lib/rules/require-prop-comment.js | 2 +- lib/rules/require-prop-type-constructor.js | 2 +- lib/rules/require-prop-types.js | 5 +- lib/rules/require-valid-default-prop.js | 14 +- lib/rules/return-in-emits-validator.js | 2 +- lib/utils/indent-ts.js | 2 +- lib/utils/index.js | 63 ++--- lib/utils/ts-utils/index.js | 66 +++++ .../{ts-ast-utils.js => ts-utils/ts-ast.js} | 143 ++++------ lib/utils/ts-utils/ts-types.js | 255 ++++++++++++++++++ lib/utils/ts-utils/typescript.js | 211 +++++++++++++++ tests/fixtures/typescript/src/test.vue | 1 + tests/fixtures/typescript/src/test01.ts | 20 ++ tests/fixtures/typescript/tsconfig.json | 6 + tests/fixtures/utils/ts-utils/src/test.ts | 0 tests/fixtures/utils/ts-utils/src/test.vue | 1 + tests/fixtures/utils/ts-utils/tsconfig.json | 6 + tests/lib/rules/no-restricted-props.js | 20 ++ tests/lib/rules/no-unused-properties.js | 21 ++ .../padding-lines-in-component-definition.js | 11 + .../rules/prefer-prop-type-boolean-first.js | 11 + tests/lib/rules/require-emit-validator.js | 11 + tests/lib/rules/require-explicit-emits.js | 33 +++ tests/lib/rules/require-prop-comment.js | 27 +- .../rules/require-prop-type-constructor.js | 11 + tests/lib/rules/require-prop-types.js | 11 + tests/lib/rules/require-valid-default-prop.js | 78 ++++++ tests/lib/rules/return-in-emits-validator.js | 11 + tests/lib/utils/index.js | 4 +- .../ts-utils/ts-types/get-component-emits.js | 122 +++++++++ .../ts-utils/ts-types/get-component-props.js | 153 +++++++++++ tests/test-utils/typescript.js | 26 ++ typings/eslint-plugin-vue/global.d.ts | 1 + .../util-types/ast/ts-ast.ts | 13 +- .../util-types/parser-services.ts | 8 + typings/eslint-plugin-vue/util-types/utils.ts | 31 ++- 43 files changed, 1300 insertions(+), 202 deletions(-) create mode 100644 lib/utils/ts-utils/index.js rename lib/utils/{ts-ast-utils.js => ts-utils/ts-ast.js} (68%) create mode 100644 lib/utils/ts-utils/ts-types.js create mode 100644 lib/utils/ts-utils/typescript.js create mode 100644 tests/fixtures/typescript/src/test.vue create mode 100644 tests/fixtures/typescript/src/test01.ts create mode 100644 tests/fixtures/typescript/tsconfig.json create mode 100644 tests/fixtures/utils/ts-utils/src/test.ts create mode 100644 tests/fixtures/utils/ts-utils/src/test.vue create mode 100644 tests/fixtures/utils/ts-utils/tsconfig.json create mode 100644 tests/lib/utils/ts-utils/ts-types/get-component-emits.js create mode 100644 tests/lib/utils/ts-utils/ts-types/get-component-props.js create mode 100644 tests/test-utils/typescript.js diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 2945e4246..5b2d65516 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -73,7 +73,7 @@ const sidebarCategories = [ } ] -const categorizedRules: DefaultTheme.SidebarGroup[] = [] +const categorizedRules: DefaultTheme.SidebarItem[] = [] for (const { title, categoryIds } of sidebarCategories) { const categoryRules = rules .filter((rule) => rule.meta.docs.categories && !rule.meta.deprecated) @@ -84,8 +84,10 @@ for (const { title, categoryIds } of sidebarCategories) { ) const children: DefaultTheme.SidebarItem[] = categoryRules .filter(({ ruleId }) => { - const exists = categorizedRules.some(({ items }) => - items.some(({ text: alreadyRuleId }) => alreadyRuleId === ruleId) + const exists = categorizedRules.some( + ({ items }) => + items && + items.some(({ text: alreadyRuleId }) => alreadyRuleId === ruleId) ) return !exists }) @@ -101,16 +103,16 @@ for (const { title, categoryIds } of sidebarCategories) { } categorizedRules.push({ text: title, - collapsible: false, + collapsed: false, items: children }) } -const extraCategories: DefaultTheme.SidebarGroup[] = [] +const extraCategories: DefaultTheme.SidebarItem[] = [] if (uncategorizedRules.length > 0) { extraCategories.push({ text: 'Uncategorized', - collapsible: false, + collapsed: false, items: uncategorizedRules.map(({ ruleId, name }) => ({ text: ruleId, link: `/rules/${name}` @@ -120,7 +122,7 @@ if (uncategorizedRules.length > 0) { if (uncategorizedExtensionRule.length > 0) { extraCategories.push({ text: 'Extension Rules', - collapsible: false, + collapsed: false, items: uncategorizedExtensionRule.map(({ ruleId, name }) => ({ text: ruleId, link: `/rules/${name}` @@ -130,7 +132,7 @@ if (uncategorizedExtensionRule.length > 0) { if (deprecatedRules.length > 0) { extraCategories.push({ text: 'Deprecated', - collapsible: false, + collapsed: false, items: deprecatedRules.map(({ ruleId, name }) => ({ text: ruleId, link: `/rules/${name}` diff --git a/lib/rules/no-restricted-props.js b/lib/rules/no-restricted-props.js index 564bf0773..db673e4c2 100644 --- a/lib/rules/no-restricted-props.js +++ b/lib/rules/no-restricted-props.js @@ -109,14 +109,17 @@ module.exports = { option.message || `Using \`${prop.propName}\` props is not allowed.` context.report({ - node: prop.key, + node: prop.type !== 'infer-type' ? prop.key : prop.node, messageId: 'restrictedProp', data: { message }, - suggest: createSuggest( - prop.key, - option, - withDefaultsProps && withDefaultsProps[prop.propName] - ) + suggest: + prop.type !== 'infer-type' + ? createSuggest( + prop.key, + option, + withDefaultsProps && withDefaultsProps[prop.propName] + ) + : null }) break } diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js index 1421ad0b8..818dcc053 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -30,7 +30,7 @@ const { * @typedef {object} ComponentNonObjectPropertyData * @property {string} name * @property {GroupName} groupName - * @property {'array' | 'type'} type + * @property {'array' | 'type' | 'infer-type'} type * @property {ASTNode} node * * @typedef { ComponentNonObjectPropertyData | ComponentObjectPropertyData } ComponentPropertyData @@ -423,7 +423,7 @@ module.exports = { type: prop.type, name: prop.propName, groupName: 'props', - node: prop.key + node: prop.type !== 'infer-type' ? prop.key : prop.node }) } } diff --git a/lib/rules/padding-lines-in-component-definition.js b/lib/rules/padding-lines-in-component-definition.js index 3f63050b4..64ceb2c1f 100644 --- a/lib/rules/padding-lines-in-component-definition.js +++ b/lib/rules/padding-lines-in-component-definition.js @@ -6,6 +6,7 @@ /** * @typedef {import('../utils').ComponentProp} ComponentProp + * @typedef {import('../utils').ComponentEmit} ComponentEmit * @typedef {import('../utils').GroupName} GroupName */ @@ -33,10 +34,19 @@ function isComma(node) { } /** - * @param {string} nodeType + * @typedef {Exclude & { node: {type: 'Property' | 'SpreadElement'} }} ValidComponentPropOrEmit */ -function isValidProperties(nodeType) { - return ['Property', 'SpreadElement'].includes(nodeType) +/** + * @template {ComponentProp | ComponentEmit} T + * @param {T} propOrEmit + * @returns {propOrEmit is ValidComponentPropOrEmit & T} + */ +function isValidProperties(propOrEmit) { + return Boolean( + propOrEmit.type !== 'infer-type' && + propOrEmit.node && + ['Property', 'SpreadElement'].includes(propOrEmit.node.type) + ) } /** @@ -320,11 +330,9 @@ module.exports = { }), utils.defineScriptSetupVisitor(context, { onDefinePropsEnter(_, props) { - const propNodes = /** @type {(Property | SpreadElement)[]} */ ( - props - .filter((prop) => prop.node && isValidProperties(prop.node.type)) - .map((prop) => prop.node) - ) + const propNodes = props + .filter(isValidProperties) + .map((prop) => prop.node) const withinOption = parseOption(options, OptionKeys.WithinOption) const propsOption = withinOption && parseOption(withinOption, 'props') @@ -337,11 +345,9 @@ module.exports = { ) }, onDefineEmitsEnter(_, emits) { - const emitNodes = /** @type {(Property | SpreadElement)[]} */ ( - emits - .filter((emit) => emit.node && isValidProperties(emit.node.type)) - .map((emit) => emit.node) - ) + const emitNodes = emits + .filter(isValidProperties) + .map((emit) => emit.node) const withinOption = parseOption(options, OptionKeys.WithinOption) const emitsOption = withinOption && parseOption(withinOption, 'emits') diff --git a/lib/rules/prefer-prop-type-boolean-first.js b/lib/rules/prefer-prop-type-boolean-first.js index e26f9462e..aa16707c4 100644 --- a/lib/rules/prefer-prop-type-boolean-first.js +++ b/lib/rules/prefer-prop-type-boolean-first.js @@ -67,6 +67,9 @@ module.exports = { * @param {import('../utils').ComponentProp} prop */ function checkProperty(prop) { + if (prop.type !== 'object') { + return + } const { value } = prop if (!value) { return diff --git a/lib/rules/require-emit-validator.js b/lib/rules/require-emit-validator.js index 6fc439fd2..8b478be9d 100644 --- a/lib/rules/require-emit-validator.js +++ b/lib/rules/require-emit-validator.js @@ -34,24 +34,29 @@ module.exports = { * @param {ComponentEmit} emit */ function checker(emit) { - if (emit.type !== 'object' && emit.type !== 'array') { - return - } - const { value, node, emitName } = emit - const hasType = - !!value && - (value.type === 'ArrowFunctionExpression' || + /** @type {Expression|null} */ + let value = null + let hasType = false + if (emit.type === 'object') { + value = emit.value + hasType = + value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression' || // validator may from outer scope - value.type === 'Identifier') + value.type === 'Identifier' + } else if (emit.type !== 'array') { + return + } if (!hasType) { + const { node, emitName } = emit const name = emitName || (node.type === 'Identifier' && node.name) || 'Unknown emit' if (value && value.type === 'Literal' && value.value === null) { + const valueNode = value context.report({ node, messageId: 'skipped', @@ -59,7 +64,7 @@ module.exports = { suggest: [ { messageId: 'emptyValidation', - fix: (fixer) => fixer.replaceText(value, '() => true') + fix: (fixer) => fixer.replaceText(valueNode, '() => true') } ] }) diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js index 167613804..267f4e610 100644 --- a/lib/rules/require-explicit-emits.js +++ b/lib/rules/require-explicit-emits.js @@ -429,7 +429,10 @@ module.exports = { function buildSuggest(define, emits, nameWithLoc, context) { const emitsKind = define.type === 'ObjectExpression' ? '`emits` option' : '`defineEmits`' - const certainEmits = emits.filter((e) => e.key) + const certainEmits = emits.filter( + /** @returns {e is ComponentEmit & {type:'array'|'object'}} */ + (e) => e.type === 'array' || e.type === 'object' + ) if (certainEmits.length > 0) { const last = certainEmits[certainEmits.length - 1] return [ diff --git a/lib/rules/require-prop-comment.js b/lib/rules/require-prop-comment.js index 8dd852cbc..845bba9df 100644 --- a/lib/rules/require-prop-comment.js +++ b/lib/rules/require-prop-comment.js @@ -62,7 +62,7 @@ module.exports = { */ function verifyProps(props) { for (const prop of props) { - if (!prop.propName) { + if (!prop.propName || prop.type === 'infer-type') { continue } diff --git a/lib/rules/require-prop-type-constructor.js b/lib/rules/require-prop-type-constructor.js index a708508e5..45354a1d1 100644 --- a/lib/rules/require-prop-type-constructor.js +++ b/lib/rules/require-prop-type-constructor.js @@ -79,7 +79,7 @@ module.exports = { /** @param {ComponentProp[]} props */ function verifyProps(props) { for (const prop of props) { - if (!prop.value || prop.propName == null) { + if (prop.type !== 'object' || prop.propName == null) { continue } if ( diff --git a/lib/rules/require-prop-types.js b/lib/rules/require-prop-types.js index e4d9fdcc1..f603fd184 100644 --- a/lib/rules/require-prop-types.js +++ b/lib/rules/require-prop-types.js @@ -51,12 +51,12 @@ module.exports = { if (prop.type !== 'object' && prop.type !== 'array') { return } - const { value, node, propName } = prop let hasType = true - if (!value) { + if (prop.type === 'array') { hasType = false } else { + const { value } = prop switch (value.type) { case 'ObjectExpression': { // foo: { @@ -77,6 +77,7 @@ module.exports = { } if (!hasType) { + const { node, propName } = prop const name = propName || (node.type === 'Identifier' && node.name) || diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js index 50a3d191b..9535f51d6 100644 --- a/lib/rules/require-valid-default-prop.js +++ b/lib/rules/require-valid-default-prop.js @@ -7,9 +7,11 @@ const utils = require('../utils') const { capitalize } = require('../utils/casing') /** + * @typedef {import('../utils').ComponentProp} ComponentProp * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp + * @typedef {import('../utils').ComponentInferTypeProp} ComponentInferTypeProp * @typedef {import('../utils').ComponentUnknownProp} ComponentUnknownProp * @typedef {import('../utils').VueObjectData} VueObjectData */ @@ -108,7 +110,7 @@ module.exports = { */ /** * @typedef {object} PropDefaultFunctionContext - * @property {ComponentObjectProp | ComponentTypeProp} prop + * @property {ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp} prop * @property {Set} types * @property {FunctionValueType} default */ @@ -225,7 +227,7 @@ module.exports = { /** * @param {*} node - * @param {ComponentObjectProp | ComponentTypeProp} prop + * @param {ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp} prop * @param {Iterable} expectedTypeNames */ function report(node, prop, expectedTypeNames) { @@ -245,7 +247,7 @@ module.exports = { } /** - * @param {(ComponentObjectDefineProp | ComponentTypeProp)[]} props + * @param {(ComponentObjectDefineProp | ComponentTypeProp | ComponentInferTypeProp)[]} props * @param { { [key: string]: Expression | undefined } } withDefaults */ function processPropDefs(props, withDefaults) { @@ -394,15 +396,15 @@ module.exports = { }), utils.defineScriptSetupVisitor(context, { onDefinePropsEnter(node, baseProps) { - /** @type {(ComponentObjectDefineProp | ComponentTypeProp)[]} */ const props = baseProps.filter( /** - * @param {ComponentObjectProp | ComponentArrayProp | ComponentTypeProp | ComponentUnknownProp} prop - * @returns {prop is ComponentObjectDefineProp | ComponentTypeProp} + * @param {ComponentProp} prop + * @returns {prop is ComponentObjectDefineProp | ComponentInferTypeProp | ComponentTypeProp} */ (prop) => Boolean( prop.type === 'type' || + prop.type === 'infer-type' || (prop.type === 'object' && prop.value.type === 'ObjectExpression') ) diff --git a/lib/rules/return-in-emits-validator.js b/lib/rules/return-in-emits-validator.js index 4b8a3b1da..86db20ac9 100644 --- a/lib/rules/return-in-emits-validator.js +++ b/lib/rules/return-in-emits-validator.js @@ -65,7 +65,7 @@ module.exports = { */ function processEmits(emits) { for (const emit of emits) { - if (!emit.value) { + if (emit.type !== 'object' || !emit.value) { continue } if ( diff --git a/lib/utils/indent-ts.js b/lib/utils/indent-ts.js index c7ad7ad63..d318bb2c1 100644 --- a/lib/utils/indent-ts.js +++ b/lib/utils/indent-ts.js @@ -12,7 +12,7 @@ const { isClosingBracketToken, isOpeningBracketToken } = require('@eslint-community/eslint-utils') -const { isTypeNode } = require('./ts-ast-utils') +const { isTypeNode } = require('./ts-utils') /** * @typedef {import('../../typings/eslint-plugin-vue/util-types/indent-helper').TSNodeListener} TSNodeListener diff --git a/lib/utils/index.js b/lib/utils/index.js index a63891bd9..ef8cd0ee9 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -15,11 +15,13 @@ * @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 {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeProp} ComponentInferTypeProp * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownProp} ComponentUnknownProp * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentProp} ComponentProp * @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 {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 */ @@ -60,7 +62,7 @@ const { getComponentPropsFromTypeDefine, getComponentEmitsFromTypeDefine, isTypeNode -} = require('./ts-ast-utils') +} = require('./ts-utils') /** * @type { WeakMap } @@ -2914,9 +2916,7 @@ function getComponentPropsFromOptions(componentObject) { return [ { type: 'unknown', - key: null, propName: null, - value: null, node: propsNode.value } ] @@ -2949,9 +2949,7 @@ function getComponentEmitsFromOptions(componentObject) { return [ { type: 'unknown', - key: null, emitName: null, - value: null, node: emitsNode.value } ] @@ -2964,7 +2962,7 @@ function getComponentEmitsFromOptions(componentObject) { * Get all props from `defineProps` call expression. * @param {RuleContext} context The rule context object. * @param {CallExpression} node `defineProps` call expression - * @return {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp | ComponentUnknownProp)[]} Array of component props + * @return {ComponentProp[]} Array of component props */ function getComponentPropsFromDefineProps(context, node) { if (node.arguments.length > 0) { @@ -2975,9 +2973,7 @@ function getComponentPropsFromDefineProps(context, node) { return [ { type: 'unknown', - key: null, propName: null, - value: null, node: node.arguments[0] } ] @@ -2991,9 +2987,7 @@ function getComponentPropsFromDefineProps(context, node) { return [ { type: 'unknown', - key: null, propName: null, - value: null, node: null } ] @@ -3003,7 +2997,7 @@ function getComponentPropsFromDefineProps(context, node) { * Get all emits from `defineEmits` call expression. * @param {RuleContext} context The rule context object. * @param {CallExpression} node `defineEmits` call expression - * @return {(ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit | ComponentUnknownEmit)[]} Array of component emits + * @return {ComponentEmit[]} Array of component emits */ function getComponentEmitsFromDefineEmits(context, node) { if (node.arguments.length > 0) { @@ -3014,9 +3008,7 @@ function getComponentEmitsFromDefineEmits(context, node) { return [ { type: 'unknown', - key: null, emitName: null, - value: null, node: node.arguments[0] } ] @@ -3030,9 +3022,7 @@ function getComponentEmitsFromDefineEmits(context, node) { return [ { type: 'unknown', - key: null, emitName: null, - value: null, node: null } ] @@ -3044,34 +3034,35 @@ function getComponentEmitsFromDefineEmits(context, node) { */ function getComponentPropsFromDefine(propsNode) { if (propsNode.type === 'ObjectExpression') { - return propsNode.properties.map((prop) => { - if (!isProperty(prop)) { - return { - type: 'unknown', - key: null, - propName: null, - value: null, - node: prop + return propsNode.properties.map( + /** @returns {ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp} */ + (prop) => { + if (!isProperty(prop)) { + return { + type: 'unknown', + propName: null, + node: prop + } + } + const propName = getStaticPropertyName(prop) + if (propName != null) { + return { + type: 'object', + key: prop.key, + propName, + value: skipTSAsExpression(prop.value), + node: prop + } } - } - const propName = getStaticPropertyName(prop) - if (propName != null) { return { type: 'object', - key: prop.key, - propName, + key: null, + propName: null, value: skipTSAsExpression(prop.value), node: prop } } - return { - type: 'object', - key: null, - propName: null, - value: skipTSAsExpression(prop.value), - node: prop - } - }) + ) } return propsNode.elements.filter(isDef).map((prop) => { diff --git a/lib/utils/ts-utils/index.js b/lib/utils/ts-utils/index.js new file mode 100644 index 000000000..13ac52f84 --- /dev/null +++ b/lib/utils/ts-utils/index.js @@ -0,0 +1,66 @@ +const { + isTypeNode, + resolveQualifiedType, + extractRuntimeProps, + isTSTypeLiteral, + isTSTypeLiteralOrTSFunctionType, + extractRuntimeEmits +} = require('./ts-ast') +const { + getComponentPropsFromTypeDefineTypes, + getComponentEmitsFromTypeDefineTypes +} = require('./ts-types') + +/** + * @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TSESTreeTypeNode + */ +/** + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeProp} ComponentInferTypeProp + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownProp} ComponentUnknownProp + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeEmit} ComponentTypeEmit + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeEmit} ComponentInferTypeEmit + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownEmit} ComponentUnknownEmit + */ + +module.exports = { + isTypeNode, + getComponentPropsFromTypeDefine, + getComponentEmitsFromTypeDefine +} + +/** + * 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 {(ComponentTypeProp|ComponentInferTypeProp|ComponentUnknownProp)[]} Array of component props + */ +function getComponentPropsFromTypeDefine(context, propsNode) { + const defNode = resolveQualifiedType( + context, + /** @type {TSESTreeTypeNode} */ (propsNode), + isTSTypeLiteral + ) + if (!defNode) { + return getComponentPropsFromTypeDefineTypes(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|ComponentInferTypeEmit|ComponentUnknownEmit)[]} Array of component emits + */ +function getComponentEmitsFromTypeDefine(context, emitsNode) { + const defNode = resolveQualifiedType( + context, + /** @type {TSESTreeTypeNode} */ (emitsNode), + isTSTypeLiteralOrTSFunctionType + ) + if (!defNode) { + return getComponentEmitsFromTypeDefineTypes(context, emitsNode) + } + return [...extractRuntimeEmits(defNode)] +} diff --git a/lib/utils/ts-ast-utils.js b/lib/utils/ts-utils/ts-ast.js similarity index 68% rename from lib/utils/ts-ast-utils.js rename to lib/utils/ts-utils/ts-ast.js index 330e6b985..5f094a1a3 100644 --- a/lib/utils/ts-ast-utils.js +++ b/lib/utils/ts-utils/ts-ast.js @@ -1,27 +1,30 @@ const { findVariable } = require('@eslint-community/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.TypeNode} TSESTreeTypeNode + * @typedef {import('@typescript-eslint/types').TSESTree.TSInterfaceBody} TSESTreeTSInterfaceBody + * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeLiteral} TSESTreeTSTypeLiteral + * @typedef {import('@typescript-eslint/types').TSESTree.TSFunctionType} TSESTreeTSFunctionType * @typedef {import('@typescript-eslint/types').TSESTree.Parameter} TSESTreeParameter - * @typedef {import('@typescript-eslint/types').TSESTree.Node} Node + * @typedef {import('@typescript-eslint/types').TSESTree.Node} TSESTreeNode * */ /** - * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp - * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeEmit} ComponentTypeEmit - * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownEmit} ComponentUnknownEmit + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeEmit} ComponentTypeEmit */ module.exports = { isTypeNode, - getComponentPropsFromTypeDefine, - getComponentEmitsFromTypeDefine + resolveQualifiedType, + isTSTypeLiteral, + isTSTypeLiteralOrTSFunctionType, + extractRuntimeProps, + extractRuntimeEmits } /** - * @param {Node | ASTNode} node - * @returns {node is TypeNode} + * @param {TSESTreeNode | ASTNode} node + * @returns {node is TSESTreeTypeNode} */ function isTypeNode(node) { return ( @@ -64,58 +67,31 @@ function isTypeNode(node) { } /** - * @param {TypeNode} node - * @returns {node is TSTypeLiteral} + * @param {TSESTreeTypeNode} node + * @returns {node is TSESTreeTSTypeLiteral} */ function isTSTypeLiteral(node) { return node.type === 'TSTypeLiteral' } /** - * @param {TypeNode} node - * @returns {node is TSFunctionType} + * @param {TSESTreeTypeNode} node + * @returns {node is TSESTreeTSFunctionType} */ function isTSFunctionType(node) { return node.type === 'TSFunctionType' } - -/** - * 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 {ComponentTypeProp[]} Array of component props - */ -function getComponentPropsFromTypeDefine(context, propsNode) { - /** @type {TSInterfaceBody | TSTypeLiteral|null} */ - const defNode = resolveQualifiedType(context, propsNode, isTSTypeLiteral) - if (!defNode) { - return [] - } - 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 + * @param {TSESTreeTypeNode} node + * @returns {node is TSESTreeTSTypeLiteral | TSESTreeTSFunctionType} */ -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)] +function isTSTypeLiteralOrTSFunctionType(node) { + return isTSTypeLiteral(node) || isTSFunctionType(node) } /** * @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L1512 * @param {RuleContext} context The ESLint rule context object. - * @param {TSTypeLiteral | TSInterfaceBody} node + * @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody} node * @returns {IterableIterator} */ function* extractRuntimeProps(context, node) { @@ -135,7 +111,6 @@ function* extractRuntimeProps(context, node) { type: 'type', key: /** @type {Identifier | Literal} */ (m.key), propName: m.key.type === 'Identifier' ? m.key.name : `${m.key.value}`, - value: null, node: /** @type {TSPropertySignature | TSMethodSignature} */ (m), required: !m.optional, @@ -147,43 +122,26 @@ 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} + * @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody | TSESTreeTSFunctionType} node + * @returns {IterableIterator} */ function* extractRuntimeEmits(node) { - if (node.type === 'TSFunctionType') { - yield* extractEventNames(node.params[0], node) - return - } - const members = node.type === 'TSTypeLiteral' ? node.members : node.body - for (const member of members) { - if (member.type === 'TSCallSignatureDeclaration') { - yield* extractEventNames( - member.params[0], - /** @type {TSCallSignatureDeclaration} */ (member) - ) - } else if ( - member.type === 'TSPropertySignature' || - member.type === 'TSMethodSignature' - ) { - if (member.key.type !== 'Identifier' && member.key.type !== 'Literal') { - yield { - type: 'unknown', - node: member.key - } - continue - } - yield { - type: 'type', - key: /** @type {Identifier | Literal} */ (member.key), - emitName: - member.key.type === 'Identifier' - ? member.key.name - : `${member.key.value}`, - value: null, - node: /** @type {TSPropertySignature | TSMethodSignature} */ (member) + 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], + /** @type {TSFunctionType} */ (node) + ) } } @@ -209,7 +167,6 @@ function* extractEventNames(eventName, member) { type: 'type', key: /** @type {TSLiteralType} */ (typeNode), emitName, - value: null, node: member } } else if (typeNode.type === 'TSUnionType') { @@ -220,7 +177,6 @@ function* extractEventNames(eventName, member) { type: 'type', key: /** @type {TSLiteralType} */ (t), emitName, - value: null, node: member } } @@ -232,9 +188,11 @@ function* extractEventNames(eventName, member) { /** * @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L425 * + * @template {TSESTreeTypeNode} R * @param {RuleContext} context The ESLint rule context object. - * @param {TypeNode} node - * @param {(n: TypeNode)=> boolean } qualifier + * @param {TSESTreeTypeNode} node + * @param {(n: TSESTreeTypeNode)=> n is R } qualifier + * @returns {R | TSESTreeTSInterfaceBody | null} */ function resolveQualifiedType(context, node, qualifier) { if (qualifier(node)) { @@ -244,22 +202,23 @@ function resolveQualifiedType(context, node, qualifier) { const refName = node.typeName.name const variable = findVariable(context.getScope(), refName) if (variable && variable.defs.length === 1) { - const def = variable.defs[0] - if (def.node.type === 'TSInterfaceDeclaration') { - return /** @type {any} */ (def.node).body + const defNode = /** @type {TSESTreeNode} */ (variable.defs[0].node) + if (defNode.type === 'TSInterfaceDeclaration') { + return defNode.body } - if (def.node.type === 'TSTypeAliasDeclaration') { - const typeAnnotation = /** @type {any} */ (def.node).typeAnnotation + if (defNode.type === 'TSTypeAliasDeclaration') { + const typeAnnotation = defNode.typeAnnotation return qualifier(typeAnnotation) ? typeAnnotation : null } } } + return null } /** * @param {RuleContext} context The ESLint rule context object. - * @param {TypeNode} node - * @param {Set} [checked] + * @param {TSESTreeTypeNode} node + * @param {Set} [checked] * @returns {string[]} */ function inferRuntimeType(context, node, checked = new Set()) { diff --git a/lib/utils/ts-utils/ts-types.js b/lib/utils/ts-utils/ts-types.js new file mode 100644 index 000000000..f9be9a77f --- /dev/null +++ b/lib/utils/ts-utils/ts-types.js @@ -0,0 +1,255 @@ +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('../../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeProp} ComponentInferTypeProp + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownProp} ComponentUnknownProp + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeEmit} ComponentInferTypeEmit + * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownEmit} ComponentUnknownEmit + */ + +module.exports = { + isAvailableTypeInformation, + getComponentPropsFromTypeDefineTypes, + getComponentEmitsFromTypeDefineTypes +} + +/** + * @typedef {object} Services + * @property {typeof import("typescript")} ts + * @property {Map} 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 tsNodeMap = context.parserServices.esTreeNodeToTSNodeMap + if (!tsNodeMap) return null + const hasFullTypeInformation = + context.parserServices.hasFullTypeInformation !== false + const checker = + (hasFullTypeInformation && + context.parserServices.program && + context.parserServices.program.getTypeChecker()) || + null + if (!checker) return null + const ts = getTypeScript() + if (!ts) return null + + return { + ts, + tsNodeMap, + checker + } +} +/** + * Checks whether type information is available or not. + * @param {RuleContext} context The ESLint rule context object. + */ +function isAvailableTypeInformation(context) { + return Boolean(getTSParserServices(context)) +} + +/** + * 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)] +} + +/** + * @param {Type} type + * @param {TypeScriptNode} tsNode + * @param {TypeNode} propsNode Type with props definition + * @param {Services} services + * @returns {IterableIterator} + */ +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) + /** @type {string[]} */ + const types = [] + for (const targetType of iterateTypes(checker.getNonNullableType(type))) { + if ( + isAny(targetType) || + isUnknown(targetType) || + isNever(targetType) || + isNull(targetType) + ) { + types.push('null') + } else if (isStringLike(targetType)) { + types.push('String') + } else if (isNumberLike(targetType)) { + types.push('Number') + } else if (isBooleanLike(targetType)) { + types.push('Boolean') + } else if (isBigIntLike(targetType)) { + types.push('BigInt') + } else if (isFunction(targetType)) { + types.push('Function') + } else if ( + isArrayLikeObject(targetType) || + (targetType.isClassOrInterface() && + ['Array', 'ReadonlyArray'].includes( + checker.getFullyQualifiedName(targetType.symbol) + )) + ) { + types.push('Array') + } else if (isObject(targetType)) { + types.push('Object') + } + } + yield { + type: 'infer-type', + propName: name, + required: !isOptional, + node: propsNode, + types: types.length > 0 ? types : ['null'] + } + } +} + +/** + * @param {Type} type + * @param {TypeScriptNode} tsNode + * @param {TypeNode} emitsNode Type with emits definition + * @param {Services} services + * @returns {IterableIterator} + */ +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 + * @returns {Iterable} + */ +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 + } +} diff --git a/lib/utils/ts-utils/typescript.js b/lib/utils/ts-utils/typescript.js new file mode 100644 index 000000000..0bc536f56 --- /dev/null +++ b/lib/utils/ts-utils/typescript.js @@ -0,0 +1,211 @@ +/** + * @typedef {typeof import("typescript")} TypeScript + * @typedef {import("typescript").Type} Type + * @typedef {import("typescript").ObjectType} ObjectType + * @typedef {import("typescript").InterfaceType} InterfaceType + * @typedef {import("typescript").TypeReference} TypeReference + * @typedef {import("typescript").UnionOrIntersectionType} UnionOrIntersectionType + * @typedef {import("typescript").TypeParameter} TypeParameter + */ + +/** @type {TypeScript | undefined} */ +let cacheTypeScript + +module.exports = { + getTypeScript, + isObject, + isAny, + isUnknown, + isNever, + isNull, + isFunction, + isArrayLikeObject, + isStringLike, + isNumberLike, + isBooleanLike, + isBigIntLike, + isReferenceObject, + extractTypeFlags, + extractObjectFlags +} + +/** + * Get TypeScript instance + */ +function getTypeScript() { + try { + return (cacheTypeScript ??= require('typescript')) + } catch (error) { + if (/** @type {any} */ (error).code === 'MODULE_NOT_FOUND') { + return undefined + } + + throw error + } +} +/** + * For debug + * @param {Type} tsType + * @returns {string[]} + */ +function extractTypeFlags(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + /** @type {string[]} */ + const result = [] + const keys = /** @type {(keyof (typeof ts.TypeFlags))[]} */ ( + Object.keys(ts.TypeFlags) + ) + for (const k of keys) { + if ((tsType.flags & ts.TypeFlags[k]) !== 0) { + result.push(k) + } + } + return result +} +/** + * For debug + * @param {Type} tsType + * @returns {string[]} + */ +function extractObjectFlags(tsType) { + if (!isObject(tsType)) { + return [] + } + const ts = /** @type {TypeScript} */ (getTypeScript()) + /** @type {string[]} */ + const result = [] + const keys = /** @type {(keyof (typeof ts.ObjectFlags))[]} */ ( + Object.keys(ts.ObjectFlags) + ) + for (const k of keys) { + if ((tsType.objectFlags & ts.ObjectFlags[k]) !== 0) { + result.push(k) + } + } + return result +} + +/** + * Check if a given type is an object type or not. + * @param {Type} tsType The type to check. + * @returns {tsType is ObjectType} + */ +function isObject(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return (tsType.flags & ts.TypeFlags.Object) !== 0 +} + +/** + * Check if a given type is an array-like type or not. + * @param {Type} tsType The type to check. + */ +function isArrayLikeObject(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return ( + isObject(tsType) && + (tsType.objectFlags & + (ts.ObjectFlags.ArrayLiteral | + ts.ObjectFlags.EvolvingArray | + ts.ObjectFlags.Tuple)) !== + 0 + ) +} +/** + * Check if a given type is an any type or not. + * @param {Type} tsType The type to check. + */ +function isAny(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return (tsType.flags & ts.TypeFlags.Any) !== 0 +} +/** + * Check if a given type is an unknown type or not. + * @param {Type} tsType The type to check. + */ +function isUnknown(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return (tsType.flags & ts.TypeFlags.Unknown) !== 0 +} +/** + * Check if a given type is a never type or not. + * @param {Type} tsType The type to check. + */ +function isNever(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return (tsType.flags & ts.TypeFlags.Never) !== 0 +} +/** + * Check if a given type is an null type or not. + * @param {Type} tsType The type to check. + */ +function isNull(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return (tsType.flags & ts.TypeFlags.Null) !== 0 +} + +/** + * Check if a given type is a string-like type or not. + * @param {Type} tsType The type to check. + * @returns {boolean} `true` if the type is a string-like type. + */ +function isStringLike(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return (tsType.flags & ts.TypeFlags.StringLike) !== 0 +} +/** + * Check if a given type is an number-like type or not. + * @param {Type} tsType The type to check. + * @returns {boolean} `true` if the type is a number-like type. + */ +function isNumberLike(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return (tsType.flags & ts.TypeFlags.NumberLike) !== 0 +} +/** + * Check if a given type is an boolean-like type or not. + * @param {Type} tsType The type to check. + * @returns {boolean} `true` if the type is a boolean-like type. + */ +function isBooleanLike(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return (tsType.flags & ts.TypeFlags.BooleanLike) !== 0 +} +/** + * Check if a given type is an bigint-like type or not. + * @param {Type} tsType The type to check. + * @returns {boolean} `true` if the type is a bigint-like type. + */ +function isBigIntLike(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return (tsType.flags & ts.TypeFlags.BigIntLike) !== 0 +} + +/** + * Check if a given type is a reference type or not. + * @param {Type} tsType The type to check. + * @returns {tsType is TypeReference} `true` if the type is a reference type. + */ +function isReferenceObject(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + return ( + isObject(tsType) && (tsType.objectFlags & ts.ObjectFlags.Reference) !== 0 + ) +} +/** + * Check if a given type is `function` or not. + * @param {Type} tsType The type to check. + */ +function isFunction(tsType) { + const ts = /** @type {TypeScript} */ (getTypeScript()) + if ( + tsType.symbol && + (tsType.symbol.flags & + (ts.SymbolFlags.Function | ts.SymbolFlags.Method)) !== + 0 + ) { + return true + } + + const signatures = tsType.getCallSignatures() + return signatures.length > 0 +} diff --git a/tests/fixtures/typescript/src/test.vue b/tests/fixtures/typescript/src/test.vue new file mode 100644 index 000000000..5bf03f4a7 --- /dev/null +++ b/tests/fixtures/typescript/src/test.vue @@ -0,0 +1 @@ + diff --git a/tests/fixtures/typescript/src/test01.ts b/tests/fixtures/typescript/src/test01.ts new file mode 100644 index 000000000..d14550843 --- /dev/null +++ b/tests/fixtures/typescript/src/test01.ts @@ -0,0 +1,20 @@ +export type Props1 = { + foo: string + bar?: number + baz?: boolean +} +export type Emits1 = { + (e: 'foo' | 'bar', payload: string): void + (e: 'baz', payload: number): void +} +export type Props2 = { + a: string + b?: number + c?: boolean + d?: boolean + e?: number | string + f?: () => number + g?: { foo?: string } + h?: string[] + i?: readonly string[] +} diff --git a/tests/fixtures/typescript/tsconfig.json b/tests/fixtures/typescript/tsconfig.json new file mode 100644 index 000000000..c13ef64e3 --- /dev/null +++ b/tests/fixtures/typescript/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "strict": true, + "skipLibCheck": true + } +} diff --git a/tests/fixtures/utils/ts-utils/src/test.ts b/tests/fixtures/utils/ts-utils/src/test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/utils/ts-utils/src/test.vue b/tests/fixtures/utils/ts-utils/src/test.vue new file mode 100644 index 000000000..5bf03f4a7 --- /dev/null +++ b/tests/fixtures/utils/ts-utils/src/test.vue @@ -0,0 +1 @@ + diff --git a/tests/fixtures/utils/ts-utils/tsconfig.json b/tests/fixtures/utils/ts-utils/tsconfig.json new file mode 100644 index 000000000..c13ef64e3 --- /dev/null +++ b/tests/fixtures/utils/ts-utils/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "strict": true, + "skipLibCheck": true + } +} diff --git a/tests/lib/rules/no-restricted-props.js b/tests/lib/rules/no-restricted-props.js index a72d5b246..0f1947ed1 100644 --- a/tests/lib/rules/no-restricted-props.js +++ b/tests/lib/rules/no-restricted-props.js @@ -6,6 +6,9 @@ const semver = require('semver') const RuleTester = require('eslint').RuleTester const rule = require('../../../lib/rules/no-restricted-props') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), @@ -577,6 +580,23 @@ tester.run('no-restricted-props', rule, { ] } ] + }, + { + code: ` + + `, + ...getTypeScriptFixtureTestOptions(), + options: [{ name: 'foo', suggest: 'Foo' }], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 4, + suggestions: null + } + ] } ]) ] diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js index 8d4b2b215..2db132c48 100644 --- a/tests/lib/rules/no-unused-properties.js +++ b/tests/lib/rules/no-unused-properties.js @@ -7,6 +7,9 @@ const { RuleTester, Linter } = require('eslint') const assert = require('assert') const rule = require('../../../lib/rules/no-unused-properties') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), @@ -2961,6 +2964,24 @@ tester.run('no-unused-properties', rule, { line: 8 } ] + }, + // script setup with typescript + { + code: ` + + `, + ...getTypeScriptFixtureTestOptions(), + errors: [ + { + message: "'baz' of property found, but never used.", + line: 4 + } + ] } ] }) diff --git a/tests/lib/rules/padding-lines-in-component-definition.js b/tests/lib/rules/padding-lines-in-component-definition.js index a4ed4d3c3..5c611fd94 100644 --- a/tests/lib/rules/padding-lines-in-component-definition.js +++ b/tests/lib/rules/padding-lines-in-component-definition.js @@ -6,6 +6,9 @@ const RuleTester = require('eslint').RuleTester const rule = require('../../../lib/rules/padding-lines-in-component-definition') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), @@ -385,6 +388,14 @@ tester.run('padding-lines-in-component-definition', rule, { `, options: ['always'] + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions() } ], invalid: [ diff --git a/tests/lib/rules/prefer-prop-type-boolean-first.js b/tests/lib/rules/prefer-prop-type-boolean-first.js index 9f67e336b..1c66f8417 100644 --- a/tests/lib/rules/prefer-prop-type-boolean-first.js +++ b/tests/lib/rules/prefer-prop-type-boolean-first.js @@ -6,6 +6,9 @@ const RuleTester = require('eslint').RuleTester const rule = require('../../../lib/rules/prefer-prop-type-boolean-first') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), @@ -62,6 +65,14 @@ tester.run('prefer-prop-type-boolean-first', rule, { }) ` + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions() } ], invalid: [ diff --git a/tests/lib/rules/require-emit-validator.js b/tests/lib/rules/require-emit-validator.js index 751f775e7..2b60e8910 100644 --- a/tests/lib/rules/require-emit-validator.js +++ b/tests/lib/rules/require-emit-validator.js @@ -5,6 +5,9 @@ 'use strict' const rule = require('../../../lib/rules/require-emit-validator') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const RuleTester = require('eslint').RuleTester @@ -169,6 +172,14 @@ ruleTester.run('require-emit-validator', rule, { sourceType: 'module', parser: require.resolve('@typescript-eslint/parser') } + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions() } ], diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js index a5bc5114c..43a3c2599 100644 --- a/tests/lib/rules/require-explicit-emits.js +++ b/tests/lib/rules/require-explicit-emits.js @@ -6,6 +6,9 @@ const RuleTester = require('eslint').RuleTester const rule = require('../../../lib/rules/require-explicit-emits') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), @@ -618,6 +621,17 @@ tester.run('require-explicit-emits', rule, { `, parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions() } ], invalid: [ @@ -1950,6 +1964,25 @@ emits: {'foo': null} line: 6 } ] + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions(), + errors: [ + { + message: + 'The "qux" event has been triggered but not declared on `defineEmits`.', + line: 8 + } + ] } ] }) diff --git a/tests/lib/rules/require-prop-comment.js b/tests/lib/rules/require-prop-comment.js index f72d6905b..900f25ec3 100644 --- a/tests/lib/rules/require-prop-comment.js +++ b/tests/lib/rules/require-prop-comment.js @@ -6,6 +6,9 @@ const RuleTester = require('eslint').RuleTester const rule = require('../../../lib/rules/require-prop-comment') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), @@ -57,10 +60,10 @@ tester.run('require-prop-comment', rule, { const goodProps = defineProps({ /** JSDoc comment */ a: Number, - + /* block comment */ b: Number, - + // line comment c: Number, }) @@ -93,6 +96,14 @@ tester.run('require-prop-comment', rule, { parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions() } ], invalid: [ @@ -102,10 +113,10 @@ tester.run('require-prop-comment', rule, { props: { // line comment b: Number, - + /* block comment */ c: Number, - + d: Number, } }) @@ -134,10 +145,10 @@ tester.run('require-prop-comment', rule, { const badProps = defineProps({ /** JSDoc comment */ b: Number, - + // line comment c: Number, - + d: Number, }) @@ -167,10 +178,10 @@ tester.run('require-prop-comment', rule, { const badProps = defineProps({ /** JSDoc comment */ b: Number, - + /* block comment */ c: Number, - + d: Number, }) diff --git a/tests/lib/rules/require-prop-type-constructor.js b/tests/lib/rules/require-prop-type-constructor.js index a662e34d2..9cd4eae6e 100644 --- a/tests/lib/rules/require-prop-type-constructor.js +++ b/tests/lib/rules/require-prop-type-constructor.js @@ -5,6 +5,9 @@ 'use strict' const rule = require('../../../lib/rules/require-prop-type-constructor') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const RuleTester = require('eslint').RuleTester const ruleTester = new RuleTester({ @@ -85,6 +88,14 @@ ruleTester.run('require-prop-type-constructor', rule, { props: ['name',,,] } ` + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions() } ], diff --git a/tests/lib/rules/require-prop-types.js b/tests/lib/rules/require-prop-types.js index bcbff9013..9b7077fc1 100644 --- a/tests/lib/rules/require-prop-types.js +++ b/tests/lib/rules/require-prop-types.js @@ -5,6 +5,9 @@ 'use strict' const rule = require('../../../lib/rules/require-prop-types') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const RuleTester = require('eslint').RuleTester @@ -176,6 +179,14 @@ ruleTester.run('require-prop-types', rule, { parser: require.resolve('@typescript-eslint/parser') }, parser: require.resolve('vue-eslint-parser') + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions() } ], diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js index 2f776ea1d..cec05ba05 100644 --- a/tests/lib/rules/require-valid-default-prop.js +++ b/tests/lib/rules/require-valid-default-prop.js @@ -5,6 +5,9 @@ 'use strict' const rule = require('../../../lib/rules/require-valid-default-prop') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const RuleTester = require('eslint').RuleTester const parserOptions = { @@ -267,6 +270,24 @@ ruleTester.run('require-valid-default-prop', rule, { parser: require.resolve('@typescript-eslint/parser') }, parser: require.resolve('vue-eslint-parser') + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions() } ], @@ -929,6 +950,63 @@ ruleTester.run('require-valid-default-prop', rule, { line: 4 } ] + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions(), + errors: [ + { + message: "Type of the default value for 'a' prop must be a string.", + line: 5 + }, + { + message: "Type of the default value for 'b' prop must be a number.", + line: 6 + }, + { + message: "Type of the default value for 'c' prop must be a boolean.", + line: 7 + }, + { + message: "Type of the default value for 'd' prop must be a boolean.", + line: 8 + }, + { + message: + "Type of the default value for 'e' prop must be a string or number.", + line: 9 + }, + { + message: "Type of the default value for 'f' prop must be a function.", + line: 10 + }, + { + message: "Type of the default value for 'g' prop must be a function.", + line: 11 + }, + { + message: "Type of the default value for 'h' prop must be a function.", + line: 12 + }, + { + message: "Type of the default value for 'i' prop must be a function.", + line: 13 + } + ] } ] }) diff --git a/tests/lib/rules/return-in-emits-validator.js b/tests/lib/rules/return-in-emits-validator.js index d4b5272ea..c72392bda 100644 --- a/tests/lib/rules/return-in-emits-validator.js +++ b/tests/lib/rules/return-in-emits-validator.js @@ -5,6 +5,9 @@ 'use strict' const rule = require('../../../lib/rules/return-in-emits-validator') +const { + getTypeScriptFixtureTestOptions +} = require('../../test-utils/typescript') const RuleTester = require('eslint').RuleTester @@ -123,6 +126,14 @@ ruleTester.run('return-in-emits-validator', rule, { }) ` + }, + { + code: ` + `, + ...getTypeScriptFixtureTestOptions() } ], diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js index 9b02ba46b..6afecaeaa 100644 --- a/tests/lib/utils/index.js +++ b/tests/lib/utils/index.js @@ -367,9 +367,9 @@ describe('getComponentProps', () => { assert.equal(props.length, 5, 'it detects all props') - assert.strictEqual(props[0].key, null) + assert.strictEqual(props[0].key, undefined) assert.strictEqual(props[0].node.type, 'SpreadElement') - assert.strictEqual(props[0].value, null) + assert.strictEqual(props[0].value, undefined) assert.strictEqual(props[1].key.type, 'Identifier') assert.strictEqual(props[1].node.type, 'Property') diff --git a/tests/lib/utils/ts-utils/ts-types/get-component-emits.js b/tests/lib/utils/ts-utils/ts-types/get-component-emits.js new file mode 100644 index 000000000..8bba2be7c --- /dev/null +++ b/tests/lib/utils/ts-utils/ts-types/get-component-emits.js @@ -0,0 +1,122 @@ +/** + * Test for getComponentEmitsFromTypeDefineTypes + */ +'use strict' + +const path = require('path') +const fs = require('fs') +const Linter = require('eslint').Linter +const parser = require('vue-eslint-parser') +const tsParser = require('@typescript-eslint/parser') +const utils = require('../../../../../lib/utils/index') +const assert = require('assert') + +const FIXTURES_ROOT = path.resolve( + __dirname, + '../../../../fixtures/utils/ts-utils' +) +const TSCONFIG_PATH = path.resolve(FIXTURES_ROOT, './tsconfig.json') +const SRC_TS_TEST_PATH = path.join(FIXTURES_ROOT, './src/test.ts') + +function extractComponentProps(code, tsFileCode) { + const linter = new Linter() + const config = { + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 2020, + parser: tsParser, + project: [TSCONFIG_PATH], + extraFileExtensions: ['.vue'] + }, + rules: { + test: 'error' + } + } + linter.defineParser('vue-eslint-parser', parser) + const result = [] + linter.defineRule('test', { + create(context) { + return utils.defineScriptSetupVisitor(context, { + onDefineEmitsEnter(_node, emits) { + result.push( + ...emits.map((emit) => ({ + type: emit.type, + name: emit.emitName + })) + ) + } + }) + } + }) + fs.writeFileSync(SRC_TS_TEST_PATH, tsFileCode || '', 'utf8') + // clean './src/test.ts' cache + tsParser.parseForESLint(tsFileCode || '', { + ...config.parserOptions, + filePath: SRC_TS_TEST_PATH + }) + assert.deepStrictEqual( + linter.verify(code, config, path.join(FIXTURES_ROOT, './src/test.vue')), + [] + ) + // reset + fs.writeFileSync(SRC_TS_TEST_PATH, '', 'utf8') + return result +} + +describe('getComponentEmitsFromTypeDefineTypes', () => { + for (const { scriptCode, tsFileCode, props: expected } of [ + { + scriptCode: `defineEmits<{(e:'foo'):void,(e:'bar'):void}>()`, + props: [ + { type: 'type', name: 'foo' }, + { type: 'type', name: 'bar' } + ] + }, + { + tsFileCode: `export type Emits = {(e:'foo'):void,(e:'bar'):void}`, + scriptCode: `import { Emits } from './test' + defineEmits()`, + props: [ + { type: 'infer-type', name: 'foo' }, + { type: 'infer-type', name: 'bar' } + ] + }, + { + tsFileCode: `export type Emits = any`, + scriptCode: `import { Emits } from './test' + defineEmits()`, + props: [{ type: 'unknown', name: null }] + }, + { + tsFileCode: `export type Emits = {(e:'foo' | 'bar'): void, (e:'baz',payload:number): void}`, + scriptCode: `import { Emits } from './test' + defineEmits()`, + props: [ + { type: 'infer-type', name: 'foo' }, + { type: 'infer-type', name: 'bar' }, + { type: 'infer-type', name: 'baz' } + ] + }, + { + tsFileCode: `export type Emits = { a: [], b: [number], c: [string]}`, + scriptCode: `import { Emits } from './test' + defineEmits()`, + props: [ + { type: 'infer-type', name: 'a' }, + { type: 'infer-type', name: 'b' }, + { type: 'infer-type', name: 'c' } + ] + } + ]) { + const code = `` + it(`should return expected props with :${code}`, () => { + const props = extractComponentProps(code, tsFileCode) + + assert.deepStrictEqual( + props, + expected, + `\n${JSON.stringify(props)}\n === \n${JSON.stringify(expected)}` + ) + }) + } +}) diff --git a/tests/lib/utils/ts-utils/ts-types/get-component-props.js b/tests/lib/utils/ts-utils/ts-types/get-component-props.js new file mode 100644 index 000000000..5ec5d78c8 --- /dev/null +++ b/tests/lib/utils/ts-utils/ts-types/get-component-props.js @@ -0,0 +1,153 @@ +/** + * Test for getComponentPropsFromTypeDefineTypes + */ +'use strict' + +const path = require('path') +const fs = require('fs') +const Linter = require('eslint').Linter +const parser = require('vue-eslint-parser') +const tsParser = require('@typescript-eslint/parser') +const utils = require('../../../../../lib/utils/index') +const assert = require('assert') + +const FIXTURES_ROOT = path.resolve( + __dirname, + '../../../../fixtures/utils/ts-utils' +) +const TSCONFIG_PATH = path.resolve(FIXTURES_ROOT, './tsconfig.json') +const SRC_TS_TEST_PATH = path.join(FIXTURES_ROOT, './src/test.ts') + +function extractComponentProps(code, tsFileCode) { + const linter = new Linter() + const config = { + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 2020, + parser: tsParser, + project: [TSCONFIG_PATH], + extraFileExtensions: ['.vue'] + }, + rules: { + test: 'error' + } + } + linter.defineParser('vue-eslint-parser', parser) + const result = [] + linter.defineRule('test', { + create(context) { + return utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(_node, props) { + result.push( + ...props.map((prop) => ({ + type: prop.type, + name: prop.propName, + required: prop.required ?? null, + types: prop.types ?? null + })) + ) + } + }) + } + }) + fs.writeFileSync(SRC_TS_TEST_PATH, tsFileCode || '', 'utf8') + // clean './src/test.ts' cache + tsParser.parseForESLint(tsFileCode || '', { + ...config.parserOptions, + filePath: SRC_TS_TEST_PATH + }) + assert.deepStrictEqual( + linter.verify(code, config, path.join(FIXTURES_ROOT, './src/test.vue')), + [] + ) + // reset + fs.writeFileSync(SRC_TS_TEST_PATH, '', 'utf8') + return result +} + +describe('getComponentPropsFromTypeDefineTypes', () => { + for (const { scriptCode, tsFileCode, props: expected } of [ + { + scriptCode: `defineProps<{foo:string,bar?:number}>()`, + props: [ + { type: 'type', name: 'foo', required: true, types: ['String'] }, + { type: 'type', name: 'bar', required: false, types: ['Number'] } + ] + }, + { + tsFileCode: `export type Props = {foo:string,bar?:number}`, + scriptCode: `import { Props } from './test' + defineProps()`, + props: [ + { type: 'infer-type', name: 'foo', required: true, types: ['String'] }, + { type: 'infer-type', name: 'bar', required: false, types: ['Number'] } + ] + }, + { + tsFileCode: `export type Props = any`, + scriptCode: `import { Props } from './test' + defineProps()`, + props: [{ type: 'unknown', name: null, required: null, types: null }] + }, + { + tsFileCode: ` + interface Props { + a?: number; + b?: string; + } + export interface Props2 extends Required { + c?: boolean; + }`, + scriptCode: `import { Props2 } from './test' + defineProps()`, + props: [ + { type: 'infer-type', name: 'c', required: false, types: ['Boolean'] }, + { type: 'infer-type', name: 'a', required: true, types: ['Number'] }, + { type: 'infer-type', name: 'b', required: true, types: ['String'] } + ] + }, + { + tsFileCode: ` + export type Props = { + a: string + b?: number + c?: boolean + d?: boolean + e?: number | string + f?: () => number + g?: { foo?: string } + h?: string[] + i?: readonly string[] + }`, + scriptCode: `import { Props } from './test' + defineProps()`, + props: [ + { type: 'infer-type', name: 'a', required: true, types: ['String'] }, + { type: 'infer-type', name: 'b', required: false, types: ['Number'] }, + { type: 'infer-type', name: 'c', required: false, types: ['Boolean'] }, + { type: 'infer-type', name: 'd', required: false, types: ['Boolean'] }, + { + type: 'infer-type', + name: 'e', + required: false, + types: ['String', 'Number'] + }, + { type: 'infer-type', name: 'f', required: false, types: ['Function'] }, + { type: 'infer-type', name: 'g', required: false, types: ['Object'] }, + { type: 'infer-type', name: 'h', required: false, types: ['Array'] }, + { type: 'infer-type', name: 'i', required: false, types: ['Array'] } + ] + } + ]) { + const code = `` + it(`should return expected props with :${code}`, () => { + const props = extractComponentProps(code, tsFileCode) + + assert.deepStrictEqual( + props, + expected, + `\n${JSON.stringify(props)}\n === \n${JSON.stringify(expected)}` + ) + }) + } +}) diff --git a/tests/test-utils/typescript.js b/tests/test-utils/typescript.js new file mode 100644 index 000000000..e06008a8f --- /dev/null +++ b/tests/test-utils/typescript.js @@ -0,0 +1,26 @@ +const path = require('path') +const tsParser = require('@typescript-eslint/parser') + +const FIXTURES_ROOT = path.resolve(__dirname, '../fixtures/typescript') +const TSCONFIG_PATH = path.resolve(FIXTURES_ROOT, './tsconfig.json') +const SRC_VUE_TEST_PATH = path.join(FIXTURES_ROOT, './src/test.vue') + +module.exports = { + getTypeScriptFixtureTestOptions +} + +function getTypeScriptFixtureTestOptions() { + const parser = require.resolve('vue-eslint-parser') + const parserOptions = { + parser: { ts: tsParser }, + ecmaVersion: 2020, + sourceType: 'module', + project: [TSCONFIG_PATH], + extraFileExtensions: ['.vue'] + } + return { + parser, + parserOptions, + filename: SRC_VUE_TEST_PATH + } +} diff --git a/typings/eslint-plugin-vue/global.d.ts b/typings/eslint-plugin-vue/global.d.ts index c2ae50632..a581b3f62 100644 --- a/typings/eslint-plugin-vue/global.d.ts +++ b/typings/eslint-plugin-vue/global.d.ts @@ -159,6 +159,7 @@ declare global { type TSLiteralType = VAST.TSLiteralType type TSCallSignatureDeclaration = VAST.TSCallSignatureDeclaration type TSFunctionType = VAST.TSFunctionType + type TypeNode = VAST.TypeNode // ---- 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 a79d457f4..0fa6d68b0 100644 --- a/typings/eslint-plugin-vue/util-types/ast/ts-ast.ts +++ b/typings/eslint-plugin-vue/util-types/ast/ts-ast.ts @@ -12,6 +12,7 @@ export type TSNode = | TSLiteralType | TSCallSignatureDeclaration | TSFunctionType + | TypeNode export interface TSAsExpression extends HasParentNode { type: 'TSAsExpression' @@ -21,7 +22,7 @@ export interface TSAsExpression extends HasParentNode { export interface TSTypeParameterInstantiation extends HasParentNode { type: 'TSTypeParameterInstantiation' - params: TSESTree.TypeNode[] + params: TypeNode[] } export type TSPropertySignature = @@ -90,3 +91,13 @@ export interface TSCallSignatureDeclaration extends TSFunctionSignatureBase { export interface TSFunctionType extends TSFunctionSignatureBase { type: 'TSFunctionType' } + +export type TypeNode = + | (HasParentNode & { + type: Exclude< + TSESTree.TypeNode['type'], + 'TSFunctionType' | 'TSLiteralType' + > + }) + | TSFunctionType + | TSLiteralType diff --git a/typings/eslint-plugin-vue/util-types/parser-services.ts b/typings/eslint-plugin-vue/util-types/parser-services.ts index d59d76d3e..6a9ccc776 100644 --- a/typings/eslint-plugin-vue/util-types/parser-services.ts +++ b/typings/eslint-plugin-vue/util-types/parser-services.ts @@ -1,6 +1,7 @@ import * as VNODE from './node' import * as VAST from './ast' import * as eslint from 'eslint' +import { ESNode, TSNode } from './ast' type TemplateListenerBase = { [T in keyof VAST.VNodeListenerMap]?: (node: VAST.VNodeListenerMap[T]) => void @@ -27,6 +28,13 @@ export interface ParserServices { } ) => eslint.Rule.RuleListener getDocumentFragment?: () => VAST.VDocumentFragment | null + // for typescript-eslint/parser + esTreeNodeToTSNodeMap?: Map< + ESNode | TSNode | import('@typescript-eslint/types').TSESTree.Node, + import('typescript').Node + > + program?: import('typescript').Program + hasFullTypeInformation?: boolean } export namespace ParserServices { export interface TokenStore { diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts index e16d86033..2039d11c6 100644 --- a/typings/eslint-plugin-vue/util-types/utils.ts +++ b/typings/eslint-plugin-vue/util-types/utils.ts @@ -55,14 +55,12 @@ type ComponentArrayPropDetectName = { type: 'array' key: Literal | TemplateLiteral propName: string - value: null node: Expression | SpreadElement } type ComponentArrayPropUnknownName = { type: 'array' key: null propName: null - value: null node: Expression | SpreadElement } export type ComponentArrayProp = @@ -89,41 +87,46 @@ export type ComponentObjectProp = export type ComponentUnknownProp = { type: 'unknown' - key: null propName: null - value: null - node: Expression | SpreadElement | null + node: Expression | SpreadElement | TypeNode | null } export type ComponentTypeProp = { type: 'type' key: Identifier | Literal propName: string - value: null node: TSPropertySignature | TSMethodSignature required: boolean types: string[] } +export type ComponentInferTypeProp = { + type: 'infer-type' + propName: string + node: TypeNode + + required: boolean + types: string[] +} + export type ComponentProp = | ComponentArrayProp | ComponentObjectProp | ComponentTypeProp + | ComponentInferTypeProp | ComponentUnknownProp 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 = @@ -150,17 +153,14 @@ export type ComponentObjectEmit = export type ComponentUnknownEmit = { type: 'unknown' - key: null emitName: null - value: null - node: Expression | SpreadElement | null + node: Expression | SpreadElement | TypeNode | null } export type ComponentTypeEmitCallSignature = { type: 'type' key: TSLiteralType emitName: string - value: null node: TSCallSignatureDeclaration | TSFunctionType } export type ComponentTypeEmitPropertySignature = { @@ -174,8 +174,15 @@ export type ComponentTypeEmit = | ComponentTypeEmitCallSignature | ComponentTypeEmitPropertySignature +export type ComponentInferTypeEmit = { + type: 'infer-type' + emitName: string + node: TypeNode +} + export type ComponentEmit = | ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit + | ComponentInferTypeEmit | ComponentUnknownEmit From 298e60cdadfe10b8919f4f0ce35f428beff6da2e Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Tue, 18 Apr 2023 22:20:54 +0900 Subject: [PATCH 2/9] fix --- lib/utils/ts-utils/typescript.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/utils/ts-utils/typescript.js b/lib/utils/ts-utils/typescript.js index 0bc536f56..8d656401e 100644 --- a/lib/utils/ts-utils/typescript.js +++ b/lib/utils/ts-utils/typescript.js @@ -33,8 +33,11 @@ module.exports = { * Get TypeScript instance */ function getTypeScript() { + if (cacheTypeScript) { + return cacheTypeScript + } try { - return (cacheTypeScript ??= require('typescript')) + return (cacheTypeScript = require('typescript')) } catch (error) { if (/** @type {any} */ (error).code === 'MODULE_NOT_FOUND') { return undefined From 61d11d6d69d0634784b8fefa452109c577616942 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Tue, 18 Apr 2023 22:30:43 +0900 Subject: [PATCH 3/9] fix --- lib/utils/ts-utils/ts-types.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/utils/ts-utils/ts-types.js b/lib/utils/ts-utils/ts-types.js index f9be9a77f..f80b0ea08 100644 --- a/lib/utils/ts-utils/ts-types.js +++ b/lib/utils/ts-utils/ts-types.js @@ -27,7 +27,6 @@ const { */ module.exports = { - isAvailableTypeInformation, getComponentPropsFromTypeDefineTypes, getComponentEmitsFromTypeDefineTypes } @@ -64,13 +63,6 @@ function getTSParserServices(context) { checker } } -/** - * Checks whether type information is available or not. - * @param {RuleContext} context The ESLint rule context object. - */ -function isAvailableTypeInformation(context) { - return Boolean(getTSParserServices(context)) -} /** * Get all props by looking at all component's properties From 5d537247599ca2c0d515e9761688060d49af599c Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Tue, 18 Apr 2023 22:46:13 +0900 Subject: [PATCH 4/9] fix --- lib/utils/ts-utils/index.js | 12 ++++++------ lib/utils/ts-utils/ts-ast.js | 4 ++-- lib/utils/ts-utils/ts-types.js | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/utils/ts-utils/index.js b/lib/utils/ts-utils/index.js index 13ac52f84..5f75e58a6 100644 --- a/lib/utils/ts-utils/index.js +++ b/lib/utils/ts-utils/index.js @@ -15,12 +15,12 @@ const { * @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TSESTreeTypeNode */ /** - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeProp} ComponentInferTypeProp - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownProp} ComponentUnknownProp - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeEmit} ComponentTypeEmit - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeEmit} ComponentInferTypeEmit - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownEmit} ComponentUnknownEmit + * @typedef {import('../index').ComponentTypeProp} ComponentTypeProp + * @typedef {import('../index').ComponentInferTypeProp} ComponentInferTypeProp + * @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp + * @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit + * @typedef {import('../index').ComponentInferTypeEmit} ComponentInferTypeEmit + * @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit */ module.exports = { diff --git a/lib/utils/ts-utils/ts-ast.js b/lib/utils/ts-utils/ts-ast.js index 5f094a1a3..4ff98e40f 100644 --- a/lib/utils/ts-utils/ts-ast.js +++ b/lib/utils/ts-utils/ts-ast.js @@ -9,8 +9,8 @@ const { findVariable } = require('@eslint-community/eslint-utils') * */ /** - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeProp} ComponentTypeProp - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentTypeEmit} ComponentTypeEmit + * @typedef {import('../index').ComponentTypeProp} ComponentTypeProp + * @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit */ module.exports = { diff --git a/lib/utils/ts-utils/ts-types.js b/lib/utils/ts-utils/ts-types.js index f80b0ea08..83c1519c2 100644 --- a/lib/utils/ts-utils/ts-types.js +++ b/lib/utils/ts-utils/ts-types.js @@ -20,10 +20,10 @@ const { * @typedef {import('typescript').Node} TypeScriptNode */ /** - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeProp} ComponentInferTypeProp - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownProp} ComponentUnknownProp - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentInferTypeEmit} ComponentInferTypeEmit - * @typedef {import('../../../typings/eslint-plugin-vue/util-types/utils').ComponentUnknownEmit} ComponentUnknownEmit + * @typedef {import('../index').ComponentInferTypeProp} ComponentInferTypeProp + * @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp + * @typedef {import('../index').ComponentInferTypeEmit} ComponentInferTypeEmit + * @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit */ module.exports = { From e33ffb52c931880ae89f11415b94745857eb2f50 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Wed, 19 Apr 2023 16:12:10 +0900 Subject: [PATCH 5/9] improve --- lib/utils/ts-utils/index.js | 54 ++-- lib/utils/ts-utils/ts-ast.js | 268 ++++++++++++++---- lib/utils/ts-utils/ts-types.js | 96 ++++--- .../get-component-emits.js | 0 .../get-component-props.js | 60 ++++ 5 files changed, 375 insertions(+), 103 deletions(-) rename tests/lib/utils/ts-utils/{ts-types => index}/get-component-emits.js (100%) rename tests/lib/utils/ts-utils/{ts-types => index}/get-component-props.js (75%) diff --git a/lib/utils/ts-utils/index.js b/lib/utils/ts-utils/index.js index 5f75e58a6..8b6c53b26 100644 --- a/lib/utils/ts-utils/index.js +++ b/lib/utils/ts-utils/index.js @@ -1,10 +1,11 @@ const { isTypeNode, - resolveQualifiedType, extractRuntimeProps, isTSTypeLiteral, isTSTypeLiteralOrTSFunctionType, - extractRuntimeEmits + extractRuntimeEmits, + flattenTypeNodes, + isTSInterfaceBody } = require('./ts-ast') const { getComponentPropsFromTypeDefineTypes, @@ -36,15 +37,24 @@ module.exports = { * @return {(ComponentTypeProp|ComponentInferTypeProp|ComponentUnknownProp)[]} Array of component props */ function getComponentPropsFromTypeDefine(context, propsNode) { - const defNode = resolveQualifiedType( + /** @type {(ComponentTypeProp|ComponentInferTypeProp|ComponentUnknownProp)[]} */ + const result = [] + for (const defNode of flattenTypeNodes( context, - /** @type {TSESTreeTypeNode} */ (propsNode), - isTSTypeLiteral - ) - if (!defNode) { - return getComponentPropsFromTypeDefineTypes(context, propsNode) + /** @type {TSESTreeTypeNode} */ (propsNode) + )) { + if (isTSInterfaceBody(defNode) || isTSTypeLiteral(defNode)) { + result.push(...extractRuntimeProps(context, defNode)) + } else { + result.push( + ...getComponentPropsFromTypeDefineTypes( + context, + /** @type {TypeNode} */ (defNode) + ) + ) + } } - return [...extractRuntimeProps(context, defNode)] + return result } /** @@ -54,13 +64,25 @@ function getComponentPropsFromTypeDefine(context, propsNode) { * @return {(ComponentTypeEmit|ComponentInferTypeEmit|ComponentUnknownEmit)[]} Array of component emits */ function getComponentEmitsFromTypeDefine(context, emitsNode) { - const defNode = resolveQualifiedType( + /** @type {(ComponentTypeEmit|ComponentInferTypeEmit|ComponentUnknownEmit)[]} */ + const result = [] + for (const defNode of flattenTypeNodes( context, - /** @type {TSESTreeTypeNode} */ (emitsNode), - isTSTypeLiteralOrTSFunctionType - ) - if (!defNode) { - return getComponentEmitsFromTypeDefineTypes(context, emitsNode) + /** @type {TSESTreeTypeNode} */ (emitsNode) + )) { + if ( + isTSInterfaceBody(defNode) || + isTSTypeLiteralOrTSFunctionType(defNode) + ) { + result.push(...extractRuntimeEmits(defNode)) + } else { + result.push( + ...getComponentEmitsFromTypeDefineTypes( + context, + /** @type {TypeNode} */ (defNode) + ) + ) + } } - return [...extractRuntimeEmits(defNode)] + return result } diff --git a/lib/utils/ts-utils/ts-ast.js b/lib/utils/ts-utils/ts-ast.js index 4ff98e40f..e9977e3bd 100644 --- a/lib/utils/ts-utils/ts-ast.js +++ b/lib/utils/ts-utils/ts-ast.js @@ -1,4 +1,5 @@ const { findVariable } = require('@eslint-community/eslint-utils') +const { inferRuntimeTypeFromTypeNode } = require('./ts-types') /** * @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TSESTreeTypeNode * @typedef {import('@typescript-eslint/types').TSESTree.TSInterfaceBody} TSESTreeTSInterfaceBody @@ -15,7 +16,8 @@ const { findVariable } = require('@eslint-community/eslint-utils') module.exports = { isTypeNode, - resolveQualifiedType, + flattenTypeNodes, + isTSInterfaceBody, isTSTypeLiteral, isTSTypeLiteralOrTSFunctionType, extractRuntimeProps, @@ -66,6 +68,13 @@ function isTypeNode(node) { ) } +/** + * @param {TSESTreeTypeNode|TSESTreeTSInterfaceBody} node + * @returns {node is TSESTreeTSInterfaceBody} + */ +function isTSInterfaceBody(node) { + return node.type === 'TSInterfaceBody' +} /** * @param {TSESTreeTypeNode} node * @returns {node is TSESTreeTSTypeLiteral} @@ -101,11 +110,12 @@ function* extractRuntimeProps(context, node) { (m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature') && (m.key.type === 'Identifier' || m.key.type === 'Literal') ) { - let type + /** @type {string[]|undefined} */ + let types if (m.type === 'TSMethodSignature') { - type = ['Function'] + types = ['Function'] } else if (m.typeAnnotation) { - type = inferRuntimeType(context, m.typeAnnotation.typeAnnotation) + types = inferRuntimeType(context, m.typeAnnotation.typeAnnotation) } yield { type: 'type', @@ -114,7 +124,7 @@ function* extractRuntimeProps(context, node) { node: /** @type {TSPropertySignature | TSMethodSignature} */ (m), required: !m.optional, - types: type || [`null`] + types: types || [`null`] } } } @@ -186,33 +196,67 @@ function* extractEventNames(eventName, member) { } /** - * @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L425 - * - * @template {TSESTreeTypeNode} R * @param {RuleContext} context The ESLint rule context object. * @param {TSESTreeTypeNode} node - * @param {(n: TSESTreeTypeNode)=> n is R } qualifier - * @returns {R | TSESTreeTSInterfaceBody | null} + * @returns {(TSESTreeTypeNode|TSESTreeTSInterfaceBody)[]} */ -function resolveQualifiedType(context, node, qualifier) { - if (qualifier(node)) { - return node - } - if (node.type === 'TSTypeReference' && node.typeName.type === 'Identifier') { - const refName = node.typeName.name - const variable = findVariable(context.getScope(), refName) - if (variable && variable.defs.length === 1) { - const defNode = /** @type {TSESTreeNode} */ (variable.defs[0].node) - if (defNode.type === 'TSInterfaceDeclaration') { - return defNode.body +function flattenTypeNodes(context, node) { + /** + * @typedef {object} TraversedData + * @property {Set} nodes + * @property {boolean} finished + */ + /** @type {Map} */ + const traversed = new Map() + + return [...flattenImpl(node)] + /** + * @param {TSESTreeTypeNode} node + * @returns {Iterable} + */ + function* flattenImpl(node) { + if (node.type === 'TSUnionType' || node.type === 'TSIntersectionType') { + for (const typeNode of node.types) { + yield* flattenImpl(typeNode) } - if (defNode.type === 'TSTypeAliasDeclaration') { - const typeAnnotation = defNode.typeAnnotation - return qualifier(typeAnnotation) ? typeAnnotation : null + return + } + if ( + node.type === 'TSTypeReference' && + node.typeName.type === 'Identifier' + ) { + const refName = node.typeName.name + const variable = findVariable(context.getScope(), refName) + if (variable && variable.defs.length === 1) { + const defNode = /** @type {TSESTreeNode} */ (variable.defs[0].node) + if (defNode.type === 'TSInterfaceDeclaration') { + yield defNode.body + return + } else if (defNode.type === 'TSTypeAliasDeclaration') { + const typeAnnotation = defNode.typeAnnotation + let traversedData = traversed.get(typeAnnotation) + if (traversedData) { + const copy = [...traversedData.nodes] + yield* copy + if (!traversedData.finished) { + // Include the node because it will probably be referenced recursively. + yield typeAnnotation + } + return + } + traversedData = { nodes: new Set(), finished: false } + traversed.set(typeAnnotation, traversedData) + for (const e of flattenImpl(typeAnnotation)) { + traversedData.nodes.add(e) + } + traversedData.finished = true + yield* traversedData.nodes + return + } } } + yield node } - return null } /** @@ -224,6 +268,7 @@ function resolveQualifiedType(context, node, qualifier) { function inferRuntimeType(context, node, checked = new Set()) { switch (node.type) { case 'TSStringKeyword': + case 'TSTemplateLiteralType': return ['String'] case 'TSNumberKeyword': return ['Number'] @@ -232,12 +277,14 @@ function inferRuntimeType(context, node, checked = new Set()) { case 'TSObjectKeyword': return ['Object'] case 'TSTypeLiteral': - return ['Object'] + return inferTypeLiteralType(node) case 'TSFunctionType': return ['Function'] case 'TSArrayType': case 'TSTupleType': return ['Array'] + case 'TSSymbolKeyword': + return ['Symbol'] case 'TSLiteralType': if (node.literal.type === 'Literal') { @@ -254,60 +301,175 @@ function inferRuntimeType(context, node, checked = new Set()) { return ['RegExp'] } } - return [`null`] + return inferRuntimeTypeFromTypeNode( + context, + /** @type {TypeNode} */ (node) + ) case 'TSTypeReference': if (node.typeName.type === 'Identifier') { const variable = findVariable(context.getScope(), node.typeName.name) if (variable && variable.defs.length === 1) { - const def = variable.defs[0] - if (def.node.type === 'TSInterfaceDeclaration') { + const defNode = /** @type {TSESTreeNode} */ (variable.defs[0].node) + if (defNode.type === 'TSInterfaceDeclaration') { return [`Object`] } - if (def.node.type === 'TSTypeAliasDeclaration') { - const typeAnnotation = /** @type {any} */ (def.node).typeAnnotation + if (defNode.type === 'TSTypeAliasDeclaration') { + const typeAnnotation = defNode.typeAnnotation if (!checked.has(typeAnnotation)) { checked.add(typeAnnotation) return inferRuntimeType(context, typeAnnotation, checked) } } + if (defNode.type === 'TSEnumDeclaration') { + return inferEnumType(context, defNode) + } + } + for (const name of [ + node.typeName.name, + ...(node.typeName.name.startsWith('Readonly') + ? [node.typeName.name.slice(8)] + : []) + ]) { + switch (name) { + case 'Array': + case 'Function': + case 'Object': + case 'Set': + case 'Map': + case 'WeakSet': + case 'WeakMap': + case 'Date': + return [name] + } } + switch (node.typeName.name) { - case 'Array': - case 'Function': - case 'Object': - case 'Set': - case 'Map': - case 'WeakSet': - case 'WeakMap': - case 'Date': - return [node.typeName.name] case 'Record': case 'Partial': case 'Readonly': case 'Pick': case 'Omit': - case 'Exclude': - case 'Extract': case 'Required': case 'InstanceType': return ['Object'] + case 'Uppercase': + case 'Lowercase': + case 'Capitalize': + case 'Uncapitalize': + return ['String'] + case 'Parameters': + case 'ConstructorParameters': + return ['Array'] + case 'NonNullable': + if (node.typeParameters && node.typeParameters.params[0]) { + return inferRuntimeType( + context, + node.typeParameters.params[0], + checked + ).filter((t) => t !== 'null') + } + break + case 'Extract': + if (node.typeParameters && node.typeParameters.params[1]) { + return inferRuntimeType( + context, + node.typeParameters.params[1], + checked + ) + } + break + case 'Exclude': + case 'OmitThisParameter': + if (node.typeParameters && node.typeParameters.params[0]) { + return inferRuntimeType( + context, + node.typeParameters.params[0], + checked + ) + } + break } } - return [`null`] + return inferRuntimeTypeFromTypeNode( + context, + /** @type {TypeNode} */ (node) + ) case 'TSUnionType': - const set = new Set() - for (const t of node.types) { - for (const tt of inferRuntimeType(context, t, checked)) { - set.add(tt) - } - } - return [...set] - case 'TSIntersectionType': - return ['Object'] + return inferUnionType(node) default: - return [`null`] // no runtime check + return inferRuntimeTypeFromTypeNode( + context, + /** @type {TypeNode} */ (node) + ) + } + + /** + * @param {import('@typescript-eslint/types').TSESTree.TSUnionType|import('@typescript-eslint/types').TSESTree.TSIntersectionType} node + * @returns {string[]} + */ + function inferUnionType(node) { + const types = new Set() + for (const t of node.types) { + for (const tt of inferRuntimeType(context, t, checked)) { + types.add(tt) + } + } + return [...types] + } +} + +/** + * @param {import('@typescript-eslint/types').TSESTree.TSTypeLiteral} node + * @returns {string[]} + */ +function inferTypeLiteralType(node) { + const types = new Set() + for (const m of node.members) { + switch (m.type) { + case 'TSCallSignatureDeclaration': + case 'TSConstructSignatureDeclaration': + types.add('Function') + break + default: + types.add('Object') + } + } + return types.size > 0 ? [...types] : ['Object'] +} +/** + * @param {RuleContext} context The ESLint rule context object. + * @param {import('@typescript-eslint/types').TSESTree.TSEnumDeclaration} node + * @returns {string[]} + */ +function inferEnumType(context, node) { + const types = new Set() + for (const m of node.members) { + if (m.initializer) { + if (m.initializer.type === 'Literal') { + switch (typeof m.initializer.value) { + case 'string': + types.add('String') + break + case 'number': + case 'bigint': // Now it's a syntax error. + types.add('Number') + break + case 'boolean': // Now it's a syntax error. + types.add('Boolean') + break + } + } else { + for (const type of inferRuntimeTypeFromTypeNode( + context, + /** @type {Expression} */ (m.initializer) + )) { + types.add(type) + } + } + } } + return types.size > 0 ? [...types] : ['Number'] } diff --git a/lib/utils/ts-utils/ts-types.js b/lib/utils/ts-utils/ts-types.js index 83c1519c2..e684831e4 100644 --- a/lib/utils/ts-utils/ts-types.js +++ b/lib/utils/ts-utils/ts-types.js @@ -28,7 +28,8 @@ const { module.exports = { getComponentPropsFromTypeDefineTypes, - getComponentEmitsFromTypeDefineTypes + getComponentEmitsFromTypeDefineTypes, + inferRuntimeTypeFromTypeNode } /** @@ -120,6 +121,21 @@ function getComponentEmitsFromTypeDefineTypes(context, emitsNode) { return [...extractRuntimeEmits(type, tsNode, emitsNode, services)] } +/** + * @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 @@ -134,48 +150,60 @@ function* extractRuntimeProps(type, tsNode, propsNode, services) { const name = property.getName() const type = checker.getTypeOfSymbolAtLocation(property, tsNode) - /** @type {string[]} */ - const types = [] - for (const targetType of iterateTypes(checker.getNonNullableType(type))) { - if ( - isAny(targetType) || - isUnknown(targetType) || - isNever(targetType) || - isNull(targetType) - ) { - types.push('null') - } else if (isStringLike(targetType)) { - types.push('String') - } else if (isNumberLike(targetType)) { - types.push('Number') - } else if (isBooleanLike(targetType)) { - types.push('Boolean') - } else if (isBigIntLike(targetType)) { - types.push('BigInt') - } else if (isFunction(targetType)) { - types.push('Function') - } else if ( - isArrayLikeObject(targetType) || - (targetType.isClassOrInterface() && - ['Array', 'ReadonlyArray'].includes( - checker.getFullyQualifiedName(targetType.symbol) - )) - ) { - types.push('Array') - } else if (isObject(targetType)) { - types.push('Object') - } - } + yield { type: 'infer-type', propName: name, required: !isOptional, node: propsNode, - types: types.length > 0 ? types : ['null'] + types: inferRuntimeTypeInternal(type, services) } } } +/** + * @param {Type} type + * @param {Services} services + * @returns {string[]} + */ +function inferRuntimeTypeInternal(type, services) { + const { checker } = services + /** @type {Set} */ + const types = new Set() + 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 diff --git a/tests/lib/utils/ts-utils/ts-types/get-component-emits.js b/tests/lib/utils/ts-utils/index/get-component-emits.js similarity index 100% rename from tests/lib/utils/ts-utils/ts-types/get-component-emits.js rename to tests/lib/utils/ts-utils/index/get-component-emits.js diff --git a/tests/lib/utils/ts-utils/ts-types/get-component-props.js b/tests/lib/utils/ts-utils/index/get-component-props.js similarity index 75% rename from tests/lib/utils/ts-utils/ts-types/get-component-props.js rename to tests/lib/utils/ts-utils/index/get-component-props.js index 5ec5d78c8..2ab0070c7 100644 --- a/tests/lib/utils/ts-utils/ts-types/get-component-props.js +++ b/tests/lib/utils/ts-utils/index/get-component-props.js @@ -74,6 +74,19 @@ describe('getComponentPropsFromTypeDefineTypes', () => { { type: 'type', name: 'bar', required: false, types: ['Number'] } ] }, + { + scriptCode: `defineProps<{foo:string,bar?:number} & {baz?:string|number}>()`, + props: [ + { type: 'type', name: 'foo', required: true, types: ['String'] }, + { type: 'type', name: 'bar', required: false, types: ['Number'] }, + { + type: 'type', + name: 'baz', + required: false, + types: ['String', 'Number'] + } + ] + }, { tsFileCode: `export type Props = {foo:string,bar?:number}`, scriptCode: `import { Props } from './test' @@ -137,6 +150,53 @@ describe('getComponentPropsFromTypeDefineTypes', () => { { type: 'infer-type', name: 'h', required: false, types: ['Array'] }, { type: 'infer-type', name: 'i', required: false, types: ['Array'] } ] + }, + { + tsFileCode: ` + export interface Props { + a?: number; + b?: string; + }`, + scriptCode: `import { Props } from './test' +defineProps()`, + props: [ + { type: 'infer-type', name: 'a', required: false, types: ['Number'] }, + { type: 'infer-type', name: 'b', required: false, types: ['String'] }, + { type: 'type', name: 'foo', required: false, types: ['String'] } + ] + }, + { + tsFileCode: ` + export type A = string | number`, + scriptCode: `import { A } from './test' +defineProps<{foo?:A}>()`, + props: [ + { + type: 'type', + name: 'foo', + required: false, + types: ['String', 'Number'] + } + ] + }, + { + scriptCode: `enum A {a = 'a', b = 'b'} +defineProps<{foo?:A}>()`, + props: [{ type: 'type', name: 'foo', required: false, types: ['String'] }] + }, + { + scriptCode: ` +const foo = 42 +enum A {a = foo, b = 'b'} +defineProps<{foo?:A}>()`, + props: [ + { + type: 'type', + name: 'foo', + required: false, + types: ['Number', 'String'] + } + ] } ]) { const code = `` From f782877e029e0365dfbf8f802c2b92d31a32fd3f Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Wed, 19 Apr 2023 17:09:42 +0900 Subject: [PATCH 6/9] fix --- lib/utils/ts-utils/ts-ast.js | 29 ++++++++++++++++--- .../util-types/ast/ts-ast.ts | 7 ++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/utils/ts-utils/ts-ast.js b/lib/utils/ts-utils/ts-ast.js index e9977e3bd..4e698aa40 100644 --- a/lib/utils/ts-utils/ts-ast.js +++ b/lib/utils/ts-utils/ts-ast.js @@ -14,6 +14,8 @@ const { inferRuntimeTypeFromTypeNode } = require('./ts-types') * @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit */ +const noop = Function.prototype + module.exports = { isTypeNode, flattenTypeNodes, @@ -25,17 +27,21 @@ module.exports = { } /** - * @param {TSESTreeNode | ASTNode} node - * @returns {node is TSESTreeTypeNode} + * @param {ASTNode} node + * @returns {node is TypeNode} */ function isTypeNode(node) { - return ( + if ( + node.type === 'TSAbstractKeyword' || node.type === 'TSAnyKeyword' || + node.type === 'TSAsyncKeyword' || node.type === 'TSArrayType' || node.type === 'TSBigIntKeyword' || node.type === 'TSBooleanKeyword' || node.type === 'TSConditionalType' || node.type === 'TSConstructorType' || + node.type === 'TSDeclareKeyword' || + node.type === 'TSExportKeyword' || node.type === 'TSFunctionType' || node.type === 'TSImportType' || node.type === 'TSIndexedAccessType' || @@ -50,7 +56,13 @@ function isTypeNode(node) { node.type === 'TSNumberKeyword' || node.type === 'TSObjectKeyword' || node.type === 'TSOptionalType' || + node.type === 'TSQualifiedName' || + node.type === 'TSPrivateKeyword' || + node.type === 'TSProtectedKeyword' || + node.type === 'TSPublicKeyword' || + node.type === 'TSReadonlyKeyword' || node.type === 'TSRestType' || + node.type === 'TSStaticKeyword' || node.type === 'TSStringKeyword' || node.type === 'TSSymbolKeyword' || node.type === 'TSTemplateLiteralType' || @@ -65,7 +77,16 @@ function isTypeNode(node) { node.type === 'TSUnionType' || node.type === 'TSUnknownKeyword' || node.type === 'TSVoidKeyword' - ) + ) { + /** @type {TypeNode['type']} for type check */ + const type = node.type + noop(type) + return true + } + /** @type {Exclude} for type check */ + const type = node.type + noop(type) + return false } /** 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 0fa6d68b0..e3c5f0faf 100644 --- a/typings/eslint-plugin-vue/util-types/ast/ts-ast.ts +++ b/typings/eslint-plugin-vue/util-types/ast/ts-ast.ts @@ -92,12 +92,11 @@ export interface TSFunctionType extends TSFunctionSignatureBase { type: 'TSFunctionType' } +type TypeNodeTypes = `${TSESTree.TypeNode['type']}` + export type TypeNode = | (HasParentNode & { - type: Exclude< - TSESTree.TypeNode['type'], - 'TSFunctionType' | 'TSLiteralType' - > + type: Exclude }) | TSFunctionType | TSLiteralType From fc32e9fce7e16ddef57620cc53b7332a16da8241 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Fri, 12 May 2023 11:33:31 +0900 Subject: [PATCH 7/9] support new `defineEmits` type syntax --- lib/utils/ts-utils/ts-ast.js | 82 +++++++++++++------ typings/eslint-plugin-vue/util-types/utils.ts | 5 -- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/lib/utils/ts-utils/ts-ast.js b/lib/utils/ts-utils/ts-ast.js index 4e698aa40..8561bf565 100644 --- a/lib/utils/ts-utils/ts-ast.js +++ b/lib/utils/ts-utils/ts-ast.js @@ -11,7 +11,9 @@ const { inferRuntimeTypeFromTypeNode } = require('./ts-types') */ /** * @typedef {import('../index').ComponentTypeProp} ComponentTypeProp + * @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp * @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit + * @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit */ const noop = Function.prototype @@ -122,29 +124,40 @@ function isTSTypeLiteralOrTSFunctionType(node) { * @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L1512 * @param {RuleContext} context The ESLint rule context object. * @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody} node - * @returns {IterableIterator} + * @returns {IterableIterator} */ function* extractRuntimeProps(context, node) { const members = node.type === 'TSTypeLiteral' ? node.members : node.body - for (const m of members) { + for (const member of members) { if ( - (m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature') && - (m.key.type === 'Identifier' || m.key.type === 'Literal') + member.type === 'TSPropertySignature' || + member.type === 'TSMethodSignature' ) { + if (member.key.type !== 'Identifier' && member.key.type !== 'Literal') { + yield { + type: 'unknown', + propName: null, + node: /** @type {Expression} */ (member.key) + } + continue + } /** @type {string[]|undefined} */ let types - if (m.type === 'TSMethodSignature') { + if (member.type === 'TSMethodSignature') { types = ['Function'] - } else if (m.typeAnnotation) { - types = inferRuntimeType(context, m.typeAnnotation.typeAnnotation) + } else if (member.typeAnnotation) { + types = inferRuntimeType(context, member.typeAnnotation.typeAnnotation) } yield { type: 'type', - key: /** @type {Identifier | Literal} */ (m.key), - propName: m.key.type === 'Identifier' ? m.key.name : `${m.key.value}`, - node: /** @type {TSPropertySignature | TSMethodSignature} */ (m), + key: /** @type {Identifier | Literal} */ (member.key), + propName: + member.key.type === 'Identifier' + ? member.key.name + : `${member.key.value}`, + node: /** @type {TSPropertySignature | TSMethodSignature} */ (member), - required: !m.optional, + required: !member.optional, types: types || [`null`] } } @@ -152,27 +165,46 @@ function* extractRuntimeProps(context, node) { } /** - * @see https://github.com/vuejs/vue-next/blob/348c3b01e56383ffa70b180d1376fdf4ac12e274/packages/compiler-sfc/src/compileScript.ts#L1632 * @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody | TSESTreeTSFunctionType} node - * @returns {IterableIterator} + * @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 { + if (node.type === 'TSFunctionType') { yield* extractEventNames( node.params[0], /** @type {TSFunctionType} */ (node) ) + return + } + const members = node.type === 'TSTypeLiteral' ? node.members : node.body + for (const member of members) { + if (member.type === 'TSCallSignatureDeclaration') { + yield* extractEventNames( + member.params[0], + /** @type {TSCallSignatureDeclaration} */ (member) + ) + } else if ( + member.type === 'TSPropertySignature' || + member.type === 'TSMethodSignature' + ) { + if (member.key.type !== 'Identifier' && member.key.type !== 'Literal') { + yield { + type: 'unknown', + emitName: null, + node: /** @type {Expression} */ (member.key) + } + continue + } + yield { + type: 'type', + key: /** @type {Identifier | Literal} */ (member.key), + emitName: + member.key.type === 'Identifier' + ? member.key.name + : `${member.key.value}`, + node: /** @type {TSPropertySignature | TSMethodSignature} */ (member) + } + } } } diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts index 2039d11c6..77aa19ec9 100644 --- a/typings/eslint-plugin-vue/util-types/utils.ts +++ b/typings/eslint-plugin-vue/util-types/utils.ts @@ -40,10 +40,6 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase { onDefinePropsExit?(node: CallExpression, props: ComponentProp[]): void onDefineEmitsEnter?(node: CallExpression, emits: ComponentEmit[]): void onDefineEmitsExit?(node: CallExpression, emits: ComponentEmit[]): void - onDefineOptionsEnter?(node: CallExpression): void - onDefineOptionsExit?(node: CallExpression): void - onDefineSlotsEnter?(node: CallExpression): void - onDefineSlotsExit?(node: CallExpression): void [query: string]: | ((node: VAST.ParamNode) => void) | ((node: CallExpression, props: ComponentProp[]) => void) @@ -167,7 +163,6 @@ export type ComponentTypeEmitPropertySignature = { type: 'type' key: Identifier | Literal emitName: string - value: null node: TSPropertySignature | TSMethodSignature } export type ComponentTypeEmit = From cc98e2c5b60c4d961082a64d938b62fc8cb9891e Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Fri, 12 May 2023 17:16:09 +0900 Subject: [PATCH 8/9] fix --- typings/eslint-plugin-vue/util-types/utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts index 77aa19ec9..b2704769f 100644 --- a/typings/eslint-plugin-vue/util-types/utils.ts +++ b/typings/eslint-plugin-vue/util-types/utils.ts @@ -40,6 +40,10 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase { onDefinePropsExit?(node: CallExpression, props: ComponentProp[]): void onDefineEmitsEnter?(node: CallExpression, emits: ComponentEmit[]): void onDefineEmitsExit?(node: CallExpression, emits: ComponentEmit[]): void + onDefineOptionsEnter?(node: CallExpression): void + onDefineOptionsExit?(node: CallExpression): void + onDefineSlotsEnter?(node: CallExpression): void + onDefineSlotsExit?(node: CallExpression): void [query: string]: | ((node: VAST.ParamNode) => void) | ((node: CallExpression, props: ComponentProp[]) => void) From e32b3f1c24d67ad866e1c58e75b030274c8e0f59 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Fri, 12 May 2023 17:18:04 +0900 Subject: [PATCH 9/9] fix --- lib/utils/indent-ts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/indent-ts.js b/lib/utils/indent-ts.js index d318bb2c1..5bee66889 100644 --- a/lib/utils/indent-ts.js +++ b/lib/utils/indent-ts.js @@ -227,7 +227,7 @@ function defineVisitor({ processSemicolons(node) }, /** - * @param {TSESTreeNode} node + * @param {ASTNode} node */ // eslint-disable-next-line complexity -- ignore '*[type=/^TS/]'(node) {