Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 0c2ecc8

Browse files
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

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

Lines changed: 233 additions & 57 deletions
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-
returntext[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+
constdef=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
}

‎lib/utils/index.js‎

Lines changed: 10 additions & 8 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /