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