]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to check for the usage of var. | |
3 | * @author Jamund Ferguson | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | ||
14 | //------------------------------------------------------------------------------ | |
15 | // Helpers | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
18 | /** | |
19 | * Check whether a given variable is a global variable or not. | |
20 | * @param {eslint-scope.Variable} variable The variable to check. | |
21 | * @returns {boolean} `true` if the variable is a global variable. | |
22 | */ | |
23 | function isGlobal(variable) { | |
24 | return Boolean(variable.scope) && variable.scope.type === "global"; | |
25 | } | |
26 | ||
27 | /** | |
28 | * Finds the nearest function scope or global scope walking up the scope | |
29 | * hierarchy. | |
30 | * @param {eslint-scope.Scope} scope The scope to traverse. | |
31 | * @returns {eslint-scope.Scope} a function scope or global scope containing the given | |
32 | * scope. | |
33 | */ | |
34 | function getEnclosingFunctionScope(scope) { | |
35 | let currentScope = scope; | |
36 | ||
37 | while (currentScope.type !== "function" && currentScope.type !== "global") { | |
38 | currentScope = currentScope.upper; | |
39 | } | |
40 | return currentScope; | |
41 | } | |
42 | ||
43 | /** | |
44 | * Checks whether the given variable has any references from a more specific | |
45 | * function expression (i.e. a closure). | |
46 | * @param {eslint-scope.Variable} variable A variable to check. | |
47 | * @returns {boolean} `true` if the variable is used from a closure. | |
48 | */ | |
49 | function isReferencedInClosure(variable) { | |
50 | const enclosingFunctionScope = getEnclosingFunctionScope(variable.scope); | |
51 | ||
52 | return variable.references.some(reference => | |
53 | getEnclosingFunctionScope(reference.from) !== enclosingFunctionScope); | |
54 | } | |
55 | ||
56 | /** | |
57 | * Checks whether the given node is the assignee of a loop. | |
58 | * @param {ASTNode} node A VariableDeclaration node to check. | |
59 | * @returns {boolean} `true` if the declaration is assigned as part of loop | |
60 | * iteration. | |
61 | */ | |
62 | function isLoopAssignee(node) { | |
63 | return (node.parent.type === "ForOfStatement" || node.parent.type === "ForInStatement") && | |
64 | node === node.parent.left; | |
65 | } | |
66 | ||
67 | /** | |
68 | * Checks whether the given variable declaration is immediately initialized. | |
69 | * @param {ASTNode} node A VariableDeclaration node to check. | |
70 | * @returns {boolean} `true` if the declaration has an initializer. | |
71 | */ | |
72 | function isDeclarationInitialized(node) { | |
73 | return node.declarations.every(declarator => declarator.init !== null); | |
74 | } | |
75 | ||
76 | const SCOPE_NODE_TYPE = /^(?:Program|BlockStatement|SwitchStatement|ForStatement|ForInStatement|ForOfStatement)$/u; | |
77 | ||
78 | /** | |
79 | * Gets the scope node which directly contains a given node. | |
80 | * @param {ASTNode} node A node to get. This is a `VariableDeclaration` or | |
81 | * an `Identifier`. | |
82 | * @returns {ASTNode} A scope node. This is one of `Program`, `BlockStatement`, | |
83 | * `SwitchStatement`, `ForStatement`, `ForInStatement`, and | |
84 | * `ForOfStatement`. | |
85 | */ | |
86 | function getScopeNode(node) { | |
87 | for (let currentNode = node; currentNode; currentNode = currentNode.parent) { | |
88 | if (SCOPE_NODE_TYPE.test(currentNode.type)) { | |
89 | return currentNode; | |
90 | } | |
91 | } | |
92 | ||
8f9d1d4d | 93 | /* c8 ignore next */ |
eb39fafa DC |
94 | return null; |
95 | } | |
96 | ||
97 | /** | |
98 | * Checks whether a given variable is redeclared or not. | |
99 | * @param {eslint-scope.Variable} variable A variable to check. | |
100 | * @returns {boolean} `true` if the variable is redeclared. | |
101 | */ | |
102 | function isRedeclared(variable) { | |
103 | return variable.defs.length >= 2; | |
104 | } | |
105 | ||
106 | /** | |
107 | * Checks whether a given variable is used from outside of the specified scope. | |
108 | * @param {ASTNode} scopeNode A scope node to check. | |
109 | * @returns {Function} The predicate function which checks whether a given | |
110 | * variable is used from outside of the specified scope. | |
111 | */ | |
112 | function isUsedFromOutsideOf(scopeNode) { | |
113 | ||
114 | /** | |
115 | * Checks whether a given reference is inside of the specified scope or not. | |
116 | * @param {eslint-scope.Reference} reference A reference to check. | |
117 | * @returns {boolean} `true` if the reference is inside of the specified | |
118 | * scope. | |
119 | */ | |
120 | function isOutsideOfScope(reference) { | |
121 | const scope = scopeNode.range; | |
122 | const id = reference.identifier.range; | |
123 | ||
124 | return id[0] < scope[0] || id[1] > scope[1]; | |
125 | } | |
126 | ||
127 | return function(variable) { | |
128 | return variable.references.some(isOutsideOfScope); | |
129 | }; | |
130 | } | |
131 | ||
132 | /** | |
133 | * Creates the predicate function which checks whether a variable has their references in TDZ. | |
134 | * | |
135 | * The predicate function would return `true`: | |
136 | * | |
137 | * - if a reference is before the declarator. E.g. (var a = b, b = 1;)(var {a = b, b} = {};) | |
138 | * - if a reference is in the expression of their default value. E.g. (var {a = a} = {};) | |
139 | * - if a reference is in the expression of their initializer. E.g. (var a = a;) | |
140 | * @param {ASTNode} node The initializer node of VariableDeclarator. | |
141 | * @returns {Function} The predicate function. | |
142 | * @private | |
143 | */ | |
144 | function hasReferenceInTDZ(node) { | |
145 | const initStart = node.range[0]; | |
146 | const initEnd = node.range[1]; | |
147 | ||
148 | return variable => { | |
149 | const id = variable.defs[0].name; | |
150 | const idStart = id.range[0]; | |
151 | const defaultValue = (id.parent.type === "AssignmentPattern" ? id.parent.right : null); | |
152 | const defaultStart = defaultValue && defaultValue.range[0]; | |
153 | const defaultEnd = defaultValue && defaultValue.range[1]; | |
154 | ||
155 | return variable.references.some(reference => { | |
156 | const start = reference.identifier.range[0]; | |
157 | const end = reference.identifier.range[1]; | |
158 | ||
159 | return !reference.init && ( | |
160 | start < idStart || | |
161 | (defaultValue !== null && start >= defaultStart && end <= defaultEnd) || | |
f2a92ac6 | 162 | (!astUtils.isFunction(node) && start >= initStart && end <= initEnd) |
eb39fafa DC |
163 | ); |
164 | }); | |
165 | }; | |
166 | } | |
167 | ||
168 | /** | |
169 | * Checks whether a given variable has name that is allowed for 'var' declarations, | |
170 | * but disallowed for `let` declarations. | |
171 | * @param {eslint-scope.Variable} variable The variable to check. | |
172 | * @returns {boolean} `true` if the variable has a disallowed name. | |
173 | */ | |
174 | function hasNameDisallowedForLetDeclarations(variable) { | |
175 | return variable.name === "let"; | |
176 | } | |
177 | ||
178 | //------------------------------------------------------------------------------ | |
179 | // Rule Definition | |
180 | //------------------------------------------------------------------------------ | |
181 | ||
34eeec05 | 182 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
183 | module.exports = { |
184 | meta: { | |
185 | type: "suggestion", | |
186 | ||
187 | docs: { | |
8f9d1d4d | 188 | description: "Require `let` or `const` instead of `var`", |
eb39fafa | 189 | recommended: false, |
f2a92ac6 | 190 | url: "https://eslint.org/docs/latest/rules/no-var" |
eb39fafa DC |
191 | }, |
192 | ||
193 | schema: [], | |
194 | fixable: "code", | |
195 | ||
196 | messages: { | |
197 | unexpectedVar: "Unexpected var, use let or const instead." | |
198 | } | |
199 | }, | |
200 | ||
201 | create(context) { | |
f2a92ac6 | 202 | const sourceCode = context.sourceCode; |
eb39fafa DC |
203 | |
204 | /** | |
205 | * Checks whether the variables which are defined by the given declarator node have their references in TDZ. | |
206 | * @param {ASTNode} declarator The VariableDeclarator node to check. | |
207 | * @returns {boolean} `true` if one of the variables which are defined by the given declarator node have their references in TDZ. | |
208 | */ | |
209 | function hasSelfReferenceInTDZ(declarator) { | |
210 | if (!declarator.init) { | |
211 | return false; | |
212 | } | |
f2a92ac6 | 213 | const variables = sourceCode.getDeclaredVariables(declarator); |
eb39fafa DC |
214 | |
215 | return variables.some(hasReferenceInTDZ(declarator.init)); | |
216 | } | |
217 | ||
218 | /** | |
219 | * Checks whether it can fix a given variable declaration or not. | |
220 | * It cannot fix if the following cases: | |
221 | * | |
222 | * - A variable is a global variable. | |
223 | * - A variable is declared on a SwitchCase node. | |
224 | * - A variable is redeclared. | |
225 | * - A variable is used from outside the scope. | |
226 | * - A variable is used from a closure within a loop. | |
227 | * - A variable might be used before it is assigned within a loop. | |
228 | * - A variable might be used in TDZ. | |
229 | * - A variable is declared in statement position (e.g. a single-line `IfStatement`) | |
230 | * - A variable has name that is disallowed for `let` declarations. | |
231 | * | |
232 | * ## A variable is declared on a SwitchCase node. | |
233 | * | |
234 | * If this rule modifies 'var' declarations on a SwitchCase node, it | |
235 | * would generate the warnings of 'no-case-declarations' rule. And the | |
236 | * 'eslint:recommended' preset includes 'no-case-declarations' rule, so | |
237 | * this rule doesn't modify those declarations. | |
238 | * | |
239 | * ## A variable is redeclared. | |
240 | * | |
241 | * The language spec disallows redeclarations of `let` declarations. | |
242 | * Those variables would cause syntax errors. | |
243 | * | |
244 | * ## A variable is used from outside the scope. | |
245 | * | |
246 | * The language spec disallows accesses from outside of the scope for | |
247 | * `let` declarations. Those variables would cause reference errors. | |
248 | * | |
249 | * ## A variable is used from a closure within a loop. | |
250 | * | |
251 | * A `var` declaration within a loop shares the same variable instance | |
252 | * across all loop iterations, while a `let` declaration creates a new | |
253 | * instance for each iteration. This means if a variable in a loop is | |
254 | * referenced by any closure, changing it from `var` to `let` would | |
255 | * change the behavior in a way that is generally unsafe. | |
256 | * | |
257 | * ## A variable might be used before it is assigned within a loop. | |
258 | * | |
259 | * Within a loop, a `let` declaration without an initializer will be | |
260 | * initialized to null, while a `var` declaration will retain its value | |
261 | * from the previous iteration, so it is only safe to change `var` to | |
262 | * `let` if we can statically determine that the variable is always | |
263 | * assigned a value before its first access in the loop body. To keep | |
264 | * the implementation simple, we only convert `var` to `let` within | |
265 | * loops when the variable is a loop assignee or the declaration has an | |
266 | * initializer. | |
267 | * @param {ASTNode} node A variable declaration node to check. | |
268 | * @returns {boolean} `true` if it can fix the node. | |
269 | */ | |
270 | function canFix(node) { | |
f2a92ac6 | 271 | const variables = sourceCode.getDeclaredVariables(node); |
eb39fafa DC |
272 | const scopeNode = getScopeNode(node); |
273 | ||
274 | if (node.parent.type === "SwitchCase" || | |
275 | node.declarations.some(hasSelfReferenceInTDZ) || | |
276 | variables.some(isGlobal) || | |
277 | variables.some(isRedeclared) || | |
278 | variables.some(isUsedFromOutsideOf(scopeNode)) || | |
279 | variables.some(hasNameDisallowedForLetDeclarations) | |
280 | ) { | |
281 | return false; | |
282 | } | |
283 | ||
284 | if (astUtils.isInLoop(node)) { | |
285 | if (variables.some(isReferencedInClosure)) { | |
286 | return false; | |
287 | } | |
288 | if (!isLoopAssignee(node) && !isDeclarationInitialized(node)) { | |
289 | return false; | |
290 | } | |
291 | } | |
292 | ||
293 | if ( | |
294 | !isLoopAssignee(node) && | |
295 | !(node.parent.type === "ForStatement" && node.parent.init === node) && | |
296 | !astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type) | |
297 | ) { | |
298 | ||
299 | // If the declaration is not in a block, e.g. `if (foo) var bar = 1;`, then it can't be fixed. | |
300 | return false; | |
301 | } | |
302 | ||
303 | return true; | |
304 | } | |
305 | ||
306 | /** | |
307 | * Reports a given variable declaration node. | |
308 | * @param {ASTNode} node A variable declaration node to report. | |
309 | * @returns {void} | |
310 | */ | |
311 | function report(node) { | |
312 | context.report({ | |
313 | node, | |
314 | messageId: "unexpectedVar", | |
315 | ||
316 | fix(fixer) { | |
317 | const varToken = sourceCode.getFirstToken(node, { filter: t => t.value === "var" }); | |
318 | ||
319 | return canFix(node) | |
320 | ? fixer.replaceText(varToken, "let") | |
321 | : null; | |
322 | } | |
323 | }); | |
324 | } | |
325 | ||
326 | return { | |
327 | "VariableDeclaration:exit"(node) { | |
328 | if (node.kind === "var") { | |
329 | report(node); | |
330 | } | |
331 | } | |
332 | }; | |
333 | } | |
334 | }; |