Skip to content

Commit 05b7559

Browse files
authored
Add support for props destructure to vue/require-valid-default-prop rule (#2551)
1 parent 8b877f7 commit 05b7559

File tree

3 files changed

+209
-51
lines changed

3 files changed

+209
-51
lines changed

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

+62-45
Original file line numberDiff line numberDiff line change
@@ -250,71 +250,81 @@ module.exports = {
250250
}
251251

252252
/**
253-
* @param {(ComponentObjectDefineProp | ComponentTypeProp | ComponentInferTypeProp)[]} props
254-
* @param { { [key: string]: Expression | undefined } } withDefaults
253+
* @param {(ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp)[]} props
254+
* @param {(propName: string) => Expression[]} otherDefaultProvider
255255
*/
256-
function processPropDefs(props, withDefaults) {
256+
function processPropDefs(props, otherDefaultProvider) {
257257
/** @type {PropDefaultFunctionContext[]} */
258258
const propContexts = []
259259
for (const prop of props) {
260260
let typeList
261-
let defExpr
261+
/** @type {Expression[]} */
262+
const defExprList = []
262263
if (prop.type === 'object') {
263-
const type = getPropertyNode(prop.value, 'type')
264-
if (!type) continue
264+
if (prop.value.type === 'ObjectExpression') {
265+
const type = getPropertyNode(prop.value, 'type')
266+
if (!type) continue
265267

266-
typeList = getTypes(type.value)
268+
typeList = getTypes(type.value)
267269

268-
const def = getPropertyNode(prop.value, 'default')
269-
if (!def) continue
270+
const def = getPropertyNode(prop.value, 'default')
271+
if (!def) continue
270272

271-
defExpr = def.value
273+
defExprList.push(def.value)
274+
} else {
275+
typeList = getTypes(prop.value)
276+
}
272277
} else {
273278
typeList = prop.types
274-
defExpr = withDefaults[prop.propName]
275279
}
276-
if (!defExpr) continue
280+
if (prop.propName != null) {
281+
defExprList.push(...otherDefaultProvider(prop.propName))
282+
}
283+
284+
if (defExprList.length === 0) continue
277285

278286
const typeNames = new Set(
279287
typeList.filter((item) => NATIVE_TYPES.has(item))
280288
)
281289
// There is no native types detected
282290
if (typeNames.size === 0) continue
283291

284-
const defType = getValueType(defExpr)
292+
for (const defExpr of defExprList) {
293+
const defType = getValueType(defExpr)
285294

286-
if (!defType) continue
295+
if (!defType) continue
287296

288-
if (defType.function) {
289-
if (typeNames.has('Function')) {
290-
continue
291-
}
292-
if (defType.expression) {
293-
if (!defType.returnType || typeNames.has(defType.returnType)) {
297+
if (defType.function) {
298+
if (typeNames.has('Function')) {
294299
continue
295300
}
296-
report(defType.functionBody, prop, typeNames)
301+
if (defType.expression) {
302+
if (!defType.returnType || typeNames.has(defType.returnType)) {
303+
continue
304+
}
305+
report(defType.functionBody, prop, typeNames)
306+
} else {
307+
propContexts.push({
308+
prop,
309+
types: typeNames,
310+
default: defType
311+
})
312+
}
297313
} else {
298-
propContexts.push({
314+
if (
315+
typeNames.has(defType.type) &&
316+
!FUNCTION_VALUE_TYPES.has(defType.type)
317+
) {
318+
continue
319+
}
320+
report(
321+
defExpr,
299322
prop,
300-
types: typeNames,
301-
default: defType
302-
})
303-
}
304-
} else {
305-
if (
306-
typeNames.has(defType.type) &&
307-
!FUNCTION_VALUE_TYPES.has(defType.type)
308-
) {
309-
continue
310-
}
311-
report(
312-
defExpr,
313-
prop,
314-
[...typeNames].map((type) =>
315-
FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
323+
[...typeNames].map((type) =>
324+
FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
325+
)
316326
)
317-
)
327+
}
318328
}
319329
}
320330
return propContexts
@@ -364,7 +374,7 @@ module.exports = {
364374
prop.type === 'object' && prop.value.type === 'ObjectExpression'
365375
)
366376
)
367-
const propContexts = processPropDefs(props, {})
377+
const propContexts = processPropDefs(props, () => [])
368378
vueObjectPropsContexts.set(obj, propContexts)
369379
},
370380
/**
@@ -402,18 +412,25 @@ module.exports = {
402412
const props = baseProps.filter(
403413
/**
404414
* @param {ComponentProp} prop
405-
* @returns {prop is ComponentObjectDefineProp | ComponentInferTypeProp | ComponentTypeProp}
415+
* @returns {prop is ComponentObjectProp | ComponentInferTypeProp | ComponentTypeProp}
406416
*/
407417
(prop) =>
408418
Boolean(
409419
prop.type === 'type' ||
410420
prop.type === 'infer-type' ||
411-
(prop.type === 'object' &&
412-
prop.value.type === 'ObjectExpression')
421+
prop.type === 'object'
413422
)
414423
)
415-
const defaults = utils.getWithDefaultsPropExpressions(node)
416-
const propContexts = processPropDefs(props, defaults)
424+
const defaultsByWithDefaults =
425+
utils.getWithDefaultsPropExpressions(node)
426+
const defaultsByAssignmentPatterns =
427+
utils.getDefaultPropExpressionsForPropsDestructure(node)
428+
const propContexts = processPropDefs(props, (propName) =>
429+
[
430+
defaultsByWithDefaults[propName],
431+
defaultsByAssignmentPatterns[propName]?.expression
432+
].filter(utils.isDef)
433+
)
417434
scriptSetupPropsContexts.push({ node, props: propContexts })
418435
},
419436
/**

Diff for: lib/utils/index.js

+84
Original file line numberDiff line numberDiff line change
@@ -1537,6 +1537,28 @@ 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+
},
1556+
/**
1557+
* Gets the props destructure property nodes for defineProp.
1558+
* @param {CallExpression} node The node of defineProps
1559+
* @returns { Record<string, AssignmentProperty | undefined> }
1560+
*/
1561+
getPropsDestructure,
15401562

15411563
getVueObjectType,
15421564
/**
@@ -3144,6 +3166,68 @@ function getWithDefaultsProps(node) {
31443166
return result
31453167
}
31463168

3169+
/**
3170+
* Gets the props destructure property nodes for defineProp.
3171+
* @param {CallExpression} node The node of defineProps
3172+
* @returns { Record<string, AssignmentProperty | undefined> }
3173+
*/
3174+
function getPropsDestructure(node) {
3175+
/** @type {ReturnType<typeof getPropsDestructure>} */
3176+
const result = Object.create(null)
3177+
const left = getLeftOfDefineProps(node)
3178+
if (!left || left.type !== 'ObjectPattern') {
3179+
return result
3180+
}
3181+
for (const prop of left.properties) {
3182+
if (prop.type !== 'Property') continue
3183+
const name = getStaticPropertyName(prop)
3184+
if (name != null) {
3185+
result[name] = prop
3186+
}
3187+
}
3188+
return result
3189+
}
3190+
3191+
/**
3192+
* Gets the default definition nodes for defineProp
3193+
* using the props destructure with assignment pattern.
3194+
* @param {CallExpression} node The node of defineProps
3195+
* @returns { Record<string, {prop: AssignmentProperty , expression: Expression} | undefined> }
3196+
*/
3197+
function getDefaultPropExpressionsForPropsDestructure(node) {
3198+
/** @type {ReturnType<typeof getDefaultPropExpressionsForPropsDestructure>} */
3199+
const result = Object.create(null)
3200+
for (const [name, prop] of Object.entries(getPropsDestructure(node))) {
3201+
if (!prop) continue
3202+
const value = prop.value
3203+
if (value.type !== 'AssignmentPattern') continue
3204+
result[name] = { prop, expression: value.right }
3205+
}
3206+
return result
3207+
}
3208+
3209+
/**
3210+
* Gets the pattern of the left operand of defineProps.
3211+
* @param {CallExpression} node The node of defineProps
3212+
* @returns {Pattern | null} The pattern of the left operand of defineProps
3213+
*/
3214+
function getLeftOfDefineProps(node) {
3215+
let target = node
3216+
if (hasWithDefaults(target)) {
3217+
target = target.parent
3218+
}
3219+
if (!target.parent) {
3220+
return null
3221+
}
3222+
if (
3223+
target.parent.type === 'VariableDeclarator' &&
3224+
target.parent.init === target
3225+
) {
3226+
return target.parent.id
3227+
}
3228+
return null
3229+
}
3230+
31473231
/**
31483232
* Get all props from component options object.
31493233
* @param {ObjectExpression} componentObject Object with component definition

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

+63-6
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,7 @@ ruleTester.run('require-valid-default-prop', rule, {
223223
parser: require('@typescript-eslint/parser'),
224224
ecmaVersion: 6,
225225
sourceType: 'module'
226-
},
227-
errors: errorMessage('function')
226+
}
228227
},
229228
{
230229
filename: 'test.vue',
@@ -241,8 +240,7 @@ ruleTester.run('require-valid-default-prop', rule, {
241240
parser: require('@typescript-eslint/parser'),
242241
ecmaVersion: 6,
243242
sourceType: 'module'
244-
},
245-
errors: errorMessage('function')
243+
}
246244
},
247245
{
248246
filename: 'test.vue',
@@ -259,8 +257,7 @@ ruleTester.run('require-valid-default-prop', rule, {
259257
parser: require('@typescript-eslint/parser'),
260258
ecmaVersion: 6,
261259
sourceType: 'module'
262-
},
263-
errors: errorMessage('function')
260+
}
264261
},
265262
{
266263
// https://github.com/vuejs/eslint-plugin-vue/issues/1853
@@ -304,6 +301,21 @@ ruleTester.run('require-valid-default-prop', rule, {
304301
})
305302
</script>`,
306303
...getTypeScriptFixtureTestOptions()
304+
},
305+
{
306+
filename: 'test.vue',
307+
code: `
308+
<script setup>
309+
const { foo = 'abc' } = defineProps({
310+
foo: {
311+
type: String,
312+
}
313+
})
314+
</script>
315+
`,
316+
languageOptions: {
317+
parser: require('vue-eslint-parser')
318+
}
307319
}
308320
],
309321

@@ -1041,6 +1053,51 @@ ruleTester.run('require-valid-default-prop', rule, {
10411053
}
10421054
],
10431055
...getTypeScriptFixtureTestOptions()
1056+
},
1057+
{
1058+
filename: 'test.vue',
1059+
code: `
1060+
<script setup>
1061+
const { foo = 123 } = defineProps({
1062+
foo: String
1063+
})
1064+
</script>
1065+
`,
1066+
languageOptions: {
1067+
parser: require('vue-eslint-parser')
1068+
},
1069+
errors: [
1070+
{
1071+
message: "Type of the default value for 'foo' prop must be a string.",
1072+
line: 3
1073+
}
1074+
]
1075+
},
1076+
{
1077+
filename: 'test.vue',
1078+
code: `
1079+
<script setup>
1080+
const { foo = 123 } = defineProps({
1081+
foo: {
1082+
type: String,
1083+
default: 123
1084+
}
1085+
})
1086+
</script>
1087+
`,
1088+
languageOptions: {
1089+
parser: require('vue-eslint-parser')
1090+
},
1091+
errors: [
1092+
{
1093+
message: "Type of the default value for 'foo' prop must be a string.",
1094+
line: 3
1095+
},
1096+
{
1097+
message: "Type of the default value for 'foo' prop must be a string.",
1098+
line: 6
1099+
}
1100+
]
10441101
}
10451102
]
10461103
})

0 commit comments

Comments
 (0)