5
5
'use strict'
6
6
const utils = require ( '../utils' )
7
7
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
+
8
23
const NATIVE_TYPES = new Set ( [
9
24
'String' ,
10
25
'Number' ,
11
26
'Boolean' ,
12
27
'Function' ,
13
28
'Object' ,
14
29
'Array' ,
15
- 'Symbol'
30
+ 'Symbol' ,
31
+ 'BigInt'
16
32
] )
17
33
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
+
18
76
// ------------------------------------------------------------------------------
19
77
// Rule Definition
20
78
// ------------------------------------------------------------------------------
@@ -32,93 +90,211 @@ module.exports = {
32
90
} ,
33
91
34
92
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
+ */
42
101
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 ( )
49
106
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 }
59
111
}
60
112
61
- function ucFirst ( text ) {
62
- return text [ 0 ] . toUpperCase ( ) + text . slice ( 1 )
113
+ function onFunctionExit ( ) {
114
+ scopeStack = scopeStack . upper
63
115
}
64
116
117
+ /**
118
+ * @param {Expression | Pattern } node
119
+ * @returns { StandardValueType | FunctionExprValueType | FunctionValueType | null }
120
+ */
65
121
function getValueType ( node ) {
66
122
if ( node . type === 'CallExpression' ) { // Symbol(), Number() ...
67
123
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
+ }
69
128
}
70
129
} else if ( node . type === 'TemplateLiteral' ) { // String
71
- return 'String'
130
+ return {
131
+ function : false ,
132
+ type : 'String'
133
+ }
72
134
} 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 )
75
137
if ( NATIVE_TYPES . has ( type ) ) {
76
- return type
138
+ return {
139
+ function : false ,
140
+ type
141
+ }
77
142
}
78
143
} else if ( node . type === 'ArrayExpression' ) { // Array
79
- return 'Array'
144
+ return {
145
+ function : false ,
146
+ type : 'Array'
147
+ }
80
148
} 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
+ }
82
180
}
83
- // FunctionExpression, ArrowFunctionExpression
84
181
return null
85
182
}
86
183
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
+
87
203
// ----------------------------------------------------------------------
88
204
// Public
89
205
// ----------------------------------------------------------------------
90
206
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
94
218
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 ) ) )
98
221
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
102
224
103
- // There is no native types detected
104
- if ( typeNames . size === 0 ) continue
225
+ const def = getPropertyNode ( prop . value , 'default' )
226
+ if ( ! def ) continue
105
227
106
- const def = getPropertyNode ( prop . value , 'default' )
107
- if ( ! def ) continue
228
+ const defType = getValueType ( def . value )
108
229
109
- const defType = getValueType ( def . value )
110
- if ( ! defType || typeNames . has ( defType ) ) continue
230
+ if ( ! defType ) continue
111
231
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
+ }
119
264
}
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
+ }
121
297
}
122
- } )
298
+ )
123
299
}
124
300
}
0 commit comments