2 * @fileoverview Rule to check for "block scoped" variables by binding context
3 * @author Matt DuVall <http://www.mattduvall.com>
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
11 module.exports = function(context) {
15 //--------------------------------------------------------------------------
17 //--------------------------------------------------------------------------
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.
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;
31 case "VariableDeclarator":
32 return id === parent.id;
35 return id === parent.param;
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.
48 function isProperty(id, parent) {
49 switch (parent.type) {
50 case "MemberExpression":
51 return id === parent.property && !parent.computed;
54 return id === parent.key;
62 * Pushes a new scope object on the scope stack.
65 function pushScope() {
70 * Removes the topmost scope object from the scope stack.
78 * Declares the given names in the topmost scope object.
79 * @param {[String]} names A list of names to declare.
82 function declare(names) {
83 [].push.apply(scopeStack[scopeStack.length - 1], names);
86 //--------------------------------------------------------------------------
88 //--------------------------------------------------------------------------
91 * Declares all relevant identifiers for module imports.
92 * @param {ASTNode} node The AST node representing an import.
96 function declareImports(node) {
97 declare([node.local.name]);
99 if (node.imported && node.imported.name !== node.local.name) {
100 declare([node.imported.name]);
105 * Declares all relevant identifiers for classes.
106 * @param {ASTNode} node The AST node representing a class.
110 function declareClass(node) {
114 declare([node.id.name]);
119 * Declares all relevant identifiers for classes.
120 * @param {ASTNode} node The AST node representing a class.
124 function declareClassMethod(node) {
127 declare([node.key.name]);
131 * Add declarations based on the type of node being passed.
132 * @param {ASTNode} node The node containing declarations.
136 function declareByNodeType(node) {
138 var declarations = [];
142 declarations.push(node.name);
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);
155 node.elements.forEach(function(element) {
157 declarations.push(element.name);
165 declare(declarations);
169 function functionHandler(node) {
172 node.params.forEach(function(param) {
173 declareByNodeType(param);
176 declare(node.id ? [node.id.name] : []);
177 declare(node.rest ? [node.rest.name] : []);
178 declare(["arguments"]);
181 function variableDeclarationHandler(node) {
182 node.declarations.forEach(function(declaration) {
183 declareByNodeType(declaration.id);
189 "Program": function() {
190 var scope = context.getScope();
191 scopeStack = [scope.variables.map(function(v) {
195 // global return creates another scope
196 if (context.ecmaFeatures.globalReturn) {
197 scope = scope.childScopes[0];
198 scopeStack.push(scope.variables.map(function(v) {
204 "ImportSpecifier": declareImports,
205 "ImportDefaultSpecifier": declareImports,
206 "ImportNamespaceSpecifier": declareImports,
208 "BlockStatement": function(node) {
209 var statements = node.body;
211 statements.forEach(function(stmt) {
212 if (stmt.type === "VariableDeclaration") {
213 variableDeclarationHandler(stmt);
214 } else if (stmt.type === "FunctionDeclaration") {
215 declare([stmt.id.name]);
220 "VariableDeclaration": function (node) {
221 variableDeclarationHandler(node);
224 "BlockStatement:exit": popScope,
226 "CatchClause": function(node) {
228 declare([node.param.name]);
230 "CatchClause:exit": popScope,
232 "FunctionDeclaration": functionHandler,
233 "FunctionDeclaration:exit": popScope,
235 "ClassDeclaration": declareClass,
236 "ClassDeclaration:exit": popScope,
238 "ClassExpression": declareClass,
239 "ClassExpression:exit": popScope,
241 "MethodDefinition": declareClassMethod,
242 "MethodDefinition:exit": popScope,
244 "FunctionExpression": functionHandler,
245 "FunctionExpression:exit": popScope,
247 "ArrowFunctionExpression": functionHandler,
248 "ArrowFunctionExpression:exit": popScope,
250 "ForStatement": function() {
253 "ForStatement:exit": popScope,
255 "ForInStatement": function() {
258 "ForInStatement:exit": popScope,
260 "ForOfStatement": function() {
263 "ForOfStatement:exit": popScope,
265 "Identifier": function(node) {
266 var ancestor = context.getAncestors().pop();
267 if (isDeclaration(node, ancestor) || isProperty(node, ancestor) || ancestor.type === "LabeledStatement") {
271 for (var i = 0, l = scopeStack.length; i < l; i++) {
272 if (scopeStack[i].indexOf(node.name) > -1) {
277 context.report(node, "\"" + node.name + "\" used outside of binding context.");