From 05b75592d63be3edce0bb10382145b6d282e57f1 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Wed, 18 Sep 2024 10:43:34 +0900 Subject: [PATCH] Add support for props destructure to `vue/require-valid-default-prop` rule (#2551) --- lib/rules/require-valid-default-prop.js | 107 ++++++++++-------- lib/utils/index.js | 84 ++++++++++++++ tests/lib/rules/require-valid-default-prop.js | 69 ++++++++++- 3 files changed, 209 insertions(+), 51 deletions(-) diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js index ddae351ec..ca834d620 100644 --- a/lib/rules/require-valid-default-prop.js +++ b/lib/rules/require-valid-default-prop.js @@ -250,30 +250,38 @@ module.exports = { } /** - * @param {(ComponentObjectDefineProp | ComponentTypeProp | ComponentInferTypeProp)[]} props - * @param { { [key: string]: Expression | undefined } } withDefaults + * @param {(ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp)[]} props + * @param {(propName: string) => Expression[]} otherDefaultProvider */ - function processPropDefs(props, withDefaults) { + function processPropDefs(props, otherDefaultProvider) { /** @type {PropDefaultFunctionContext[]} */ const propContexts = [] for (const prop of props) { let typeList - let defExpr + /** @type {Expression[]} */ + const defExprList = [] if (prop.type === 'object') { - const type = getPropertyNode(prop.value, 'type') - if (!type) continue + if (prop.value.type === 'ObjectExpression') { + const type = getPropertyNode(prop.value, 'type') + if (!type) continue - typeList = getTypes(type.value) + typeList = getTypes(type.value) - const def = getPropertyNode(prop.value, 'default') - if (!def) continue + const def = getPropertyNode(prop.value, 'default') + if (!def) continue - defExpr = def.value + defExprList.push(def.value) + } else { + typeList = getTypes(prop.value) + } } else { typeList = prop.types - defExpr = withDefaults[prop.propName] } - if (!defExpr) continue + if (prop.propName != null) { + defExprList.push(...otherDefaultProvider(prop.propName)) + } + + if (defExprList.length === 0) continue const typeNames = new Set( typeList.filter((item) => NATIVE_TYPES.has(item)) @@ -281,40 +289,42 @@ module.exports = { // There is no native types detected if (typeNames.size === 0) continue - const defType = getValueType(defExpr) + for (const defExpr of defExprList) { + const defType = getValueType(defExpr) - if (!defType) continue + if (!defType) continue - if (defType.function) { - if (typeNames.has('Function')) { - continue - } - if (defType.expression) { - if (!defType.returnType || typeNames.has(defType.returnType)) { + if (defType.function) { + if (typeNames.has('Function')) { continue } - report(defType.functionBody, prop, typeNames) + if (defType.expression) { + if (!defType.returnType || typeNames.has(defType.returnType)) { + continue + } + report(defType.functionBody, prop, typeNames) + } else { + propContexts.push({ + prop, + types: typeNames, + default: defType + }) + } } else { - propContexts.push({ + if ( + typeNames.has(defType.type) && + !FUNCTION_VALUE_TYPES.has(defType.type) + ) { + continue + } + report( + defExpr, prop, - types: typeNames, - default: defType - }) - } - } else { - if ( - typeNames.has(defType.type) && - !FUNCTION_VALUE_TYPES.has(defType.type) - ) { - continue - } - report( - defExpr, - prop, - [...typeNames].map((type) => - FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type + [...typeNames].map((type) => + FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type + ) ) - ) + } } } return propContexts @@ -364,7 +374,7 @@ module.exports = { prop.type === 'object' && prop.value.type === 'ObjectExpression' ) ) - const propContexts = processPropDefs(props, {}) + const propContexts = processPropDefs(props, () => []) vueObjectPropsContexts.set(obj, propContexts) }, /** @@ -402,18 +412,25 @@ module.exports = { const props = baseProps.filter( /** * @param {ComponentProp} prop - * @returns {prop is ComponentObjectDefineProp | ComponentInferTypeProp | ComponentTypeProp} + * @returns {prop is ComponentObjectProp | ComponentInferTypeProp | ComponentTypeProp} */ (prop) => Boolean( prop.type === 'type' || prop.type === 'infer-type' || - (prop.type === 'object' && - prop.value.type === 'ObjectExpression') + prop.type === 'object' ) ) - const defaults = utils.getWithDefaultsPropExpressions(node) - const propContexts = processPropDefs(props, defaults) + const defaultsByWithDefaults = + utils.getWithDefaultsPropExpressions(node) + const defaultsByAssignmentPatterns = + utils.getDefaultPropExpressionsForPropsDestructure(node) + const propContexts = processPropDefs(props, (propName) => + [ + defaultsByWithDefaults[propName], + defaultsByAssignmentPatterns[propName]?.expression + ].filter(utils.isDef) + ) scriptSetupPropsContexts.push({ node, props: propContexts }) }, /** diff --git a/lib/utils/index.js b/lib/utils/index.js index 9671c00d4..c31f2d6af 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1537,6 +1537,28 @@ module.exports = { * @returns { { [key: string]: Property | undefined } } */ getWithDefaultsProps, + /** + * Gets the default definition nodes for defineProp + * using the props destructure with assignment pattern. + * @param {CallExpression} node The node of defineProps + * @returns { Record } + */ + getDefaultPropExpressionsForPropsDestructure, + /** + * Checks whether the given defineProps node is using Props Destructure. + * @param {CallExpression} node The node of defineProps + * @returns {boolean} + */ + isUsingPropsDestructure(node) { + const left = getLeftOfDefineProps(node) + return left?.type === 'ObjectPattern' + }, + /** + * Gets the props destructure property nodes for defineProp. + * @param {CallExpression} node The node of defineProps + * @returns { Record } + */ + getPropsDestructure, getVueObjectType, /** @@ -3144,6 +3166,68 @@ function getWithDefaultsProps(node) { return result } +/** + * Gets the props destructure property nodes for defineProp. + * @param {CallExpression} node The node of defineProps + * @returns { Record } + */ +function getPropsDestructure(node) { + /** @type {ReturnType} */ + const result = Object.create(null) + const left = getLeftOfDefineProps(node) + if (!left || left.type !== 'ObjectPattern') { + return result + } + for (const prop of left.properties) { + if (prop.type !== 'Property') continue + const name = getStaticPropertyName(prop) + if (name != null) { + result[name] = prop + } + } + return result +} + +/** + * Gets the default definition nodes for defineProp + * using the props destructure with assignment pattern. + * @param {CallExpression} node The node of defineProps + * @returns { Record } + */ +function getDefaultPropExpressionsForPropsDestructure(node) { + /** @type {ReturnType} */ + const result = Object.create(null) + for (const [name, prop] of Object.entries(getPropsDestructure(node))) { + if (!prop) continue + const value = prop.value + if (value.type !== 'AssignmentPattern') continue + result[name] = { prop, expression: value.right } + } + return result +} + +/** + * Gets the pattern of the left operand of defineProps. + * @param {CallExpression} node The node of defineProps + * @returns {Pattern | null} The pattern of the left operand of defineProps + */ +function getLeftOfDefineProps(node) { + let target = node + if (hasWithDefaults(target)) { + target = target.parent + } + if (!target.parent) { + return null + } + if ( + target.parent.type === 'VariableDeclarator' && + target.parent.init === target + ) { + return target.parent.id + } + return null +} + /** * Get all props from component options object. * @param {ObjectExpression} componentObject Object with component definition diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js index c9e506a4a..0f4fd1902 100644 --- a/tests/lib/rules/require-valid-default-prop.js +++ b/tests/lib/rules/require-valid-default-prop.js @@ -223,8 +223,7 @@ ruleTester.run('require-valid-default-prop', rule, { parser: require('@typescript-eslint/parser'), ecmaVersion: 6, sourceType: 'module' - }, - errors: errorMessage('function') + } }, { filename: 'test.vue', @@ -241,8 +240,7 @@ ruleTester.run('require-valid-default-prop', rule, { parser: require('@typescript-eslint/parser'), ecmaVersion: 6, sourceType: 'module' - }, - errors: errorMessage('function') + } }, { filename: 'test.vue', @@ -259,8 +257,7 @@ ruleTester.run('require-valid-default-prop', rule, { parser: require('@typescript-eslint/parser'), ecmaVersion: 6, sourceType: 'module' - }, - errors: errorMessage('function') + } }, { // https://github.com/vuejs/eslint-plugin-vue/issues/1853 @@ -304,6 +301,21 @@ ruleTester.run('require-valid-default-prop', rule, { }) `, ...getTypeScriptFixtureTestOptions() + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser') + } } ], @@ -1041,6 +1053,51 @@ ruleTester.run('require-valid-default-prop', rule, { } ], ...getTypeScriptFixtureTestOptions() + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser') + }, + errors: [ + { + message: "Type of the default value for 'foo' prop must be a string.", + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser') + }, + errors: [ + { + message: "Type of the default value for 'foo' prop must be a string.", + line: 3 + }, + { + message: "Type of the default value for 'foo' prop must be a string.", + line: 6 + } + ] } ] })