UNPKG

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