Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update vue/require-default-prop rule to support <script setup> #1545

Merged
merged 2 commits into from Jul 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
148 changes: 84 additions & 64 deletions lib/rules/require-default-prop.js
Expand Up @@ -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
*/

Expand Down Expand Up @@ -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) {
Expand All @@ -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' &&
Expand All @@ -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'
)
Expand All @@ -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)
}

/**
Expand Down Expand Up @@ -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))
})
)
}
}
30 changes: 23 additions & 7 deletions lib/utils/index.js
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down