]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
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 | ||
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: { | |
5422a9cc TL |
47 | noShadow: "'{{name}}' is already declared in the upper scope on line {{shadowedLine}} column {{shadowedColumn}}.", |
48 | noShadowGlobal: "'{{name}}' is already a global variable." | |
eb39fafa DC |
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 | ||
5422a9cc TL |
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 | ||
eb39fafa DC |
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 | ) { | |
5422a9cc TL |
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 | } | |
eb39fafa DC |
200 | context.report({ |
201 | node: variable.identifiers[0], | |
5422a9cc TL |
202 | messageId, |
203 | data | |
eb39fafa DC |
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 | }; |