From 8ffbe006af64a540c28cad81c6ef78f924140bf3 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Fri, 13 Sep 2024 19:33:14 +0900 Subject: [PATCH 1/3] Add support for props destructure to `vue/require-valid-default-prop` rule --- lib/rules/require-valid-default-prop.js | 160 +++++++++++++----- tests/lib/rules/require-valid-default-prop.js | 69 +++++++- 2 files changed, 178 insertions(+), 51 deletions(-) diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js index ddae351ec..520840b15 100644 --- a/lib/rules/require-valid-default-prop.js +++ b/lib/rules/require-valid-default-prop.js @@ -70,6 +70,59 @@ function getTypes(targetNode) { return [] } +/** + * Extracts default definitions using assignment patterns. + * @param {CallExpression} node The node of defineProps + * @returns { { [key: string]: Expression | undefined } } + */ +function getDefaultsPropExpressionsForAssignmentPatterns(node) { + const left = getLeftOfDefineProps(node) + if (!left || left.type !== 'ObjectPattern') { + return {} + } + /** @type { { [key: string]: Expression | undefined } } */ + const result = Object.create(null) + for (const prop of left.properties) { + if (prop.type !== 'Property') continue + const value = prop.value + if (value.type !== 'AssignmentPattern') continue + const defaultNode = value.right + const name = utils.getStaticPropertyName(prop) + if (name != null) { + result[name] = defaultNode + } + } + 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 ( + target.parent && + target.parent.type === 'CallExpression' && + target.parent.arguments[0] === target && + target.parent.callee.type === 'Identifier' && + target.parent.callee.name === 'withDefaults' + ) { + target = target.parent + } + if (!target.parent) { + return null + } + if ( + target.parent.type === 'VariableDeclarator' && + target.parent.init === target + ) { + return target.parent.id + } + return null +} + module.exports = { meta: { type: 'suggestion', @@ -250,30 +303,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 +342,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 +427,7 @@ module.exports = { prop.type === 'object' && prop.value.type === 'ObjectExpression' ) ) - const propContexts = processPropDefs(props, {}) + const propContexts = processPropDefs(props, () => []) vueObjectPropsContexts.set(obj, propContexts) }, /** @@ -402,18 +465,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 = + getDefaultsPropExpressionsForAssignmentPatterns(node) + const propContexts = processPropDefs(props, (propName) => + [ + defaultsByWithDefaults[propName], + defaultsByAssignmentPatterns[propName] + ].filter(utils.isDef) + ) scriptSetupPropsContexts.push({ node, props: propContexts }) }, /** 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 + } + ] } ] }) From 0f38fa24be5b711bac67b8b7acfec34db0444d24 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 14 Sep 2024 10:07:26 +0900 Subject: [PATCH 2/3] minor refactor --- lib/rules/require-valid-default-prop.js | 57 +------------------------ lib/utils/index.js | 54 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js index 520840b15..ca834d620 100644 --- a/lib/rules/require-valid-default-prop.js +++ b/lib/rules/require-valid-default-prop.js @@ -70,59 +70,6 @@ function getTypes(targetNode) { return [] } -/** - * Extracts default definitions using assignment patterns. - * @param {CallExpression} node The node of defineProps - * @returns { { [key: string]: Expression | undefined } } - */ -function getDefaultsPropExpressionsForAssignmentPatterns(node) { - const left = getLeftOfDefineProps(node) - if (!left || left.type !== 'ObjectPattern') { - return {} - } - /** @type { { [key: string]: Expression | undefined } } */ - const result = Object.create(null) - for (const prop of left.properties) { - if (prop.type !== 'Property') continue - const value = prop.value - if (value.type !== 'AssignmentPattern') continue - const defaultNode = value.right - const name = utils.getStaticPropertyName(prop) - if (name != null) { - result[name] = defaultNode - } - } - 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 ( - target.parent && - target.parent.type === 'CallExpression' && - target.parent.arguments[0] === target && - target.parent.callee.type === 'Identifier' && - target.parent.callee.name === 'withDefaults' - ) { - target = target.parent - } - if (!target.parent) { - return null - } - if ( - target.parent.type === 'VariableDeclarator' && - target.parent.init === target - ) { - return target.parent.id - } - return null -} - module.exports = { meta: { type: 'suggestion', @@ -477,11 +424,11 @@ module.exports = { const defaultsByWithDefaults = utils.getWithDefaultsPropExpressions(node) const defaultsByAssignmentPatterns = - getDefaultsPropExpressionsForAssignmentPatterns(node) + utils.getDefaultPropExpressionsForPropsDestructure(node) const propContexts = processPropDefs(props, (propName) => [ defaultsByWithDefaults[propName], - defaultsByAssignmentPatterns[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..b8683f647 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1537,6 +1537,13 @@ 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, getVueObjectType, /** @@ -3144,6 +3151,53 @@ function getWithDefaultsProps(node) { 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) { + const left = getLeftOfDefineProps(node) + if (!left || left.type !== 'ObjectPattern') { + return {} + } + /** @type {ReturnType} */ + const result = Object.create(null) + for (const prop of left.properties) { + if (prop.type !== 'Property') continue + const value = prop.value + if (value.type !== 'AssignmentPattern') continue + const name = getStaticPropertyName(prop) + if (name != null) { + 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 From 463eaaed2ca9f4a59003ad70871cadfc247cb2d0 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Tue, 17 Sep 2024 18:06:22 +0900 Subject: [PATCH 3/3] Update index.js --- lib/utils/index.js | 50 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/lib/utils/index.js b/lib/utils/index.js index b8683f647..c31f2d6af 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1544,6 +1544,21 @@ module.exports = { * @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, /** @@ -3152,30 +3167,45 @@ function getWithDefaultsProps(node) { } /** - * Gets the default definition nodes for defineProp - * using the props destructure with assignment pattern. + * Gets the props destructure property nodes for defineProp. * @param {CallExpression} node The node of defineProps - * @returns { Record } + * @returns { Record } */ -function getDefaultPropExpressionsForPropsDestructure(node) { +function getPropsDestructure(node) { + /** @type {ReturnType} */ + const result = Object.create(null) const left = getLeftOfDefineProps(node) if (!left || left.type !== 'ObjectPattern') { - return {} + return result } - /** @type {ReturnType} */ - const result = Object.create(null) for (const prop of left.properties) { if (prop.type !== 'Property') continue - const value = prop.value - if (value.type !== 'AssignmentPattern') continue const name = getStaticPropertyName(prop) if (name != null) { - result[name] = { prop, expression: value.right } + 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