@@ -83,16 +83,74 @@ module.exports = function(context) {
83
83
) ;
84
84
}
85
85
86
+ /**
87
+ * Internal: Checks if the prop is declared
88
+ * @param {Object } declaredPropTypes Description of propTypes declared in the current component
89
+ * @param {String[] } keyList Dot separated name of the prop to check.
90
+ * @returns {Boolean } True if the prop is declared, false if not.
91
+ */
92
+ function _isDeclaredInComponent ( declaredPropTypes , keyList ) {
93
+ for ( var i = 0 , j = keyList . length ; i < j ; i ++ ) {
94
+ var key = keyList [ i ] ;
95
+ var propType = (
96
+ // Check if this key is declared
97
+ declaredPropTypes [ key ] ||
98
+ // If not, check if this type accepts any key
99
+ declaredPropTypes . __ANY_KEY__
100
+ ) ;
101
+
102
+ if ( ! propType ) {
103
+ // If it's a computed property, we can't make any further analysis, but is valid
104
+ return key === '__COMPUTED_PROP__' ;
105
+ }
106
+ if ( propType === true ) {
107
+ return true ;
108
+ }
109
+ // Consider every children as declared
110
+ if ( propType . children === true ) {
111
+ return true ;
112
+ }
113
+ if ( propType . acceptedProperties ) {
114
+ return key in propType . acceptedProperties ;
115
+ }
116
+ if ( propType . type === 'union' ) {
117
+ // If we fall in this case, we know there is at least one complex type in the union
118
+ if ( i + 1 >= j ) {
119
+ // this is the last key, accept everything
120
+ return true ;
121
+ }
122
+ // non trivial, check all of them
123
+ var unionTypes = propType . children ;
124
+ var unionPropType = { } ;
125
+ for ( var k = 0 , z = unionTypes . length ; k < z ; k ++ ) {
126
+ unionPropType [ key ] = unionTypes [ k ] ;
127
+ var isValid = _isDeclaredInComponent (
128
+ unionPropType ,
129
+ keyList . slice ( i )
130
+ ) ;
131
+ if ( isValid ) {
132
+ return true ;
133
+ }
134
+ }
135
+
136
+ // every possible union were invalid
137
+ return false ;
138
+ }
139
+ declaredPropTypes = propType . children ;
140
+ }
141
+ return true ;
142
+ }
143
+
86
144
/**
87
145
* Checks if the prop is declared
88
- * @param {String } name Name of the prop to check.
89
146
* @param {Object } component The component to process
147
+ * @param {String } name Dot separated name of the prop to check.
90
148
* @returns {Boolean } True if the prop is declared, false if not.
91
149
*/
92
150
function isDeclaredInComponent ( component , name ) {
93
- return (
94
- component . declaredPropTypes &&
95
- component . declaredPropTypes . indexOf ( name ) !== - 1
151
+ return _isDeclaredInComponent (
152
+ component . declaredPropTypes || { } ,
153
+ name . split ( '.' )
96
154
) ;
97
155
}
98
156
@@ -106,15 +164,169 @@ module.exports = function(context) {
106
164
return tokens . length && tokens [ 0 ] . value === '...' ;
107
165
}
108
166
167
+ /**
168
+ * Iterates through a properties node, like a customized forEach.
169
+ * @param {Object[] } properties Array of properties to iterate.
170
+ * @param {Function } fn Function to call on each property, receives property key
171
+ and property value. (key, value) => void
172
+ */
173
+ function iterateProperties ( properties , fn ) {
174
+ if ( properties . length && typeof fn === 'function' ) {
175
+ for ( var i = 0 , j = properties . length ; i < j ; i ++ ) {
176
+ var node = properties [ i ] ;
177
+ var key = node . key ;
178
+ var keyName = key . type === 'Identifier' ? key . name : key . value ;
179
+
180
+ var value = node . value ;
181
+ fn ( keyName , value ) ;
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Creates the representation of the React propTypes for the component.
188
+ * The representation is used to verify nested used properties.
189
+ * @param {ASTNode } value Node of the React.PropTypes for the desired propery
190
+ * @return {Object|Boolean } The representation of the declaration, true means
191
+ * the property is declared without the need for further analysis.
192
+ */
193
+ function buildReactDeclarationTypes ( value ) {
194
+ if (
195
+ value . type === 'MemberExpression' &&
196
+ value . property &&
197
+ value . property . name &&
198
+ value . property . name === 'isRequired'
199
+ ) {
200
+ value = value . object ;
201
+ }
202
+
203
+ // Verify React.PropTypes that are functions
204
+ if (
205
+ value . type === 'CallExpression' &&
206
+ value . callee &&
207
+ value . callee . property &&
208
+ value . callee . property . name &&
209
+ value . arguments &&
210
+ value . arguments . length > 0
211
+ ) {
212
+ var callName = value . callee . property . name ;
213
+ var argument = value . arguments [ 0 ] ;
214
+ switch ( callName ) {
215
+ case 'shape' :
216
+ if ( argument . type !== 'ObjectExpression' ) {
217
+ // Invalid proptype or cannot analyse statically
218
+ return true ;
219
+ }
220
+ var shapeTypeDefinition = {
221
+ type : 'shape' ,
222
+ children : { }
223
+ } ;
224
+ iterateProperties ( argument . properties , function ( childKey , childValue ) {
225
+ shapeTypeDefinition . children [ childKey ] = buildReactDeclarationTypes ( childValue ) ;
226
+ } ) ;
227
+ return shapeTypeDefinition ;
228
+ case 'arrayOf' :
229
+ return {
230
+ type : 'array' ,
231
+ children : {
232
+ // Accept only array prototype and computed properties
233
+ __ANY_KEY__ : {
234
+ acceptedProperties : Array . prototype
235
+ } ,
236
+ __COMPUTED_PROP__ : buildReactDeclarationTypes ( argument )
237
+ }
238
+ } ;
239
+ case 'objectOf' :
240
+ return {
241
+ type : 'object' ,
242
+ children : {
243
+ __ANY_KEY__ : buildReactDeclarationTypes ( argument )
244
+ }
245
+ } ;
246
+ case 'oneOfType' :
247
+ if (
248
+ ! argument . elements ||
249
+ ! argument . elements . length
250
+ ) {
251
+ // Invalid proptype or cannot analyse statically
252
+ return true ;
253
+ }
254
+ var unionTypeDefinition = {
255
+ type : 'union' ,
256
+ children : [ ]
257
+ } ;
258
+ for ( var i = 0 , j = argument . elements . length ; i < j ; i ++ ) {
259
+ var type = buildReactDeclarationTypes ( argument . elements [ i ] ) ;
260
+ // keep only complex type
261
+ if ( type !== true ) {
262
+ if ( type . children === true ) {
263
+ // every child is accepted for one type, abort type analysis
264
+ unionTypeDefinition . children = true ;
265
+ return unionTypeDefinition ;
266
+ }
267
+ unionTypeDefinition . children . push ( type ) ;
268
+ }
269
+ }
270
+ if ( unionTypeDefinition . length === 0 ) {
271
+ // no complex type found, simply accept everything
272
+ return true ;
273
+ }
274
+ return unionTypeDefinition ;
275
+ case 'instanceOf' :
276
+ return {
277
+ type : 'instance' ,
278
+ // Accept all children because we can't know what type they are
279
+ children : true
280
+ } ;
281
+ case 'oneOf' :
282
+ default :
283
+ return true ;
284
+ }
285
+ }
286
+ if (
287
+ value . type === 'MemberExpression' &&
288
+ value . property &&
289
+ value . property . name
290
+ ) {
291
+ var name = value . property . name ;
292
+ // React propTypes with limited possible properties
293
+ var propertiesMap = {
294
+ array : Array . prototype ,
295
+ bool : Boolean . prototype ,
296
+ func : Function . prototype ,
297
+ number : Number . prototype ,
298
+ string : String . prototype
299
+ } ;
300
+ if ( name in propertiesMap ) {
301
+ return {
302
+ type : name ,
303
+ children : {
304
+ __ANY_KEY__ : {
305
+ acceptedProperties : propertiesMap [ name ]
306
+ }
307
+ }
308
+ } ;
309
+ }
310
+ }
311
+ // Unknown property or accepts everything (any, object, ...)
312
+ return true ;
313
+ }
314
+
109
315
/**
110
316
* Mark a prop type as used
111
317
* @param {ASTNode } node The AST node being marked.
112
318
*/
113
- function markPropTypesAsUsed ( node ) {
114
- var component = componentList . getByNode ( context , node ) ;
115
- var usedPropTypes = component && component . usedPropTypes || [ ] ;
319
+ function markPropTypesAsUsed ( node , parentName ) {
116
320
var type ;
117
- if ( node . parent . property && node . parent . property . name && ! node . parent . computed ) {
321
+ var name = node . parent . computed ?
322
+ '__COMPUTED_PROP__'
323
+ : node . parent . property && node . parent . property . name ;
324
+ var fullName = parentName ? parentName + '.' + name : name ;
325
+
326
+ if ( node . parent . type === 'MemberExpression' ) {
327
+ markPropTypesAsUsed ( node . parent , fullName ) ;
328
+ }
329
+ if ( name && ! node . parent . computed ) {
118
330
type = 'direct' ;
119
331
} else if (
120
332
node . parent . parent . declarations &&
@@ -123,15 +335,17 @@ module.exports = function(context) {
123
335
) {
124
336
type = 'destructuring' ;
125
337
}
338
+ var component = componentList . getByNode ( context , node ) ;
339
+ var usedPropTypes = component && component . usedPropTypes || [ ] ;
126
340
127
341
switch ( type ) {
128
342
case 'direct' :
129
343
// Ignore Object methods
130
- if ( Object . prototype [ node . parent . property . name ] ) {
344
+ if ( Object . prototype [ name ] ) {
131
345
break ;
132
346
}
133
347
usedPropTypes . push ( {
134
- name : node . parent . property . name ,
348
+ name : fullName ,
135
349
node : node . parent . property
136
350
} ) ;
137
351
break ;
@@ -163,18 +377,39 @@ module.exports = function(context) {
163
377
*/
164
378
function markPropTypesAsDeclared ( node , propTypes ) {
165
379
var component = componentList . getByNode ( context , node ) ;
166
- var declaredPropTypes = component && component . declaredPropTypes || [ ] ;
380
+ var declaredPropTypes = component && component . declaredPropTypes || { } ;
167
381
var ignorePropsValidation = false ;
168
382
169
383
switch ( propTypes && propTypes . type ) {
170
384
case 'ObjectExpression' :
171
- for ( var i = 0 , j = propTypes . properties . length ; i < j ; i ++ ) {
172
- var key = propTypes . properties [ i ] . key ;
173
- declaredPropTypes . push ( key . type === 'Identifier' ? key . name : key . value ) ;
174
- }
385
+ iterateProperties ( propTypes . properties , function ( key , value ) {
386
+ declaredPropTypes [ key ] = buildReactDeclarationTypes ( value ) ;
387
+ } ) ;
175
388
break ;
176
389
case 'MemberExpression' :
177
- declaredPropTypes . push ( propTypes . property . name ) ;
390
+ var curDeclaredPropTypes = declaredPropTypes ;
391
+ // Walk the list of properties, until we reach the assignment
392
+ // ie: ClassX.propTypes.a.b.c = ...
393
+ while (
394
+ propTypes &&
395
+ propTypes . parent . type !== 'AssignmentExpression' &&
396
+ propTypes . property &&
397
+ curDeclaredPropTypes
398
+ ) {
399
+ var propName = propTypes . property . name ;
400
+ if ( propName in curDeclaredPropTypes ) {
401
+ curDeclaredPropTypes = curDeclaredPropTypes [ propName ] . children ;
402
+ propTypes = propTypes . parent ;
403
+ } else {
404
+ // This will crash at runtime because we haven't seen this key before
405
+ // stop this and do not declare it
406
+ propTypes = null ;
407
+ }
408
+ }
409
+ if ( propTypes ) {
410
+ curDeclaredPropTypes [ propTypes . property . name ] =
411
+ buildReactDeclarationTypes ( propTypes . parent . right ) ;
412
+ }
178
413
break ;
179
414
case null :
180
415
break ;
@@ -187,7 +422,6 @@ module.exports = function(context) {
187
422
declaredPropTypes : declaredPropTypes ,
188
423
ignorePropsValidation : ignorePropsValidation
189
424
} ) ;
190
-
191
425
}
192
426
193
427
/**
@@ -198,13 +432,16 @@ module.exports = function(context) {
198
432
var name ;
199
433
for ( var i = 0 , j = component . usedPropTypes . length ; i < j ; i ++ ) {
200
434
name = component . usedPropTypes [ i ] . name ;
201
- if ( isDeclaredInComponent ( component , name ) || isIgnored ( name ) ) {
435
+ if (
436
+ isIgnored ( name . split ( '.' ) . pop ( ) ) ||
437
+ isDeclaredInComponent ( component , name )
438
+ ) {
202
439
continue ;
203
440
}
204
441
context . report (
205
442
component . usedPropTypes [ i ] . node ,
206
443
component . name === componentUtil . DEFAULT_COMPONENT_NAME ? MISSING_MESSAGE : MISSING_MESSAGE_NAMED_COMP , {
207
- name : name ,
444
+ name : name . replace ( / \. _ _ C O M P U T E D _ P R O P _ _ / g , '[]' ) ,
208
445
component : component . name
209
446
}
210
447
) ;
0 commit comments