55'use strict'
66const 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+
823const 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}
0 commit comments