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 0a2dd4e22..dc7f3d280 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -1139,19 +1139,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]
@@ -2384,6 +2385,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
+ }
+ ]
+ }
+ ])
]
})