diff --git a/lib/rules/define-props-declaration.js b/lib/rules/define-props-declaration.js index f05e6da3c..17dfb8830 100644 --- a/lib/rules/define-props-declaration.js +++ b/lib/rules/define-props-declaration.js @@ -36,6 +36,127 @@ const mapNativeType = (/** @type {string} */ nativeType) => { } } +/** + * @param {ComponentProp} prop + * @param {SourceCode} sourceCode + */ +function getComponentPropData(prop, sourceCode) { + const unknownType = { + name: prop.propName, + type: 'unknown', + required: undefined, + defaultValue: undefined + } + + if (prop.type !== 'object') { + return unknownType + } + const type = optionGetType(prop.value, sourceCode) + if (type === null) { + return unknownType + } + const required = optionGetRequired(prop.value) + const defaultValue = optionGetDefault(prop.value) + + return { + name: prop.propName, + type: mapNativeType(type), + required, + defaultValue + } +} + +/** + * @param {Expression} node + * @param {SourceCode} sourceCode + * @returns {string | null} + */ +function optionGetType(node, sourceCode) { + switch (node.type) { + case 'Identifier': { + return node.name + } + case 'ObjectExpression': { + // foo: { + const typeProperty = utils.findProperty(node, 'type') + if (typeProperty == null) { + return null + } + if (typeProperty.value.type === 'TSAsExpression') { + const typeAnnotation = typeProperty.value.typeAnnotation + if (typeAnnotation.typeName.name !== 'PropType') { + return null + } + + // in some project configuration parser populates deprecated field `typeParameters` instead of `typeArguments` + const typeArguments = + 'typeArguments' in typeProperty.value + ? typeAnnotation.typeArguments + : typeAnnotation.typeParameters + + const typeArgument = Array.isArray(typeArguments) + ? typeArguments[0].params[0] + : typeArguments.params[0] + + if (typeArgument === undefined) { + return null + } + + return sourceCode.getText(typeArgument) + } + return optionGetType(typeProperty.value, sourceCode) + } + case 'ArrayExpression': { + return null + } + case 'FunctionExpression': + case 'ArrowFunctionExpression': { + return null + } + } + + // Unknown + return null +} + +/** + * @param {Expression} node + * @returns {boolean | undefined } + */ +function optionGetRequired(node) { + if (node.type === 'ObjectExpression') { + const requiredProperty = utils.findProperty(node, 'required') + if (requiredProperty == null) { + return undefined + } + + if (requiredProperty.value.type === 'Literal') { + return Boolean(requiredProperty.value.value) + } + } + + // Unknown + return undefined +} + +/** + * @param {Expression} node + * @returns {Expression | undefined } + */ +function optionGetDefault(node) { + if (node.type === 'ObjectExpression') { + const defaultProperty = utils.findProperty(node, 'default') + if (defaultProperty == null) { + return undefined + } + + return defaultProperty.value + } + + // Unknown + return undefined +} + /** * @typedef {import('../utils').ComponentProp} ComponentProp */ @@ -72,93 +193,6 @@ module.exports = { create(context) { const sourceCode = context.getSourceCode() - /** - * @param {Expression} node - * @returns {string | null} - */ - function optionGetType(node) { - switch (node.type) { - case 'Identifier': { - return node.name - } - case 'ObjectExpression': { - // foo: { - const typeProperty = utils.findProperty(node, 'type') - if (typeProperty == null) { - return null - } - if (typeProperty.value.type === 'TSAsExpression') { - if ( - typeProperty.value.typeAnnotation.typeName.name !== 'PropType' - ) { - return null - } - - const typeArgument = - typeProperty.value.typeAnnotation.typeArguments.params[0] - if (typeArgument === undefined) { - return null - } - - return sourceCode.getText(typeArgument) - } - return optionGetType(typeProperty.value) - } - case 'ArrayExpression': { - // foo: [ - return null - // return node.elements.map((arrayElement) => - // optionGetType(arrayElement) - // ) - } - case 'FunctionExpression': - case 'ArrowFunctionExpression': { - return null - } - } - - // Unknown - return null - } - - /** - * @param {Expression} node - * @returns {boolean | undefined } - */ - function optionGetRequired(node) { - if (node.type === 'ObjectExpression') { - const requiredProperty = utils.findProperty(node, 'required') - if (requiredProperty == null) { - return undefined - } - - if (requiredProperty.value.type === 'Literal') { - return Boolean(requiredProperty.value.value) - } - } - - // Unknown - return undefined - } - - /** - * @param {Expression} node - * @returns {Expression | undefined } - */ - function optionGetDefault(node) { - if (node.type === 'ObjectExpression') { - const defaultProperty = utils.findProperty(node, 'default') - if (defaultProperty == null) { - return undefined - } - - return defaultProperty.value - } - - // Unknown - return undefined - } - const scriptSetup = utils.getScriptSetupElement(context) if (!scriptSetup || !utils.hasAttribute(scriptSetup, 'lang', 'ts')) { return {} @@ -176,31 +210,9 @@ module.exports = { node, messageId: 'hasArg', *fix(fixer) { - const propTypes = props.map((prop) => { - const unknownType = { - name: prop.propName, - type: 'unknown', - required: undefined, - defaultValue: undefined - } - - if (prop.type !== 'object') { - return unknownType - } - const type = optionGetType(prop.value) - if (type === null) { - return unknownType - } - const required = optionGetRequired(prop.value) - const defaultValue = optionGetDefault(prop.value) - - return { - name: prop.propName, - type: mapNativeType(type), - required, - defaultValue - } - }) + const propTypes = props.map((prop) => + getComponentPropData(prop, sourceCode) + ) const definePropsType = `{ ${propTypes .map( @@ -209,8 +221,10 @@ module.exports = { ) .join(', ')} }` + // remove defineProps function parameters yield fixer.replaceText(node.arguments[0], '') + // add type annotation if (separateInterface) { const variableDeclarationNode = node.parent.parent if (!variableDeclarationNode) return @@ -227,6 +241,7 @@ module.exports = { ) } + // add defaults if needed const defaults = propTypes.filter( ({ defaultValue }) => defaultValue ) diff --git a/tests/lib/rules/define-props-declaration.js b/tests/lib/rules/define-props-declaration.js index 87c6633ca..dd98c3a2f 100644 --- a/tests/lib/rules/define-props-declaration.js +++ b/tests/lib/rules/define-props-declaration.js @@ -10,7 +10,7 @@ const rule = require('../../../lib/rules/define-props-declaration') const tester = new RuleTester({ languageOptions: { parser: require('vue-eslint-parser'), - ecmaVersion: 'latest', + ecmaVersion: '2020', sourceType: 'module', parserOptions: { parser: require.resolve('@typescript-eslint/parser') @@ -289,6 +289,28 @@ tester.run('define-props-declaration', rule, { } ] }, + // Custom type + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + errors: [ + { + message: 'Use type-based declaration instead of runtime declaration.', + line: 3 + } + ] + }, // Native Type with PropType { filename: 'test.vue', @@ -337,6 +359,62 @@ tester.run('define-props-declaration', rule, { } ] }, + // Object with PropType with separate type + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + errors: [ + { + message: 'Use type-based declaration instead of runtime declaration.', + line: 5 + } + ] + }, + // Object with PropType with separate imported type + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + errors: [ + { + message: 'Use type-based declaration instead of runtime declaration.', + line: 5 + } + ] + }, // Array with PropType { filename: 'test.vue',