diff --git a/lib/rules/require-default-prop.js b/lib/rules/require-default-prop.js index e0e29b69e..d35c2f8ea 100644 --- a/lib/rules/require-default-prop.js +++ b/lib/rules/require-default-prop.js @@ -5,7 +5,9 @@ 'use strict' /** + * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp + * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp * @typedef {ComponentObjectProp & { value: ObjectExpression} } ComponentObjectPropObject */ @@ -35,7 +37,10 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/require-default-prop.html' }, fixable: null, // or "code" or "whitespace" - schema: [] + schema: [], + messages: { + missingDefault: `Prop '{{propName}}' requires default value to be set.` + } }, /** @param {RuleContext} context */ create(context) { @@ -45,11 +50,11 @@ module.exports = { /** * Checks if the passed prop is required - * @param {ComponentObjectPropObject} prop - Property AST node for a single prop + * @param {ObjectExpression} propValue - ObjectExpression AST node for a single prop * @return {boolean} */ - function propIsRequired(prop) { - const propRequiredNode = prop.value.properties.find( + function propIsRequired(propValue) { + const propRequiredNode = propValue.properties.find( (p) => p.type === 'Property' && utils.getStaticPropertyName(p) === 'required' && @@ -62,11 +67,11 @@ module.exports = { /** * Checks if the passed prop has a default value - * @param {ComponentObjectPropObject} prop - Property AST node for a single prop + * @param {ObjectExpression} propValue - ObjectExpression AST node for a single prop * @return {boolean} */ - function propHasDefault(prop) { - const propDefaultNode = prop.value.properties.find( + function propHasDefault(propValue) { + const propDefaultNode = propValue.properties.find( (p) => p.type === 'Property' && utils.getStaticPropertyName(p) === 'default' ) @@ -75,32 +80,27 @@ module.exports = { } /** - * Finds all props that don't have a default value set - * @param {ComponentObjectProp[]} props - Vue component's "props" node - * @return {ComponentObjectProp[]} Array of props without "default" value + * Checks whether the given props that don't have a default value + * @param {ComponentObjectProp} prop Vue component's "props" node + * @return {boolean} */ - function findPropsWithoutDefaultValue(props) { - return props.filter((prop) => { - if (prop.value.type !== 'ObjectExpression') { - if (prop.value.type === 'Identifier') { - return NATIVE_TYPES.has(prop.value.name) - } - if ( - prop.value.type === 'CallExpression' || - prop.value.type === 'MemberExpression' - ) { - // OK - return false - } - // NG - return true + function isWithoutDefaultValue(prop) { + if (prop.value.type !== 'ObjectExpression') { + if (prop.value.type === 'Identifier') { + return NATIVE_TYPES.has(prop.value.name) + } + if ( + prop.value.type === 'CallExpression' || + prop.value.type === 'MemberExpression' + ) { + // OK + return false } + // NG + return true + } - return ( - !propIsRequired(/** @type {ComponentObjectPropObject} */ (prop)) && - !propHasDefault(/** @type {ComponentObjectPropObject} */ (prop)) - ) - }) + return !propIsRequired(prop.value) && !propHasDefault(prop.value) } /** @@ -145,46 +145,66 @@ module.exports = { } /** - * Excludes purely Boolean props from the Array - * @param {ComponentObjectProp[]} props - Array with props - * @return {ComponentObjectProp[]} + * @param {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} props + * @param {boolean} [withDefaults] + * @param { { [key: string]: Expression | undefined } } [withDefaultsExpressions] */ - function excludeBooleanProps(props) { - return props.filter((prop) => !isBooleanProp(prop)) + function processProps(props, withDefaults, withDefaultsExpressions) { + for (const prop of props) { + if (prop.type === 'object' && !prop.node.shorthand) { + if (!isWithoutDefaultValue(prop)) { + continue + } + if (isBooleanProp(prop)) { + continue + } + const propName = + prop.propName != null + ? prop.propName + : `[${context.getSourceCode().getText(prop.node.key)}]` + + context.report({ + node: prop.node, + messageId: `missingDefault`, + data: { + propName + } + }) + } else if ( + prop.type === 'type' && + withDefaults && + withDefaultsExpressions + ) { + if (!withDefaultsExpressions[prop.propName]) { + context.report({ + node: prop.node, + messageId: `missingDefault`, + data: { + propName: prop.propName + } + }) + } + } + } } // ---------------------------------------------------------------------- // Public // ---------------------------------------------------------------------- - return utils.executeOnVue(context, (obj) => { - const props = utils - .getComponentProps(obj) - .filter( - (prop) => - prop.value && - !(prop.node.type === 'Property' && prop.node.shorthand) - ) - - const propsWithoutDefault = findPropsWithoutDefaultValue( - /** @type {ComponentObjectProp[]} */ (props) - ) - const propsToReport = excludeBooleanProps(propsWithoutDefault) - - for (const prop of propsToReport) { - const propName = - prop.propName != null - ? prop.propName - : `[${context.getSourceCode().getText(prop.node.key)}]` - - context.report({ - node: prop.node, - message: `Prop '{{propName}}' requires default value to be set.`, - data: { - propName - } - }) - } - }) + return utils.compositingVisitors( + utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(node, props) { + processProps( + props, + utils.hasWithDefaults(node), + utils.getWithDefaultsPropExpressions(node) + ) + } + }), + utils.executeOnVue(context, (obj) => { + processProps(utils.getComponentProps(obj)) + }) + ) } } diff --git a/lib/utils/index.js b/lib/utils/index.js index 0b93ef9c4..082569748 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1177,19 +1177,20 @@ module.exports = { return scriptSetupVisitor }, + /** + * Checks whether given defineProps call node has withDefaults. + * @param {CallExpression} node The node of defineProps + * @returns {node is CallExpression & { parent: CallExpression }} + */ + hasWithDefaults, + /** * Gets a map of the expressions defined in withDefaults. * @param {CallExpression} node The node of defineProps * @returns { { [key: string]: Expression | undefined } } */ getWithDefaultsPropExpressions(node) { - if ( - !node.parent || - node.parent.type !== 'CallExpression' || - node.parent.arguments[0] !== node || - node.parent.callee.type !== 'Identifier' || - node.parent.callee.name !== 'withDefaults' - ) { + if (!hasWithDefaults(node)) { return {} } const param = node.parent.arguments[1] @@ -2422,6 +2423,21 @@ function hasDirective(node, name, argument) { return Boolean(getDirective(node, name, argument)) } +/** + * Checks whether given defineProps call node has withDefaults. + * @param {CallExpression} node The node of defineProps + * @returns {node is CallExpression & { parent: CallExpression }} + */ +function hasWithDefaults(node) { + return ( + node.parent && + node.parent.type === 'CallExpression' && + node.parent.arguments[0] === node && + node.parent.callee.type === 'Identifier' && + node.parent.callee.name === 'withDefaults' + ) +} + /** * Get all props by looking at all component's properties * @param {ObjectExpression|ArrayExpression} propsNode Object with props definition diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js index dcd551560..d93c5e5da 100644 --- a/tests/lib/rules/require-default-prop.js +++ b/tests/lib/rules/require-default-prop.js @@ -8,6 +8,7 @@ // Requirements // ------------------------------------------------------------------------------ +const semver = require('semver') const rule = require('../../../lib/rules/require-default-prop') const RuleTester = require('eslint').RuleTester const parserOptions = { @@ -181,6 +182,14 @@ ruleTester.run('require-default-prop', rule, { } ` }, + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'] + }`, + parserOptions + }, // sparse array { @@ -195,6 +204,83 @@ ruleTester.run('require-default-prop', rule, { } } ` + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + parserOptions + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + parserOptions + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ...parserOptions, + parser: require.resolve('@typescript-eslint/parser') + } + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ...parserOptions, + parser: require.resolve('@typescript-eslint/parser') + } + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ...parserOptions, + parser: require.resolve('@typescript-eslint/parser') + } } ], @@ -387,6 +473,53 @@ ruleTester.run('require-default-prop', rule, { "Prop 'baz' requires default value to be set.", "Prop 'bar1' requires default value to be set." ] - } + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + parserOptions, + errors: [ + { + message: "Prop 'foo' requires default value to be set.", + line: 4 + } + ] + }, + ...(semver.lt( + require('@typescript-eslint/parser/package.json').version, + '4.0.0' + ) + ? [] + : [ + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ...parserOptions, + parser: require.resolve('@typescript-eslint/parser') + }, + errors: [ + { + message: "Prop 'foo' requires default value to be set.", + line: 4 + } + ] + } + ]) ] })