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