Skip to content

Commit 8788bc8

Browse files
committed
Add support for props destructure to vue/require-default-prop rule
1 parent 8b877f7 commit 8788bc8

File tree

3 files changed

+187
-22
lines changed

3 files changed

+187
-22
lines changed

Diff for: lib/rules/require-default-prop.js

+46-21
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
/**
88
* @typedef {import('../utils').ComponentProp} ComponentProp
99
* @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
10+
* @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
1011
* @typedef {ComponentObjectProp & { value: ObjectExpression} } ComponentObjectPropObject
1112
*/
1213

@@ -137,18 +138,23 @@ module.exports = {
137138

138139
/**
139140
* @param {ComponentProp[]} props
140-
* @param {boolean} [withDefaults]
141-
* @param { { [key: string]: Expression | undefined } } [withDefaultsExpressions]
141+
* @param {(prop: ComponentObjectProp|ComponentTypeProp)=>boolean} [ignore]
142142
*/
143-
function processProps(props, withDefaults, withDefaultsExpressions) {
143+
function processProps(props, ignore) {
144144
for (const prop of props) {
145-
if (prop.type === 'object' && !prop.node.shorthand) {
145+
if (prop.type === 'object') {
146+
if (prop.node.shorthand) {
147+
continue
148+
}
146149
if (!isWithoutDefaultValue(prop)) {
147150
continue
148151
}
149152
if (isBooleanProp(prop)) {
150153
continue
151154
}
155+
if (ignore?.(prop)) {
156+
continue
157+
}
152158
const propName =
153159
prop.propName == null
154160
? `[${context.getSourceCode().getText(prop.node.key)}]`
@@ -161,38 +167,57 @@ module.exports = {
161167
propName
162168
}
163169
})
164-
} else if (
165-
prop.type === 'type' &&
166-
withDefaults &&
167-
withDefaultsExpressions
168-
) {
170+
} else if (prop.type === 'type') {
169171
if (prop.required) {
170172
continue
171173
}
172174
if (prop.types.length === 1 && prop.types[0] === 'Boolean') {
173175
continue
174176
}
175-
if (!withDefaultsExpressions[prop.propName]) {
176-
context.report({
177-
node: prop.node,
178-
messageId: `missingDefault`,
179-
data: {
180-
propName: prop.propName
181-
}
182-
})
177+
if (ignore?.(prop)) {
178+
continue
183179
}
180+
context.report({
181+
node: prop.node,
182+
messageId: `missingDefault`,
183+
data: {
184+
propName: prop.propName
185+
}
186+
})
184187
}
185188
}
186189
}
187190

188191
return utils.compositingVisitors(
189192
utils.defineScriptSetupVisitor(context, {
190193
onDefinePropsEnter(node, props) {
191-
processProps(
192-
props,
193-
utils.hasWithDefaults(node),
194+
const hasWithDefaults = utils.hasWithDefaults(node)
195+
const defaultsByWithDefaults =
194196
utils.getWithDefaultsPropExpressions(node)
195-
)
197+
const isUsingPropsDestructure = utils.isUsingPropsDestructure(node)
198+
const defaultsByAssignmentPatterns =
199+
utils.getDefaultPropExpressionsForPropsDestructure(node)
200+
201+
processProps(props, (prop) => {
202+
if (prop.type === 'type') {
203+
if (!hasWithDefaults) {
204+
// If don't use withDefaults(), exclude it from the report.
205+
return true
206+
}
207+
if (defaultsByWithDefaults[prop.propName]) {
208+
return true
209+
}
210+
}
211+
if (!isUsingPropsDestructure) {
212+
return false
213+
}
214+
if (prop.propName == null) {
215+
// If using Props Destructure but the property name cannot be determined,
216+
// it will be ignored.
217+
return true
218+
}
219+
return Boolean(defaultsByAssignmentPatterns[prop.propName])
220+
})
196221
}
197222
}),
198223
utils.executeOnVue(context, (obj) => {

Diff for: lib/utils/index.js

+63
Original file line numberDiff line numberDiff line change
@@ -1537,6 +1537,22 @@ module.exports = {
15371537
* @returns { { [key: string]: Property | undefined } }
15381538
*/
15391539
getWithDefaultsProps,
1540+
/**
1541+
* Gets the default definition nodes for defineProp
1542+
* using the props destructure with assignment pattern.
1543+
* @param {CallExpression} node The node of defineProps
1544+
* @returns { Record<string, {prop: AssignmentProperty , expression: Expression} | undefined> }
1545+
*/
1546+
getDefaultPropExpressionsForPropsDestructure,
1547+
/**
1548+
* Checks whether the given defineProps node is using Props Destructure.
1549+
* @param {CallExpression} node The node of defineProps
1550+
* @returns {boolean}
1551+
*/
1552+
isUsingPropsDestructure(node) {
1553+
const left = getLeftOfDefineProps(node)
1554+
return left?.type === 'ObjectPattern'
1555+
},
15401556

15411557
getVueObjectType,
15421558
/**
@@ -3144,6 +3160,53 @@ function getWithDefaultsProps(node) {
31443160
return result
31453161
}
31463162

3163+
/**
3164+
* Gets the default definition nodes for defineProp
3165+
* using the props destructure with assignment pattern.
3166+
* @param {CallExpression} node The node of defineProps
3167+
* @returns { Record<string, {prop: AssignmentProperty , expression: Expression} | undefined> }
3168+
*/
3169+
function getDefaultPropExpressionsForPropsDestructure(node) {
3170+
const left = getLeftOfDefineProps(node)
3171+
if (!left || left.type !== 'ObjectPattern') {
3172+
return {}
3173+
}
3174+
/** @type {ReturnType<typeof getDefaultPropExpressionsForPropsDestructure>} */
3175+
const result = Object.create(null)
3176+
for (const prop of left.properties) {
3177+
if (prop.type !== 'Property') continue
3178+
const value = prop.value
3179+
if (value.type !== 'AssignmentPattern') continue
3180+
const name = getStaticPropertyName(prop)
3181+
if (name != null) {
3182+
result[name] = { prop, expression: value.right }
3183+
}
3184+
}
3185+
return result
3186+
}
3187+
3188+
/**
3189+
* Gets the pattern of the left operand of defineProps.
3190+
* @param {CallExpression} node The node of defineProps
3191+
* @returns {Pattern | null} The pattern of the left operand of defineProps
3192+
*/
3193+
function getLeftOfDefineProps(node) {
3194+
let target = node
3195+
if (hasWithDefaults(target)) {
3196+
target = target.parent
3197+
}
3198+
if (!target.parent) {
3199+
return null
3200+
}
3201+
if (
3202+
target.parent.type === 'VariableDeclarator' &&
3203+
target.parent.init === target
3204+
) {
3205+
return target.parent.id
3206+
}
3207+
return null
3208+
}
3209+
31473210
/**
31483211
* Get all props from component options object.
31493212
* @param {ObjectExpression} componentObject Object with component definition

Diff for: tests/lib/rules/require-default-prop.js

+78-1
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,43 @@ ruleTester.run('require-default-prop', rule, {
351351
...languageOptions,
352352
parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
353353
}
354+
},
355+
{
356+
filename: 'test.vue',
357+
code: `
358+
<script setup>
359+
const {foo=42,bar=42} = defineProps({foo: Number, bar: {type: Number}})
360+
</script>
361+
`,
362+
languageOptions: {
363+
parser: require('vue-eslint-parser'),
364+
...languageOptions
365+
}
366+
},
367+
{
368+
filename: 'test.vue',
369+
code: `
370+
<script setup>
371+
const {foo,bar} = defineProps({foo: Boolean, bar: {type: Boolean}})
372+
</script>
373+
`,
374+
languageOptions: {
375+
parser: require('vue-eslint-parser'),
376+
...languageOptions
377+
}
378+
},
379+
{
380+
filename: 'test.vue',
381+
code: `
382+
<script setup>
383+
// ignore
384+
const {bar = 42, foo = 42} = defineProps({[x]: Number, bar: {type: Number}})
385+
</script>
386+
`,
387+
languageOptions: {
388+
parser: require('vue-eslint-parser'),
389+
...languageOptions
390+
}
354391
}
355392
],
356393

@@ -623,6 +660,46 @@ ruleTester.run('require-default-prop', rule, {
623660
}
624661
]
625662
}
626-
])
663+
]),
664+
{
665+
filename: 'test.vue',
666+
code: `
667+
<script setup>
668+
const {foo,bar} = defineProps({foo: Boolean, bar: {type: String}})
669+
</script>
670+
`,
671+
languageOptions: {
672+
parser: require('vue-eslint-parser'),
673+
...languageOptions
674+
},
675+
errors: [
676+
{
677+
message: "Prop 'bar' requires default value to be set.",
678+
line: 3
679+
}
680+
]
681+
},
682+
{
683+
filename: 'test.vue',
684+
code: `
685+
<script setup>
686+
const {foo,bar} = defineProps({foo: Number, bar: {type: Number}})
687+
</script>
688+
`,
689+
languageOptions: {
690+
parser: require('vue-eslint-parser'),
691+
...languageOptions
692+
},
693+
errors: [
694+
{
695+
message: "Prop 'foo' requires default value to be set.",
696+
line: 3
697+
},
698+
{
699+
message: "Prop 'bar' requires default value to be set.",
700+
line: 3
701+
}
702+
]
703+
}
627704
]
628705
})

0 commit comments

Comments
 (0)