6
6
7
7
var Components = require ( '../util/Components' ) ;
8
8
var variableUtil = require ( '../util/variable' ) ;
9
-
10
- // ------------------------------------------------------------------------------
11
- // Helpers
12
- // ------------------------------------------------------------------------------
13
-
14
- /**
15
- * Checks if the Identifier node passed in looks like a propTypes declaration.
16
- * @param {ASTNode } node The node to check. Must be an Identifier node.
17
- * @returns {Boolean } `true` if the node is a propTypes declaration, `false` if not
18
- */
19
- function isPropTypesDeclaration ( node ) {
20
- return node . type === 'Identifier' && node . name === 'propTypes' ;
21
- }
22
-
23
- /**
24
- * Checks if the Identifier node passed in looks like a defaultProps declaration.
25
- * @param {ASTNode } node The node to check. Must be an Identifier node.
26
- * @returns {Boolean } `true` if the node is a defaultProps declaration, `false` if not
27
- */
28
- function isDefaultPropsDeclaration ( node ) {
29
- return node . type === 'Identifier' &&
30
- ( node . name === 'defaultProps' || node . name === 'getDefaultProps' ) ;
31
- }
32
-
33
- /**
34
- * Checks if the PropTypes MemberExpression node passed in declares a required propType.
35
- * @param {ASTNode } propTypeExpression node to check. Must be a `PropTypes` MemberExpression.
36
- * @returns {Boolean } `true` if this PropType is required, `false` if not.
37
- */
38
- function isRequiredPropType ( propTypeExpression ) {
39
- return propTypeExpression . type === 'MemberExpression' && propTypeExpression . property . name === 'isRequired' ;
40
- }
41
-
42
- /**
43
- * Extracts a PropType from an ObjectExpression node.
44
- * @param {ASTNode } objectExpression ObjectExpression node.
45
- * @returns {Object } Object representation of a PropType, to be consumed by `addPropTypesToComponent`.
46
- */
47
- function getPropTypesFromObjectExpression ( objectExpression ) {
48
- var props = objectExpression . properties . filter ( function ( property ) {
49
- return property . type !== 'ExperimentalSpreadProperty' ;
50
- } ) ;
51
-
52
- return props . map ( function ( property ) {
53
- return {
54
- name : property . key . name ,
55
- isRequired : isRequiredPropType ( property . value ) ,
56
- node : property
57
- } ;
58
- } ) ;
59
- }
60
-
61
- /**
62
- * Extracts a DefaultProp from an ObjectExpression node.
63
- * @param {ASTNode } objectExpression ObjectExpression node.
64
- * @returns {Object|string } Object representation of a defaultProp, to be consumed by
65
- * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
66
- * from this ObjectExpression can't be resolved.
67
- */
68
- function getDefaultPropsFromObjectExpression ( objectExpression ) {
69
- var hasSpread = objectExpression . properties . find ( function ( property ) {
70
- return property . type === 'ExperimentalSpreadProperty' ;
71
- } ) ;
72
-
73
- if ( hasSpread ) {
74
- return 'unresolved' ;
75
- }
76
-
77
- return objectExpression . properties . map ( function ( property ) {
78
- return property . key . name ;
79
- } ) ;
80
- }
9
+ var annotations = require ( '../util/annotations' ) ;
81
10
82
11
// ------------------------------------------------------------------------------
83
12
// Rule Definition
@@ -94,6 +23,189 @@ module.exports = {
94
23
} ,
95
24
96
25
create : Components . detect ( function ( context , components , utils ) {
26
+ /**
27
+ * Checks if the Identifier node passed in looks like a propTypes declaration.
28
+ * @param {ASTNode } node The node to check. Must be an Identifier node.
29
+ * @returns {Boolean } `true` if the node is a propTypes declaration, `false` if not
30
+ */
31
+ function isPropTypesDeclaration ( node ) {
32
+ return node . type === 'Identifier' && node . name === 'propTypes' ;
33
+ }
34
+
35
+ /**
36
+ * Checks if the Identifier node passed in looks like a defaultProps declaration.
37
+ * @param {ASTNode } node The node to check. Must be an Identifier node.
38
+ * @returns {Boolean } `true` if the node is a defaultProps declaration, `false` if not
39
+ */
40
+ function isDefaultPropsDeclaration ( node ) {
41
+ return node . type === 'Identifier' &&
42
+ ( node . name === 'defaultProps' || node . name === 'getDefaultProps' ) ;
43
+ }
44
+
45
+ /**
46
+ * Checks if the PropTypes MemberExpression node passed in declares a required propType.
47
+ * @param {ASTNode } propTypeExpression node to check. Must be a `PropTypes` MemberExpression.
48
+ * @returns {Boolean } `true` if this PropType is required, `false` if not.
49
+ */
50
+ function isRequiredPropType ( propTypeExpression ) {
51
+ return propTypeExpression . type === 'MemberExpression' && propTypeExpression . property . name === 'isRequired' ;
52
+ }
53
+
54
+ /**
55
+ * Find a variable by name in the current scope.
56
+ * @param {string } name Name of the variable to look for.
57
+ * @returns {ASTNode|null } Return null if the variable could not be found, ASTNode otherwise.
58
+ */
59
+ function findVariableByName ( name ) {
60
+ var variable = variableUtil . variablesInScope ( context ) . find ( function ( item ) {
61
+ return item . name === name ;
62
+ } ) ;
63
+
64
+ if ( ! variable || ! variable . defs [ 0 ] || ! variable . defs [ 0 ] . node ) {
65
+ return null ;
66
+ }
67
+
68
+ if ( variable . defs [ 0 ] . node . type === 'TypeAlias' ) {
69
+ return variable . defs [ 0 ] . node . right ;
70
+ }
71
+
72
+ // FIXME(vitorbal): is this needed?
73
+ if ( ! variable . defs [ 0 ] . node . init ) {
74
+ return null ;
75
+ }
76
+
77
+ return variable . defs [ 0 ] . node . init ;
78
+ }
79
+
80
+ /**
81
+ * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
82
+ * an Identifier, then the node is simply returned.
83
+ * @param {ASTNode } node The node to resolve.
84
+ * @returns {ASTNode|null } Return null if the value could not be resolved, ASTNode otherwise.
85
+ */
86
+ function resolveNodeValue ( node ) {
87
+ if ( node . type === 'Identifier' ) {
88
+ return findVariableByName ( node . name ) ;
89
+ }
90
+
91
+ return node ;
92
+ }
93
+
94
+ /**
95
+ * Tries to find the definition of a GenericTypeAnnotation in the current scope.
96
+ * @param {ASTNode } node The node GenericTypeAnnotation node to resolve.
97
+ * @return {ASTNode|null } Return null if definition cannot be found, ASTNode otherwise.
98
+ */
99
+ function resolveGenericTypeAnnotation ( node ) {
100
+ if ( node . type !== 'GenericTypeAnnotation' || node . id . type !== 'Identifier' ) {
101
+ return null ;
102
+ }
103
+
104
+ return findVariableByName ( node . id . name ) ;
105
+ }
106
+
107
+ function resolveUnionTypeAnnotation ( node ) {
108
+ // Go through all the union and resolve any generic types.
109
+ return node . types . map ( function ( annotation ) {
110
+ if ( annotation . type === 'GenericTypeAnnotation' ) {
111
+ return resolveGenericTypeAnnotation ( annotation ) ;
112
+ }
113
+
114
+ return annotation ;
115
+ } ) ;
116
+ }
117
+
118
+ /**
119
+ * Extracts a PropType from an ObjectExpression node.
120
+ * @param {ASTNode } objectExpression ObjectExpression node.
121
+ * @returns {Object[] } Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
122
+ */
123
+ function getPropTypesFromObjectExpression ( objectExpression ) {
124
+ var props = objectExpression . properties . filter ( function ( property ) {
125
+ return property . type !== 'ExperimentalSpreadProperty' ;
126
+ } ) ;
127
+
128
+ return props . map ( function ( property ) {
129
+ return {
130
+ name : property . key . name ,
131
+ isRequired : isRequiredPropType ( property . value ) ,
132
+ node : property
133
+ } ;
134
+ } ) ;
135
+ }
136
+
137
+ /**
138
+ * Extracts a PropType from a TypeAnnotation node.
139
+ * @param {ASTNode } node TypeAnnotation node.
140
+ * @returns {Object[] } Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
141
+ */
142
+ function getPropTypesFromTypeAnnotation ( node ) {
143
+ var properties ;
144
+
145
+ switch ( node . typeAnnotation . type ) {
146
+ case 'GenericTypeAnnotation' :
147
+ var annotation = resolveGenericTypeAnnotation ( node . typeAnnotation ) ;
148
+ properties = annotation ? annotation . properties : [ ] ;
149
+ break ;
150
+
151
+ case 'UnionTypeAnnotation' :
152
+ var union = resolveUnionTypeAnnotation ( node . typeAnnotation ) ;
153
+ properties = union . reduce ( function ( acc , curr ) {
154
+ if ( ! curr ) {
155
+ return acc ;
156
+ }
157
+
158
+ return acc . concat ( curr . properties ) ;
159
+ } , [ ] ) ;
160
+ break ;
161
+
162
+ case 'ObjectTypeAnnotation' :
163
+ properties = node . typeAnnotation . properties ;
164
+ break ;
165
+
166
+ default :
167
+ properties = [ ] ;
168
+ break ;
169
+ }
170
+
171
+ var props = properties . filter ( function ( property ) {
172
+ return property . type === 'ObjectTypeProperty' ;
173
+ } ) ;
174
+
175
+ return props . map ( function ( property ) {
176
+ // the `key` property is not present in ObjectTypeProperty nodes, so we need to get the key name manually.
177
+ var tokens = context . getFirstTokens ( property , 1 ) ;
178
+ var name = tokens [ 0 ] . value ;
179
+
180
+ return {
181
+ name : name ,
182
+ isRequired : ! property . optional ,
183
+ node : property
184
+ } ;
185
+ } ) ;
186
+ }
187
+
188
+ /**
189
+ * Extracts a DefaultProp from an ObjectExpression node.
190
+ * @param {ASTNode } objectExpression ObjectExpression node.
191
+ * @returns {Object|string } Object representation of a defaultProp, to be consumed by
192
+ * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
193
+ * from this ObjectExpression can't be resolved.
194
+ */
195
+ function getDefaultPropsFromObjectExpression ( objectExpression ) {
196
+ var hasSpread = objectExpression . properties . find ( function ( property ) {
197
+ return property . type === 'ExperimentalSpreadProperty' ;
198
+ } ) ;
199
+
200
+ if ( hasSpread ) {
201
+ return 'unresolved' ;
202
+ }
203
+
204
+ return objectExpression . properties . map ( function ( property ) {
205
+ return property . key . name ;
206
+ } ) ;
207
+ }
208
+
97
209
/**
98
210
* Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is
99
211
* marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations
@@ -151,34 +263,36 @@ module.exports = {
151
263
}
152
264
153
265
/**
154
- * Find a variable by name in the current scope .
155
- * @param {string } name Name of the variable to look for.
156
- * @returns { ASTNode|null } Return null if the variable could not be found, ASTNode otherwise.
266
+ * Tries to find a props type annotation in a stateless component .
267
+ * @param {ASTNode } node The AST node to look for a props type annotation .
268
+ * @return { void }
157
269
*/
158
- function findVariableByName ( name ) {
159
- var variable = variableUtil . variablesInScope ( context ) . find ( function ( item ) {
160
- return item . name === name ;
161
- } ) ;
270
+ function handleStatelessComponent ( node ) {
271
+ if ( ! node . params || ! node . params . length || ! annotations . isAnnotatedFunctionPropsDeclaration ( node , context ) ) {
272
+ return ;
273
+ }
162
274
163
- if ( ! variable || ! variable . defs [ 0 ] || ! variable . defs [ 0 ] . node . init ) {
164
- return null ;
275
+ // find component this props annotation belongs to
276
+ var component = components . get ( utils . getParentStatelessComponent ( ) ) ;
277
+ if ( ! component ) {
278
+ return ;
165
279
}
166
280
167
- return variable . defs [ 0 ] . node . init ;
281
+ addPropTypesToComponent ( component , getPropTypesFromTypeAnnotation ( node . params [ 0 ] . typeAnnotation , context ) ) ;
168
282
}
169
283
170
- /**
171
- * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
172
- * an Identifier, then the node is simply returned.
173
- * @param {ASTNode } node The node to resolve.
174
- * @returns {ASTNode|null } Return null if the value could not be resolved, ASTNode otherwise.
175
- */
176
- function resolveNodeValue ( node ) {
177
- if ( node . type === 'Identifier' ) {
178
- return findVariableByName ( node . name ) ;
284
+ function handlePropTypeAnnotationClassProperty ( node ) {
285
+ // find component this props annotation belongs to
286
+ var component = components . get ( utils . getParentES6Component ( ) ) ;
287
+ if ( ! component ) {
288
+ return ;
179
289
}
180
290
181
- return node ;
291
+ addPropTypesToComponent ( component , getPropTypesFromTypeAnnotation ( node . typeAnnotation , context ) ) ;
292
+ }
293
+
294
+ function isPropTypeAnnotation ( node ) {
295
+ return ( node . key . name === 'props' && ! ! node . typeAnnotation ) ;
182
296
}
183
297
184
298
/**
@@ -344,10 +458,19 @@ module.exports = {
344
458
// };
345
459
// }
346
460
ClassProperty : function ( node ) {
461
+ if ( isPropTypeAnnotation ( node ) ) {
462
+ handlePropTypeAnnotationClassProperty ( node ) ;
463
+ return ;
464
+ }
465
+
347
466
if ( ! node . static ) {
348
467
return ;
349
468
}
350
469
470
+ if ( ! node . value ) {
471
+ return ;
472
+ }
473
+
351
474
var isPropType = isPropTypesDeclaration ( node . key ) ;
352
475
var isDefaultProp = isDefaultPropsDeclaration ( node . key ) ;
353
476
@@ -423,6 +546,11 @@ module.exports = {
423
546
} ) ;
424
547
} ,
425
548
549
+ // Check for type annotations in stateless components
550
+ FunctionDeclaration : handleStatelessComponent ,
551
+ ArrowFunctionExpression : handleStatelessComponent ,
552
+ FunctionExpression : handleStatelessComponent ,
553
+
426
554
'Program:exit' : function ( ) {
427
555
var list = components . list ( ) ;
428
556
0 commit comments