UNPKG

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