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