Skip to content

Commit 0c2ecc8

Browse files
authored
Improved vue/require-valid-default-prop rule (#1160)
* WIP * Improved `require-valid-default-prop` rule. - Change `vue/require-valid-default-prop` rule to track the` return` statement in the `function` defined in `default`. - Change `vue/require-valid-default-prop` rule to check `BigInt`. - Improved the location of reporting errors in `vue/require-valid-default-prop` rule. * Add testcases
1 parent 2606a02 commit 0c2ecc8

File tree

3 files changed

+493
-68
lines changed

3 files changed

+493
-68
lines changed

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

+233-57
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,74 @@
55
'use strict'
66
const utils = require('../utils')
77

8+
/**
9+
* @typedef {import('vue-eslint-parser').AST.ESLintObjectExpression} ObjectExpression
10+
* @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
11+
* @typedef {import('vue-eslint-parser').AST.ESLintProperty} Property
12+
* @typedef {import('vue-eslint-parser').AST.ESLintBlockStatement} BlockStatement
13+
* @typedef {import('vue-eslint-parser').AST.ESLintPattern} Pattern
14+
*/
15+
/**
16+
* @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
17+
*/
18+
19+
// ----------------------------------------------------------------------
20+
// Helpers
21+
// ----------------------------------------------------------------------
22+
823
const NATIVE_TYPES = new Set([
924
'String',
1025
'Number',
1126
'Boolean',
1227
'Function',
1328
'Object',
1429
'Array',
15-
'Symbol'
30+
'Symbol',
31+
'BigInt'
1632
])
1733

34+
const FUNCTION_VALUE_TYPES = new Set([
35+
'Function',
36+
'Object',
37+
'Array'
38+
])
39+
40+
/**
41+
* @param {ObjectExpression} obj
42+
* @param {string} name
43+
* @returns {Property | null}
44+
*/
45+
function getPropertyNode (obj, name) {
46+
for (const p of obj.properties) {
47+
if (p.type === 'Property' &&
48+
!p.computed &&
49+
p.key.type === 'Identifier' &&
50+
p.key.name === name) {
51+
return p
52+
}
53+
}
54+
return null
55+
}
56+
57+
/**
58+
* @param {Expression | Pattern} node
59+
* @returns {string[]}
60+
*/
61+
function getTypes (node) {
62+
if (node.type === 'Identifier') {
63+
return [node.name]
64+
} else if (node.type === 'ArrayExpression') {
65+
return node.elements
66+
.filter(item => item.type === 'Identifier')
67+
.map(item => item.name)
68+
}
69+
return []
70+
}
71+
72+
function capitalize (text) {
73+
return text[0].toUpperCase() + text.slice(1)
74+
}
75+
1876
// ------------------------------------------------------------------------------
1977
// Rule Definition
2078
// ------------------------------------------------------------------------------
@@ -32,93 +90,211 @@ module.exports = {
3290
},
3391

3492
create (context) {
35-
// ----------------------------------------------------------------------
36-
// Helpers
37-
// ----------------------------------------------------------------------
38-
39-
function isPropertyIdentifier (node) {
40-
return node.type === 'Property' && node.key.type === 'Identifier'
41-
}
93+
/**
94+
* @typedef { { type: string, function: false } } StandardValueType
95+
* @typedef { { type: 'Function', function: true, expression: true, functionBody: BlockStatement, returnType: string | null } } FunctionExprValueType
96+
* @typedef { { type: 'Function', function: true, expression: false, functionBody: BlockStatement, returnTypes: ReturnType[] } } FunctionValueType
97+
* @typedef { ComponentObjectProp & { value: ObjectExpression } } ComponentObjectDefineProp
98+
* @typedef { { prop: ComponentObjectDefineProp, type: Set<string>, default: FunctionValueType } } PropDefaultFunctionContext
99+
* @typedef { { type: string, node: Expression } } ReturnType
100+
*/
42101

43-
function getPropertyNode (obj, name) {
44-
return obj.properties.find(p =>
45-
isPropertyIdentifier(p) &&
46-
p.key.name === name
47-
)
48-
}
102+
/**
103+
* @type {Map<ObjectExpression, PropDefaultFunctionContext[]>}
104+
*/
105+
const vueObjectPropsContexts = new Map()
49106

50-
function getTypes (node) {
51-
if (node.type === 'Identifier') {
52-
return [node.name]
53-
} else if (node.type === 'ArrayExpression') {
54-
return node.elements
55-
.filter(item => item.type === 'Identifier')
56-
.map(item => item.name)
57-
}
58-
return []
107+
/** @type { { upper: any, body: null | BlockStatement, returnTypes?: null | ReturnType[] } } */
108+
let scopeStack = { upper: null, body: null, returnTypes: null }
109+
function onFunctionEnter (node) {
110+
scopeStack = { upper: scopeStack, body: node.body, returnTypes: null }
59111
}
60112

61-
function ucFirst (text) {
62-
return text[0].toUpperCase() + text.slice(1)
113+
function onFunctionExit () {
114+
scopeStack = scopeStack.upper
63115
}
64116

117+
/**
118+
* @param {Expression | Pattern} node
119+
* @returns { StandardValueType | FunctionExprValueType | FunctionValueType | null }
120+
*/
65121
function getValueType (node) {
66122
if (node.type === 'CallExpression') { // Symbol(), Number() ...
67123
if (node.callee.type === 'Identifier' && NATIVE_TYPES.has(node.callee.name)) {
68-
return node.callee.name
124+
return {
125+
function: false,
126+
type: node.callee.name
127+
}
69128
}
70129
} else if (node.type === 'TemplateLiteral') { // String
71-
return 'String'
130+
return {
131+
function: false,
132+
type: 'String'
133+
}
72134
} else if (node.type === 'Literal') { // String, Boolean, Number
73-
if (node.value === null) return null
74-
const type = ucFirst(typeof node.value)
135+
if (node.value === null && !node.bigint) return null
136+
const type = node.bigint ? 'BigInt' : capitalize(typeof node.value)
75137
if (NATIVE_TYPES.has(type)) {
76-
return type
138+
return {
139+
function: false,
140+
type
141+
}
77142
}
78143
} else if (node.type === 'ArrayExpression') { // Array
79-
return 'Array'
144+
return {
145+
function: false,
146+
type: 'Array'
147+
}
80148
} else if (node.type === 'ObjectExpression') { // Object
81-
return 'Object'
149+
return {
150+
function: false,
151+
type: 'Object'
152+
}
153+
} else if (node.type === 'FunctionExpression') {
154+
return {
155+
function: true,
156+
expression: false,
157+
type: 'Function',
158+
functionBody: node.body,
159+
returnTypes: []
160+
}
161+
} else if (node.type === 'ArrowFunctionExpression') {
162+
if (node.expression) {
163+
const valueType = getValueType(node.body)
164+
return {
165+
function: true,
166+
expression: true,
167+
type: 'Function',
168+
functionBody: node.body,
169+
returnType: valueType ? valueType.type : null
170+
}
171+
} else {
172+
return {
173+
function: true,
174+
expression: false,
175+
type: 'Function',
176+
functionBody: node.body,
177+
returnTypes: []
178+
}
179+
}
82180
}
83-
// FunctionExpression, ArrowFunctionExpression
84181
return null
85182
}
86183

184+
/**
185+
* @param {*} node
186+
* @param {ComponentObjectProp} prop
187+
* @param {Iterable<string>} expectedTypeNames
188+
*/
189+
function report (node, prop, expectedTypeNames) {
190+
const propName = prop.propName != null ? prop.propName : `[${context.getSourceCode().getText(prop.key)}]`
191+
context.report({
192+
node,
193+
message: "Type of the default value for '{{name}}' prop must be a {{types}}.",
194+
data: {
195+
name: propName,
196+
types: Array.from(expectedTypeNames)
197+
.join(' or ')
198+
.toLowerCase()
199+
}
200+
})
201+
}
202+
87203
// ----------------------------------------------------------------------
88204
// Public
89205
// ----------------------------------------------------------------------
90206

91-
return utils.executeOnVue(context, obj => {
92-
const props = utils.getComponentProps(obj)
93-
.filter(prop => prop.key && prop.value && prop.value.type === 'ObjectExpression')
207+
return utils.defineVueVisitor(context,
208+
{
209+
onVueObjectEnter (obj) {
210+
/** @type {ComponentObjectDefineProp[]} */
211+
const props = utils.getComponentProps(obj)
212+
.filter(prop => prop.key && prop.value && prop.value.type === 'ObjectExpression')
213+
/** @type {PropDefaultFunctionContext[]} */
214+
const propContexts = []
215+
for (const prop of props) {
216+
const type = getPropertyNode(prop.value, 'type')
217+
if (!type) continue
94218

95-
for (const prop of props) {
96-
const type = getPropertyNode(prop.value, 'type')
97-
if (!type) continue
219+
const typeNames = new Set(getTypes(type.value)
220+
.filter(item => NATIVE_TYPES.has(item)))
98221

99-
const typeNames = new Set(getTypes(type.value)
100-
.map(item => item === 'Object' || item === 'Array' ? 'Function' : item) // Object and Array require function
101-
.filter(item => NATIVE_TYPES.has(item)))
222+
// There is no native types detected
223+
if (typeNames.size === 0) continue
102224

103-
// There is no native types detected
104-
if (typeNames.size === 0) continue
225+
const def = getPropertyNode(prop.value, 'default')
226+
if (!def) continue
105227

106-
const def = getPropertyNode(prop.value, 'default')
107-
if (!def) continue
228+
const defType = getValueType(def.value)
108229

109-
const defType = getValueType(def.value)
110-
if (!defType || typeNames.has(defType)) continue
230+
if (!defType) continue
111231

112-
const propName = prop.propName != null ? prop.propName : `[${context.getSourceCode().getText(prop.key)}]`
113-
context.report({
114-
node: def,
115-
message: "Type of the default value for '{{name}}' prop must be a {{types}}.",
116-
data: {
117-
name: propName,
118-
types: Array.from(typeNames).join(' or ').toLowerCase()
232+
if (!defType.function) {
233+
if (typeNames.has(defType.type)) {
234+
if (!FUNCTION_VALUE_TYPES.has(defType.type)) {
235+
continue
236+
}
237+
}
238+
report(
239+
def.value,
240+
prop,
241+
Array.from(typeNames).map(type => FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type)
242+
)
243+
} else {
244+
if (typeNames.has('Function')) {
245+
continue
246+
}
247+
if (defType.expression) {
248+
if (!defType.returnType || typeNames.has(defType.returnType)) {
249+
continue
250+
}
251+
report(
252+
defType.functionBody,
253+
prop,
254+
typeNames
255+
)
256+
} else {
257+
propContexts.push({
258+
prop,
259+
type: typeNames,
260+
default: defType
261+
})
262+
}
263+
}
119264
}
120-
})
265+
vueObjectPropsContexts.set(obj, propContexts)
266+
},
267+
':function' (node, { node: vueNode }) {
268+
onFunctionEnter(node)
269+
270+
for (const { default: defType } of vueObjectPropsContexts.get(vueNode)) {
271+
if (node.body === defType.functionBody) {
272+
scopeStack.returnTypes = defType.returnTypes
273+
}
274+
}
275+
},
276+
ReturnStatement (node) {
277+
if (scopeStack.returnTypes && node.argument) {
278+
const type = getValueType(node.argument)
279+
if (type) {
280+
scopeStack.returnTypes.push({
281+
type: type.type,
282+
node: node.argument
283+
})
284+
}
285+
}
286+
},
287+
':function:exit': onFunctionExit,
288+
onVueObjectExit (obj) {
289+
for (const { prop, type: typeNames, default: defType } of vueObjectPropsContexts.get(obj)) {
290+
for (const returnType of defType.returnTypes) {
291+
if (typeNames.has(returnType.type)) continue
292+
293+
report(returnType.node, prop, typeNames)
294+
}
295+
}
296+
}
121297
}
122-
})
298+
)
123299
}
124300
}

Diff for: lib/utils/index.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
/**
3030
* @typedef { {key: Literal | null, value: null, node: ArrayExpression['elements'][0], propName: string} } ComponentArrayProp
31-
* @typedef { {key: Property['key'], value: Property['value'], node: Property, propName: string} } ComponentObjectProp
31+
* @typedef { {key: Property['key'], value: Expression, node: Property, propName: string} } ComponentObjectProp
3232
*/
3333
/**
3434
* @typedef { {key: Literal | null, value: null, node: ArrayExpression['elements'][0], emitName: string} } ComponentArrayEmit
@@ -664,15 +664,17 @@ module.exports = {
664664
vueStack = vueStack.parent
665665
}
666666
}
667-
vueVisitor['Property[value.type=/^(Arrow)?FunctionExpression$/] > :function'] = (node) => {
668-
/** @type {Property} */
669-
const prop = node.parent
670-
if (vueStack && prop.parent === vueStack.node) {
671-
if (getStaticPropertyName(prop) === 'setup' && prop.value === node) {
672-
callVisitor('onSetupFunctionEnter', node)
667+
if (visitor.onSetupFunctionEnter) {
668+
vueVisitor['Property[value.type=/^(Arrow)?FunctionExpression$/] > :function'] = (node) => {
669+
/** @type {Property} */
670+
const prop = node.parent
671+
if (vueStack && prop.parent === vueStack.node) {
672+
if (getStaticPropertyName(prop) === 'setup' && prop.value === node) {
673+
callVisitor('onSetupFunctionEnter', node)
674+
}
673675
}
676+
callVisitor('Property[value.type=/^(Arrow)?FunctionExpression$/] > :function', node)
674677
}
675-
callVisitor('Property[value.type=/^(Arrow)?FunctionExpression$/] > :function', node)
676678
}
677679

678680
return vueVisitor

0 commit comments

Comments
 (0)