e99496cb4b4e63e3966fd126263c865af336fb59
[platform/framework/web/crosswalk-tizen.git] /
1 /**
2  * @fileoverview Rule to check for "block scoped" variables by binding context
3  * @author Matt DuVall <http://www.mattduvall.com>
4  */
5 "use strict";
6
7 //------------------------------------------------------------------------------
8 // Rule Definition
9 //------------------------------------------------------------------------------
10
11 module.exports = function(context) {
12
13     var scopeStack = [];
14
15     //--------------------------------------------------------------------------
16     // Helpers
17     //--------------------------------------------------------------------------
18
19     /**
20      * Determines whether an identifier is in declaration position or is a non-declaration reference.
21      * @param {ASTNode} id The identifier.
22      * @param {ASTNode} parent The identifier's parent AST node.
23      * @returns {Boolean} true when the identifier is in declaration position.
24      */
25     function isDeclaration(id, parent) {
26         switch (parent.type) {
27             case "FunctionDeclaration":
28             case "FunctionExpression":
29                 return parent.params.indexOf(id) > -1 || id === parent.id;
30
31             case "VariableDeclarator":
32                 return id === parent.id;
33
34             case "CatchClause":
35                 return id === parent.param;
36
37             default:
38                 return false;
39         }
40     }
41
42     /**
43      * Determines whether an identifier is in property position.
44      * @param {ASTNode} id The identifier.
45      * @param {ASTNode} parent The identifier's parent AST node.
46      * @returns {Boolean} true when the identifier is in property position.
47      */
48     function isProperty(id, parent) {
49         switch (parent.type) {
50             case "MemberExpression":
51                 return id === parent.property && !parent.computed;
52
53             case "Property":
54                 return id === parent.key;
55
56             default:
57                 return false;
58         }
59     }
60
61     /**
62      * Pushes a new scope object on the scope stack.
63      * @returns {void}
64      */
65     function pushScope() {
66         scopeStack.push([]);
67     }
68
69     /**
70      * Removes the topmost scope object from the scope stack.
71      * @returns {void}
72      */
73     function popScope() {
74         scopeStack.pop();
75     }
76
77     /**
78      * Declares the given names in the topmost scope object.
79      * @param {[String]} names A list of names to declare.
80      * @returns {void}
81      */
82     function declare(names) {
83         [].push.apply(scopeStack[scopeStack.length - 1], names);
84     }
85
86     //--------------------------------------------------------------------------
87     // Public API
88     //--------------------------------------------------------------------------
89
90     /**
91      * Declares all relevant identifiers for module imports.
92      * @param {ASTNode} node The AST node representing an import.
93      * @returns {void}
94      * @private
95      */
96     function declareImports(node) {
97         declare([node.local.name]);
98
99         if (node.imported && node.imported.name !== node.local.name) {
100             declare([node.imported.name]);
101         }
102     }
103
104     /**
105      * Declares all relevant identifiers for classes.
106      * @param {ASTNode} node The AST node representing a class.
107      * @returns {void}
108      * @private
109      */
110     function declareClass(node) {
111         pushScope();
112
113         if (node.id) {
114             declare([node.id.name]);
115         }
116     }
117
118     /**
119      * Declares all relevant identifiers for classes.
120      * @param {ASTNode} node The AST node representing a class.
121      * @returns {void}
122      * @private
123      */
124     function declareClassMethod(node) {
125         pushScope();
126
127         declare([node.key.name]);
128     }
129
130     /**
131      * Add declarations based on the type of node being passed.
132      * @param {ASTNode} node The node containing declarations.
133      * @returns {void}
134      * @private
135      */
136     function declareByNodeType(node) {
137
138         var declarations = [];
139
140         switch (node.type) {
141             case "Identifier":
142                 declarations.push(node.name);
143                 break;
144
145             case "ObjectPattern":
146                 node.properties.forEach(function(property) {
147                     declarations.push(property.key.name);
148                     if (property.value) {
149                         declarations.push(property.value.name);
150                     }
151                 });
152                 break;
153
154             case "ArrayPattern":
155                 node.elements.forEach(function(element) {
156                     if (element) {
157                         declarations.push(element.name);
158                     }
159                 });
160                 break;
161
162             // no default
163         }
164
165         declare(declarations);
166
167     }
168
169     function functionHandler(node) {
170         pushScope();
171
172         node.params.forEach(function(param) {
173             declareByNodeType(param);
174         });
175
176         declare(node.id ? [node.id.name] : []);
177         declare(node.rest ? [node.rest.name] : []);
178         declare(["arguments"]);
179     }
180
181     function variableDeclarationHandler(node) {
182         node.declarations.forEach(function(declaration) {
183             declareByNodeType(declaration.id);
184         });
185
186     }
187
188     return {
189         "Program": function() {
190             var scope = context.getScope();
191             scopeStack = [scope.variables.map(function(v) {
192                 return v.name;
193             })];
194
195             // global return creates another scope
196             if (context.ecmaFeatures.globalReturn) {
197                 scope = scope.childScopes[0];
198                 scopeStack.push(scope.variables.map(function(v) {
199                     return v.name;
200                 }));
201             }
202         },
203
204         "ImportSpecifier": declareImports,
205         "ImportDefaultSpecifier": declareImports,
206         "ImportNamespaceSpecifier": declareImports,
207
208         "BlockStatement": function(node) {
209             var statements = node.body;
210             pushScope();
211             statements.forEach(function(stmt) {
212                 if (stmt.type === "VariableDeclaration") {
213                     variableDeclarationHandler(stmt);
214                 } else if (stmt.type === "FunctionDeclaration") {
215                     declare([stmt.id.name]);
216                 }
217             });
218         },
219
220         "VariableDeclaration": function (node) {
221             variableDeclarationHandler(node);
222         },
223
224         "BlockStatement:exit": popScope,
225
226         "CatchClause": function(node) {
227             pushScope();
228             declare([node.param.name]);
229         },
230         "CatchClause:exit": popScope,
231
232         "FunctionDeclaration": functionHandler,
233         "FunctionDeclaration:exit": popScope,
234
235         "ClassDeclaration": declareClass,
236         "ClassDeclaration:exit": popScope,
237
238         "ClassExpression": declareClass,
239         "ClassExpression:exit": popScope,
240
241         "MethodDefinition": declareClassMethod,
242         "MethodDefinition:exit": popScope,
243
244         "FunctionExpression": functionHandler,
245         "FunctionExpression:exit": popScope,
246
247         "ArrowFunctionExpression": functionHandler,
248         "ArrowFunctionExpression:exit": popScope,
249
250         "ForStatement": function() {
251             pushScope();
252         },
253         "ForStatement:exit": popScope,
254
255         "ForInStatement": function() {
256             pushScope();
257         },
258         "ForInStatement:exit": popScope,
259
260         "ForOfStatement": function() {
261             pushScope();
262         },
263         "ForOfStatement:exit": popScope,
264
265         "Identifier": function(node) {
266             var ancestor = context.getAncestors().pop();
267             if (isDeclaration(node, ancestor) || isProperty(node, ancestor) || ancestor.type === "LabeledStatement") {
268                 return;
269             }
270
271             for (var i = 0, l = scopeStack.length; i < l; i++) {
272                 if (scopeStack[i].indexOf(node.name) > -1) {
273                     return;
274                 }
275             }
276
277             context.report(node, "\"" + node.name + "\" used outside of binding context.");
278         }
279     };
280
281 };