UNPKG

6.09 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to flag on declaring variables already declared in the outer scope
3 * @author Ilya Volodin
4 * @copyright 2013 Ilya Volodin. All rights reserved.
5 */
6
7"use strict";
8
9//------------------------------------------------------------------------------
10// Requirements
11//------------------------------------------------------------------------------
12
13var astUtils = require("../ast-utils");
14
15//------------------------------------------------------------------------------
16// Rule Definition
17//------------------------------------------------------------------------------
18
19module.exports = function(context) {
20
21 var options = {
22 builtinGlobals: Boolean(context.options[0] && context.options[0].builtinGlobals),
23 hoist: (context.options[0] && context.options[0].hoist) || "functions",
24 allow: (context.options[0] && context.options[0].allow) || []
25 };
26
27 /**
28 * Check if variable name is allowed.
29 *
30 * @param {ASTNode} variable The variable to check.
31 * @returns {boolean} Whether or not the variable name is allowed.
32 */
33 function isAllowed(variable) {
34 return options.allow.indexOf(variable.name) !== -1;
35 }
36
37 /**
38 * Checks if a variable of the class name in the class scope of ClassDeclaration.
39 *
40 * ClassDeclaration creates two variables of its name into its outer scope and its class scope.
41 * So we should ignore the variable in the class scope.
42 *
43 * @param {Object} variable The variable to check.
44 * @returns {boolean} Whether or not the variable of the class name in the class scope of ClassDeclaration.
45 */
46 function isDuplicatedClassNameVariable(variable) {
47 var block = variable.scope.block;
48 return block.type === "ClassDeclaration" && block.id === variable.identifiers[0];
49 }
50
51 /**
52 * Checks if a variable is inside the initializer of scopeVar.
53 *
54 * To avoid reporting at declarations such as `var a = function a() {};`.
55 * But it should report `var a = function(a) {};` or `var a = function() { function a() {} };`.
56 *
57 * @param {Object} variable The variable to check.
58 * @param {Object} scopeVar The scope variable to look for.
59 * @returns {boolean} Whether or not the variable is inside initializer of scopeVar.
60 */
61 function isOnInitializer(variable, scopeVar) {
62 var outerScope = scopeVar.scope;
63 var outerDef = scopeVar.defs[0];
64 var outer = outerDef && outerDef.parent && outerDef.parent.range;
65 var innerScope = variable.scope;
66 var innerDef = variable.defs[0];
67 var inner = innerDef && innerDef.name.range;
68
69 return (
70 outer &&
71 inner &&
72 outer[0] < inner[0] &&
73 inner[1] < outer[1] &&
74 ((innerDef.type === "FunctionName" && innerDef.node.type === "FunctionExpression") || innerDef.node.type === "ClassExpression") &&
75 outerScope === innerScope.upper
76 );
77 }
78
79 /**
80 * Get a range of a variable's identifier node.
81 * @param {Object} variable The variable to get.
82 * @returns {Array|undefined} The range of the variable's identifier node.
83 */
84 function getNameRange(variable) {
85 var def = variable.defs[0];
86 return def && def.name.range;
87 }
88
89 /**
90 * Checks if a variable is in TDZ of scopeVar.
91 * @param {Object} variable The variable to check.
92 * @param {Object} scopeVar The variable of TDZ.
93 * @returns {boolean} Whether or not the variable is in TDZ of scopeVar.
94 */
95 function isInTdz(variable, scopeVar) {
96 var outerDef = scopeVar.defs[0];
97 var inner = getNameRange(variable);
98 var outer = getNameRange(scopeVar);
99 return (
100 inner &&
101 outer &&
102 inner[1] < outer[0] &&
103 // Excepts FunctionDeclaration if is {"hoist":"function"}.
104 (options.hoist !== "functions" || !outerDef || outerDef.node.type !== "FunctionDeclaration")
105 );
106 }
107
108 /**
109 * Checks the current context for shadowed variables.
110 * @param {Scope} scope - Fixme
111 * @returns {void}
112 */
113 function checkForShadows(scope) {
114 var variables = scope.variables;
115 for (var i = 0; i < variables.length; ++i) {
116 var variable = variables[i];
117
118 // Skips "arguments" or variables of a class name in the class scope of ClassDeclaration.
119 if (variable.identifiers.length === 0 ||
120 isDuplicatedClassNameVariable(variable) ||
121 isAllowed(variable)
122 ) {
123 continue;
124 }
125
126 // Gets shadowed variable.
127 var shadowed = astUtils.getVariableByName(scope.upper, variable.name);
128 if (shadowed &&
129 (shadowed.identifiers.length > 0 || (options.builtinGlobals && "writeable" in shadowed)) &&
130 !isOnInitializer(variable, shadowed) &&
131 !(options.hoist !== "all" && isInTdz(variable, shadowed))
132 ) {
133 context.report({
134 node: variable.identifiers[0],
135 message: "'{{name}}' is already declared in the upper scope.",
136 data: variable
137 });
138 }
139 }
140 }
141
142 return {
143 "Program:exit": function() {
144 var globalScope = context.getScope();
145 var stack = globalScope.childScopes.slice();
146 var scope;
147
148 while (stack.length) {
149 scope = stack.pop();
150 stack.push.apply(stack, scope.childScopes);
151 checkForShadows(scope);
152 }
153 }
154 };
155
156};
157
158module.exports.schema = [
159 {
160 "type": "object",
161 "properties": {
162 "builtinGlobals": {"type": "boolean"},
163 "hoist": {"enum": ["all", "functions", "never"]},
164 "allow": {
165 "type": "array",
166 "items": {
167 "type": "string"
168 }
169 }
170 },
171 "additionalProperties": false
172 }
173];