Skip to content

Commit

Permalink
Update vue/require-default-prop rule to support <script setup> (#…
Browse files Browse the repository at this point in the history
…1545)

* Update `vue/require-default-prop` rule to support `<script setup>`

* fix test on node v8
  • Loading branch information
ota-meshi committed Jul 4, 2021
1 parent 86aff15 commit 9c8f293
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 72 deletions.
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 @@ -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]
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 9c8f293

Please sign in to comment.