b397f79d73975a2553229a9dcde559ebe9317a2b
[platform/framework/web/crosswalk-tizen.git] /
1 /**
2  * @fileoverview Prevent missing props validation in a React component definition
3  * @author Yannick Croissant
4  */
5 'use strict';
6
7 // As for exceptions for props.children or props.className (and alike) look at
8 // https://github.com/yannickcr/eslint-plugin-react/issues/7
9
10 var componentUtil = require('../util/component');
11 var ComponentList = componentUtil.List;
12
13 // ------------------------------------------------------------------------------
14 // Rule Definition
15 // ------------------------------------------------------------------------------
16
17 module.exports = function(context) {
18
19   var configuration = context.options[0] || {};
20   var ignored = configuration.ignore || [];
21
22   var componentList = new ComponentList();
23
24   var MISSING_MESSAGE = '\'{{name}}\' is missing in props validation';
25   var MISSING_MESSAGE_NAMED_COMP = '\'{{name}}\' is missing in props validation for {{component}}';
26
27   /**
28    * Checks if we are using a prop
29    * @param {ASTNode} node The AST node being checked.
30    * @returns {Boolean} True if we are using a prop, false if not.
31    */
32   function isPropTypesUsage(node) {
33     return Boolean(
34       node.object.type === 'ThisExpression' &&
35       node.property.name === 'props'
36     );
37   }
38
39   /**
40    * Checks if we are declaring a prop
41    * @param {ASTNode} node The AST node being checked.
42    * @returns {Boolean} True if we are declaring a prop, false if not.
43    */
44   function isPropTypesDeclaration(node) {
45
46     // Special case for class properties
47     // (babel-eslint does not expose property name so we have to rely on tokens)
48     if (node.type === 'ClassProperty') {
49       var tokens = context.getFirstTokens(node, 2);
50       if (tokens[0].value === 'propTypes' || tokens[1].value === 'propTypes') {
51         return true;
52       }
53       return false;
54     }
55
56     return Boolean(
57       node &&
58       node.name === 'propTypes'
59     );
60
61   }
62
63   /**
64    * Checks if the prop is ignored
65    * @param {String} name Name of the prop to check.
66    * @returns {Boolean} True if the prop is ignored, false if not.
67    */
68   function isIgnored(name) {
69     return ignored.indexOf(name) !== -1;
70   }
71
72   /**
73    * Checks if the component must be validated
74    * @param {Object} component The component to process
75    * @returns {Boolean} True if the component must be validated, false if not.
76    */
77   function mustBeValidated(component) {
78     return (
79       component &&
80       component.isReactComponent &&
81       component.usedPropTypes &&
82       !component.ignorePropsValidation
83     );
84   }
85
86   /**
87    * Checks if the prop is declared
88    * @param {String} name Name of the prop to check.
89    * @param {Object} component The component to process
90    * @returns {Boolean} True if the prop is declared, false if not.
91    */
92   function isDeclaredInComponent(component, name) {
93     return (
94       component.declaredPropTypes &&
95       component.declaredPropTypes.indexOf(name) !== -1
96     );
97   }
98
99   /**
100    * Checks if the prop has spread operator.
101    * @param {ASTNode} node The AST node being marked.
102    * @returns {Boolean} True if the prop has spread operator, false if not.
103    */
104   function hasSpreadOperator(node) {
105     var tokens = context.getTokens(node);
106     return tokens.length && tokens[0].value === '...';
107   }
108
109   /**
110    * Mark a prop type as used
111    * @param {ASTNode} node The AST node being marked.
112    */
113   function markPropTypesAsUsed(node) {
114     var component = componentList.getByNode(context, node);
115     var usedPropTypes = component && component.usedPropTypes || [];
116     var type;
117     if (node.parent.property && node.parent.property.name && !node.parent.computed) {
118       type = 'direct';
119     } else if (
120       node.parent.parent.declarations &&
121       node.parent.parent.declarations[0].id.properties &&
122       node.parent.parent.declarations[0].id.properties[0].key.name
123     ) {
124       type = 'destructuring';
125     }
126
127     switch (type) {
128       case 'direct':
129         usedPropTypes.push({
130           name: node.parent.property.name,
131           node: node
132         });
133         break;
134       case 'destructuring':
135         var properties = node.parent.parent.declarations[0].id.properties;
136         for (var i = 0, j = properties.length; i < j; i++) {
137           if (hasSpreadOperator(properties[i])) {
138             continue;
139           }
140           usedPropTypes.push({
141             name: properties[i].key.name,
142             node: node
143           });
144         }
145         break;
146       default:
147         break;
148     }
149
150     componentList.set(context, node, {
151       usedPropTypes: usedPropTypes
152     });
153   }
154
155   /**
156    * Mark a prop type as declared
157    * @param {ASTNode} node The AST node being checked.
158    * @param {propTypes} node The AST node containing the proptypes
159    */
160   function markPropTypesAsDeclared(node, propTypes) {
161     var component = componentList.getByNode(context, node);
162     var declaredPropTypes = component && component.declaredPropTypes || [];
163     var ignorePropsValidation = false;
164
165     switch (propTypes.type) {
166       case 'ObjectExpression':
167         for (var i = 0, j = propTypes.properties.length; i < j; i++) {
168           declaredPropTypes.push(propTypes.properties[i].key.name);
169         }
170         break;
171       case 'MemberExpression':
172         declaredPropTypes.push(propTypes.property.name);
173         break;
174       default:
175         ignorePropsValidation = true;
176         break;
177     }
178
179     componentList.set(context, node, {
180       declaredPropTypes: declaredPropTypes,
181       ignorePropsValidation: ignorePropsValidation
182     });
183
184   }
185
186   /**
187    * Reports undeclared proptypes for a given component
188    * @param {Object} component The component to process
189    */
190   function reportUndeclaredPropTypes(component) {
191     var name;
192     for (var i = 0, j = component.usedPropTypes.length; i < j; i++) {
193       name = component.usedPropTypes[i].name;
194       if (isDeclaredInComponent(component, name) || isIgnored(name)) {
195         continue;
196       }
197       context.report(
198         component.usedPropTypes[i].node,
199         component.name === componentUtil.DEFAULT_COMPONENT_NAME ? MISSING_MESSAGE : MISSING_MESSAGE_NAMED_COMP, {
200           name: name,
201           component: component.name
202         }
203       );
204     }
205   }
206
207   // --------------------------------------------------------------------------
208   // Public
209   // --------------------------------------------------------------------------
210
211   return {
212
213     ClassProperty: function(node) {
214       if (!isPropTypesDeclaration(node)) {
215         return;
216       }
217
218       markPropTypesAsDeclared(node, node.value);
219     },
220
221     MemberExpression: function(node) {
222       var type;
223       if (isPropTypesUsage(node)) {
224         type = 'usage';
225       } else if (isPropTypesDeclaration(node.property)) {
226         type = 'declaration';
227       }
228
229       switch (type) {
230         case 'usage':
231           markPropTypesAsUsed(node);
232           break;
233         case 'declaration':
234           var component = componentList.getByName(node.object.name);
235           if (!component) {
236             return;
237           }
238           markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
239           break;
240         default:
241           break;
242       }
243     },
244
245     MethodDefinition: function(node) {
246       if (!isPropTypesDeclaration(node.key)) {
247         return;
248       }
249
250       var i = node.value.body.body.length - 1;
251       for (; i >= 0; i--) {
252         if (node.value.body.body[i].type === 'ReturnStatement') {
253           break;
254         }
255       }
256
257       markPropTypesAsDeclared(node, node.value.body.body[i].argument);
258     },
259
260     ObjectExpression: function(node) {
261       // Search for the displayName declaration
262       node.properties.forEach(function(property) {
263         if (!isPropTypesDeclaration(property.key)) {
264           return;
265         }
266         markPropTypesAsDeclared(node, property.value);
267       });
268     },
269
270     'Program:exit': function() {
271       var list = componentList.getList();
272       // Report undeclared proptypes for all classes
273       for (var component in list) {
274         if (!list.hasOwnProperty(component) || !mustBeValidated(list[component])) {
275           continue;
276         }
277         reportUndeclaredPropTypes(list[component]);
278       }
279     },
280
281     ReturnStatement: function(node) {
282       if (!componentUtil.isReactComponent(context, node)) {
283         return;
284       }
285       componentList.set(context, node, {
286         isReactComponent: true
287       });
288     }
289   };
290
291 };