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("./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 */ 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 * @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 */ 34function 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 */ 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 * @param {ASTNode} node A VariableDeclaration node to check. 59 * @returns {boolean} `true` if the declaration is assigned as part of loop 60 * iteration. 61 */ 62function 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 */ 72function isDeclarationInitialized(node) { 73 return node.declarations.every(declarator => declarator.init !== null); 74} 75 76const 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 */ 86function 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 93 /* istanbul ignore next : unreachable */ 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 */ 102function 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 */ 112function 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 */ 144function 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) || 162 (start >= initStart && end <= initEnd) 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 */ 174function hasNameDisallowedForLetDeclarations(variable) { 175 return variable.name === "let"; 176} 177 178//------------------------------------------------------------------------------ 179// Rule Definition 180//------------------------------------------------------------------------------ 181 182module.exports = { 183 meta: { 184 type: "suggestion", 185 186 docs: { 187 description: "require `let` or `const` instead of `var`", 188 category: "ECMAScript 6", 189 recommended: false, 190 url: "https://eslint.org/docs/rules/no-var" 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) { 202 const sourceCode = context.getSourceCode(); 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 } 213 const variables = context.getDeclaredVariables(declarator); 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) { 271 const variables = context.getDeclaredVariables(node); 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}; 335