Blame view
node_modules/eslint/lib/rules/no-var.js
11 KB
f7563de62
|
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 |
/** * @fileoverview Rule to check for the usage of var. * @author Jamund Ferguson */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require("../ast-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ /** * Finds the nearest function scope or global scope walking up the scope * hierarchy. * * @param {escope.Scope} scope - The scope to traverse. * @returns {escope.Scope} a function scope or global scope containing the given * scope. */ function getEnclosingFunctionScope(scope) { while (scope.type !== "function" && scope.type !== "global") { scope = scope.upper; } return scope; } /** * Checks whether the given variable has any references from a more specific * function expression (i.e. a closure). * * @param {escope.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)$/; /** * 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) { while (node) { if (SCOPE_NODE_TYPE.test(node.type)) { return node; } node = node.parent; } /* istanbul ignore next : unreachable */ return null; } /** * Checks whether a given variable is redeclared or not. * * @param {escope.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 {escope.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) ); }); }; } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: "require `let` or `const` instead of `var`", category: "ECMAScript 6", recommended: false }, 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 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 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(isRedeclared) || variables.some(isUsedFromOutsideOf(scopeNode)) ) { 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) { const varToken = sourceCode.getFirstToken(node); context.report({ node, message: "Unexpected var, use let or const instead.", fix(fixer) { if (canFix(node)) { return fixer.replaceText(varToken, "let"); } return null; } }); } return { "VariableDeclaration:exit"(node) { if (node.kind === "var") { report(node); } } }; } }; |