diff --git a/lib/rules/no-required-prop-with-default.js b/lib/rules/no-required-prop-with-default.js index 0976e0d23..912af6a65 100644 --- a/lib/rules/no-required-prop-with-default.js +++ b/lib/rules/no-required-prop-with-default.js @@ -47,14 +47,16 @@ module.exports = { } /** - * @param {ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp | ComponentProp} prop - * */ - const handleObjectProp = (prop) => { + * @param {ComponentProp} prop + * @param {Set} [defaultProps] + **/ + const handleObjectProp = (prop, defaultProps) => { if ( prop.type === 'object' && prop.propName && prop.value.type === 'ObjectExpression' && - utils.findProperty(prop.value, 'default') + (utils.findProperty(prop.value, 'default') || + defaultProps?.has(prop.propName)) ) { const requiredProperty = utils.findProperty(prop.value, 'required') if (!requiredProperty) return @@ -84,62 +86,61 @@ module.exports = { ] }) } + } else if ( + prop.type === 'type' && + defaultProps?.has(prop.propName) && + prop.required + ) { + // skip setter & getter case + if ( + prop.node.type === 'TSMethodSignature' && + (prop.node.kind === 'get' || prop.node.kind === 'set') + ) { + return + } + // skip computed + if (prop.node.computed) { + return + } + context.report({ + node: prop.node, + loc: prop.node.loc, + data: { + key: prop.propName + }, + messageId: 'requireOptional', + fix: canAutoFix + ? (fixer) => fixer.insertTextAfter(prop.key, '?') + : null, + suggest: canAutoFix + ? null + : [ + { + messageId: 'fixRequiredProp', + fix: (fixer) => fixer.insertTextAfter(prop.key, '?') + } + ] + }) } } return utils.compositingVisitors( utils.defineVueVisitor(context, { onVueObjectEnter(node) { - utils.getComponentPropsFromOptions(node).map(handleObjectProp) + utils + .getComponentPropsFromOptions(node) + .map((prop) => handleObjectProp(prop)) } }), utils.defineScriptSetupVisitor(context, { onDefinePropsEnter(node, props) { - if (!utils.hasWithDefaults(node)) { - props.map(handleObjectProp) - return - } - const withDefaultsProps = Object.keys( - utils.getWithDefaultsPropExpressions(node) - ) - const requiredProps = props.flatMap((item) => - item.type === 'type' && item.required ? [item] : [] - ) - - for (const prop of requiredProps) { - if (withDefaultsProps.includes(prop.propName)) { - // skip setter & getter case - if ( - prop.node.type === 'TSMethodSignature' && - (prop.node.kind === 'get' || prop.node.kind === 'set') - ) { - return - } - // skip computed - if (prop.node.computed) { - return - } - context.report({ - node: prop.node, - loc: prop.node.loc, - data: { - key: prop.propName - }, - messageId: 'requireOptional', - fix: canAutoFix - ? (fixer) => fixer.insertTextAfter(prop.key, '?') - : null, - suggest: canAutoFix - ? null - : [ - { - messageId: 'fixRequiredProp', - fix: (fixer) => fixer.insertTextAfter(prop.key, '?') - } - ] - }) - } - } + const defaultProps = new Set([ + ...Object.keys(utils.getWithDefaultsPropExpressions(node)), + ...Object.keys( + utils.getDefaultPropExpressionsForPropsDestructure(node) + ) + ]) + props.map((prop) => handleObjectProp(prop, defaultProps)) } }) ) 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/no-required-prop-with-default.js b/tests/lib/rules/no-required-prop-with-default.js index 7bdd9c611..9252e0980 100644 --- a/tests/lib/rules/no-required-prop-with-default.js +++ b/tests/lib/rules/no-required-prop-with-default.js @@ -189,6 +189,49 @@ tester.run('no-required-prop-with-default', rule, { }) ` + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + } + }, + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + } + }, + { + filename: 'test.vue', + code: ` + + ` } ], invalid: [ @@ -918,6 +961,94 @@ tester.run('no-required-prop-with-default', rule, { line: 4 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + languageOptions: { + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + }, + errors: [ + { + message: 'Prop "name" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + languageOptions: { + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } + }, + errors: [ + { + message: 'Prop "name" should be optional.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [{ autofix: true }], + errors: [ + { + message: 'Prop "name" should be optional.', + line: 4 + } + ] } ] })