Skip to content

Commit 8ffbe00

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

File tree

2 files changed

+178
-51
lines changed

2 files changed

+178
-51
lines changed

lib/rules/require-valid-default-prop.js

Lines changed: 115 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,59 @@ function getTypes(targetNode) {
7070
return []
7171
}
7272

73+
/**
74+
* Extracts default definitions using assignment patterns.
75+
* @param {CallExpression} node The node of defineProps
76+
* @returns { { [key: string]: Expression | undefined } }
77+
*/
78+
function getDefaultsPropExpressionsForAssignmentPatterns(node) {
79+
const left = getLeftOfDefineProps(node)
80+
if (!left || left.type !== 'ObjectPattern') {
81+
return {}
82+
}
83+
/** @type { { [key: string]: Expression | undefined } } */
84+
const result = Object.create(null)
85+
for (const prop of left.properties) {
86+
if (prop.type !== 'Property') continue
87+
const value = prop.value
88+
if (value.type !== 'AssignmentPattern') continue
89+
const defaultNode = value.right
90+
const name = utils.getStaticPropertyName(prop)
91+
if (name != null) {
92+
result[name] = defaultNode
93+
}
94+
}
95+
return result
96+
}
97+
98+
/**
99+
* Gets the pattern of the left operand of defineProps.
100+
* @param {CallExpression} node The node of defineProps
101+
* @returns {Pattern | null} The pattern of the left operand of defineProps
102+
*/
103+
function getLeftOfDefineProps(node) {
104+
let target = node
105+
if (
106+
target.parent &&
107+
target.parent.type === 'CallExpression' &&
108+
target.parent.arguments[0] === target &&
109+
target.parent.callee.type === 'Identifier' &&
110+
target.parent.callee.name === 'withDefaults'
111+
) {
112+
target = target.parent
113+
}
114+
if (!target.parent) {
115+
return null
116+
}
117+
if (
118+
target.parent.type === 'VariableDeclarator' &&
119+
target.parent.init === target
120+
) {
121+
return target.parent.id
122+
}
123+
return null
124+
}
125+
73126
module.exports = {
74127
meta: {
75128
type: 'suggestion',
@@ -250,71 +303,81 @@ module.exports = {
250303
}
251304

252305
/**
253-
* @param {(ComponentObjectDefineProp | ComponentTypeProp | ComponentInferTypeProp)[]} props
254-
* @param { { [key: string]: Expression | undefined } } withDefaults
306+
* @param {(ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp)[]} props
307+
* @param {(propName: string) => Expression[]} otherDefaultProvider
255308
*/
256-
function processPropDefs(props, withDefaults) {
309+
function processPropDefs(props, otherDefaultProvider) {
257310
/** @type {PropDefaultFunctionContext[]} */
258311
const propContexts = []
259312
for (const prop of props) {
260313
let typeList
261-
let defExpr
314+
/** @type {Expression[]} */
315+
const defExprList = []
262316
if (prop.type === 'object') {
263-
const type = getPropertyNode(prop.value, 'type')
264-
if (!type) continue
317+
if (prop.value.type === 'ObjectExpression') {
318+
const type = getPropertyNode(prop.value, 'type')
319+
if (!type) continue
265320

266-
typeList = getTypes(type.value)
321+
typeList = getTypes(type.value)
267322

268-
const def = getPropertyNode(prop.value, 'default')
269-
if (!def) continue
323+
const def = getPropertyNode(prop.value, 'default')
324+
if (!def) continue
270325

271-
defExpr = def.value
326+
defExprList.push(def.value)
327+
} else {
328+
typeList = getTypes(prop.value)
329+
}
272330
} else {
273331
typeList = prop.types
274-
defExpr = withDefaults[prop.propName]
275332
}
276-
if (!defExpr) continue
333+
if (prop.propName != null) {
334+
defExprList.push(...otherDefaultProvider(prop.propName))
335+
}
336+
337+
if (defExprList.length === 0) continue
277338

278339
const typeNames = new Set(
279340
typeList.filter((item) => NATIVE_TYPES.has(item))
280341
)
281342
// There is no native types detected
282343
if (typeNames.size === 0) continue
283344

284-
const defType = getValueType(defExpr)
345+
for (const defExpr of defExprList) {
346+
const defType = getValueType(defExpr)
285347

286-
if (!defType) continue
348+
if (!defType) continue
287349

288-
if (defType.function) {
289-
if (typeNames.has('Function')) {
290-
continue
291-
}
292-
if (defType.expression) {
293-
if (!defType.returnType || typeNames.has(defType.returnType)) {
350+
if (defType.function) {
351+
if (typeNames.has('Function')) {
294352
continue
295353
}
296-
report(defType.functionBody, prop, typeNames)
354+
if (defType.expression) {
355+
if (!defType.returnType || typeNames.has(defType.returnType)) {
356+
continue
357+
}
358+
report(defType.functionBody, prop, typeNames)
359+
} else {
360+
propContexts.push({
361+
prop,
362+
types: typeNames,
363+
default: defType
364+
})
365+
}
297366
} else {
298-
propContexts.push({
367+
if (
368+
typeNames.has(defType.type) &&
369+
!FUNCTION_VALUE_TYPES.has(defType.type)
370+
) {
371+
continue
372+
}
373+
report(
374+
defExpr,
299375
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
376+
[...typeNames].map((type) =>
377+
FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
378+
)
316379
)
317-
)
380+
}
318381
}
319382
}
320383
return propContexts
@@ -364,7 +427,7 @@ module.exports = {
364427
prop.type === 'object' && prop.value.type === 'ObjectExpression'
365428
)
366429
)
367-
const propContexts = processPropDefs(props, {})
430+
const propContexts = processPropDefs(props, () => [])
368431
vueObjectPropsContexts.set(obj, propContexts)
369432
},
370433
/**
@@ -402,18 +465,25 @@ module.exports = {
402465
const props = baseProps.filter(
403466
/**
404467
* @param {ComponentProp} prop
405-
* @returns {prop is ComponentObjectDefineProp | ComponentInferTypeProp | ComponentTypeProp}
468+
* @returns {prop is ComponentObjectProp | ComponentInferTypeProp | ComponentTypeProp}
406469
*/
407470
(prop) =>
408471
Boolean(
409472
prop.type === 'type' ||
410473
prop.type === 'infer-type' ||
411-
(prop.type === 'object' &&
412-
prop.value.type === 'ObjectExpression')
474+
prop.type === 'object'
413475
)
414476
)
415-
const defaults = utils.getWithDefaultsPropExpressions(node)
416-
const propContexts = processPropDefs(props, defaults)
477+
const defaultsByWithDefaults =
478+
utils.getWithDefaultsPropExpressions(node)
479+
const defaultsByAssignmentPatterns =
480+
getDefaultsPropExpressionsForAssignmentPatterns(node)
481+
const propContexts = processPropDefs(props, (propName) =>
482+
[
483+
defaultsByWithDefaults[propName],
484+
defaultsByAssignmentPatterns[propName]
485+
].filter(utils.isDef)
486+
)
417487
scriptSetupPropsContexts.push({ node, props: propContexts })
418488
},
419489
/**

tests/lib/rules/require-valid-default-prop.js

Lines changed: 63 additions & 6 deletions
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)