66
77var Components = require ( '../util/Components' ) ;
88var 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' ) ;
8110
8211// ------------------------------------------------------------------------------
8312// Rule Definition
@@ -94,6 +23,189 @@ module.exports = {
9423 } ,
9524
9625 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+
97209 /**
98210 * Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is
99211 * marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations
@@ -151,34 +263,36 @@ module.exports = {
151263 }
152264
153265 /**
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 }
157269 */
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+ }
162274
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 ;
165279 }
166280
167- return variable . defs [ 0 ] . node . init ;
281+ addPropTypesToComponent ( component , getPropTypesFromTypeAnnotation ( node . params [ 0 ] . typeAnnotation , context ) ) ;
168282 }
169283
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 ;
179289 }
180290
181- return node ;
291+ addPropTypesToComponent ( component , getPropTypesFromTypeAnnotation ( node . typeAnnotation , context ) ) ;
292+ }
293+
294+ function isPropTypeAnnotation ( node ) {
295+ return ( node . key . name === 'props' && ! ! node . typeAnnotation ) ;
182296 }
183297
184298 /**
@@ -344,10 +458,19 @@ module.exports = {
344458 // };
345459 // }
346460 ClassProperty : function ( node ) {
461+ if ( isPropTypeAnnotation ( node ) ) {
462+ handlePropTypeAnnotationClassProperty ( node ) ;
463+ return ;
464+ }
465+
347466 if ( ! node . static ) {
348467 return ;
349468 }
350469
470+ if ( ! node . value ) {
471+ return ;
472+ }
473+
351474 var isPropType = isPropTypesDeclaration ( node . key ) ;
352475 var isDefaultProp = isDefaultPropsDeclaration ( node . key ) ;
353476
@@ -423,6 +546,11 @@ module.exports = {
423546 } ) ;
424547 } ,
425548
549+ // Check for type annotations in stateless components
550+ FunctionDeclaration : handleStatelessComponent ,
551+ ArrowFunctionExpression : handleStatelessComponent ,
552+ FunctionExpression : handleStatelessComponent ,
553+
426554 'Program:exit' : function ( ) {
427555 var list = components . list ( ) ;
428556
0 commit comments