From 8788bc89f9b28f80180023280fd090b3a068577d Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 14 Sep 2024 10:40:21 +0900 Subject: [PATCH 1/2] Add support for props destructure to `vue/require-default-prop` rule --- lib/rules/require-default-prop.js | 67 ++++++++++++++------- lib/utils/index.js | 63 ++++++++++++++++++++ tests/lib/rules/require-default-prop.js | 79 ++++++++++++++++++++++++- 3 files changed, 187 insertions(+), 22 deletions(-) diff --git a/lib/rules/require-default-prop.js b/lib/rules/require-default-prop.js index 313574a14..b5d2564e8 100644 --- a/lib/rules/require-default-prop.js +++ b/lib/rules/require-default-prop.js @@ -7,6 +7,7 @@ /** * @typedef {import('../utils').ComponentProp} ComponentProp * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp + * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp * @typedef {ComponentObjectProp & { value: ObjectExpression} } ComponentObjectPropObject */ @@ -137,18 +138,23 @@ module.exports = { /** * @param {ComponentProp[]} props - * @param {boolean} [withDefaults] - * @param { { [key: string]: Expression | undefined } } [withDefaultsExpressions] + * @param {(prop: ComponentObjectProp|ComponentTypeProp)=>boolean} [ignore] */ - function processProps(props, withDefaults, withDefaultsExpressions) { + function processProps(props, ignore) { for (const prop of props) { - if (prop.type === 'object' && !prop.node.shorthand) { + if (prop.type === 'object') { + if (prop.node.shorthand) { + continue + } if (!isWithoutDefaultValue(prop)) { continue } if (isBooleanProp(prop)) { continue } + if (ignore?.(prop)) { + continue + } const propName = prop.propName == null ? `[${context.getSourceCode().getText(prop.node.key)}]` @@ -161,26 +167,23 @@ module.exports = { propName } }) - } else if ( - prop.type === 'type' && - withDefaults && - withDefaultsExpressions - ) { + } else if (prop.type === 'type') { if (prop.required) { continue } if (prop.types.length === 1 && prop.types[0] === 'Boolean') { continue } - if (!withDefaultsExpressions[prop.propName]) { - context.report({ - node: prop.node, - messageId: `missingDefault`, - data: { - propName: prop.propName - } - }) + if (ignore?.(prop)) { + continue } + context.report({ + node: prop.node, + messageId: `missingDefault`, + data: { + propName: prop.propName + } + }) } } } @@ -188,11 +191,33 @@ module.exports = { return utils.compositingVisitors( utils.defineScriptSetupVisitor(context, { onDefinePropsEnter(node, props) { - processProps( - props, - utils.hasWithDefaults(node), + const hasWithDefaults = utils.hasWithDefaults(node) + const defaultsByWithDefaults = utils.getWithDefaultsPropExpressions(node) - ) + const isUsingPropsDestructure = utils.isUsingPropsDestructure(node) + const defaultsByAssignmentPatterns = + utils.getDefaultPropExpressionsForPropsDestructure(node) + + processProps(props, (prop) => { + if (prop.type === 'type') { + if (!hasWithDefaults) { + // If don't use withDefaults(), exclude it from the report. + return true + } + if (defaultsByWithDefaults[prop.propName]) { + return true + } + } + if (!isUsingPropsDestructure) { + return false + } + if (prop.propName == null) { + // If using Props Destructure but the property name cannot be determined, + // it will be ignored. + return true + } + return Boolean(defaultsByAssignmentPatterns[prop.propName]) + }) } }), utils.executeOnVue(context, (obj) => { diff --git a/lib/utils/index.js b/lib/utils/index.js index 9671c00d4..467a0b486 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1537,6 +1537,22 @@ 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' + }, getVueObjectType, /** @@ -3144,6 +3160,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 diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js index 3ac13bcb8..e352eddf3 100644 --- a/tests/lib/rules/require-default-prop.js +++ b/tests/lib/rules/require-default-prop.js @@ -351,6 +351,43 @@ ruleTester.run('require-default-prop', rule, { ...languageOptions, parserOptions: { parser: require.resolve('@typescript-eslint/parser') } } + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser'), + ...languageOptions + } + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser'), + ...languageOptions + } + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser'), + ...languageOptions + } } ], @@ -623,6 +660,46 @@ ruleTester.run('require-default-prop', rule, { } ] } - ]) + ]), + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser'), + ...languageOptions + }, + errors: [ + { + message: "Prop 'bar' requires default value to be set.", + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser'), + ...languageOptions + }, + errors: [ + { + message: "Prop 'foo' requires default value to be set.", + line: 3 + }, + { + message: "Prop 'bar' requires default value to be set.", + line: 3 + } + ] + } ] }) From 38a38f16191d9c13b3a3bb57cae0bb684e2a4b51 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Tue, 17 Sep 2024 18:07:14 +0900 Subject: [PATCH 2/2] Update index.js --- lib/utils/index.js | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/utils/index.js b/lib/utils/index.js index 467a0b486..c31f2d6af 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1553,6 +1553,12 @@ module.exports = { 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, /** @@ -3161,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